Overview
cert-manager automates issuance and renewal of TLS certificates inside Kubernetes, backed by ACME (Let's Encrypt), a self-hosted CA, or Vault. This guide covers a production install on a self-managed cluster running on RamNode infrastructure (KVM/OpenStack nodes, kubeadm or k3s), with Let's Encrypt as the primary issuer.
Assumptions
- Kubernetes 1.27+ cluster reachable via
kubectl, nodes provisioned on RamNode KVM/OpenStack instances - Helm 3 installed
- Public DNS for the zones you'll issue certs for is on Cloudflare — used here for DNS-01 challenges
- An nginx-ingress or Traefik ingress controller already deployed, OR you're issuing certs for non-ingress workloads (mail, internal services, etc.)
1. Install cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update
kubectl create namespace cert-manager
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version v1.16.2 \
--set crds.enabled=true \
--set prometheus.enabled=trueVerify:
kubectl get pods -n cert-manager
# cert-manager, cert-manager-cainjector, cert-manager-webhook should be Running2. Choose a challenge type
| Challenge | Use when | Notes |
|---|---|---|
| HTTP-01 | Cert is for a host with a public ingress already routed | Simplest, but needs port 80 reachable per-host |
| DNS-01 | Wildcard certs, internal-only hosts, or multi-region hosts (ATL, EWR, PNW, LAX, NLX) that don't all sit behind one ingress | Requires a DNS API token — use Cloudflare |
For RamNode's multi-datacenter footprint, DNS-01 via Cloudflare is recommended as the default — it avoids needing HTTP-01 reachability from every region and supports wildcards for things like *.apps.example.com or per-DC subdomains.
Cloudflare API token scope
Create a scoped token (not the global key) with Zone.DNS: Edit on the relevant zone(s).
kubectl create secret generic cloudflare-api-token \
--namespace cert-manager \
--from-literal=api-token=<CF_SCOPED_TOKEN>3. Create a ClusterIssuer
# cluster-issuer-letsencrypt.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- "ramnode.com"Apply, and add a letsencrypt-staging issuer pointed at https://acme-staging-v02.api.letsencrypt.org/directory for testing — always validate against staging first to avoid Let's Encrypt rate limits.
kubectl apply -f cluster-issuer-letsencrypt.yaml
kubectl describe clusterissuer letsencrypt-prod4. Request a certificate
Option A — via Ingress annotation (HTTP-01 or DNS-01)
metadata:
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- lookingglass.ramnode.com
secretName: lookingglass-tlsOption B — explicit Certificate resource (recommended for non-ingress or wildcard certs)
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-ramnode
namespace: default
spec:
secretName: wildcard-ramnode-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- "*.ramnode.com"
- "ramnode.com"kubectl apply -f certificate.yaml
kubectl get certificate -A
kubectl describe certificate wildcard-ramnodeA Ready: True condition confirms issuance. Cert renews automatically at ~2/3 of its lifetime (Let's Encrypt certs are 90 days, so renewal fires around day 60).
5. Multi-datacenter considerations
If your nodes span RamNode regions (ATL, EWR, PNW, LAX, NLX):
- If each region runs its own cluster (rather than one cluster spanning regions), each cluster needs its own cert-manager install and its own Cloudflare token secret — tokens aren't automatically shared across clusters.
- Consider a single wildcard cert issued once and synced via a tool like
kubernetes-replicatoror manual secret sync if multiple clusters need to present the same cert (e.g.,*.example.commirrored across regions), rather than re-issuing identical certs five times against Let's Encrypt's rate limits. - For internal-only services (Nagios, NRPE endpoints, jump-host-facing dashboards) that don't need public trust, consider a self-signed
ClusterIssuer(kind: CA) instead of burning Let's Encrypt issuance quota.
6. Operational checklist
- Staging issuer validated before switching Ingress/Certificate to
letsencrypt-prod - Cloudflare token scoped to DNS edit only, stored as a Secret (not committed to the Ansible repo)
-
cert-manager-controllerpod resource requests set (default is fine for small clusters, bump for large fleets) - Prometheus scraping enabled (
--set prometheus.enabled=trueabove) and alerting oncertmanager_certificate_expiration_timestamp_secondsto catch renewal failures before expiry - Confirm webhook TLS bootstrap succeeded:
kubectl get validatingwebhookconfigurations | grep cert-manager
7. Common failure modes
| Symptom | Likely cause |
|---|---|
Certificate stuck in Issuing | DNS-01 record not propagating — check kubectl describe order and challenge resources |
401 from Cloudflare | Token missing Zone.DNS:Edit scope or wrong zone |
| Rate limit errors from Let's Encrypt | Testing against prod instead of staging, or re-issuing wildcard too often across regions |
Webhook timeout on cert-manager install | CNI/network policy blocking the webhook pod — check if cluster has restrictive NetworkPolicies |
