If you've ever had to rebuild, re-tag, and redeploy a container image just because an internal CA certificate rotated, this article is for you. We'll walk through why baking certificates into images is an anti-pattern, and two production-ready alternatives that decouple certificate lifecycle from application deployment entirely.
The Problem: Certificates Baked Into the Image
A common pattern for Java applications that need to trust an internal/private CA looks like this:
# Package stage
FROM registry.example.com/eclipse-temurin:21.0.7_6-jre-alpine-3.21
WORKDIR /app
COPY ./internal-ca.crt internal-ca.crt
COPY ./*.jar ./app.jar
RUN keytool -noprompt -import -alias internal-ca \
-keystore "/opt/java/openjdk/lib/security/cacerts" \
-file internal-ca.crt -storepass "changeit"
ENTRYPOINT ["java", "-jar", "app.jar"]
It works, and that's exactly the problem — it works until the certificate rotates. At that point:
- The image must be rebuilt from scratch, even though your application code hasn't changed.
- Every environment (dev, staging, prod) needs a fresh image tag pushed to the registry.
- Every Deployment manifest referencing that tag needs updating and rolling out.
- CI/CD pipelines that have nothing to do with certificate management suddenly become part of the incident response process.
This couples two completely unrelated lifecycles — application releases and certificate rotation — into a single artifact. In regulated environments where CAs rotate on a fixed cadence (90 days, one year, etc.), this becomes a recurring operational headache, not a one-off task. fouts
The Core Idea: Externalize Trust Material
The fix is straightforward in principle: the certificate should never live inside the image. Instead, it should be:
- Stored as a Kubernetes-native object (Secret or ConfigMap).
- Mounted into the pod at runtime.
- Imported into the JVM's truststore by an init container, before the application container starts.
This means your Dockerfile becomes cert-agnostic — it only changes when your application code changes:
# Package stage — no certificate logic at all
FROM registry.example.com/eclipse-temurin:21.0.7_6-jre-alpine-3.21
WORKDIR /app
COPY ./*.jar ./app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Certificate rotation now becomes a Kubernetes object update, not an image rebuild.
Approach 1: Init Container + emptyDir Truststore
This is the most portable option and works on any Kubernetes distribution without extra operators.
Step 1 — Store the certificate as a Secret
kubectl create secret generic internal-ca-cert \
--from-file=ca.crt=./internal-ca.crt \
-n my-namespace
Rotating the cert later is a single idempotent command:
kubectl create secret generic internal-ca-cert \
--from-file=ca.crt=./internal-ca-renewed.crt \
-n my-namespace \
--dry-run=client -o yaml | kubectl apply -f -
Step 2 — Deployment template with init container
The JDK's shipped cacerts file lives on a read-only image layer, so we copy it into a writable emptyDir volume, then run keytool against that copy inside an init container:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: my-namespace
spec:
replicas: 2
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
initContainers:
- name: import-ca-cert
image: registry.example.com/eclipse-temurin:21.0.7_6-jre-alpine-3.21
command:
- sh
- -c
- |
cp /opt/java/openjdk/lib/security/cacerts /truststore/cacerts
keytool -noprompt -import -alias internal-ca \
-keystore /truststore/cacerts \
-file /certs/ca.crt \
-storepass changeit
volumeMounts:
- name: cert-input
mountPath: /certs
- name: truststore
mountPath: /truststore
containers:
- name: app
image: registry.example.com/my-app:1.4.2
env:
- name: JAVA_TOOL_OPTIONS
value: "-Djavax.net.ssl.trustStore=/truststore/cacerts -Djavax.net.ssl.trustStorePassword=changeit"
volumeMounts:
- name: truststore
mountPath: /truststore
volumes:
- name: cert-input
secret:
secretName: internal-ca-cert
- name: truststore
emptyDir: {}
Every time the pod restarts (rolling update, node eviction, scale event), the init container re-imports whatever certificate is currently in the Secret — so the truststore is always current without a rebuild.
Closing the loop: triggering restarts on rotation
Since the JVM reads the truststore only at startup, updating the Secret alone won't refresh a running pod. Pair this with a controller like Reloader that watches Secrets/ConfigMaps and triggers a rolling restart automatically:
metadata:
annotations:
secret.reloader.stakater.com/reload: "internal-ca-cert"
With this annotation on the Deployment, a kubectl apply on the Secret is the only manual step required for full rotation — everything downstream (restart, re-import, rollout) happens automatically.
Approach 2: Cluster-Wide Trust Distribution with cert-manager + trust-manager
If you're managing certificates for many services across many namespaces, manually keeping Secrets in sync everywhere doesn't scale. trust-manager (a companion project to cert-manager) solves this by continuously reconciling a Bundle custom resource into a ConfigMap replicated across selected namespaces — including native JKS output for JVMs. cert-manager
Step 1 — Define a Bundle resource
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
name: internal-ca-bundle
spec:
sources:
- useDefaultCAs: true
- secret:
name: internal-ca-cert
key: ca.crt
target:
configMap:
key: trust-bundle.pem
additionalFormats:
jks:
key: bundle.jks
namespaceSelector:
matchLabels:
trust: "enabled"
Any namespace labeled trust: "enabled" automatically receives (and keeps in sync) a ConfigMap named internal-ca-bundle, containing both a PEM bundle and a ready-to-use bundle.jks file. lestak
Step 2 — Mount it directly, no init container needed
Because the ConfigMap already contains a JKS file, you can skip the keytool step entirely and just point the JVM at it:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: my-namespace
labels:
trust: "enabled"
spec:
replicas: 2
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: registry.example.com/my-app:1.4.2
env:
- name: JAVA_TOOL_OPTIONS
value: "-Djavax.net.ssl.trustStore=/certs/bundle.jks -Djavax.net.ssl.trustStorePassword=changeit"
volumeMounts:
- name: trust-bundle
mountPath: /certs
volumes:
- name: trust-bundle
configMap:
name: internal-ca-bundle
items:
- key: bundle.jks
path: bundle.jks
Kubernetes automatically syncs updated ConfigMap content into the pod's mounted volume within seconds of trust-manager reconciling a rotation — no init container, no manual keytool call, no rebuild. github
Comparing the Two Approaches
| Aspect | Init container + emptyDir | cert-manager + trust-manager |
|---|---|---|
| Extra components required | None (standard K8s features) | cert-manager + trust-manager operators |
| Rotation trigger | Update Secret manually | Automatic reconciliation via Bundle CR |
| Multi-namespace distribution | Manual, per-namespace Secret | Automatic via namespaceSelector |
| JVM truststore format | Built via keytool at pod start | Native JKS output, no keytool needed |
| Restart on rotation | Needs Reloader or similar | Needs Reloader or similar |
| Best for | Single service, simple clusters | Many services, many namespaces, compliance-driven environments |
Key Takeaways
- Never bake rotating trust material into an image layer — it couples unrelated lifecycles and forces unnecessary rebuilds.
- Kubernetes Secrets/ConfigMaps plus volume mounts let certificate updates propagate to running pods without touching the image.
- Init containers are the simplest way to run
keytoolagainst a writable truststore copy at pod startup. -
trust-manageris the better long-term investment if you're distributing the same CA trust to many services across a cluster, since it automates both the reconciliation and the JKS conversion. - Whichever approach you choose, pair it with a restart-triggering controller (e.g., Reloader) so rotation is fully hands-off from Secret update to running pod.
With either pattern, your Dockerfile stays clean, your image stops changing for reasons unrelated to your code, and certificate rotation becomes a routine Kubernetes operation instead of a CI/CD fire drill.
Top comments (0)