How to Run Kafka on Kubernetes with Strimzi 1.3: KRaft Setup (2026)
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.
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
How to Fix AWS ALB Unhealthy Targets and 502/503 Errors (2026)
Fix AWS Application Load Balancer unhealthy targets and 502, 503 and 504 errors step by step: read the health reason code, fix health check paths and success codes, security groups, ip vs instance targets, EKS readiness gates, deregistration delay, keep-alive timeouts and access logs.
13 minNov 10, 2026How to Fix Kubernetes PVC Pending and Multi-Attach Errors (2026)
Fix a Kubernetes PersistentVolumeClaim stuck in Pending and the Multi-Attach (now Waiting for detach) error: missing default StorageClass, EBS CSI driver not installed, zone conflicts, access modes, stuck Terminating and failed resizes, with commands for Kubernetes 1.37.
15 minNov 10, 2026How to Run Kubernetes on Spot Instances: Interruption Handling (2026)
Run EKS workloads safely on Spot in 2026: pick Karpenter 1.15 interruption handling, managed node groups or Node Termination Handler 1.25.6 queue mode, then add PDBs, topology spread, an on-demand floor, short grace periods and an AWS FIS interruption test.
13 min