Configure TLS
What's covered
| Connection | Mechanism |
|---|---|
| Camunda components → Elasticsearch (private CA, self-hosted or AWS) | global.tls.caBundle |
| Camunda components → OpenSearch (private CA, self-hosted or AWS-managed) | global.tls.caBundle |
| Camunda components → external OIDC issuer with private CA (Entra, Okta, internal Keycloak) | global.tls.caBundle |
| Browser / external client → Ingress / GatewayAPI (UI, gRPC) | Standard Kubernetes Ingress TLS — configured via per-component *.ingress.tls or global.gateway.tls, not global.tls.caBundle. See Ingress configuration. |
RDBMS secondary storage for the orchestration engine is not available in the 8.8 chart. Components that connect to PostgreSQL over TLS still benefit from the CA bundle — the cert is mounted at /etc/camunda/tls/ca.crt and a JDBC client can reference it with sslmode=verify-full&sslrootcert=/etc/camunda/tls/ca.crt.
In-cluster pod-to-pod traffic is not covered by this overlay — see In-cluster transport (service mesh required).
How it works
Camunda components span three trust ecosystems that each require a different CA input format:
| Runtime | Components | Trust input |
|---|---|---|
| OS / OpenSSL native | libcurl, Go crypto/x509, OpenSearch native client (post-8.6.7), PostgreSQL JDBC sslrootcert= | PEM via SSL_CERT_FILE |
| JVM | Operate, Tasklist, Optimize, Web Modeler restapi, Identity, Connectors, Zeebe broker | PKCS12/JKS keystore via -Djavax.net.ssl.trustStore= |
| Node.js | Console, Web Modeler websockets | PEM via NODE_EXTRA_CA_CERTS |
The values-tls.yaml overlay bridges all three from a single PEM bundle:
- Mounts the bundle at
/etc/camunda/tls/ca.crt. - Sets
SSL_CERT_FILEandNODE_EXTRA_CA_CERTSto that path on every component. - Runs a per-JVM-component init container that copies the JRE's
cacerts(PKCS12 on Java 21) into a sharedemptyDirand imports each certificate in the bundle viakeytool -importcert. Overrideglobal.tls.caBundle.imageonly if a component image lackskeytool. - Prepends
JAVA_TOOL_OPTIONSwith-Djavax.net.ssl.trustStore=/var/camunda/tls-truststore/cacerts -Djavax.net.ssl.trustStorePassword=changeit.
SSL_CERT_FILE replaces the system bundleSSL_CERT_FILE replaces (not appends to) the OS CA bundle for OpenSSL clients. Include all public CAs your components reach alongside your private CA:
cat /etc/ssl/certs/ca-certificates.crt your-private-ca.pem > camunda-ca-bundle.pem
Python-based connector containers are an exception: requests reads REQUESTS_CA_BUNDLE / CURL_CA_BUNDLE, not SSL_CERT_FILE. Set REQUESTS_CA_BUNDLE=/etc/camunda/tls/ca.crt on those containers.
Quickstart
Prerequisites
- Helm 3.10+
- A PEM-encoded CA bundle file (
your-ca-bundle.pem) containing the root and any intermediate certs that signed your datastore / IdP certs
1. Create the CA bundle Secret
NAMESPACE=camunda
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
kubectl -n "$NAMESPACE" create secret generic camunda-ca-bundle \
--from-file=ca.crt=./your-ca-bundle.pem
2. Apply the overlay
Download the overlay (ships in the chart repo, not in the helm repo cache):
curl -fsSLO https://raw.githubusercontent.com/camunda/camunda-platform-helm/main/charts/camunda-platform-8.8/values-tls.yaml
Install or upgrade with the overlay:
helm upgrade --install camunda camunda/camunda-platform \
--version 13.x \
--namespace "$NAMESPACE" \
-f values-tls.yaml \
-f your-values.yaml
your-values.yaml provides datastore URLs, credentials, and other scenario config. The TLS overlay is additive — it does not replace your existing values.
Download the overlay from the same chart version you install. The main branch tracks the latest chart, so if you pin a specific chart release, fetch values-tls.yaml from the matching release tag instead of main.
3. Verify
Confirm SSL_CERT_FILE, JAVA_TOOL_OPTIONS (with truststore path), and a ca-bundle volume appear in the pod spec:
kubectl -n "$NAMESPACE" get pod -l app.kubernetes.io/component=zeebe-broker -o yaml | \
grep -A 1 'JAVA_TOOL_OPTIONS\|SSL_CERT_FILE\|ca-bundle'
See also Verify no plaintext fallback.
Configuring datastore TLS
values-tls.yaml ships commented templates for each datastore. Uncomment and fill in the section that matches your backend.
Elasticsearch
In the 8.8 chart, Elasticsearch is configured via global.elasticsearch (the orchestration.data.secondaryStorage block was introduced in 8.9):
global:
elasticsearch:
url:
protocol: https
host: "your-elasticsearch.example.com"
port: 9200
auth:
username: elastic
existingSecret: "your-elasticsearch-credentials"
existingSecretKey: password
The Zeebe ElasticsearchExporter uses its own auth env path (ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_ARGS_AUTHENTICATION_USERNAME / _PASSWORD). global.elasticsearch.auth does not fill it — set those env vars via the component's env block if needed.
OpenSearch
global:
opensearch:
url:
protocol: https
host: "your-opensearch.example.com"
port: 9200
auth:
username: admin
existingSecret: "your-opensearch-credentials"
existingSecretKey: password
# For AWS-managed OpenSearch with IRSA, set global.opensearch.aws.enabled: true
# and omit auth.* — the caBundle still supplies trust for the TLS endpoint.
For AWS-managed OpenSearch with IRSA, leave auth unset and configure aws.enabled: true plus the appropriate service account annotations.
External OIDC issuer with private CA
Only required when your IdP uses a private or internal CA. Public-CA issuers (Entra, Google) work without additional configuration.
global:
identity:
auth:
issuerBackendUrl: "https://your-idp.example.com/realms/camunda"
tokenUrl: "https://your-idp.example.com/realms/camunda/protocol/openid-connect/token"
jwksUrl: "https://your-idp.example.com/realms/camunda/protocol/openid-connect/certs"
authUrl: "https://your-idp.example.com/realms/camunda/protocol/openid-connect/auth"
cert-manager integration
Issue the CA bundle Secret via a Certificate resource and reference it as global.tls.caBundle.secret.existingSecret:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: camunda-ca-bundle
namespace: camunda
spec:
secretName: camunda-ca-bundle
issuerRef:
name: your-internal-ca-issuer
kind: ClusterIssuer
commonName: camunda-ca
isCA: true
duration: 8760h
renewBefore: 720h
Set existingSecretKey to match cert-manager's output key (ca.crt by default in values-tls.yaml).
If your ClusterIssuer is signed by an offline root CA, cert-manager outputs only the issuing intermediate. Concatenate the offline root into the bundle before creating the Secret. PKIX validation needs a chain that ends at a root present in the bundle.
Server certs for Elasticsearch, OpenSearch, and PostgreSQL must be issued separately via additional Certificate resources signed by the same ClusterIssuer.
Install-time guardrails
After each helm install or helm upgrade, check the NOTES.txt output for these warnings (re-display it at any time with helm status <release>):
- Per-component JKS overrides the bundle. If a component has a legacy
tls.secretJKS configured, you must remove it to use the bundle. Legacy JKS fields take priority overglobal.tls.caBundle. - Bundle is trust, not encryption.
global.tls.caBundleadds CA trust but does not enable TLS on a plaintext URL. Set the URL tohttps://(or set the JDBCsslmode). JAVA_TOOL_OPTIONSin componentenvoverrides the truststore flags. Set it viajavaOpts(orchestration, optimize, Web Modeler restapi) instead, which the chart appends to.
Verify no plaintext fallback
curl -fsSLO https://raw.githubusercontent.com/camunda/camunda-platform-helm/main/scripts/check-no-plaintext-datastore.sh
chmod +x check-no-plaintext-datastore.sh
./check-no-plaintext-datastore.sh \
--namespace "$NAMESPACE" \
--kube-context "$KUBE_CONTEXT"
Exit code 0 + [no-plaintext-check] PASS means no Camunda pod is talking plaintext to a known datastore.
Updating the CA
kubectl -n "$NAMESPACE" create secret generic camunda-ca-bundle \
--from-file=ca.crt=./new-ca-bundle.pem \
--dry-run=client -o yaml | kubectl -n "$NAMESPACE" apply -f -
kubectl -n "$NAMESPACE" rollout restart \
statefulset,deployment \
-l app.kubernetes.io/part-of=camunda-platform
The init container re-runs on each pod start and imports the new CA into a fresh truststore.
Optional: automatic rollout on helm upgrade
Set global.tls.caBundle.autoRollout: true to stamp a checksum/ca-bundle annotation on Java pods so they automatically roll out when the CA Secret changes.
- Requires
geton Secrets in the release namespace —lookupfails withForbiddenwithout it. - Argo CD and Flux render via
helm template(no cluster access), so the annotation stays constant. Drive restarts from your GitOps stack instead.
Legacy: per-component JKS truststore (deprecated)
Deprecated as of chart 13.x. Affected fields:
global.elasticsearch.tls.secret.existingSecret/existingSecretKeyglobal.opensearch.tls.secret.existingSecret/existingSecretKeyglobal.elasticsearch.tls.jks.secret.*global.opensearch.tls.jks.secret.*optimize.database.elasticsearch.tls.secret.*optimize.database.opensearch.tls.secret.*
If a legacy JKS field and global.tls.caBundle are both set, the legacy field takes precedence. To migrate:
- Convert JKS to PEM:
keytool -list -keystore your.jks -storepass changeit -rfc \
| awk '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/' \
> your-ca-bundle.pem - Create the
camunda-ca-bundleSecret as in step 1. - Remove all
*.tls.secret.existingSecret/*.tls.jks.*entries from your values file. - Remove
-Djavax.net.ssl.trustStore…and-Djavax.net.ssl.trustStoreType=jksfromjavaOpts. - Run
helm upgradewith-f values-tls.yaml.
Common gotchas
Java 21 default trustStoreType is PKCS12
The init container builds a PKCS12 truststore; the chart omits -Djavax.net.ssl.trustStoreType to match the JVM default. If you supply a legacy JKS via tls.secret.existingSecret, add -Djavax.net.ssl.trustStoreType=jks to javaOpts explicitly.
Bitnami PostgreSQL tls.certCAFilename enables mTLS
Do not set tls.certCAFilename on the bundled Bitnami PostgreSQL subchart. It switches PostgreSQL into clientcert=verify-full mode (pg_hba.conf) and breaks plain clients. Use tls.certFilename and tls.certKeyFilename only.
Console and Web Modeler websockets are Node.js
The chart sets both SSL_CERT_FILE and NODE_EXTRA_CA_CERTS on Node.js components automatically. Do not add NODE_EXTRA_CA_CERTS via console.env or webModeler.websockets.env — Kubernetes last-wins env semantics make the value undefined.
In-cluster transport (service mesh required)
global.tls.caBundle covers component-to-datastore and component-to-IdP connections only.
The following in-cluster connections are plaintext by default:
| Connection | Protocol |
|---|---|
| Operate / Tasklist / Connectors → Zeebe gateway | gRPC |
| Web Modeler / Console / Optimize → Identity | REST |
| Spring Boot management / metrics (probes) | HTTP |
Encrypt these at the pod level with a service mesh (Linkerd, Istio, or Cilium). See the TLS coverage matrix for the full connection inventory.
Related
- Helm chart TLS coverage matrix — per-connection support level
- Helm chart
values-tls.yaml— the overlay this guide describes