How to Run Kafka on Kubernetes with Strimzi 1.3: KRaft Setup (2026)

Nov 10, 2026 · 13 min · Ajay Kumar

This guide is for platform engineers who need a production-shaped Apache Kafka cluster on Kubernetes and would rather declare it in YAML than babysit StatefulSets. By the end you will have a KRaft-only Kafka 4.3.2 cluster with three dedicated controllers and three brokers, managed by Strimzi, with topics and users as custom resources, an mTLS-only listener with ACLs, Prometheus metrics and alerts for under-replicated partitions and consumer lag, and a rehearsed upgrade path. Versions are current as of 10 November 2026: Strimzi 1.3.1 (4 November 2026), which supports Kafka 4.2.0 to 4.3.2 and requires Kubernetes 1.32 or newer, and the quay.io/strimzi/kafka:1.3.1-kafka-4.3.2 image. Every Strimzi manifest below validates against the 1.3.1 CRD schemas. You need a Kubernetes 1.32+ cluster spread across three zones, a default StorageClass, Helm 3.8 or newer (for OCI charts), and cluster-admin rights for the CRDs. If you are still deciding whether you need Kafka at all, read the companion post, Apache Kafka in 2026: Kafka 4.4, No ZooKeeper, Queues and Diskless, first.

Helm chart strimzi 1.3.1, ns kafka Cluster Operator watches Strimzi CRs Kafka CR + 2 KafkaNodePools Entity Operator Topic + User Ops Controllers x3 KRaft quorum, 20Gi Brokers x3 mTLS listener :9093 Topics, Users KafkaTopic, KafkaUser Prometheus scrapes tcp-prometheus on brokers, controllers and Kafka Exporter
What this guide builds: Helm installs the Strimzi 1.3.1 Cluster Operator, which turns one Kafka resource and two KafkaNodePools into a KRaft cluster plus the Entity Operator. Structure based on the Strimzi 1.3.1 example manifests.

Step 1: Check your cluster and tool versions

Strimzi 1.3 dropped Kubernetes 1.30 and 1.31, so confirm the server version before anything else. Confirm also that your nodes carry the standard zone label, because the cluster below uses rack awareness to spread replicas across zones.

kubectl version            # Server Version must be v1.32 or newer
helm version --short       # v3.8+ for oci:// charts (Helm 4 also works)
kubectl get storageclass   # one should be marked (default)
kubectl get nodes -L topology.kubernetes.io/zone

If the zone column is empty, either label the nodes or remove the rack block in Step 3. Without zones, "three replicas" can quietly mean three copies in one data centre.

Step 2: Install the Strimzi operator with Helm

Strimzi publishes its chart as an OCI artifact on Quay, as the chart README shows. Pin the version. By default the operator watches only its own namespace, which is what you want for a first cluster.

kubectl create namespace kafka

helm install strimzi oci://quay.io/strimzi-helm/strimzi-kafka-operator \
  --version 1.3.1 \
  --namespace kafka

kubectl -n kafka rollout status deployment/strimzi-cluster-operator
kubectl get crd | grep kafka.strimzi.io

You should see the CRDs for kafkas, kafkanodepools, kafkatopics, kafkausers, kafkarebalances and friends. Rendering the 1.3.1 chart locally with helm template produces one Deployment named strimzi-cluster-operator running quay.io/strimzi/operator:1.3.1, plus the ClusterRoles and RoleBindings it needs. If you want one operator for several namespaces, set watchNamespaces (a list) or watchAnyNamespace=true at install time.

One thing that trips everyone who copies older tutorials: since Strimzi 1.0.0 the only supported API is kafka.strimzi.io/v1. Any manifest with v1beta2 will be rejected, and the strimzi.io/kraft and strimzi.io/node-pools annotations that 0.4x-era guides tell you to add are gone. KRaft and node pools are simply how it works now.

Step 3: Create KRaft node pools and the Kafka cluster

The cluster is three resources: a KafkaNodePool for controllers, a KafkaNodePool for brokers, and the Kafka resource that ties them together with listeners, authorization and config. Controllers are small and need fast disks; brokers carry the data. Both pools use JBOD with one volume and put the KRaft metadata log on it (kraftMetadata: shared), matching the upstream examples.

Prometheus metrics need a JMX exporter ConfigMap. Take it from the Strimzi example rather than writing your own:

