Automate HTTPS for Django on Kubernetes With cert-manager
Use cert-manager to issue and renew Let’s Encrypt certificates for a Django app on Kubernetes: install it with Helm, create a ClusterIssuer with HTTP-01 or DNS-01, request certificates through Ingress or Gateway annotations, and troubleshoot stuck challenges.
Problem statement
Your Django app runs on Kubernetes behind an ingress controller or Gateway, and you need HTTPS certificates that renew automatically for every host name: the main site, the API subdomain, and maybe customer domains. Running Certbot by hand does not fit a cluster. cert-manager is the standard Kubernetes controller for this: it requests certificates from Let's Encrypt (or another ACME CA), stores them in Secrets, and renews them before expiry.
- Install cert-manager with Helm, including CRDs:
helm install cert-manager jetstack/cert-manager -n cert-manager --create-namespace --set crds.enabled=true. - Create a
ClusterIssuerfor Let's Encrypt staging and one for production. - Use HTTP-01 when the ingress is publicly reachable on port 80; use DNS-01 for wildcards or private clusters.
- Request certificates by annotating the Ingress (
cert-manager.io/cluster-issuer: letsencrypt-prod) or Gateway, or with aCertificateresource. - Check
kubectl describe certificate,order,challengewhen issuance stalls.
Step-by-step solution
1. Install cert-manager
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--set crds.enabled=true
kubectl -n cert-manager get pods
Pin a chart version in production and upgrade deliberately; CRD changes come with major versions.
2. Create issuers
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-prod-account
solvers:
- http01:
gatewayHTTPRoute:
parentRefs:
- name: public-gateway
namespace: gateway
Create a matching letsencrypt-staging issuer with the staging directory URL and test with it first; Let's Encrypt production has rate limits. With a classic Ingress controller, the solver is http01: { ingress: { ingressClassName: <class> } } instead.
3. Request a certificate
With Gateway API, reference a TLS Secret on the listener and let cert-manager create it via a Certificate:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: shop-tls
namespace: gateway
spec:
secretName: shop-tls
issuerRef: { name: letsencrypt-prod, kind: ClusterIssuer }
dnsNames: ["shop.example.com", "api.shop.example.com"]
With an Ingress, add the annotation and a tls block; cert-manager creates the Certificate automatically:
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts: ["shop.example.com"]
secretName: shop-tls
4. Make Django HTTPS-aware
TLS terminates at the gateway or ingress, which forwards plain HTTP to Django pods. Set:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SECURE_REDIRECT_EXEMPT = [r"^healthz/"]
Exempting the health path keeps Kubernetes probes, which use plain HTTP, from getting redirects.
5. Troubleshoot issuance
kubectl describe certificate shop-tls -n gateway
kubectl get orders,challenges -A
kubectl describe challenge <name> -n gateway
Common causes of stuck challenges: DNS not yet pointing at the load balancer, port 80 blocked, an HTTP-to-HTTPS redirect applied before the challenge path, or the solver configured for the wrong ingress class or Gateway.
Explanation
cert-manager adds custom resources (Issuer, ClusterIssuer, Certificate, Order, Challenge) and controllers that run the ACME protocol. For each Certificate, it creates an order, solves the challenges by creating temporary routes or DNS records, stores the issued certificate and key in a Secret, and renews it automatically, by default when two-thirds of the lifetime has passed. Ingress controllers and Gateways read the Secret, so Django never handles certificates.
Edge cases / notes
- Rate limits. Let's Encrypt limits certificates per registered domain per week; use staging while experimenting.
- Many customer domains. For per-tenant custom domains, each needs DNS pointing at your cluster and its own certificate; consider on-demand TLS with a proxy like Caddy for large numbers.
- Secret replication. Gateways in another namespace need the Secret where the listener lives, or a ReferenceGrant.
Next steps
- Route traffic with Expose Django on Kubernetes With Gateway API or Ingress.
- Review the full cluster setup in Deploy Django on Kubernetes.
- Watch expiry anyway with Monitor TLS Certificate Expiry for Django.
FAQ
Does cert-manager renew certificates automatically?
Yes, before they expire, and updates the Secret the ingress or Gateway uses.
HTTP-01 or DNS-01?
HTTP-01 for public sites without wildcards; DNS-01 for wildcard certificates or private clusters.
Why is my certificate stuck in pending?
Usually the challenge cannot be validated: DNS, port 80, redirects, or solver configuration. Describe the Challenge resource.
Does Django need to know about cert-manager?
No. Django only needs SECURE_PROXY_SSL_HEADER because TLS ends at the ingress.