Terminate TLS at the Ingress with cert-manager
Request a certificate through an ACME issuer, store it in a Secret, and redirect every plain HTTP request to HTTPS.
Task
Serve the public hostname over HTTPS with automatic issuance and a redirect, without manual certificate uploads.
When to use this
A new hostname is added to the ingress but no certificate exists; the site serves HTTP for two days until someone notices the browser warning.
Prerequisites
- ingress-nginx and cert-manager installed
- A DNS record pointing at the ingress load balancer
- An ACME account, starting with the Let's Encrypt staging endpoint
Steps
1. Create a staging ClusterIssuer so failed validations do not consume production rate limits.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
ingressClassName: nginx
Expected output: The ClusterIssuer reports Ready=True after it is applied.
2. Annotate the Ingress to issue a certificate into a named Secret and force HTTPS.
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- app.example.com
secretName: app-example-tls
Expected output: cert-manager solves the HTTP-01 challenge and writes the app-example-tls Secret.
3. Watch the certificate and the challenge until both are ready.
kubectl get certificate,challenge -n prod -o wideExpected output: The Certificate shows Ready=True and the challenge is marked Valid.
4. Verify the served chain and that plain HTTP redirects.
curl -sI http://app.example.com | head -3 && echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | openssl x509 -noout -issuer -datesExpected output: HTTP returns a redirect to HTTPS, and the certificate is issued by a trusted CA with roughly 90 days of validity.
Troubleshooting
Problem
The HTTP-01 challenge fails with a 404
Solution
The solver ingress is unreachable from the internet. Check DNS, firewall rules, and that the challenge path is not redirected.
Problem
The certificate renewed but browsers show the old one
Solution
The ingress controller has not reloaded the Secret. Check controller logs and restart or reload it, then re-check the chain.
Limitations
- Let's Encrypt rate limits apply, so validate with the staging endpoint first.
- HTTP-01 cannot issue wildcard certificates; wildcards require DNS-01 with provider credentials.
Certificates belong in the deploy path
A manual certificate upload is a credential with an expiry date and no owner. cert-manager fixes that by making the certificate part of the Ingress manifest: an annotation names the issuer, a TLS block names the Secret, and a controller handles issuance and renewal. The first attempt should always use the staging ACME endpoint, because a misconfigured challenge against production burns rate limit that blocks the real fix for hours. Once staging issues successfully, switch the annotation to the production issuer.
Issue and redirect
The HTTP-01 solver needs port 80 reachable from the internet for the challenge path, and the annotation ssl-redirect sends everything else to HTTPS. That combination is why redirecting at the ingress rather than at the application is useful: the challenge stays reachable while user traffic is forced to TLS. Keep the TLS block in the same manifest as the host rule, so adding a hostname cannot silently skip the certificate.
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts: [app.example.com]
secretName: app-example-tls
Verify what clients see
Checking Ready=True proves cert-manager finished, not that the ingress serves the new chain. Close the loop from outside the cluster: fetch the headers over HTTP to confirm the redirect, then read the issuer and expiry from the TLS handshake. Do this after every issuer change and after the first renewal. A renewal that succeeds in the API but never reaches the data plane is invisible until the old certificate expires, which is the failure mode this setup exists to prevent.