# Extract only the ConfigMap (named kafka-metrics) from the 1.3.1 example
curl -sL https://raw.githubusercontent.com/strimzi/strimzi-kafka-operator/1.3.1/packaging/examples/metrics/kafka-metrics.yaml \
  | yq 'select(.kind == "ConfigMap")' \
  | kubectl -n kafka apply -f -

Now the cluster itself. Save as kafka-cluster.yaml:

# Strimzi 1.3.1, Kafka 4.3.2, KRaft only
apiVersion: kafka.strimzi.io/v1
kind: KafkaNodePool
metadata:
  name: controller
  namespace: kafka
  labels:
    strimzi.io/cluster: my-cluster
spec:
  replicas: 3
  roles:
    - controller
  resources:
    requests: { cpu: 500m, memory: 2Gi }
    limits: { memory: 2Gi }
  storage:
    type: jbod
    volumes:
      - id: 0
        type: persistent-claim
        size: 20Gi
        kraftMetadata: shared
        deleteClaim: false      # keep PVCs if the pool is deleted
---
apiVersion: kafka.strimzi.io/v1
kind: KafkaNodePool
metadata:
  name: broker
  namespace: kafka
  labels:
    strimzi.io/cluster: my-cluster
spec:
  replicas: 3
  roles:
    - broker
  resources:
    requests: { cpu: "1", memory: 8Gi }
    limits: { memory: 8Gi }
  storage:
    type: jbod
    volumes:
      - id: 0
        type: persistent-claim
        size: 100Gi
        kraftMetadata: shared
        deleteClaim: false
---
apiVersion: kafka.strimzi.io/v1
kind: Kafka
metadata:
  name: my-cluster
  namespace: kafka
spec:
  kafka:
    version: 4.3.2
    metadataVersion: "4.3-IV0"   # quote it, or YAML may parse it oddly
    rack:
      topologyKey: topology.kubernetes.io/zone
    listeners:
      - name: tls
        port: 9093
        type: internal
        tls: true
        authentication:
          type: tls              # mTLS client certificates from KafkaUser
    authorization:
      type: simple
      superUsers:
        - CN=kafka-admin
    config:
      default.replication.factor: 3
      min.insync.replicas: 2
      offsets.topic.replication.factor: 3
      transaction.state.log.replication.factor: 3
      transaction.state.log.min.isr: 2
      auto.create.topics.enable: false
    metricsConfig:
      type: jmxPrometheusExporter
      valueFrom:
        configMapKeyRef:
          name: kafka-metrics
          key: kafka-metrics-config.yml
  entityOperator:
    topicOperator: {}
    userOperator: {}
  kafkaExporter:               # consumer lag metrics
    topicRegex: ".*"
    groupRegex: ".*"

The 4.3-IV0 metadata version is what Strimzi's own kafka-versions.yaml maps to Kafka 4.3.2, and 4.3.2 is the default Kafka version in 1.3.1. There is deliberately no plaintext listener: with simple authorization, an unauthenticated listener would just produce ANONYMOUS denials anyway.

kubectl apply -f kafka-cluster.yaml
kubectl -n kafka wait kafka/my-cluster --for=condition=Ready --timeout=600s
kubectl -n kafka get kafkanodepool
kubectl -n kafka get pods -l strimzi.io/cluster=my-cluster

Expect pods named my-cluster-controller-N and my-cluster-broker-N (node IDs are assigned across pools, so the numbers are not 0 to 2 for both), plus my-cluster-entity-operator and my-cluster-kafka-exporter Deployments. The KafkaNodePool output lists each pool's node IDs and roles.

Step 4: Create topics and users with KafkaTopic and KafkaUser

With auto.create.topics.enable: false, topics exist only if someone declares them. That is the point: topic config goes through code review like everything else. Save as topics-users.yaml:

apiVersion: kafka.strimzi.io/v1
kind: KafkaTopic
metadata:
  name: orders
  namespace: kafka
  labels:
    strimzi.io/cluster: my-cluster   # required, or nothing reconciles it
spec:
  partitions: 6
  replicas: 3
  config:
    retention.ms: 604800000          # 7 days
    min.insync.replicas: 2
---
apiVersion: kafka.strimzi.io/v1
kind: KafkaUser
metadata:
  name: orders-app
  namespace: kafka
  labels:
    strimzi.io/cluster: my-cluster
