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 Kafka CR that Connect will connect to.

  • Secrets for mTLS: beta-cluster-ca-cert and beta-beta-pool-0 in 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

pluginDelivery

imageVolumes

Delivery mode: imageVolumes or prebuiltImage

plugins

see values.yaml

Plugin names to load. Each name must have a spec at plugins/<name>.yaml. imageVolumes mode only.

pluginRegistry

registry.axual.io/internal/axual/connect-plugins

Registry prefix for plugin images

connectBaseImageRegistry

registry.axual.io/internal/axual/kafka-connect-base

Registry+repo for the Connect base image: the worker image in imageVolumes mode, and the ACL-bootstrap Job image in every mode. The full tag is built as <registry>:<chart-version>-strimzi-<strimziVersion>-kafka-<kafkaVersion>. Image is plain Strimzi plus vault-config-provider baked into /opt/kafka/libs/ (see Dockerfile.connect-base).

connectBaseImage

""

Full override for the base image. Sets the worker image in imageVolumes mode and the ACL-bootstrap Job image in every mode - so in prebuiltImage mode it still controls the Job’s image, just not the worker’s. Empty ⇒ computed from the chart version. Pin an existing image when testing an unreleased chart version whose CI-built base image does not exist yet.

prebuiltImageRegistry

registry.axual.io/internal/axual/kafka-connect

Registry+repo the profile image is built from: <prebuiltImageRegistry>/<profile>:<chart-version>-strimzi-<strimziVersion>-kafka-<kafkaVersion>. Redirect it for a mirrored or air-gapped install. prebuiltImage mode only.

prebuiltImageProfile

default

Profile name for the prebuilt image. prebuiltImage mode only.

strimziVersion

0.51.0

Strimzi version. Used to construct the prebuilt image tag.

kafkaVersion

4.1.1

Kafka version for the Connect workers.

replicas

1

Number of Connect worker pods.

bootstrapServers.tls / bootstrapServers.sasl

"" / ""

Kafka broker address per listener. Each consumer selects by its auth type: the worker by authentication.type, the ACL-bootstrap Job by aclBootstrap.auth.type. Set the one(s) you use.

tls.trustedCertificates

beta-cluster-ca-cert / ca.crt

CA cert secret for broker TLS. Required for any TLS listener (mTLS or SASL_SSL).

authentication.type

tls

Worker auth method. One of tls, scram-sha-512. SCRAM-SHA-256 is not supported (Strimzi is SCRAM-SHA-512 only); OAuth is v2.

authentication.certificateAndKey

beta-beta-pool-0

Client cert reference when type=tls.

authentication.username

(unset)

SASL username. Required for SASL types.

authentication.passwordSecret

(unset)

K8s Secret reference with the SASL password. Required for SASL types.

imagePullSecrets

[]

Pull secrets for the plugin registry.

runtimeClassName

""

Container runtime for the worker pods, e.g. kata-vm-isolation. Needs Kyverno in the cluster and needs affinity set too. See “Running the workers on a different container runtime”.

affinity

{}

Passed through to spec.template.pod.affinity. Strimzi has no nodeSelector field, so node pinning goes here as nodeAffinity.

tolerations

[]

Passed through to spec.template.pod.tolerations.

resources

requests 250m/1Gi, limits 2Gi

Connect worker pod resources.

podSecurityContext

runAsNonRoot: true, RuntimeDefault seccomp

Pod-level security context for the workers, rendered into spec.template.pod.securityContext. Strimzi’s default pod security provider is baseline, which sets none. Set to null to render none.

securityContext

drop ALL, no privilege escalation

Container-level security context for the workers, rendered into spec.template.connectContainer.securityContext. readOnlyRootFilesystem is not a default: Connect and its plugins write outside the /tmp emptyDir Strimzi mounts, and which paths depends on the plugins in use.

useConnectorResources

"false"

Set to "true" to manage connectors via KafkaConnector CRs.

tenant

(required)

Tenant short name, lowercase. Rendered as the axual.io/tenant pod label, which is how the Provisioner / log viewer finds this cluster’s pods.

instance

(required)

Instance short name, lowercase. Rendered as the axual.io/instance pod label.

clusterName

(required)

Connect cluster name as registered in Self-Service, lowercase. Rendered as the axual.io/connect-cluster pod label. Self-Service allows capitals and names up to 255 characters; the log viewer lowercases a name before it looks for pods, so lowercase it here. A name over 63 characters must be shortened in Self-Service first.

groupId

derived

Connect consumer group. Empty derives _<tenant>-<instance>-<clusterName>-connect. See “Internal topic names”.

