k8wiz
All graphs

cert-manager graph

A private CA with cert-manager and trust-manager

Run your own certificate authority inside the cluster: a self-signed root, a CA ClusterIssuer, leaf certificates for internal services, and trust-manager copying the root to every namespace so clients trust it.

Services that talk to each other inside the cluster need TLS too, and a public CA won't sign payments-api.payments.svc. The usual answer is a private CA: cert-manager creates a root certificate with a self-signed issuer, then uses that root to sign certificates for your services.

Issuing is half the job. Every client also has to trust the root. trust-manager takes the root's public certificate and copies it into a ConfigMap in each namespace you pick, so Pods can mount it as their CA bundle.

Interactive graph

Click a resource to see what it does, or follow the setup step by step.

You write itCreated by a controllerreferencescreatestraffic

How it works

All 11 resources and what they do

ClusterIssuer selfsigned-bootstrapcert-manager.io/v1
Signs a certificate with its own private key. It exists only to create the root CA, and you shouldn't use it for anything else.
Certificate internal-root-cacert-manager.io/v1
The root CA. isCA: true adds the CA basic constraint, and the long duration avoids frequent root rotations. It lives in the cert-manager namespace because a ClusterIssuer reads its Secret from there.
Secret internal-root-cav1
Holds the root's tls.crt, tls.key and ca.crt. The private key here can sign certificates for any name, so lock down who can read Secrets in this namespace.
ClusterIssuer internal-cacert-manager.io/v1
Signs certificates with the root's key. Any namespace can ask it for a certificate, so pair it with a policy, such as cert-manager's approver-policy, if teams shouldn't get each other's names.
Certificate payments-api-tlscert-manager.io/v1
A server certificate for the payments API, valid for its in-cluster DNS names. It lasts 90 days and renews after 60, so a short lifetime costs nothing.
CertificateRequest payments-api-tls-1cert-manager.io/v1
Carries the CSR in spec.request. An approver must set the Approved condition before the CA issuer signs it. By default cert-manager approves every request itself. Disable that and install approver-policy to control which names each namespace can get.
Secret payments-api-tlsv1
tls.crt and tls.key for the server, plus ca.crt with the root. The payments API mounts it and serves HTTPS with it.
Deployment payments-apiapps/v1
Mounts payments-api-tls as a volume and serves HTTPS on 8443. Most servers don't reload certificates on their own, so either pick one that watches the files or restart it on renewal with a tool such as Reloader.
Bundle internal-ca-bundletrust.cert-manager.io/v1alpha1
A trust-manager object. It reads ca.crt from the root's Secret, adds the public CAs if you ask it to, and writes the result to a ConfigMap in every namespace with the label trust: internal. trust-manager only reads sources from its trust namespace, cert-manager by default.
ConfigMap internal-ca-bundlev1
Written by trust-manager into each matching namespace, here checkout. It holds only public certificates, so it's fine as a ConfigMap. When the root rotates, trust-manager updates every copy.
Deployment checkoutapps/v1
Calls https://payments-api.payments.svc:8443. It mounts the bundle at /etc/ssl/certs/ca-certificates.crt, where most Linux TLS libraries look, so the payments certificate verifies.

Common mistakes

  • A ClusterIssuer of type ca reads its Secret from the cluster resource namespace, cert-manager by default. Put the root's Secret anywhere else and the issuer reports secret not found.
  • Mounting a Secret or ConfigMap with subPath, as the client does here, stops the kubelet from updating that file. That's fine for a root that changes once in years. Mount the whole volume where files must follow renewals.
  • When the root expires, every certificate it signed stops verifying at once. Rotate it well before then: add the new root to the Bundle first, wait for clients to pick it up, then switch the issuer.
  • With the default approver, anyone who can create a Certificate in any namespace can get a certificate for any name from internal-ca. Use a namespaced Issuer per team, or approver-policy, to limit that.

The manifests you write

Everything above that is marked "You write it", in one file. The rest comes from add-ons and controllers.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned-bootstrap
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: internal-root-ca
  namespace: cert-manager
spec:
  isCA: true
  commonName: Example Internal Root CA
  secretName: internal-root-ca
  duration: 87600h # 10 years
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    kind: ClusterIssuer
    name: selfsigned-bootstrap
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  ca:
    secretName: internal-root-ca
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: payments-api-tls
  namespace: payments
spec:
  secretName: payments-api-tls
  duration: 2160h # 90 days
  renewBefore: 720h # 30 days
  dnsNames:
    - payments-api.payments.svc
    - payments-api.payments.svc.cluster.local
  usages: [server auth]
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    kind: ClusterIssuer
    name: internal-ca
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments-api
  namespace: payments
spec:
  selector:
    matchLabels:
      app: payments-api
  template:
    metadata:
      labels:
        app: payments-api
    spec:
      containers:
        - name: api
          image: ghcr.io/acme/payments-api:2.3.0
          args: ["--tls-cert=/tls/tls.crt", "--tls-key=/tls/tls.key"]
          ports:
            - containerPort: 8443
          volumeMounts:
            - name: tls
              mountPath: /tls
              readOnly: true
      volumes:
        - name: tls
          secret:
            secretName: payments-api-tls
---
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
  name: internal-ca-bundle
spec:
  sources:
    - useDefaultCAs: true
    - secret:
        name: internal-root-ca
        key: ca.crt
  target:
    configMap:
      key: ca-certificates.crt
    namespaceSelector:
      matchLabels:
        trust: internal
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
  namespace: checkout
spec:
  selector:
    matchLabels:
      app: checkout
  template:
    metadata:
      labels:
        app: checkout
    spec:
      containers:
        - name: checkout
          image: ghcr.io/acme/checkout:5.1.0
          volumeMounts:
            - name: ca-bundle
              mountPath: /etc/ssl/certs/ca-certificates.crt
              subPath: ca-certificates.crt
              readOnly: true
      volumes:
        - name: ca-bundle
          configMap:
            name: internal-ca-bundle

Keep going

More graphs