spec:
  authentication:
    type: tls
  authorization:
    type: simple
    acls:
      - resource:
          type: topic
          name: orders
          patternType: literal
        operations: [Describe, Read, Write]
      - resource:
          type: group
          name: orders-app
          patternType: literal
        operations: [Read]
---
apiVersion: kafka.strimzi.io/v1
kind: KafkaUser
metadata:
  name: kafka-admin                  # matches superUsers CN=kafka-admin
  namespace: kafka
  labels:
    strimzi.io/cluster: my-cluster
spec:
  authentication:
    type: tls
kubectl apply -f topics-users.yaml
kubectl -n kafka get kafkatopic orders
kubectl -n kafka get kafkauser
kubectl -n kafka get secret orders-app kafka-admin

Each KafkaUser gets a Secret of the same name containing ca.crt, user.crt, user.key and PKCS #12 files. The PKCS #12 files are deprecated as of 1.3 and will stop being generated by default in a future release, so build new clients around the PEM files now.

Step 5: Connect a client over mTLS

Run a throwaway client pod in the namespace with the cluster CA and the admin user's Secret mounted. The Strimzi Kafka image ships the Kafka CLI tools under /opt/kafka/bin, and it runs as a non-root user, so it passes the restricted Pod Security Standard.

# kafka-client.yaml
apiVersion: v1
kind: Pod
metadata:
  name: kafka-client
  namespace: kafka
spec:
  securityContext:
    runAsNonRoot: true
    seccompProfile: { type: RuntimeDefault }
  containers:
    - name: client
      image: quay.io/strimzi/kafka:1.3.1-kafka-4.3.2
      command: ["sleep", "infinity"]
      securityContext:
        allowPrivilegeEscalation: false
        capabilities: { drop: ["ALL"] }
      volumeMounts:
        - { name: cluster-ca, mountPath: /certs/ca, readOnly: true }
        - { name: admin, mountPath: /certs/user, readOnly: true }
  volumes:
    - name: cluster-ca
      secret: { secretName: my-cluster-cluster-ca-cert }
    - name: admin
      secret: { secretName: kafka-admin }

Kafka clients accept PEM files directly: a truststore with ssl.truststore.type=PEM, and a keystore file that contains both the private key and the certificate chain. In the Kafka 4.3.2 source, the PEM loader expects a PKCS #8 key (BEGIN PRIVATE KEY, or BEGIN ENCRYPTED PRIVATE KEY together with ssl.key.password), and you must not set a keystore password for PEM.

kubectl apply -f kafka-client.yaml
kubectl -n kafka wait pod/kafka-client --for=condition=Ready
kubectl -n kafka exec -it kafka-client -- bash

# Inside the pod:
cat /certs/user/user.key /certs/user/user.crt > /tmp/admin.pem
cat > /tmp/client.properties <<'EOF'
security.protocol=SSL
ssl.truststore.type=PEM
ssl.truststore.location=/certs/ca/ca.crt
ssl.keystore.type=PEM
ssl.keystore.location=/tmp/admin.pem
EOF
export BS=my-cluster-kafka-bootstrap.kafka.svc:9093
export CFG="--command-config /tmp/client.properties"

/opt/kafka/bin/kafka-topics.sh --bootstrap-server $BS $CFG --describe --topic orders
echo "order-1" | /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server $BS $CFG --topic orders
/opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server $BS $CFG \
  --topic orders --group orders-app --from-beginning --max-messages 1

Note --command-config. Since Kafka 4.2 the console producer and consumer still accept --producer.config and --consumer.config, but print a deprecation warning and tell you to use --command-config. For an application, mount the orders-app Secret instead of kafka-admin, and the ACLs from Step 4 apply.

Step 6: Add Prometheus monitoring and alerts

Strimzi ships a PodMonitor and PrometheusRules for the Prometheus Operator. If you run kube-prometheus-stack (see my kube-prometheus-stack setup guide), apply them with the namespace fixed:

BASE=https://raw.githubusercontent.com/strimzi/strimzi-kafka-operator/1.3.1/packaging/examples/metrics/prometheus-install

# PodMonitor: the example watches namespace "myproject"; point it at kafka
curl -sL $BASE/pod-monitors/kafka-resources-metrics.yaml \
  | yq '.spec.namespaceSelector.matchNames = ["kafka"]' \
  | kubectl -n kafka apply -f -