configStorageTopic

derived

Empty derives the above plus -configs.

offsetStorageTopic

derived

Empty derives the above plus -offsets.

statusStorageTopic

derived

Empty derives the above plus -status.

podAnnotations

{}

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 fluentbit.io/parser: json. Not set for you, because which pipeline is in front of the cluster is not the chart’s to know.

logging.level

INFO

Root log level.

logging.loggers

org.reflections: ERROR

Levels for individual loggers, as name: level. Each writes to the same console appender as the root logger, so raising a level really does add output. Use OFF to silence one.

logging.monitorInterval

30

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 PUT /admin/loggers/<name> is not reverted on this timer: it lasts until the config really changes or the pod restarts. 0 turns the check off, which also means a config change then needs a pod restart.

vault.enabled

false

Turn on Vault integration at runtime. When true, the chart (a) renders the AppRole role_id + secret_id as env vars on the Connect container (spec.template.connectContainer.env), and (b) configures the worker config.providers for env + vault. The vault-config-provider JAR itself is always in the Connect base image (baked by Dockerfile.connect-base) regardless of this flag - this only controls the runtime wiring.

vault.address

""

Vault server URL (e.g. https://vault.example.com:8200). Required when vault.enabled: true.

vault.authMethod

APPROLE

Vault auth method. Currently only APPROLE is supported.

vault.approlePath

approle

AppRole auth mount path in Vault.

vault.namespace

""

Vault Enterprise namespace. Required for on-prem customers running Vault Enterprise with namespaces; leave empty for Axual Cloud (Vault OSS).

vault.testPath

""

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.

vault.sslVerify

true

Whether the provider verifies the Vault server’s TLS certificate. Set false only for local dev against a self-signed cert that is not in the truststore.

vault.credentialsSecret

""

Mode B (default, recommended): name of a pre-existing Kubernetes Secret with VAULT_APPROLE_ROLE_ID + VAULT_APPROLE_SECRET_ID keys. Rendered via valueFrom.secretKeyRef, so the secret_id stays out of the rendered CR. Overrides approleRoleId / approleSecretId when set.

vault.approleRoleId

""

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 credentialsSecret is empty.

vault.approleSecretId

""

Mode A (opt-in shortcut): the worker AppRole secret_id pasted inline.

vault.truststoreSecret

""

Optional Kubernetes Secret with truststore.p12 and truststore.password keys for HTTPS to Vault.

aclBootstrap.enabled

false

Run a superuser Helm hook Job that creates Connect’s internal topics and grants the worker principal(s) the required ACLs. See Bootstrapping internal topics and ACLs.

aclBootstrap.verifyHostname

true

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).

aclBootstrap.auth.type

tls

Superuser auth for the Job. One of tls, scram-sha-512. Must be a Kafka superuser: the worker cert cannot grant its own ACLs.

aclBootstrap.auth.tls.secretName

""

K8s Secret with the superuser cert + key (PEM). Required when auth.type=tls.

aclBootstrap.auth.tls.certificate / key

tls.crt / tls.key

Keys within that Secret holding the certificate and private key.

aclBootstrap.auth.scram.username

""

Superuser username. Required when auth.type=scram-sha-512.

aclBootstrap.auth.scram.passwordSecret.secretName

""

K8s Secret holding the superuser password. Required when auth.type=scram-sha-512.

aclBootstrap.auth.scram.passwordSecret.password

password

Key within that Secret holding the password.

aclBootstrap.principal

""

Principal the ACLs are granted to = the worker identity. User:<cert DN> for tls, User:<username> for scram. Required when enabled.

aclBootstrap.distributionPrincipal

""

Optional extra principal (e.g. a “read all” distributor) that also receives the ACLs.

aclBootstrap.replicationFactor / minInsyncReplicas

1 / 1

Durability of the internal topics. Match your broker count.

aclBootstrap.offsetTopicPartitions

25

Partition count for the offsets topic.

aclBootstrap.statusTopicPartitions

5

Partition count for the status topic. The config topic is always single-partition (a Connect requirement).

aclBootstrap.segmentMs

3600000

segment.ms for the compacted internal topics; controls how often log compaction runs.

aclBootstrap.additionalClientConfigs

[]

Extra lines appended verbatim to the admin client .properties (e.g. retries or extra security settings). Request timeouts are already set by the chart.

aclBootstrap.backoffLimit

3

Job retry budget.

aclBootstrap.activeDeadlineSeconds

240

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.

aclBootstrap.image

""

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.

aclBootstrap.resources

requests 100m/256Mi, limits 512Mi

