← Recipes

阅读中文版本

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.

~30 mintlskubernetesingress

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 wide

Expected 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 -dates

Expected 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.