# Alert rules for brokers and for Kafka Exporter (lag, under-replication)
kubectl -n kafka apply -f $BASE/prometheus-rules/prometheus-kafka-rules.yaml
kubectl -n kafka apply -f $BASE/prometheus-rules/prometheus-kafka-exporter-topic-rules-group.yaml

kube-prometheus-stack only picks up PodMonitors and rules carrying its release label unless you set podMonitorSelectorNilUsesHelmValues: false and ruleSelectorNilUsesHelmValues: false, so add the label if nothing shows up. The alerts that matter most on day one, straight from those files:

- alert: UnderReplicatedPartitions
  expr: kafka_server_replicamanager_underreplicatedpartitions > 0
- alert: UnderMinIsrPartitionCount
  expr: kafka_server_replicamanager_underminisrpartitioncount > 0
- alert: OfflinePartitions
  expr: sum(kafka_controller_kafkacontroller_offlinepartitionscount) by (namespace, pod) > 0
- alert: TooLargeConsumerGroupLag   # from Kafka Exporter
  expr: kafka_consumergroup_lag > 1000

The upstream rules use for: 10s, which pages on every rolling restart. I would raise the under-replication alerts to five or ten minutes and set lag thresholds per consumer group in seconds of backlog rather than a flat 1,000 messages. The Strimzi lag docs give the arithmetic: at 100 messages a second, a lag of 1,000 is a 10-second delay. They also note the lag metric only exists for groups that commit offsets.

Step 7: Upgrade Strimzi and Kafka safely

Two separate upgrades, never at the same time. First the operator, then Kafka.

Operator upgrade. helm upgrade does not touch CRDs, and the chart README says to apply them manually after upgrading the operator. Check the release notes for the Kafka versions the new operator supports before you start: the unreleased Strimzi 1.4.0 in the changelog adds Kafka 4.4.0 and removes 4.2.x, so any cluster still on 4.2 must move to 4.3 first.

NEW=1.3.1   # set to the target Strimzi release
helm upgrade strimzi oci://quay.io/strimzi-helm/strimzi-kafka-operator \
  --version $NEW --namespace kafka
kubectl apply -f https://github.com/strimzi/strimzi-kafka-operator/releases/download/$NEW/strimzi-crds-$NEW.yaml
kubectl -n kafka rollout status deployment/strimzi-cluster-operator

Kafka upgrade. The Strimzi procedure is: change spec.kafka.version while leaving metadataVersion at the old value, wait for the rolling update, then bump metadataVersion. Example for a cluster on 4.2.2:

# Phase 1: new binaries, old metadata version
spec:
  kafka:
    version: 4.3.2
    metadataVersion: "4.2-IV1"
# Phase 2, after every pod has rolled and clients look healthy:
#   metadataVersion: "4.3-IV0"
kubectl -n kafka get kafka my-cluster -o jsonpath='{.status.conditions}{"\n"}'
kubectl -n kafka get pods -l strimzi.io/cluster=my-cluster \
  -o custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image

Treat phase 2 as one-way. The Strimzi docs warn that you cannot downgrade below a metadata version you have already raised, so leave a soak period between the two phases.

Troubleshooting

Most Kafka incidents on Kubernetes come down to two symptoms: partitions without enough in-sync replicas, and consumers falling behind. Start with the commands, then the table.

Under-replicated partitions. Run these from the client pod:

/opt/kafka/bin/kafka-topics.sh --bootstrap-server $BS $CFG --describe --under-replicated-partitions
/opt/kafka/bin/kafka-topics.sh --bootstrap-server $BS $CFG --describe --under-min-isr-partitions
/opt/kafka/bin/kafka-topics.sh --bootstrap-server $BS $CFG --describe --unavailable-partitions

If every affected partition lists the same broker as missing from the ISR, the problem is that broker: check kubectl -n kafka describe pod, its PVC usage and its logs. If the missing replicas are spread out, look at network or disk throughput across the whole pool. Under-replication that clears by itself after a rolling update is normal; under-min-ISR is not, because producers with acks=all start failing.

Consumer lag.

/opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server $BS $CFG --describe --group orders-app
# Preview, then apply, an offset reset (the group must have no active members)
/opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server $BS $CFG --group orders-app \
  --topic orders --reset-offsets --to-latest --dry-run