Pod resources for the Job. Modest defaults sized for the short-lived Kafka CLI.

aclBootstrap.podSecurityContext

runAsNonRoot: true, RuntimeDefault seccomp

Pod-level security context for the Job. Hardened, restricted-PSS-friendly baseline.

aclBootstrap.securityContext

drop ALL, no privilege escalation, read-only rootfs

Container-level security context for the Job.

restApi.route.enabled

false

Publish the REST API through a route. Requires restApi.networkPolicy.enabled, see “REST API access”.

restApi.route.kind

Ingress

Ingress or HTTPRoute (Gateway API).

restApi.route.host

""

Hostname for the REST API. Required, and the address to register in Self-Service as connectUrl.

restApi.route.authMethod

""

basic or none, matching Platform Manager’s BASIC_AUTH and NO_AUTH. Required: there is no default, so a cluster cannot end up unprotected by omission.

restApi.route.annotations

{}

Annotations that are not about authentication.

restApi.route.className

""

IngressClass name, e.g. nginx-private or f5-nginx-public. Required for kind: Ingress.

restApi.route.tlsSecret

""

TLS Secret for the host (kind: Ingress). Empty serves plain HTTP. For kind: HTTPRoute, TLS belongs to the Gateway listener.

restApi.route.allowInsecureBasicAuth

false

Allow basic auth over plain HTTP (kind: Ingress, no tlsSecret). Refused by default: the password would travel in the clear. Local runs only.

restApi.route.parentRefs

[]

Gateways the route attaches to. Required for kind: HTTPRoute.

restApi.route.filters

[]

Filters on the route rule (kind: HTTPRoute). Where Gateway API implementations attach authentication.

restApi.basicAuth.existingSecret

""

Secret that already holds an htpasswd line. Takes precedence over the options below; the chart then creates nothing.

restApi.basicAuth.htpasswd

""

htpasswd line (htpasswd -nbB <user> <pass>) the chart writes into its own Secret. SOPS-encrypt the values file.

restApi.basicAuth.secretType / .secretKey

Opaque / auth

Shape of the chart-created Secret. nginx.org/htpasswd
htpasswd for the F5 NGINX Ingress Controller.

restApi.basicAuth.annotations

{}

Annotations that switch authentication on, merged onto the route. Controller-specific, so the chart only carries them. Templated.

restApi.extraObjects

[]

Objects rendered verbatim, for implementations whose authentication is a resource: an F5 NIC Policy, an NGF AuthenticationFilter, an Envoy Gateway SecurityPolicy. Templated.

restApi.networkPolicy.enabled

false

Add a NetworkPolicy limiting ingress to the REST API (port 8083). Only enforced on a CNI that supports NetworkPolicy.

restApi.networkPolicy.router

ns "", labels {}

The pods that serve the route: an ingress controller, or a Gateway’s data plane. Required when the route is enabled.

restApi.networkPolicy.strimziOperator

ns "", strimzi.io/kind: cluster-operator

The Strimzi operator, which calls the REST API on every reconcile. Without it the cluster never reports Ready.

restApi.networkPolicy.allowedFrom

[]

Extra from peers allowed to reach 8083. Required when the policy is on without a route. Prefer a podSelector over a whole namespaceSelector. Log streaming does not use this port.

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)

tls

certificateAndKey.secretName, .certificate, .key

SASL/SCRAM-SHA-512

scram-sha-512

username, passwordSecret.secretName (+ passwordSecret.password if not the default password)

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:

  1. Apply a KafkaUser CR 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
  2. Wait for the Secret to appear:

    kubectl wait kafkauser/connect-worker -n kafka --for=condition=Ready --timeout=120s
  3. 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/DescribeConfigs on those topics;

  • grants Read/Describe on 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.route publishes the REST API on one hostname, as an Ingress or an HTTPRoute. Whatever serves that route is what asks for the password.

  • restApi.networkPolicy shuts 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

  1. 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.

  2. Make the credential. htpasswd -nbB connect '<password>' prints the line. Put it in the SOPS encrypted values as restApi.basicAuth.htpasswd, or create the Secret yourself and name it in restApi.basicAuth.existingSecret. The chart never sees a plain password.

  3. Copy the block for your implementation from below, and set route.host plus route.authMethod.

  4. 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
  5. 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 out

    A 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.

  6. Register the cluster in Self-Service with connectUrl set to the route address and the auth method matching route.authMethod, then put the same password in the Governance Vault under connect-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

basicAuth.existingSecret

The chart only references it. Matching secretType and secretKey is then yours to get right.

basicAuth.htpasswd

