Kafka Connect 0.8.1 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. They are built, gated
and published by connect-plugins-lifecycle; this chart publishes no
plugin image. It lists the ones it can mount in
plugin-catalog.yaml, and the chart resolves a plugin name to the full
image reference from there.
[[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). 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.8.1 \
--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.8.1 \
--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: |
|
|
Plugin names to load. Each name must have an entry
in |
|
|
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. |
|
|
|
|
|
Not supported yet. Render a
Kyverno |
|
|
Registry prefixes managed
plugin images may come from, each ending in |
|
|
Needs Kyverno 1.19+.
Render a Kyverno |
|
|
Regular expression the signing identity must match. The default admits
|
|
|
Secrets in the Kyverno namespace that Kyverno reads the signatures with. |
|
|
Add a |
|
kube-dns in |
|
|
|
Allowed destinations, as standard NetworkPolicy egress rules. Required when enabled. |
|
|
Passed through to |
|
|
Extra labels for the worker pods. The identity labels win over the same key. |
|
|
Annotations for the worker ServiceAccount Strimzi creates. |
|
|
Extra
volumes for the worker pods and their mounts in the Connect container.
Mounts are read-only unless |
|
|
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): 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.8.1 \ --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 examples below all 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.
Verifying the base image
Every base image is signed with cosign (keyless, GitLab OIDC,
public Rekor). Turn on a Kyverno NamespacedImageValidatingPolicy that
refuses a pod whose base image lacks an Axual release signature:
baseImageVerification:
enabled: true
registryCredentialsSecrets:
- harbor-robot # in the Kyverno namespace, can pull registry.axual.io/axual/kafka-connect-base
-
Needs Kyverno 1.19 or later. On 1.17 and 1.18 the webhook for this policy kind is broken and refuses every pod it selects, so the Connect workers cannot start. Check first:
kubectl -n kyverno get deploy kyverno-admission-controller -o jsonpath='{..image}'. -
It checks only this release’s pods that use
connectBaseImageRegistry: the workers and the ACL bootstrap Job, selected by theirapp.kubernetes.io/nameandapp.kubernetes.io/instancelabels. Other pods in the namespace are never touched. Kyverno pins each admitted pod to the verified digest. -
Only release images pass: development builds are refused.
-
A
connectBaseImageoverride is not matched: we do not sign images we did not build. -
Kyverno must reach
registry.axual.ioandrekor.sigstore.dev. Air-gapped clusters keep it off. -
Base images released before 0.8.1 carry no release tag signature and are refused.
Restricting worker egress
Connector code runs inside the workers. Without an egress policy, a
compromised plugin reaches anything the pod network does, including the
cloud metadata endpoint and the Kubernetes API. egress.enabled adds a
NetworkPolicy that allows DNS, the other workers of this cluster, and
egress.rules, nothing else. Connectors share the pods, so the list
covers all of them.
egress:
enabled: true
rules:
- to: # brokers, Vault, schema registry
- ipBlock:
cidr: 10.0.0.0/8
- to: # public sources and sinks, never the metadata endpoint
- ipBlock:
cidr: 0.0.0.0/0
except: [10.0.0.0/8, 169.254.169.254/32]
Image pulls are done by the kubelet and are not affected. Check
egress.dns matches your cluster’s DNS pods
(kubectl get pod -n kube-system --show-labels). A connector that
authenticates through the metadata endpoint (managed identity without
workload identity) needs it allowed explicitly.
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 Connect base image contains vault-config-provider in
/opt/kafka/libs/. spec.image always points at this base image in
imageVolumes mode.
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.
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.8.1 \
--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): 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.
helm upgrade --install axual-dta-kafka-connect oci://registry.axual.io/axual-charts/kafka-connect \
--version 0.8.1 \
--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
Plugin images are built and published by connect-plugins-lifecycle.
The chart only decides which of them it offers, and that decision is one
entry in plugin-catalog.yaml.
-
Check the plugin is in the catalog of the chart version you deploy. Pull the chart and list the catalogued names:
helm pull oci://registry.axual.io/axual-charts/kafka-connect --version 0.8.1 --untar yq '.plugins | keys' kafka-connect/plugin-catalog.yaml -
Add the name to
pluginsin your values:plugins: - kafka-topic-name-transforms - debezium-oracle -
Deploy:
helm upgrade. The chart resolves each name to its catalogued image, pinned by tag and digest, and Strimzi mounts it.
A name that is not in the catalog fails the render and lists the names that are. A plugin that is missing from the catalog is added by Axual in a chart release.
A connector that is not in the catalog
Customer-supplied and licensed connectors live outside the catalogued
Harbor project. Give the entry an explicit image instead of a
catalogued name. name is still required, because Strimzi names the
mounted volume after it:
plugins:
- name: acme-connector
image: registry.example.com/acme/acme-connector:1.0.0@sha256:...
Pin it by digest for the same reason the catalog does: a tag can be moved, a digest cannot.
Testing a snapshot connector
A snapshot has no catalog entry. Pick one of three ways.
Plugin volume (easiest). Needs pluginDelivery: imageVolumes and
chart 0.8.1 or later. The base image links /opt/kafka/plugins/custom
to /mnt/custom-plugins, so a volume mounted there loads next to the
plugins in plugins. The link is needed because Strimzi accepts
extraVolumeMounts only under /mnt, so a volume cannot be mounted in
/opt/kafka/plugins directly. Other /mnt paths are not scanned for
plugins: JARs there never load.
extraVolumes:
- name: custom-plugins
persistentVolumeClaim:
claimName: custom-plugins
extraVolumeMounts:
- name: custom-plugins
mountPath: /mnt/custom-plugins
Every worker pod mounts the same volume, so the JARs are copied once. Copy them with a short lived pod that mounts the same PVC, then roll all workers:
kubectl cp my-connector-1.2.0-SNAPSHOT/ loader-pod:/plugins/my-connector/
kubectl annotate strimzipodset <fullname>-connect strimzi.io/manual-rolling-update=true
Connect reads plugins only at start, so roll every worker, not just one. A worker that did not restart lacks the connector, and its tasks fail when they land there.
-
No volume, or an empty one, is fine: Connect skips the link without an error or a warning, and loads only the plugins in
plugins. -
Everything on the volume is one plugin with one classloader. Test one connector at a time, or make sure the connectors on it do not ship conflicting dependencies.
-
With more than one worker the PVC must be
ReadWriteMany, unless every worker runs on the same node. AReadWriteOncePVC keeps a worker on another node inContainerCreating. -
The worker can never write there: a mount at
/mnt/custom-plugins, inside it or above it (such as/mnt) withreadOnly: falsefails the render. Otherwise a compromised connector could plant code that every worker loads on its next start. -
Whoever can write to the volume itself runs code in every worker, and nothing pins what it holds. Use it for testing. For production use a plugin image pinned by digest.
Plugin image. Needs pluginDelivery: imageVolumes. Strimzi mounts the
image root at /opt/kafka/plugins/<name>, so put the JARs at
the root:
FROM scratch
COPY my-connector-1.2.0-SNAPSHOT/ /
docker build -t registry.example.com/test/my-connector:1.2.0-SNAPSHOT .
docker push registry.example.com/test/my-connector:1.2.0-SNAPSHOT
plugins:
- name: my-connector
image: registry.example.com/test/my-connector:1.2.0-SNAPSHOT@sha256:...
No access to registry.axual.io? Axual sends the image as a file
(docker save) and you copy it into your own registry:
skopeo copy docker-archive:my-connector.tar docker://registry.example.com/test/my-connector:1.2.0-SNAPSHOT
Prebuilt image. For clusters without image volumes. Build on the
Connect base image of your chart version and put the JARs in their own
folder under /opt/kafka/plugins/:
FROM registry.axual.io/axual/kafka-connect-base:0.8.1-strimzi-0.51.0-kafka-4.1.1
COPY my-connector-1.2.0-SNAPSHOT/ /opt/kafka/plugins/my-connector/
pluginDelivery: prebuiltImage
prebuiltConnectImage: registry.example.com/test/kafka-connect-my-connector:1.2.0-SNAPSHOT
Every new snapshot means a new image of ~600 MB, and the catalogued plugins are not mounted in this mode. Copy any other JARs you need into the same image.
Add the registry’s pull secret to imagePullSecrets or
registryCredentials in both cases.
Plugins managed by a provisioner
Not supported yet. No provisioner implements this side so far. The chart part ships now so that turning it on later needs no new chart release.
For plugins chosen at runtime (Self-Service) instead of in git. Off by default; needs Kyverno.
managedPlugins:
enabled: true
allowedRegistries:
- registry.axual.io/axual/connect-plugins/
The chart renders a Kyverno Policy and nothing else. A provisioner
owns the ConfigMap <fullname>-managed-plugins in the release
namespace:
data:
plugins: '[{"name":"debezium-postgres","image":"registry.axual.io/axual/connect-plugins/debezium-postgres:3.6.3.Final@sha256:..."}]'
pullSecrets: '["harbor-robot"]'
After each change it rolls the workers with
kubectl annotate strimzipodset <fullname>-connect strimzi.io/manual-rolling-update=true.
Every new pod gets one image volume per plugin at
/opt/kafka/plugins/managed-<name> and the listed pull secrets.
Argo CD stays in sync: it tracks neither the ConfigMap nor the pods.
-
namemust be a DNS label of at most 48 characters, or the API server refuses the pod. -
Images outside
allowedRegistries, or not pinned by@sha256:, are refused. -
No ConfigMap means no managed plugins. While Kyverno is down, new worker pods are refused.
-
Write the ConfigMap and roll in one step: a pod replaced later reads whatever the ConfigMap says.
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+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/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, by name. Each name must appear in plugin-catalog.yaml, which carries the full image
# reference (tag + digest) published by connect-plugins-lifecycle. An unknown name fails the
# render and lists the catalogued plugins.
plugins: []
# Plugins managed outside git by a provisioner, on top of `plugins`. Off by default.
# NOT SUPPORTED YET: no provisioner implements this side so far. Shipped now so turning it on later
# needs no new chart release.
# When on, the chart renders a Kyverno Policy that reads the ConfigMap <fullname>-managed-plugins
# (owned by the provisioner, never by the chart) and mounts one image volume per plugin into each
# new worker pod. Kyverno must run in the cluster. See README "Plugins managed by a provisioner".
managedPlugins:
# -- Render the Kyverno Policy.
enabled: false
# -- Registry prefixes managed plugin images may come from. Required when enabled. Every image
# must also be pinned by digest. Example: [registry.axual.io/axual/connect-plugins/]
allowedRegistries: []
# Kyverno Policy that admits a pod running the Connect base image only when it is signed by this
# chart's release pipeline (cosign keyless, GitLab OIDC). Off by default. Needs Kyverno 1.19+: on
# 1.17 and 1.18 it refuses every worker pod. Kyverno must reach registry.axual.io and
# rekor.sigstore.dev, so it does not work air-gapped.
# See README "Verifying the base image".
baseImageVerification:
# -- Render the Kyverno Policy.
enabled: false
# -- Regular expression the signing identity must match. The default admits only images built by
# an X.Y.Z release tag of this repository, so main and branch images are refused.
subjectRegExp: '^https://gitlab\.com/axual/product/kafka-connect-helm//\.gitlab-ci\.yml@refs/tags/[0-9]+\.[0-9]+\.[0-9]+$'
# -- Secrets in the Kyverno namespace Kyverno reads the signatures with, when the registry needs
# credentials. Example: [harbor-robot]
registryCredentialsSecrets: []
# -- 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: []
# -- Credentials for the registries the worker and plugin images come from, one entry per registry.
# The chart writes them into one kubernetes.io/dockerconfigjson Secret and adds it to the worker
# pods' imagePullSecrets. Plain text in values: SOPS-encrypt the file. The ACL bootstrap Job runs
# before this Secret exists, so the registry of its image must be covered by imagePullSecrets.
registryCredentials: []
# - registry: registry.axual.io
# username: robot$connect-pull
# password: ""
# - registry: acme.azurecr.io
# username: 00000000-0000-0000-0000-000000000000
# password: ""
# -- 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: []
# -- Worker JVM options, passed through to spec.jvmOptions. javaSystemProperties is how worker level
# plugin settings are set. Values are plain text in the pod spec: never put secrets here.
jvmOptions: {}
# javaSystemProperties:
# - name: com.example.plugin.setting
# value: some-value
# -- Extra labels for the worker pods. The chart's identity labels win over the same key.
podLabels: {}
# -- Annotations for the worker service account that Strimzi creates.
serviceAccountAnnotations: {}
# -- Extra volumes for the worker pods, passed through to spec.template.pod.volumes.
# Every connector on the worker can read them.
extraVolumes: []
# - name: my-secret
# secret:
# secretName: my-secret
# -- Mounts of extraVolumes in the Connect container. readOnly defaults to true. mountPath must be
# under /mnt (a Strimzi rule). Writable mounts are fine for connector files. Only /mnt/custom-plugins
# loads plugins, and it must stay read-only.
extraVolumeMounts: []
# - name: my-secret
# mountPath: /mnt/my-secret
# NetworkPolicy limiting where the worker pods may connect to. Off by default. Connector code runs
# in these pods, so without it a compromised plugin reaches anything the pod network does, the
# cloud metadata endpoint (169.254.169.254) and the Kubernetes API included. Connectors share the
# pods, so the allow list is per Connect cluster. Only enforced on a CNI that supports it.
egress:
# -- Turn the egress NetworkPolicy on.
enabled: false
# -- "to" peers that serve DNS, allowed on port 53. The default fits kube-dns / CoreDNS.
dns:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
# -- Allowed destinations, as standard NetworkPolicy egress rules. Required when enabled: list
# the brokers, Vault, the schema registry and every connector source/sink. Other workers of this
# cluster are always allowed. Example:
# - to:
# - ipBlock:
# cidr: 10.0.0.0/8
# ports:
# - port: 9093
# # anything public, except the cloud metadata endpoint
# - to:
# - ipBlock:
# cidr: 0.0.0.0/0
# except: [10.0.0.0/8, 169.254.169.254/32]
rules: []
# 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: []