The LAG column per partition tells you whether lag is uniform (consumers are too slow overall) or concentrated on a few partitions (hot keys, or one stuck consumer). Skipping data with --reset-offsets is a business decision, not a fix.

Symptom Likely cause Fix
no matches for kind "Kafka" in version "kafka.strimzi.io/v1beta2" Manifest copied from a pre-1.0 tutorial Change apiVersion to kafka.strimzi.io/v1; see the Strimzi API conversion docs
Kafka resource never becomes Ready, operator logs mention an unsupported version spec.kafka.version not supported by this operator release Use 4.2.0 to 4.3.2 with Strimzi 1.3.1, or upgrade the operator first
Broker pods Pending No default StorageClass, or not enough nodes in distinct zones for rack placement Set class on the volume, add nodes, or remove rack for non-zonal clusters
SSL handshake failed from a client Client does not trust the cluster CA, or uses a hostname not in the broker certificate Mount my-cluster-cluster-ca-cert and connect to my-cluster-kafka-bootstrap.kafka.svc:9093
Failed to load PEM SSL keystore Keystore file missing the key or certificate, or a password set for PEM Concatenate user.key and user.crt; remove ssl.keystore.password
TopicAuthorizationException or GroupAuthorizationException KafkaUser ACL missing the topic or consumer group Add the resource and operations to the KafkaUser, or fix the group.id
KafkaTopic stays NotReady Missing strimzi.io/cluster label, or replicas greater than broker count Add the label; keep replicas at or below the broker pool size
Persistent under-replicated partitions on one broker Disk full, throttled volume, or a crash-looping broker Expand the PVC, check KafkaRunningOutOfSpace, read broker logs
Lag grows on a few partitions only Hot keys or one slow consumer instance Fix key distribution, or add partitions and rebalance with Cruise Control
Lag grows everywhere after a deploy Slower processing, or frequent rebalances Compare processing time before and after; try group.protocol=consumer (KIP-848)

For lasting partition skew, enable cruiseControl: {} in the Kafka spec and create a KafkaRebalance resource; Strimzi computes a proposal, and you approve it with the strimzi.io/rebalance=approve annotation. Practise on a non-production cluster first, because rebalances move a lot of data.

FAQ

Does Strimzi still need ZooKeeper?

No. Strimzi 1.x runs Kafka 4.x only, and Kafka 4.x is KRaft-only. Controllers are just a KafkaNodePool with the controller role. If you still have a ZooKeeper-based Strimzi cluster, you must migrate it to KRaft on Strimzi 0.45 or earlier (the changelog shows 0.46.0 removed ZooKeeper support and the migration path) before you can move to 1.x.

Which Kafka versions does Strimzi 1.3.1 support?

Kafka 4.2.0, 4.2.1, 4.2.2, 4.3.0, 4.3.1 and 4.3.2, with 4.3.2 as the default. Kafka 4.4.0 is listed for Strimzi 1.4.0 in the changelog, which has not been released as of 10 November 2026.

Should controllers and brokers share nodes?

For development, a single pool with both roles is fine. In production, keep dedicated controller and broker pools, as in this guide, so a busy broker cannot starve the metadata quorum, and spread both pools across zones.

How do I expose Kafka outside the Kubernetes cluster?

Add a second listener of type loadbalancer, nodeport, ingress, route or, since Strimzi 1.1.0, tlsroute for the Gateway API. Clients must reach every broker individually, not just the bootstrap address, so each type creates per-broker endpoints.

What I check first in production

When a Kafka cluster on Kubernetes misbehaves, I look at under-min-ISR partitions before anything else, because that is the moment producers start failing. Then I check whether one broker is the common factor, then PVC usage, then consumer lag by partition. I keep operator upgrades and Kafka upgrades in separate change windows, and I never bump metadataVersion on the same day as the binaries. If you are deciding between self-hosting like this and a managed or diskless service, the Kafka in 2026 explainer covers the trade-offs, and the same ownership questions apply as for running databases on Kubernetes. Official references: the Strimzi deploying and upgrading docs, the Strimzi releases and the Apache Kafka downloads and release notes.

Related: How to Run Postgres on Kubernetes with CloudNativePG 1.30 (2026), How to Set Up kube-prometheus-stack: Prometheus and Grafana (2026) and Should You Run Databases on Kubernetes in 2026?.

Spot an error or something out of date? Email me and I will fix it and log it on the corrections page.

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related