The chart renders its own Secret from that line. Generate it with htpasswd -nbB <user> '<password>' and SOPS-encrypt the values file.

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

  1. PM writes the connector’s credentials to Vault. There is one shared engine, connector-auth/. Under each cluster’s path there is a tls/ sub-path for certificates and a sasl/ 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} holding private.key and certificate.chain (PEM).

    • For SASL connectors: connector-auth/{tenant}/{instance}/{cluster_name}/sasl/{env}/{app}/{credential_type} holding sasl.username and sasl.password.

  2. 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 for producer.override.* and admin.override.*).

    • SASL: inside consumer.override.sasl.jaas.config as username="${vault:...}" and password="${vault:...}".

  3. When the connector task starts, the worker’s VaultConfigProvider authenticates 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 via valueFrom.secretKeyRef against vault.credentialsSecret; Mode A (opt-in shortcut) takes them from vault.approleRoleId + vault.approleSecretId chart values.

  • Sets config.providers = "env,vault" and uses ${env:...} to feed the env vars into VaultConfigProvider’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 uses EnvVarConfigProvider to 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

  1. Create plugins/<name>.yaml. Most artifacts need a url: or maven: line and its sha512::

    # 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: 48dec1b01b7cdafc6c17103e476e0be522dd1e9414607e5b11f0ee9923d6400ee1af076d219d112d6dcca42d520e3a542300319c4fde4847f08ab54682409db3

    The plugin name is the filename without .yaml. The version comes from the URL path, or from the third field of maven: group:artifact:version. sha512: is mandatory and is never fetched for you; compute it with curl -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 with tar -tzf <file> | head first.

    # 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: 0

    version: is required in this form: nothing can derive it from sources:, 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 .zip or a plain .tar the way Lenses ships stream-reactor, stays on the short url: form. The bundle is unpacked and every jar inside it is copied flat into the plugin dir, wherever it sat, because that is the layout plugin.path expects, and LICENSE/NOTICE files come with it. There is no strip-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: 422f7289d309dca15844c21c2e30713ba2412f169950190fb35868df4f7a01c4cad7b8ecca1085435565c5591035591f513486f6906ff9e5a57eaa46170c1be1

    plugin_spec_smoke runs on the merge request and does two things: it checks every spec against plugins/_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
  2. Add the name to plugins in values.yaml:

    plugins:
      - kafka-topic-name-transforms
      - debezium-oracle
  3. Push to main. CI detects the changed spec file and builds the plugin OCI image automatically.

  4. 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

values.yaml

strimziVersion, and kafkaVersion if Kafka moves too

2

values.yaml

prebuiltConnectImage, whose tag spells out both versions

3

profiles/default.yaml

the strimzi: entry, version and kafka

4

profiles/with-cdc.yaml

the same entry, and any other profile you add later

5

ci/crds/

run ./ci/refresh-strimzi-crd.sh, which downloads the new CRD and removes the old

6

Chart.yaml

bump version and appVersion

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 from profiles/*.yaml. Change one and not the other and the chart points at a tag CI never built, which shows up as ImagePullBackOff at deploy time rather than as a failing pipeline. The check_version_consistency job compares them, along with prebuiltConnectImage and the vendored CRD, so this fails the MR instead.

  • 6 is enforced. check_version_bumped_when_image_changes fails the MR if profiles/ 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_validation fails if ci/crds/kafkaconnect-<strimziVersion>.yaml is missing, and prints the exact curl to 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 KafkaConnect resource. helm_crd_validation catches a field the new CRD rejects, but not a field whose meaning changed.

  • Confirm the new Strimzi supports the kafkaVersion you set. Strimzi supports a small range per release.

What’s inside

Path Purpose

plugins/*.yaml

One spec file per plugin - source URL/coordinates and optional digest

profiles/*.yaml

Named plugin combinations for prebuilt image mode

templates/kafkaconnect.yaml

The KafkaConnect custom resource

templates/runtimeclass-policy.yaml

Optional Kyverno Policy that puts the worker pods on another container runtime

templates/rest-api-route.yaml

Optional Ingress or HTTPRoute for the REST API

templates/rest-api-basic-auth-secret.yaml

Chart-owned htpasswd Secret, unless an existing one is named

templates/rest-api-extra-objects.yaml

Auth objects your implementation needs, rendered verbatim

templates/networkpolicy.yaml

Optional NetworkPolicy that leaves the route as the only way to port 8083

templates/_helpers.tpl

Naming, label, and plugin resolution helpers

values.yaml

All configuration with inline documentation

License

Apache 2.0 - see LICENSE.txt.

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: []