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-bundlecert-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.
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: trueadds the CA basic constraint, and the long duration avoids frequent root rotations. It lives in thecert-managernamespace because a ClusterIssuer reads its Secret from there. - Secret internal-root-cav1
- Holds the root's
tls.crt,tls.keyandca.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 theApprovedcondition 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.crtandtls.keyfor the server, plusca.crtwith the root. The payments API mounts it and serves HTTPS with it.- Deployment payments-apiapps/v1
- Mounts
payments-api-tlsas 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.crtfrom 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 labeltrust: internal. trust-manager only reads sources from its trust namespace,cert-managerby 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
careads its Secret from the cluster resource namespace,cert-managerby default. Put the root's Secret anywhere else and the issuer reportssecret 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.
Keep going
More graphs
- Deployment, Service and Ingress: how a request reaches your Pods
- Gateway API: GatewayClass, Gateway and HTTPRoute with a canary split
- PVC, StorageClass and CSI: how a cloud disk ends up in your Pod
- RBAC: how a ServiceAccount gets its permissions
- cert-manager with Let's Encrypt: Certificate, CertificateRequest, Order and Challenge
- Istio traffic management: Gateway, VirtualService, DestinationRule and policies
- Flux dependencies: Git and OCI sources, Kustomizations and HelmReleases
- Argo CD: ApplicationSet, AppProject and the Applications it generates
- Inside an OCI image: tag, index, manifests, config and layers
- OCI artifacts: Helm charts, Flux artifacts and image volumes