Kafka Connect 0.7.0 Helm Readme
Helm chart that deploys a Strimzi KafkaConnect cluster for the Axual
platform. The chart manages a KafkaConnect custom resource. The
Strimzi operator owns pod lifecycle; this chart does not template a
Deployment.
Plugins are pre-built OCI images stored in Harbor. The chart resolves
plugin names to image references automatically from spec files in
plugins/.
[[TOC]]
How plugins are delivered
Two modes are supported, controlled by pluginDelivery in
values.yaml.
Per-plugin images (pluginDelivery: imageVolumes, default)
Each plugin is a tiny OCI image (~5-50 MB) containing only its JARs. Kubernetes mounts each image directly into the Connect pod at startup. The Connect base image stays vanilla Strimzi - nothing is baked in.
-
Update one plugin without touching anything else.
-
Requires Kubernetes >= 1.31 and Strimzi >= 0.47.
Prebuilt Connect image (pluginDelivery: prebuiltImage)
All plugins are baked into a full Strimzi Connect image (~600 MB) in CI. The pod runs that image - no mounting at startup.
-
Works on any Kubernetes and Strimzi version.
-
Slower to update: changing any plugin requires rebuilding the whole image.
-
The prebuilt image must already exist in Harbor before you deploy.
Which to use? Use per-plugin images if your cluster runs Kubernetes 1.31 or newer. Use the prebuilt image for older clusters or air-gapped environments.
Prerequisites
-
Strimzi cluster operator installed in the cluster.
-
A running
KafkaCR that Connect will connect to. -
Secrets for mTLS:
beta-cluster-ca-certandbeta-beta-pool-0in the target namespace (or override via values). -
A pull secret for Harbor (
regcred-harbor).
Install
Put the values for your cluster in a file. tenant, instance and
clusterName are required: they become the pod labels the log viewer
uses to find this cluster’s pods, so set them to the names registered in
Self-Service, in lowercase.
# my-cluster.yaml
tenant: axual
instance: dta
clusterName: my-connect
bootstrapServers:
tls: beta-kafka-bootstrap:9093
tls:
trustedCertificates:
- secretName: beta-cluster-ca-cert
certificate: ca.crt
authentication:
type: tls
certificateAndKey:
secretName: beta-beta-pool-0
certificate: user.crt
key: user.key
helm upgrade --install axual-dta-kafka-connect oci://registry.axual.io/axual-charts/kafka-connect \
--version 0.7.0 \
--namespace kafka \
--values my-cluster.yaml \
--set "imagePullSecrets[0].name=regcred-harbor"
Wait for Connect to be ready:
kubectl wait kafkaconnect/axual-dta-kafka-connect \
--for=condition=Ready --namespace kafka --timeout=120s
For a prebuilt image deployment, add two settings to the same command:
helm upgrade --install axual-dta-kafka-connect oci://registry.axual.io/axual-charts/kafka-connect \
--version 0.7.0 \
--namespace kafka \
--values my-cluster.yaml \
--set pluginDelivery=prebuiltImage \
--set prebuiltImageProfile=default \
--set "imagePullSecrets[0].name=regcred-harbor"
Values reference
| Key | Default | Description |
|---|---|---|
|
|
Delivery mode: |
|
see |
Plugin names to load. Each name must
have a spec at |
|
|
Registry prefix for plugin images |
|
|
Registry+repo
for the Connect base image: the worker image in |
|
|
Full override for the base image. Sets
the worker image in |
|
|
Registry+repo the
profile image is built from:
|
|
|
Profile name for the prebuilt
image. |
|
|
Strimzi version. Used to construct the prebuilt image tag. |
|
|
Kafka version for the Connect workers. |
|
|
Number of Connect worker pods. |
|
|
Kafka broker address per listener. Each consumer selects by its auth
type: the worker by |
|
|
CA cert secret for broker TLS. Required for any TLS listener (mTLS or SASL_SSL). |
|
|
Worker auth method. One of |
|
|
Client cert
reference when |
|
(unset) |
SASL username. Required for SASL types. |
|
(unset) |
K8s Secret reference with the SASL password. Required for SASL types. |
|
|
Pull secrets for the plugin registry. |
|
|
Container runtime for the worker pods,
e.g. |
|
|
Passed through to |
|
|
Passed through to
|
|
requests 250m/1Gi, limits 2Gi |
Connect worker pod resources. |
|
|
Pod-level security context for the workers, rendered into
|
|
drop |
Container-level security context for the workers, rendered into
|
|
|
Set to |
|
(required) |
Tenant short name, lowercase. Rendered as the
|
|
(required) |
Instance short name, lowercase. Rendered as
the |
|
(required) |
Connect cluster name as registered in
Self-Service, lowercase. Rendered as the |
|
derived |
Connect consumer group. Empty derives
|
|
derived |
Empty derives the above plus
|
|
derived |
Empty derives the above plus
|
|
derived |
Empty derives the above plus
|
|
|
Annotations for the worker pods. A log
pipeline that reads container output has to be told the format, and
these workers write JSON: Fluent Bit takes it as a pod annotation, so a
cluster behind Fluent Bit wants |
|
|
Root log level. |
|
|
Levels for individual
loggers, as |
|
|
Seconds between checks of the log
config, so a change needs no restart. Strimzi copies the config into its
own ConfigMap first, so a change takes a few minutes to reach a running
worker. log4j2 reconfigures only when the file has changed, not on every
check, so a level set by hand via |
|
|
Turn on Vault integration at runtime.
When |
|
|
Vault server URL
(e.g. |
|
|
Vault auth method. Currently only
|
|
|
AppRole auth mount path in Vault. |
|
|
Vault Enterprise namespace. Required for on-prem customers running Vault Enterprise with namespaces; leave empty for Axual Cloud (Vault OSS). |
|
|
Optional Vault path the provider reads once at startup as a liveness check. Surfaces a broken Vault setup at startup instead of at first connector deploy. Leave empty to skip. |
|
|
Whether the provider verifies the Vault
server’s TLS certificate. Set |
|
|
Mode B (default, recommended):
name of a pre-existing Kubernetes Secret with |
|
|
Mode A (opt-in shortcut, matches AC’s
prod default): the worker AppRole role_id pasted inline. Rendered as a
direct env var value, so the secret_id appears in the rendered CR.
SOPS-encrypt the values file. Used only when |
|
|
Mode A (opt-in shortcut): the worker AppRole secret_id pasted inline. |
|
|
Optional Kubernetes Secret with
|
|
|
Run a superuser Helm hook |
|
|
Verify the broker TLS
hostname against the certificate SAN. Set |
|
|
Superuser auth for the Job. One of
|
|
|
K8s Secret with the
superuser cert + key (PEM). Required when |
|
|
Keys within that Secret holding the certificate and private key. |
|
|
Superuser username.
Required when |
|
|
K8s
Secret holding the superuser password. Required when
|
|
|
Key within that Secret holding the password. |
|
|
Principal the ACLs are granted to =
the worker identity. |
|
|
Optional extra principal (e.g. a “read all” distributor) that also receives the ACLs. |
|
|
Durability of the internal topics. Match your broker count. |
|
|
Partition count for the offsets topic. |
|
|
Partition count for the status topic. The config topic is always single-partition (a Connect requirement). |
|
|
|
|
|
Extra lines appended
verbatim to the admin client |
|
|
Job retry budget. |
|
|
Hard time limit for the Job in seconds. Kept below Helm’s default 300s hook timeout so the Job (not Helm) reports the failure first. |
|
|
Image for the Job. Always the
|
|
requests 100m/256Mi, limits 512Mi |
Pod resources for the Job. Modest defaults sized for the short-lived Kafka CLI. |
|
|
Pod-level security context for the Job. Hardened, restricted-PSS-friendly baseline. |
|
drop |
Container-level security context for the Job. |
|
|
Publish the REST API through a
route. Requires |
|
|
|
|
|
Hostname for the REST API. Required,
and the address to register in Self-Service as |
|
|
|
|
|
Annotations that are not about authentication. |
|
|
IngressClass name,
e.g. |
|
|
TLS Secret for the host
( |
|
|
Allow basic auth
over plain HTTP ( |
|
|
Gateways the route attaches to.
Required for |
|
|
Filters on the route rule
( |
|
|
Secret that already holds an htpasswd line. Takes precedence over the options below; the chart then creates nothing. |
|
|
htpasswd line
( |
|
|
Shape of the chart-created Secret. |
|
|
Annotations that switch authentication on, merged onto the route. Controller-specific, so the chart only carries them. Templated. |
|
|
Objects rendered verbatim, for
implementations whose authentication is a resource: an F5 NIC
|
|
|
Add a |
|
ns |
The pods that serve the route: an ingress controller, or a Gateway’s data plane. Required when the route is enabled. |
|
ns |
The Strimzi operator, which calls the REST API on every reconcile. Without it the cluster never reports Ready. |
|
|
Extra |
Worker log format
Workers write one JSON object per log entry, in the ECS layout, and the
format is not configurable. A Java error keeps its whole stack trace
inside one entry instead of arriving as ninety loose lines, and the
severity is a field rather than a word in a sentence, which is what lets
the log viewer filter by level. connector.context arrives as a field
too, so the viewer can tell which connector a line belongs to without
matching names in prose.
There is one layout on purpose. The Axual platform reads these logs, and a free-form pattern or a different JSON template would break the severity filter, the connector filter, or both, without any error to point at. The chart is the one place that could introduce that, so it does not offer it.
The cost is that kubectl logs and k9s show raw JSON. To read it by
hand:
kubectl -n <namespace> logs <pod> \
| jq -R -r '. as $l | try (fromjson | "\(.["@timestamp"]) \(.["log.level"]) \(.message)") catch $l'
The -R and the try ... catch matter. Strimzi’s startup scripts
write plain text before log4j2 takes over, so the output is mixed, and a
plain jq stops at the first plain line. This version prints those
lines unchanged. k9s users can get the same result with a plugin.
A log pipeline in front of the cluster has to be told the format too. For Fluent Bit that is a pod annotation:
podAnnotations:
fluentbit.io/parser: json
Worker authentication
The Connect worker authenticates to the broker using one of these
methods. The chart renders the value into spec.authentication of the
KafkaConnect CR; Strimzi handles the rest.
| Method | authentication.type |
Required fields |
|---|---|---|
mTLS (default) |
|
|
SASL/SCRAM-SHA-512 |
|
|
SASL/SCRAM-SHA-256 is not supported: Strimzi supports SCRAM-SHA-512
only (see the
Strimzi
security overview - its KafkaUser and listener authentication offer
only scram-sha-512). SASL/PLAIN (type: plain) is not supported -
Platform Manager does not support it. OAuth (type: oauth) is planned
for v2 and is not yet supported by this chart.
mTLS (default)
The chart ships with mTLS as the default. Replace the example Secret names for non-local deployments:
tls:
trustedCertificates:
- secretName: my-cluster-ca-cert
certificate: ca.crt
authentication:
type: tls
certificateAndKey:
secretName: connect-worker-cert
certificate: tls.crt
key: tls.key
SASL/SCRAM example
On Strimzi-managed Kafka, the SCRAM password is normally provisioned by
the Strimzi User Operator from a KafkaUser CR. The User Operator
creates a K8s Secret of the same name as the user, with the password
under the password key. The chart references that Secret in
passwordSecret.secretName.
Typical install order:
-
Apply a
KafkaUserCR for the worker:apiVersion: kafka.strimzi.io/v1beta2 kind: KafkaUser metadata: name: connect-worker namespace: kafka labels: strimzi.io/cluster: my-cluster spec: authentication: type: scram-sha-512 -
Wait for the Secret to appear:
kubectl wait kafkauser/connect-worker -n kafka --for=condition=Ready --timeout=120s -
Install the chart with SASL values:
helm upgrade --install axual-dta-kafka-connect oci://registry.axual.io/axual-charts/kafka-connect \ --version 0.7.0 \ --namespace kafka \ --set tenant=axual --set instance=dta --set clusterName=my-connect \ --set bootstrapServers.sasl=my-cluster-kafka-bootstrap:9094 \ --set authentication.type=scram-sha-512 \ --set authentication.username=connect-worker \ --set authentication.passwordSecret.secretName=connect-worker \ --set 'imagePullSecrets[0].name=regcred-harbor'
tls.trustedCertificates stays unchanged - the broker still uses TLS
for the SASL_SSL listener, just with SASL on top.
Per-connector authentication is separate
The worker’s auth (authentication.type) and a connector’s auth (the
*.override.* keys PM injects) are independent. A worker on
scram-sha-512 can host connectors that themselves authenticate to a
different listener with different credentials - the override mechanism
picks up producer.override.bootstrap.servers,
producer.override.security.protocol,
producer.override.sasl.jaas.config, and the matching
${vault:...} references PM puts in the connector config.
Internal topic names
Connect keeps its own state in three topics (config, offsets, status) and coordinates its workers through a consumer group. All four names default to being derived from the cluster identity:
groupId _<tenant>-<instance>-<clusterName>-connect configStorageTopic _<tenant>-<instance>-<clusterName>-connect-configs offsetStorageTopic _<tenant>-<instance>-<clusterName>-connect-offsets statusStorageTopic _<tenant>-<instance>-<clusterName>-connect-status
This matters because these topics live on a Kafka cluster that is
usually shared by many tenants. Two workers that share a group.id
and a config topic are one Connect cluster as far as Kafka Connect is
concerned: they elect a leader between them and distribute each other’s
connectors. Deriving the names from the identity keeps two clusters on
the same Kafka apart by construction.
The leading underscore marks them as platform-internal and keeps them
out of the governed <tenant>-<instance>-<environment>- space where
user topics live. tenant, instance and clusterName are
already required by the chart and are validated to be lowercase
DNS-style labels, so the derived names are always valid Kafka topic
names and stay well inside Kafka’s 249-character limit.
Set groupId, configStorageTopic, offsetStorageTopic or
statusStorageTopic to use a name verbatim, for example when adopting
state that already lives under other names. Renaming these topics loses
the worker’s connector configurations, offsets and status, so changing
tenant, instance, clusterName or any of the four values on a
running cluster is refused; see “Upgrade safety” below.
Upgrade safety
Before rendering the KafkaConnect resource, the chart looks up the
cluster’s live resource (if any) and fails the upgrade if any of the
four names from your values differs from what that cluster already runs
with:
Error: UPGRADE FAILED: execution error at (kafka-connect/templates/kafkaconnect.yaml:1:4): this cluster's configStorageTopic is "_axual-dta-mycluster-connect-configs" but these values give "_axual-dta-othercluster-connect-configs". Set the four names in your values file to what the cluster runs with, or the worker loses its connectors, offsets and status. See "Internal topic names" in the README.
Nothing is applied when this fires - the check runs during template rendering, before Helm talks to the API server, so a blocked upgrade changes nothing. Fix it by restoring the identity values, or by pinning the four names to what the cluster already uses, then upgrade again.
This is a best-effort check, not a guarantee. It does nothing on a fresh
install or under helm template / --dry-run, by design. It also
does nothing if the identity running helm upgrade cannot read
KafkaConnect resources - for example a scoped Argo CD service
account (see “How this looks in Argo CD”) - since a failed lookup and
a resource that does not exist look the same to the chart. Not changing
the identity of a running cluster is still the only guarantee.
Bootstrapping internal topics and ACLs
A Connect worker needs three internal topics (configs, offsets,
status) and ACLs on those topics plus its consumer group before it
can start. On an authorized cluster the worker’s own certificate
cannot create these ACLs: only a superuser can. Normally Platform
Manager / Self-Service provisions them. Where that is not the case,
enable aclBootstrap to have the chart do it, replacing the manual
kafka-acls work.
Strimzi does not allow a custom init container on the KafkaConnect
pod (discussion
#6853), so instead of an init container the chart runs a
pre-install / pre-upgrade Helm hook Job. The Job
authenticates as a superuser and:
-
creates the three internal topics (compacted, with your replication factor /
min.insync.replicas); -
grants the worker principal(s)
Read/Write/Describe/DescribeConfigson those topics; -
grants
Read/Describeon the consumer group (groupId).
Being a pre-upgrade hook, it runs on every helm upgrade (a
fresh -acl-bootstrap Job and one broker round-trip each time), even
when nothing changed. It is idempotent (--create --if-not-exists,
ACL --add), so re-running is safe: expect a new Job to appear on
each upgrade. Its hook-delete-policy is before-hook-creation, so
the completed Job and its pod are kept (removed only just before the
next run, so the logs stay readable). As Helm hook resources the release
does not track, the -acl-bootstrap Job and its ConfigMap are not
removed by aclBootstrap.enabled: false or by helm uninstall —
delete them by hand in either case.
It reuses tls.trustedCertificates, the same derived
*StorageTopic names and groupId as the worker (see “Internal
topic names” below), and picks its bootstrap from bootstrapServers
by aclBootstrap.auth.type. It verifies the broker TLS hostname by
default; set aclBootstrap.verifyHostname: false only if the broker
cert SAN does not include the bootstrap address.
The Job’s bootstrap follows its auth type, independently of the
worker. So when the worker uses SASL_SSL but you already have an mTLS
superuser cert, set aclBootstrap.auth.type: tls and the Job
automatically uses bootstrapServers.tls (the mTLS listener): no need
to create a SASL superuser or edit the broker superUsers (which
would force a restart).
mTLS superuser
aclBootstrap:
enabled: true
auth:
type: tls
tls:
secretName: kafka-superuser # Secret with the superuser cert + key (PEM)
certificate: tls.crt
key: tls.key
principal: "User:CN=connect-worker,O=Axual B.V.,C=NL" # the worker cert DN
replicationFactor: 3
minInsyncReplicas: 2
SASL/SCRAM superuser
aclBootstrap:
enabled: true
auth:
type: scram-sha-512
scram:
username: admin
passwordSecret:
secretName: kafka-superuser
password: password # key within the Secret
principal: "User:connect-worker" # the worker SCRAM username
Logs
The Job logs timestamped, step-numbered progress (topics created, ACLs granted) and prints the resulting ACLs at the end: no secrets.
The topics and ACLs it created are broker-side state Helm never manages,
so they remain even after you disable aclBootstrap or
helm uninstall the chart. Read the logs any time:
kubectl -n <namespace> logs job/<fullname>-acl-bootstrap
<fullname> is the release name (or fullnameOverride).
helm install/upgrade blocks on this hook but does not stream
its logs: tail it from a second terminal
(kubectl -n <namespace> logs -f job/<fullname>-acl-bootstrap) to
watch it live.
REST API access
The Connect REST API is plain HTTP on port 8083 behind a ClusterIP
Service, and Kafka Connect asks for no password. Its own Basic Auth
needs rest.extension.classes, which Strimzi refuses to pass through
(Configuration option "rest.extension.classes" is forbidden and will be ignored),
so the check cannot live inside the worker. Two values put it back:
-
restApi.routepublishes the REST API on one hostname, as anIngressor anHTTPRoute. Whatever serves that route is what asks for the password. -
restApi.networkPolicyshuts every other way to port 8083. A route only sees traffic that chooses to go through it: without the policy a pod inside the cluster calls the Service directly and never meets the password.
Neither half works alone, so the chart refuses to render a route while the policy is off.
Setting it up
-
Pick what fronts the API. A private class or Gateway keeps it inside the cluster; a public one lets another cluster reach it. This is the only thing that decides public or private.
-
Make the credential.
htpasswd -nbB connect '<password>'prints the line. Put it in the SOPS encrypted values asrestApi.basicAuth.htpasswd, or create the Secret yourself and name it inrestApi.basicAuth.existingSecret. The chart never sees a plain password. -
Copy the block for your implementation from below, and set
route.hostplusroute.authMethod. -
Find the router for the NetworkPolicy, because a guess here blocks the route:
kubectl get pod -A -l app.kubernetes.io/name=ingress-nginx --show-labels # ingress controller kubectl get pod -n <gateway namespace> --show-labels # Gateway data plane -
Install, then prove both halves. Through the route, and directly at the Service from any other pod:
curl -o /dev/null -w '%{http_code}\n' https://<host>/connectors # 401 curl -o /dev/null -w '%{http_code}\n' -u connect:<password> https://<host>/connectors # 200 kubectl run probe --rm -it --image=curlimages/curl --restart=Never -- \ curl -m 5 http://<release>-connect-api.<namespace>.svc:8083/connectors # times outA 200 on the first line means nothing is checking the password. A reply on the last one means the policy is not enforced, and the route is decoration.
-
Register the cluster in Self-Service with
connectUrlset to the route address and the auth method matchingroute.authMethod, then put the same password in the Governance Vault underconnect-api-password.
The chart does not know your ingress implementation
Every implementation wires authentication differently, and not just by
annotation name: ingress-nginx takes annotations, the F5 NGINX Ingress
Controller takes a Policy object plus an annotation and insists on
its own Secret type, and Gateway API has no authentication at all, so
NGINX Gateway Fabric attaches an AuthenticationFilter to the route
rule. The chart therefore renders three things only: the route, the
credentials Secret, and any objects you hand it. The wiring is values.
Because of that, authMethod: basic is refused unless something
actually carries the check: restApi.basicAuth.annotations,
restApi.route.filters or restApi.extraObjects. A route that
claims to be protected and is not is worse than an open one.
That check reaches as far as the chart can see, and no further: it
catches an empty wiring, never a wrong one. Annotations for the
controller you are not running, a filter naming a CRD that is not
installed, a Secret name with a typo in it, all render and all leave the
route open. Only the live route settles it, so helm install prints
the unauthenticated curl to run against it. Expect 401, and
treat anything else as an open REST API.
The three blocks below are the fixtures CI renders on every change,
under ci/rest-api-*-values.yaml. All of them share:
restApi:
route:
enabled: true
host: connect-<cluster>.<env>.axual.cloud
authMethod: basic # or none, matching Platform Manager's NO_AUTH
basicAuth:
existingSecret: connect-basic-auth
networkPolicy:
enabled: true
community ingress-nginx
Annotations on the Ingress, reading an Opaque Secret with the key
auth (the chart defaults).
restApi:
route:
kind: Ingress
className: nginx-private
tlsSecret: axual-cloud-cluster-wildcard-tls
basicAuth:
annotations:
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: '{{ include "kafka-connect.restApi.authSecretName" . }}'
nginx.ingress.kubernetes.io/auth-realm: "Kafka Connect REST API"
networkPolicy:
router:
namespace: ingress
podLabels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/component: controller
F5 NGINX Ingress Controller
A Policy object referenced by the nginx.org/policies annotation.
It rejects any Secret that is not of type nginx.org/htpasswd with
the content under the key htpasswd, which is why both are values.
extraObjects entries and basicAuth.annotations are templated, so
both can name the chart’s own Secret instead of repeating it by hand.
restApi:
route:
kind: Ingress
className: f5-nginx-public
tlsSecret: axual-cloud-cluster-wildcard-tls
basicAuth:
secretType: nginx.org/htpasswd
secretKey: htpasswd
annotations:
nginx.org/policies: connect-api-basic-auth
extraObjects:
- apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
name: connect-api-basic-auth
spec:
basicAuth:
secret: '{{ include "kafka-connect.restApi.authSecretName" . }}'
realm: Kafka Connect REST API
networkPolicy:
router:
namespace: f5-nginx-ingress
podLabels:
app.kubernetes.io/name: nginx-ingress
Policies must live in the same namespace as the resource that references
them, so this one is part of the release. Check your controller version
supports policies on Ingress resources rather than only on
VirtualServer; if it does not, put the same Policy behind a
VirtualServer in extraObjects and leave route.enabled off.
NGINX Gateway Fabric (Gateway API)
An AuthenticationFilter attached to the rule through an
ExtensionRef filter. The Secret is Opaque with the key auth,
so the chart defaults apply. TLS belongs to the Gateway listener, not to
this route, so tlsSecret is unused here.
restApi:
route:
kind: HTTPRoute
parentRefs:
- name: nginx-cluster-private
namespace: infra-gateways
filters:
- type: ExtensionRef
extensionRef:
group: gateway.nginx.org
kind: AuthenticationFilter
name: connect-api-basic-auth
extraObjects:
- apiVersion: gateway.nginx.org/v1alpha1
kind: AuthenticationFilter
metadata:
name: connect-api-basic-auth
spec:
type: Basic
basic:
realm: Kafka Connect REST API
secretRef:
name: '{{ include "kafka-connect.restApi.authSecretName" . }}'
networkPolicy:
router:
namespace: infra-gateways
podLabels:
gateway.networking.k8s.io/gateway-name: nginx-cluster-private
A Gateway in another namespace has to allow this one, either through the
listener’s allowedRoutes or a ReferenceGrant.
Attach straight to the Gateway when its listener already covers the
hostname and accepts routes from this namespace. When the cluster needs
its own hostname or certificate, and the Gateway sets
allowedListeners, bring a ListenerSet instead and point
parentRefs at that: it is one more entry in extraObjects,
nothing new in the chart.
restApi:
route:
kind: HTTPRoute
parentRefs:
- group: gateway.networking.k8s.io
kind: ListenerSet
name: connect-api
extraObjects:
- apiVersion: gateway.networking.k8s.io/v1
kind: ListenerSet
metadata:
name: connect-api
spec:
parentRef:
name: nginx-cluster-private
namespace: infra-gateways
listeners:
- name: https
port: 443
protocol: HTTPS
hostname: connect-<cluster>.<env>.axual.cloud
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: axual-cloud-cluster-wildcard-tls
Envoy Gateway follows the same shape with a SecurityPolicy carrying
basicAuth.users, and Traefik with a Middleware: same three
values, different objects.
Public routes and other clusters
Public or private is not a chart setting, it is which class or Gateway
you name: f5-nginx-public instead of nginx-private, or a Gateway
on a public address. That is all a cluster somewhere else needs to reach
this REST API, and the NetworkPolicy stays correct either way,
because the traffic still arrives through the router pods.
Two things change once the address is public. Basic auth over plain HTTP
now leaks the password outside the cluster, so the chart refuses
kind: Ingress with authMethod: basic and no tlsSecret unless
you set allowInsecureBasicAuth: true; for kind: HTTPRoute the
TLS lives on the Gateway listener, where the chart cannot see it, so
check it yourself. And a password is the only thing standing in front of
the API, so narrow the source range at the router if you can:
nginx.ingress.kubernetes.io/whitelist-source-range for
ingress-nginx, an AccessControl Policy for F5 NIC, both of which go
in the values you already pass.
Credentials
Two ways to give the route an htpasswd entry, existingSecret first
if both are set:
| Value | Result |
|---|---|
|
The chart only references it. Matching
|
|
The chart renders its own Secret from that
line. Generate it with |
The chart takes a hash and never a plain password. bcrypt picks a new salt every time it runs, so hashing at render time would rewrite the Secret on every sync and leave Argo CD permanently out of sync with itself.
Registering the cluster
Platform Manager already sends Authorization: Basic on every call
once a cluster is registered with BASIC_AUTH, so no code change is
needed. Register connectUrl as the route address rather than the
internal Service, and store the same password in the Governance Vault
under connect-api-password. The password now lives in two places:
change one without the other and Self-Service stops working.
What the policy allows
The rendered policy allows the same-cluster Connect pods (inter-worker
REST forwarding), the Strimzi operator, and the router. Everything else,
Platform Manager included, goes through the route. Add peers of your own
with allowedFrom, preferring a podSelector over a whole
namespace. Log streaming reads pod logs through the Kubernetes API and
never touches 8083.
Without a route, allowedFrom is required. The operator is allowed by
default but calls nothing on your behalf, so a policy carrying only that
default locks out Platform Manager instead of defending anything, and
the chart refuses it.
Find the router’s real namespace and labels rather than trusting an
example: for an ingress controller
kubectl get pod -A -l app.kubernetes.io/name=ingress-nginx --show-labels,
for a Gateway kubectl get pod -n <gateway namespace> --show-labels.
A wrong selector leaks nothing, it blocks the route instead.
The policy is only enforced on a CNI that supports NetworkPolicy (kind’s
kindnet does not). Cilium and Azure CNI let the kubelet health probe
through by default; on a CNI that does not, the workers never turn Ready
until the node range is added to allowedFrom.
Running the workers on a different container runtime
Some clusters run sensitive workloads in a sandboxed runtime instead of
the shared host kernel. On AKS that is Pod Sandboxing, where a pod asks
for it with runtimeClassName: kata-vm-isolation. (The older name
kata-mshv-vm-isolation still works but is on its way out.)
Two values turn this on: runtimeClassName picks the runtime, and
affinity sends the pods to nodes that provide it. Both are needed.
runtimeClassName: kata-vm-isolation
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.azure.com/kata-vm-isolation
operator: In
values: ["true"]
Use the label your own kata node pool carries.
kubectl get runtimeclass kata-vm-isolation -o yaml and
kubectl get nodes --show-labels will show you which one that is. Add
tolerations too if the node pool is tainted.
How the runtime class reaches the pod
Strimzi’s KafkaConnect pod template exposes a fixed list of pod
fields, and runtimeClassName is not one of them, so the chart
applies it at admission time instead. Setting runtimeClassName
renders a namespaced Kyverno Policy that matches the pods Strimzi
creates for this cluster, by strimzi.io/kind: KafkaConnect and
strimzi.io/cluster: <fullname>, and sets the field on them.
This means Kyverno must be installed in the cluster. The policy keeps
Kyverno’s default failurePolicy: Fail, so while Kyverno is
unavailable new worker pods are refused rather than started on the
shared kernel.
It also explains why affinity is part of the setup. A RuntimeClass
can carry its own scheduling.nodeSelector, which Kubernetes copies
onto any pod that asks for that runtime. The admission plugin that does
the copying runs ahead of webhooks, so it sees the pod before Kyverno
adds the field, and the pod keeps whatever placement it already had.
Setting affinity here gives the pods that placement directly.
Rolling it out
The policy applies to pods created from that point on. Existing workers keep their current runtime until they are replaced, so roll them once the policy is in place:
kubectl annotate -n <namespace> strimzipodset <fullname>-connect strimzi.io/manual-rolling-update=true
Then confirm the field landed:
kubectl get pod -n <namespace> -l strimzi.io/cluster=<fullname> \
-o jsonpath='{.items[*].spec.runtimeClassName}'
The ACL bootstrap Job keeps the node’s normal runtime. Helm creates that
pod, not Strimzi, so it carries none of the strimzi.io/* labels the
policy matches on.
How this looks in Argo CD
Argo CD compares the resources rendered from the Application source
against their live state. For this release that is the KafkaConnect
resource, its ConfigMaps, and the Policy itself, all of which come
from the chart and therefore from git. The worker pods are created at
runtime by a StrimziPodSet and are not part of that set, so the
mutation happens outside what Argo tracks and sync status is unaffected.
With runtimeClassName empty, the default, the chart renders no
policy at all.
The runtimeClassName value on the pod is the one thing no manifest
holds, because Strimzi has no field for it. That is a property of the
feature rather than of how the chart ships it: any way of setting this
field today happens at admission time.
If your platform team owns the Kyverno policies
Some setups keep every Kyverno policy in the cluster bootstrap repo
rather than in application charts. The policy is then owned by a
different Argo Application, still from git. To do it that way, leave
runtimeClassName empty so the chart renders nothing, keep
affinity set here, and hand this to whoever owns that repo:
apiVersion: kyverno.io/v1
kind: Policy
metadata:
name: kafka-connect-runtime-class
namespace: <the Connect namespace>
annotations:
pod-policies.kyverno.io/autogen-controllers: none
spec:
failurePolicy: Fail
rules:
- name: set-runtime-class
match:
any:
- resources:
kinds:
- Pod
operations:
- CREATE
selector:
matchLabels:
strimzi.io/kind: KafkaConnect
strimzi.io/cluster: <the KafkaConnect resource name>
mutate:
patchStrategicMerge:
spec:
runtimeClassName: kata-vm-isolation
Everything above about affinity, rolling the existing pods, and
verifying still applies.
Vault integration
The chart can be configured to resolve ${vault:...} placeholders in
connector configs at task startup. This is used by Platform Manager to
give each connector its own per-connector identity (mTLS PEM or SASL
username/password) stored in Vault. Disabled by default; turn it on per
deployment.
How it works at runtime
-
PM writes the connector’s credentials to Vault. There is one shared engine,
connector-auth/. Under each cluster’s path there is atls/sub-path for certificates and asasl/sub-path for passwords. The path starts with the tenant (paths use{cluster_name}, not{cluster_id}), so one AppRole policy per cluster (connector-auth/data/{tenant}/{instance}/{cluster_name}/*) covers both sub-paths:-
For mTLS connectors:
connector-auth/{tenant}/{instance}/{cluster_name}/tls/{env}/{app}holdingprivate.keyandcertificate.chain(PEM). -
For SASL connectors:
connector-auth/{tenant}/{instance}/{cluster_name}/sasl/{env}/{app}/{credential_type}holdingsasl.usernameandsasl.password.
-
-
PM sends a connector config to Connect with
${vault:...}placeholders in the*.override.*keys:-
mTLS: in
consumer.override.ssl.keystore.key/consumer.override.ssl.keystore.certificate.chain(and the same forproducer.override.*andadmin.override.*). -
SASL: inside
consumer.override.sasl.jaas.configasusername="${vault:...}"andpassword="${vault:...}".
-
-
When the connector task starts, the worker’s
VaultConfigProviderauthenticates to Vault using the AppRole credentials, reads the values, and substitutes them into the connector’s Kafka client config. No JKS file is ever created. The same provider resolves placeholders inside JAAS strings the same way it resolves them at the top of a value.
The chart’s role is to make the worker capable of step 3. It does NOT
write certs to Vault and does NOT inject ${vault:...} references
into connector configs - PM does both of those at connector deploy time.
Operator setup
The chart’s CI always builds a Connect base image that contains
vault-config-provider in /opt/kafka/libs/ (see
Dockerfile.connect-base). spec.image always points at this base
image in imageVolumes mode. The version + sha512 of the provider
live as `ARG`s in the Dockerfile - bump them together to upgrade.
vault-config-provider is on the worker classpath this way because
Kafka loads ConfigProvider classes at worker startup, before plugin
scanning runs - so they cannot be delivered via plugin.path /
spec.plugins.
Whether the vault:// placeholder resolution actually happens at task
startup is controlled separately by vault.enabled at runtime. When
vault.enabled: true, the chart:
-
Renders the worker AppRole role_id + secret_id as env vars on the Connect container (
spec.template.connectContainer.env). Mode B (default, recommended) renders them viavalueFrom.secretKeyRefagainstvault.credentialsSecret; Mode A (opt-in shortcut) takes them fromvault.approleRoleId+vault.approleSecretIdchart values. -
Sets
config.providers = "env,vault"and uses${env:...}to feed the env vars intoVaultConfigProvider’s params. This matches AC’s chart pattern (AC renders the same env vars and the Confluent base image’s env-to-property convention picks them up; KC usesEnvVarConfigProviderto reach the same end state under Strimzi).
You do not need to build a custom image or manage a Dockerfile.
pluginDelivery: imageVolumes (the default) continues to work for
connectors / transforms / converters.
Mode B (default, recommended): use a Kubernetes Secret. The secret_id
stays in the Secret and never appears in the rendered KafkaConnect
CR.
kubectl create secret generic kc-dta-vault-creds \
--namespace kafka \
--from-literal=VAULT_APPROLE_ROLE_ID=<connect-role-id> \
--from-literal=VAULT_APPROLE_SECRET_ID=<connect-secret-id>
helm upgrade --install axual-dta-kafka-connect oci://registry.axual.io/axual-charts/kafka-connect \
--version 0.7.0 \
--namespace kafka \
--set vault.enabled=true \
--set vault.address=https://vault.example.com:8200 \
--set vault.credentialsSecret=kc-dta-vault-creds \
--set 'imagePullSecrets[0].name=regcred-harbor'
Mode A (opt-in shortcut, matches AC’s prod default): paste the values inline. Simpler, but the secret_id ends up in plain text in the rendered CR, so SOPS-encrypt the values file in production - same operational pattern as AC.
helm upgrade --install axual-dta-kafka-connect oci://registry.axual.io/axual-charts/kafka-connect \
--version 0.7.0 \
--namespace kafka \
--set vault.enabled=true \
--set vault.address=https://vault.example.com:8200 \
--set vault.approleRoleId=<connect-role-id> \
--set vault.approleSecretId=<connect-secret-id> \
--set 'imagePullSecrets[0].name=regcred-harbor'
If your Vault uses HTTPS with a private CA, also create a Secret
containing truststore.p12 and truststore.password keys, then
pass --set vault.truststoreSecret=vault-ca-truststore. This is a
separate Secret from the AppRole credentials on purpose: the
truststore.p12 file must be mounted into the pod as a file, so the
chart keeps the file and its password together in their own Secret.
Adding a plugin
-
Create
plugins/<name>.yaml. Most artifacts need aurl:ormaven:line and itssha512::# plugins/debezium-oracle.yaml url: https://repo1.maven.org/maven2/io/debezium/debezium-connector-oracle/3.5.1.Final/debezium-connector-oracle-3.5.1.Final-plugin.tar.gz sha512: 48dec1b01b7cdafc6c17103e476e0be522dd1e9414607e5b11f0ee9923d6400ee1af076d219d112d6dcca42d520e3a542300319c4fde4847f08ab54682409db3The plugin name is the filename without
.yaml. The version comes from the URL path, or from the third field ofmaven: group:artifact:version.sha512:is mandatory and is never fetched for you; compute it withcurl -sL <url> | shasum -a 512 | awk '{print $1}'.A tarball whose files sit at the top level, with no wrapping directory, needs the longer
sources:form instead.url:always unpacks with--strip-components=1, which for that shape discards every entry and publishes an empty image without failing. Check withtar -tzf <file> | headfirst.# plugins/apicurio-converter.yaml version: 2.6.13.Final sources: - kind: tarball-url url: https://repo1.maven.org/maven2/io/apicurio/apicurio-registry-distro-connect-converter/2.6.13.Final/apicurio-registry-distro-connect-converter-2.6.13.Final.tar.gz sha512: f53074cabb911f893e06fc70410d561beff6487b831601210fdff48807e6878beb62525c9986c19b97158c32ca93056753697d794f5fc668862de50fb1d22b8f strip-components: 0version:is required in this form: nothing can derive it fromsources:, and without it the image tag and the chart’s fallback reference are both empty. Every field:plugins/_schema.json.A connector published as a release bundle, a
.zipor a plain.tarthe way Lenses ships stream-reactor, stays on the shorturl:form. The bundle is unpacked and every jar inside it is copied flat into the plugin dir, wherever it sat, because that is the layoutplugin.pathexpects, andLICENSE/NOTICEfiles come with it. There is nostrip-components:to choose. The build fails if the bundle holds no jar, or if two jars in it share a filename.# plugins/stream-reactor-ftp.yaml # renovate: datasource=github-releases depName=lensesio/stream-reactor versioning=semver url: https://github.com/lensesio/stream-reactor/releases/download/7.3.2/kafka-connect-ftp-7.3.2.zip sha512: 422f7289d309dca15844c21c2e30713ba2412f169950190fb35868df4f7a01c4cad7b8ecca1085435565c5591035591f513486f6906ff9e5a57eaa46170c1be1plugin_spec_smokeruns on the merge request and does two things: it checks every spec againstplugins/_schema.json, and it tests the bundle extraction against archives it builds itself. Both run locally, with no network:python3 ci/validate-plugin-specs.py plugins/_schema.json plugins/*.yaml bash ci/test-extract-bundle-jars.sh -
Add the name to
pluginsinvalues.yaml:plugins: - kafka-topic-name-transforms - debezium-oracle -
Push to
main. CI detects the changed spec file and builds the plugin OCI image automatically. -
Deploy:
helm upgrade. The chart resolves the name to its image reference and Strimzi mounts it.
Upgrading Strimzi or Kafka
The version appears in six places and they have to move together. Do it all in one commit.
| # | File | What to change |
|---|---|---|
1 |
|
|
2 |
|
|
3 |
|
the |
4 |
|
the same entry, and any other profile you add later |
5 |
|
run |
6 |
|
bump |
Then check you got them all:
./ci/check-version-consistency.py
Why each one matters:
-
1 and 3/4 are read by different things. The chart builds the worker image tag from
values.yaml; CI builds and pushes images fromprofiles/*.yaml. Change one and not the other and the chart points at a tag CI never built, which shows up asImagePullBackOffat deploy time rather than as a failing pipeline. Thecheck_version_consistencyjob compares them, along withprebuiltConnectImageand the vendored CRD, so this fails the MR instead. -
6 is enforced.
check_version_bumped_when_image_changesfails the MR ifprofiles/changed without a chart version bump, because the image tag contains the chart version and the publish jobs skip a tag that already exists. Without the bump the rebuilt image never ships. -
5 is enforced.
helm_crd_validationfails ifci/crds/kafkaconnect-<strimziVersion>.yamlis missing, and prints the exactcurlto run.
After merging to main, the publish jobs build the new base and
profile images. Until they finish, a deploy from the branch points at a
tag that does not exist yet; set connectBaseImage to an existing
image if you need to test before then.
Two things worth checking by hand, because no job can:
-
Read the Strimzi release notes for changes to the
KafkaConnectresource.helm_crd_validationcatches a field the new CRD rejects, but not a field whose meaning changed. -
Confirm the new Strimzi supports the
kafkaVersionyou set. Strimzi supports a small range per release.
What’s inside
| Path | Purpose |
|---|---|
|
One spec file per plugin - source URL/coordinates and optional digest |
|
Named plugin combinations for prebuilt image mode |
|
The |
|
Optional Kyverno |
|
Optional |
|
Chart-owned htpasswd Secret, unless an existing one is named |
|
Auth objects your implementation needs, rendered verbatim |
|
Optional |
|
Naming, label, and plugin resolution helpers |
|
All configuration with inline documentation |
Related
-
SMT plugin:
kafka-topic-name-transforms
Reference Helm VALUES.YAML for Kafka Connect
# Default values for the kafka-connect chart.
# Templates a Strimzi KafkaConnect custom resource; the Strimzi operator runs the pods.
# -- Override the chart name used to build the release name. Rarely needed.
# nameOverride: ""
# -- Override the KafkaConnect CR's name entirely (and so every Strimzi-derived resource: pods,
# the -connect-api Service). Without it the name follows the Helm release name. Pin this when a
# GitOps tool's release name would otherwise differ from an already-running workload's name and
# rename/orphan it.
# fullnameOverride: ""
# -- Kafka version for the Connect workers. Must be supported by the installed Strimzi operator.
kafkaVersion: "4.1.1"
# -- Number of Connect worker pods.
replicas: 1
# -- Kafka broker bootstrap per listener/auth mechanism. The worker selects by authentication.type,
# the ACL-bootstrap Job by aclBootstrap.auth.type; set only the one(s) you use. See README.
bootstrapServers:
# -- Bootstrap for the TLS/mTLS listener (used when auth type is "tls").
tls: ""
# -- Bootstrap for the SASL_SSL listener (used when auth type is "scram-sha-512").
sasl: ""
# Broker TLS trust. Required for any TLS listener (mTLS or SASL_SSL).
tls:
# -- CA(s) that verify the broker's server cert. Example entry:
# - secretName: my-cluster-ca-cert
# certificate: ca.crt
trustedCertificates: []
# Worker authentication to the broker. Rendered into spec.authentication.
# Supported types: tls (mTLS), scram-sha-512. SASL/SCRAM-SHA-256 is not supported (Strimzi
# supports SCRAM-SHA-512 only); SASL/PLAIN and OAuth are not supported either.
authentication:
type: tls
# For type: tls - a K8s Secret holding the worker's client cert.
certificateAndKey:
secretName: ""
certificate: ""
key: ""
# For type: scram-sha-512:
# username: ""
# passwordSecret:
# secretName: ""
# password: password
# -- Plugin delivery mode: "imageVolumes" (default; per-plugin OCI images, Strimzi >=0.47 + K8s >=1.31)
# or "prebuiltImage" (a full Connect image with plugins baked in).
pluginDelivery: imageVolumes
# -- Strimzi operator version. Used to build the Connect base image tag.
strimziVersion: "0.51.0"
# -- Full Connect image with plugins baked in. Used only when pluginDelivery=prebuiltImage
# and prebuiltImageProfile is unset.
prebuiltConnectImage: "registry.axual.io/internal/axual/kafka-connect-with-smt:0.0.8-strimzi-0.51.0-kafka-4.1.1"
# -- Registry+repo the profile image is built from:
# <prebuiltImageRegistry>/<profile>:<chartVersion>-strimzi-<strimziVersion>-kafka-<kafkaVersion>.
# A value like every other image path, so a mirrored or air-gapped install can redirect it.
prebuiltImageRegistry: "registry.axual.io/internal/axual/kafka-connect"
# -- Profile name for pluginDelivery=prebuiltImage. Takes precedence over prebuiltConnectImage.
prebuiltImageProfile: default
# -- Registry prefix for plugin images (imageVolumes mode): <pluginRegistry>/<name>:<version>.
pluginRegistry: "registry.axual.io/internal/axual/connect-plugins"
# -- Registry+repo for the Connect base image: the worker image in imageVolumes mode, and the
# ACL-bootstrap Job image in every mode. Plain Strimzi plus vault-config-provider baked into /opt/kafka/libs/.
connectBaseImageRegistry: "registry.axual.io/internal/axual/kafka-connect-base"
# -- Full override for the Connect base image. Sets the worker image in imageVolumes mode and the
# ACL-bootstrap Job image in every mode (in prebuiltImage mode it controls the Job, not the worker).
# Empty => computed from chart/strimzi/kafka versions. Pin an image when the CI-built one is missing. See README.
connectBaseImage: ""
# -- Plugins to mount (imageVolumes mode). Empty by default; add the plugins this deployment
# needs. String entries resolve against plugins/<name>.yaml for version and digest.
plugins: []
# -- Connect's group id and its three internal topics (config, offsets, status).
#
# Leave empty and they are derived as `_<tenant>-<instance>-<clusterName>-connect[-configs|-offsets|
# -status]`. That matters because these topics live on a Kafka cluster shared by many tenants: two
# workers sharing a group id and a config topic are one Connect cluster as far as Kafka Connect is
# concerned, so a fixed default would make two customers on one Kafka cluster either merge into a
# single Connect cluster or fail to start, depending on the ACLs.
#
# Set a value to use it verbatim. Do that only when adopting a cluster that already holds state under
# different names, because renaming these topics loses the worker's connector configurations, offsets
# and status.
groupId: ""
offsetStorageTopic: ""
configStorageTopic: ""
statusStorageTopic: ""
# -- Cluster identity. All three are required. They become the pod labels axual.io/tenant,
# axual.io/instance and axual.io/connect-cluster, which is how the log viewer finds this cluster's
# pods. Use the Self-Service names in lowercase: at most 63 characters, starting and ending with a
# letter or digit.
tenant: ""
instance: ""
clusterName: ""
# -- Annotations for the worker pods. Empty by default.
#
# A log pipeline that reads container output has to be told the format, and these workers write JSON.
# Fluent Bit takes it as a pod annotation, so a cluster behind Fluent Bit wants
# `fluentbit.io/parser: json`. The chart does not set it for you, because which pipeline is in front
# of the cluster is not the chart's to know.
podAnnotations: {}
# -- Worker log configuration, rendered into a log4j2 config for Strimzi.
#
# Entries are written as ECS JSON, one object per entry, and the format is not configurable. Strimzi's
# own default is plain text and its inline logging can only set levels, so the chart hands it a whole
# file. The platform reads these logs, and it reads one format: a Java error keeps its whole stack
# trace inside one entry, and the severity is a field rather than a word in a sentence, which is what
# lets the log viewer filter by level.
#
# `kubectl logs` and k9s therefore show raw JSON. To read it by hand, see "Worker log format" in the
# README for a jq recipe, or use a k9s plugin.
logging:
# -- Root log level.
level: "INFO"
# -- Levels for individual loggers, as name: level. Each one writes to the same console appender
# as the root logger, so raising a level here really does produce more output. Any logger name is
# fine: the log4j2 property key is generated, not derived from the name.
loggers:
org.reflections: "ERROR"
org.apache.kafka.connect.runtime.rest.RestServer: "WARN"
# -- Seconds between checks of the log config, so a change needs no restart. Strimzi copies this
# ConfigMap into its own first and the worker mounts that copy, so a change takes a few minutes to
# reach a running worker.
#
# log4j2 reconfigures only when the file has changed, not on every check, so a level raised by hand
# through Connect's REST API (PUT /admin/loggers/<name>) is not reverted on this timer. It lasts
# until the config really changes, or until the pod restarts.
#
# Setting 0 turns the check off, which also means a config change then needs a pod restart.
monitorInterval: 30
# -- Worker-level Connect configuration. Do not add plugin.path (Strimzi manages it).
config:
offset.storage.replication.factor: 1
config.storage.replication.factor: 1
status.storage.replication.factor: 1
key.converter: org.apache.kafka.connect.json.JsonConverter
value.converter: org.apache.kafka.connect.json.JsonConverter
key.converter.schemas.enable: false
value.converter.schemas.enable: false
connector.client.config.override.policy: All
# Worker Vault integration. vault-config-provider is always on the worker classpath; this only
# wires it up at runtime when enabled=true.
vault:
# -- Turn on Vault integration.
enabled: false
# -- Vault server address.
address: ""
# -- Auth method. APPROLE is currently the only supported value.
authMethod: APPROLE
# -- AppRole auth mount path.
approlePath: approle
# -- Vault Enterprise namespace (leave empty for OSS).
namespace: ""
# -- Optional Vault path read once at startup as a liveness check.
testPath: ""
# -- Verify the Vault server's TLS certificate.
sslVerify: true
# -- Recommended: name of a pre-existing K8s Secret with keys VAULT_APPROLE_ROLE_ID and
# VAULT_APPROLE_SECRET_ID. When set, the credential stays in the Secret, not in the CR.
credentialsSecret: ""
# -- Inline AppRole credentials (used only when credentialsSecret is empty). These end up in the
# KafkaConnect resource in plain text, readable by anyone who can get kafkaconnects in the
# namespace, which SOPS on the values file does not change. Prefer credentialsSecret.
approleRoleId: ""
approleSecretId: ""
# -- Optional K8s Secret with the Vault CA truststore (keys: truststore.p12, truststore.password),
# mounted at /mnt/vault-truststore/. Required when Vault uses HTTPS with a private CA.
truststoreSecret: ""
# Optional superuser Helm-hook Job that creates Connect's internal topics and grants the worker
# the required ACLs (never Create). Off by default; enable only where Self-Service does not
# provision them. See README "Bootstrapping internal topics and ACLs".
aclBootstrap:
# -- Turn the bootstrap Job on.
enabled: false
# -- Verify the broker TLS hostname against the certificate SAN. Set false only when the broker
# cert SAN does not include the bootstrap address (e.g. an internal Service name or localhost).
verifyHostname: true
auth:
# -- Superuser identity that creates the topics + ACLs. Must be a Kafka superuser: the worker's
# own certificate cannot grant its own ACLs. One of: tls, scram-sha-512.
type: tls
# For type: tls - a K8s Secret with the superuser cert + key (PEM).
tls:
secretName: ""
certificate: tls.crt
key: tls.key
# For type: scram-sha-512 - superuser username + a Secret holding its password.
scram:
username: ""
passwordSecret:
secretName: ""
password: password
# -- Principal that receives the ACLs = the Connect worker identity.
# For tls: "User:<worker cert DN>". For scram: "User:<worker username>".
principal: ""
# -- Optional extra principal (e.g. a "read all" distributor) that also receives the ACLs.
distributionPrincipal: ""
# -- Replication factor for the internal topics. Match your broker count.
replicationFactor: 1
# -- min.insync.replicas for the internal topics. Must be <= replicationFactor.
minInsyncReplicas: 1
# -- Partition counts. The config topic is always single-partition (a Connect requirement).
offsetTopicPartitions: 25
statusTopicPartitions: 5
# -- segment.ms for the (compacted) internal topics; controls how often log compaction runs.
segmentMs: 3600000
# -- Extra lines appended verbatim to the admin client .properties (e.g. retries or extra
# security settings). Request timeouts are already set by the chart.
additionalClientConfigs: []
# -- Job retry budget.
backoffLimit: 3
# -- Hard time limit for the Job in seconds. A stuck run fails here; kept below Helm's default
# 300s hook timeout so the Job (not Helm) reports the failure first.
activeDeadlineSeconds: 240
# -- Image for the Job. Always the imageVolumes Connect base image (ships the Kafka CLI),
# regardless of pluginDelivery - NOT the prebuiltImage worker image. Override here if needed.
image: ""
# -- Pod resource requests/limits for the Job. Modest defaults sized for the short-lived Kafka
# CLI (a small JVM); override for constrained namespaces.
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
# -- Security contexts for the Job. Defaults enforce a hardened, restricted-PSS-friendly baseline
# (the base image runs as non-root uid 1001; the script writes only to an emptyDir at /tmp).
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
# -- "false" keeps connectors created via the REST API (not reconciled from KafkaConnector CRs).
useConnectorResources: "false"
# -- Image pull secrets for private registries. Example: [{ name: regcred-harbor }]
imagePullSecrets: []
# -- Pod resource requests/limits. None by default.
resources: {}
# requests:
# cpu: 250m
# memory: 1Gi
# limits:
# memory: 2Gi
# -- Security contexts for the worker pods, rendered into spec.template.pod.securityContext and
# spec.template.connectContainer.securityContext. Strimzi's pod security provider is "baseline",
# which sets neither, so without these the workers run third-party connector code with the full
# default capability set and no seccomp profile.
#
# The defaults are the restricted Pod Security Standard, the same thing Strimzi's own "restricted"
# provider applies. readOnlyRootFilesystem is deliberately not among them: Connect and its plugins
# write outside the /tmp emptyDir Strimzi mounts, so it is left to be turned on per deployment.
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
# -- Container runtime for the worker pods, e.g. "kata-vm-isolation" for AKS Pod Sandboxing.
# Empty (default) means the node's normal runtime and nothing extra is installed.
#
# Strimzi's pod template has no runtimeClassName field, so the chart cannot put this on the
# KafkaConnect resource. Setting it renders a Kyverno Policy in the release namespace that adds
# the field to the worker pods at admission time, so Kyverno must be running in the cluster.
#
# Set `affinity` as well. Kubernetes normally copies a RuntimeClass's own scheduling.nodeSelector
# onto the pod, but that built-in step runs before Kyverno, so a runtime class added this way
# never gets it. Without affinity the pod can be scheduled onto a node that has no such runtime
# and will fail to start. See README "Running the workers on a different container runtime".
runtimeClassName: ""
# -- Where the worker pods may run, passed through to spec.template.pod.affinity. Strimzi has no
# nodeSelector field, so express node pinning as nodeAffinity. Example:
# nodeAffinity:
# requiredDuringSchedulingIgnoredDuringExecution:
# nodeSelectorTerms:
# - matchExpressions:
# - key: kubernetes.azure.com/kata-vm-isolation
# operator: In
# values: ["true"]
affinity: {}
# -- Node taints the worker pods tolerate, passed through to spec.template.pod.tolerations.
tolerations: []
# How the Connect REST API is reached, and who may reach it at all.
#
# Kafka Connect's own Basic Auth cannot be switched on: it needs rest.extension.classes, which
# Strimzi forbids and silently drops. The password check therefore lives in whatever fronts the API,
# and every implementation wires it differently, so the chart renders the route and the credentials
# Secret and takes the wiring from you as data. README "REST API access" has working blocks for
# ingress-nginx, F5 NGINX Ingress Controller and NGINX Gateway Fabric.
restApi:
route:
# -- Render the route. Requires restApi.networkPolicy.enabled: a route only filters traffic that
# chooses to go through it, the policy is what closes the direct Service route.
enabled: false
# -- Ingress or HTTPRoute (Gateway API).
kind: Ingress
# -- Hostname for the REST API. Required. Register it in Self-Service as connectUrl.
host: ""
# -- basic or none. Required; matches Platform Manager's BASIC_AUTH and NO_AUTH. With basic,
# something has to carry out the check: basicAuth.annotations, route.filters or extraObjects.
authMethod: ""
# -- Annotations that are not about authentication. Auth ones belong in basicAuth.annotations.
annotations: {}
# -- IngressClass name, e.g. nginx-private or f5-nginx-public. Required for kind: Ingress.
className: ""
# -- TLS Secret for the host (kind: Ingress). Empty serves plain HTTP. For kind: HTTPRoute, TLS
# belongs to the Gateway listener and this is unused.
tlsSecret: ""
# -- Allow basic auth over plain HTTP (kind: Ingress, no tlsSecret). Local runs only: the
# password would otherwise leave the cluster unencrypted. No effect on kind: HTTPRoute, whose
# TLS lives on the Gateway listener.
allowInsecureBasicAuth: false
# -- Gateways this route attaches to. Required for kind: HTTPRoute. Example:
# - name: nginx-cluster-private
# namespace: infra-gateways
parentRefs: []
# -- Filters on the route rule (kind: HTTPRoute). This is where Gateway API implementations put
# authentication, e.g. an NGINX Gateway Fabric AuthenticationFilter through ExtensionRef.
filters: []
# Credentials for authMethod: basic. Point at a Secret, or let the chart create one. The shape
# differs per implementation: ingress-nginx and NGINX Gateway Fabric read an Opaque Secret with
# the key "auth", F5 NGINX Ingress Controller a nginx.org/htpasswd Secret with the key "htpasswd".
basicAuth:
# -- Existing Secret holding an htpasswd line. Wins over htpasswd; the chart then creates
# nothing, and matching secretType/secretKey is yours to get right.
existingSecret: ""
# -- htpasswd line (`htpasswd -nbB <user> <pass>`) the chart writes into its own Secret.
# A hash, never a plain password: see README "Credentials". SOPS-encrypt the values file.
htpasswd: ""
# -- Type and data key of the chart-created Secret. Match what your controller expects.
secretType: Opaque
secretKey: auth
# -- Annotations that switch authentication on. Controller-specific by nature, so the chart
# only merges them onto the route. Templated, so an annotation can name the Secret above
# whichever way it was given. Example for ingress-nginx:
# nginx.ingress.kubernetes.io/auth-type: basic
# nginx.ingress.kubernetes.io/auth-secret: '{{ include "kafka-connect.restApi.authSecretName" . }}'
# nginx.ingress.kubernetes.io/auth-realm: "Kafka Connect REST API"
annotations: {}
# -- Objects the chart renders verbatim, for implementations whose authentication is a resource
# rather than an annotation: an F5 NIC Policy, an NGF AuthenticationFilter, an Envoy Gateway
# SecurityPolicy. Entries are templated, so `{{ include "kafka-connect.fullname" . }}` works.
extraObjects: []
# NetworkPolicy restricting ingress to the REST API (HTTP :8083). Only enforced on a CNI that
# supports NetworkPolicy. With `route` it leaves the route as the only way in; on its own it is
# defence in depth.
networkPolicy:
# -- Turn the NetworkPolicy on. Same-cluster Connect pods are always allowed (inter-worker
# REST forwarding).
enabled: false
# -- The pods that terminate the route and forward to Connect: the ingress controller, or a
# Gateway's data plane. Required when route.enabled, otherwise the policy blocks the very thing
# serving the route. Rendered only when namespace is set. Verify against what runs:
# `kubectl get pod -n <namespace> --show-labels`.
router:
namespace: ""
podLabels: {}
# -- The Strimzi operator, which calls the REST API on every reconcile: without it the cluster
# never reports Ready. Empty namespace means the release namespace.
strimziOperator:
namespace: ""
podLabels:
strimzi.io/kind: cluster-operator
# -- Extra peers, as standard NetworkPolicy "from" entries. Prefer a podSelector over a whole
# namespaceSelector. Required when the policy is on without a route, because the operator alone
# reaches nothing. Platform Manager does not belong here once a route is in place: it should
# go through the authenticated path. Log streaming reads pod logs through the Kubernetes API and
# never touches this port. Example:
# - podSelector:
# matchLabels:
# app.kubernetes.io/name: provisioner
allowedFrom: []