Managing Internal TLS Trust in Kubernetes with cert-manager and trust-manager
How do you maintain a secure SSL/TLS connection between services within a Kubernetes cluster?
One simple (but unsafe) answer is to create a self-signed certificate and skip verification with a flag like --insecure-skip-tls-verify.
A more advanced solution is to use a service mesh (e.g., Linkerd, Istio), which automatically manages certificates and trust.
But what if you want something simpler and lighter? That’s where trust-manager comes in. It’s the easiest way to manage trust bundles in Kubernetes and OpenShift clusters.
What Is trust-manager?
trust-manager works together with cert-manager to distribute and manage trusted SSL certificates across namespaces.
These certificates are primarily used to validate other certificates during a TLS handshake, but they can also serve as trust anchors in other scenarios.
Official docs: trust-manager guide
In this post, I’ll walk you through an example setup using cert-manager and trust-manager to create an internal CA and propagate trusted CAs to workloads automatically.
Installing cert-manager and trust-manager
Step 1: Add the Helm Repository
helm repo add jetstack https://charts.jetstack.io
helm repo update
Step 2: Install cert-manager
kubectl create ns cert-manager
helm install cert-manager jetstack/cert-manager \
-n cert-manager \
--set crds.enabled=true
Step 3: Install trust-manager
helm upgrade trust-manager jetstack/trust-manager \
--install \
-n cert-manager
Architecture Overview
The overall flow of certificate issuance and trust distribution looks like this:
graph TD
subgraph CertManager["cert-manager"]
A["Root CA"] --> B["Intermediate CA"]
B --> C["ClusterIssuer (cluster-ca)"]
C --> D["Leaf Certificate (payments-tls)"]
end
subgraph TrustManager["trust-manager"]
A --> E["Trust Bundle (ConfigMap)"]
B --> E["Trust Bundle (ConfigMap)"]
E -->|Replicated to Namespaces| F["ConfigMap: trust-bundle (ca.crt)"]
end
subgraph Application["Application Namespace"]
F -->|Mounted via Volume| G["Application Pod"]
D -->|Used by| G
G -->|Verifies TLS using| F
end
cert-managerhandles certificate creation and signing.trust-managerdistributes CA bundles across namespaces.
Implementation
1. Bootstrap an Internal CA
We’ll create a root CA and an intermediate CA, then expose the intermediate as a cluster-wide issuer.
a) Self-signed bootstrap issuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: bootstrap-selfsigned
spec:
selfSigned: {}
b) Root CA certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: cluster-root-ca
namespace: cert-manager
spec:
isCA: true
commonName: cluster-root-ca
secretName: cluster-root-ca
privateKey:
algorithm: RSA
size: 4096
issuerRef:
name: bootstrap-selfsigned
kind: ClusterIssuer
c) Issuer based on the root CA
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: root-ca
namespace: cert-manager
spec:
ca:
secretName: cluster-root-ca
d) Intermediate CA signed by root
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: cluster-intermediate-ca
namespace: cert-manager
spec:
isCA: true
commonName: cluster-intermediate-ca
secretName: cluster-intermediate-ca
privateKey:
algorithm: RSA
size: 4096
usages: ["cert sign", "crl sign"]
issuerRef:
name: root-ca
kind: Issuer
e) Expose the intermediate as a ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: cluster-ca
spec:
ca:
secretName: cluster-intermediate-ca
Result: any namespace can now request a certificate from ClusterIssuer/cluster-ca.
2. Issue SSL Certificates
Example for a service payments in namespace shop that needs to be accessed by other services in the cluster:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: payments-tls
namespace: shop
spec:
secretName: payments-tls
duration: 2160h
renewBefore: 360h
privateKey:
algorithm: ECDSA
size: 256
dnsNames:
- payments.shop.svc
- payments.shop.svc.cluster.local
issuerRef:
kind: ClusterIssuer
name: cluster-ca
Mount this certificate in your Deployment or reference it in your Ingress resource.
3. Distribute Trusted CA Bundles (trust-manager)
Your workloads must trust the CA that signs the SSL certificates.
trust-manager automates this by creating a ConfigMap containing trusted CAs and replicating it into every labeled namespace.
a) Label namespaces to receive the trust bundle
kubectl label ns shop trust=enabled
b) Define the Bundle
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
name: cluster-trust-bundle
spec:
sources:
- secret:
namespace: cert-manager
name: cluster-root-ca
key: ca.crt
- secret:
namespace: cert-manager
name: cluster-intermediate-ca
key: ca.crt
target:
configMap:
key: ca.crt
name: trust-bundle
namespaceSelector:
matchLabels:
trust: enabled
trust-manager creates a ConfigMap/trust-bundle in every labeled namespace, containing concatenated CA certificates.
c) Mount the trust bundle in your pods
spec:
template:
spec:
volumes:
- name: trust-bundle
configMap:
name: trust-bundle
items:
- key: ca.crt
path: ca.crt
containers:
- name: app
image: your-image
volumeMounts:
- name: trust-bundle
mountPath: /etc/pki/ca-trust/extracted/pem
readOnly: true
env:
- name: SSL_CERT_FILE
value: /etc/pki/ca-trust/extracted/pem/ca.crt
- Go/OpenSSL apps honor
SSL_CERT_FILEorSSL_CERT_DIR. - Java needs a JKS/PKCS12 truststore (you can convert
ca.crtat startup withkeytool).
4. Alternatively, replacing default trust bundle
You can include public root CAs (e.g., Let’s Encrypt, Google, Amazon) along with your internal ones:
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
name: cluster-trust-bundle
spec:
sources:
- useDefaultCAs: true # Public CAs.
- secret:
namespace: cert-manager
name: cluster-root-ca
key: ca.crt
- secret:
namespace: cert-manager
name: cluster-intermediate-ca
key: ca.crt
target:
configMap:
key: ca.crt
name: trust-bundle
namespaceSelector:
matchLabels:
trust: enabled
Mount the bundle in your pods:
spec:
template:
spec:
volumes:
- name: trust-bundle
configMap:
name: trust-bundle
items:
- key: ca.crt
path: ca.crt
containers:
- name: app
image: your-image
volumeMounts:
- name: trust-bundle
mountPath: /etc/ssl/certs/ca-certificates.crt
subPath: ca.crt
This will replace default trust bundle with your CAs, location may vary depending on the runtime.
Conclusion
With cert-manager and trust-manager, you can build a secure, internal PKI system inside your Kubernetes cluster without resorting to insecure self-signed certificates or heavy service meshes.
This setup provides:
- Automated certificate issuance and renewal
- Seamless trust propagation across namespaces
- Compatibility with both internal and public CAs
It’s simple, efficient, and production-ready solution for most use cases where encrypted communication between services is needed.
