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-manager handles certificate creation and signing.
  • trust-manager distributes 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
Tip
  • Go/OpenSSL apps honor SSL_CERT_FILE or SSL_CERT_DIR.
  • Java needs a JKS/PKCS12 truststore (you can convert ca.crt at startup with keytool).

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.