Server

ingress-nginx with TLS and cert-manager

By Prabath Thalangama· October 24, 2026· 3 min read
#kubernetes#ingress#tls

Introduction

An Ingress is a set of L7 routing rules (host + path → Service). An Ingress controller — commonly ingress-nginx — watches Ingress objects and configures an actual reverse proxy. cert-manager automates certificate issuance so you never touch a .pem file.

Install

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx \
  -n ingress-nginx --create-namespace

helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
  -n cert-manager --create-namespace --set crds.enabled=true

kubectl get svc -n ingress-nginx — the ingress-nginx-controller Service gets an external IP (cloud LB) or a NodePort (bare metal + MetalLB). Point your DNS A/AAAA records at it.

ClusterIssuer

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            class: nginx

Start with the staging server (https://acme-staging-v02.api.letsencrypt.org/directory) while you get the setup right — production has tight rate limits.

Ingress with TLS

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts: [app.example.com]
      secretName: app-tls          # cert-manager creates this
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 80

cert-manager sees the annotation + tls: block, creates a Certificate, solves the HTTP-01 challenge (it injects a temporary path into your Ingress), and writes the cert into app-tls. ingress-nginx picks it up.

Useful annotations

nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/ssl-redirect: "true"          # default true when TLS present
nginx.ingress.kubernetes.io/rewrite-target: /$2           # with a path regex capture
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"    # websockets/SSE
nginx.ingress.kubernetes.io/enable-cors: "true"

Verification and troubleshooting

kubectl get ingress
kubectl describe ingress app
kubectl get certificate,certificaterequest,order,challenge -A
kubectl describe challenge <name>            # why HTTP-01 is failing
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller
curl -vI https://app.example.com
  • 404 from ingress-nginx ("default backend - 404") — no Ingress matched: wrong host, missing ingressClassName, Ingress in a different namespace than the Service, or the Service has no endpoints.
  • Certificate stuck False / Issuing — check the challenge object. Common: DNS not pointing at the ingress IP yet, HTTP-01 path not reachable (a ssl-redirect loop, or a WAF blocking /.well-known/acme-challenge/), or Let's Encrypt rate-limited (you were on prod while testing).
  • 503 Service Temporarily Unavailable — backend Service has no ready endpoints. kubectl get endpoints <svc>.
  • TLS works but redirects to http:// — app behind ingress doesn't know it's HTTPS; it should trust X-Forwarded-Proto (ingress-nginx sets it).
  • Path routing sends everything to one backend — pathType: Prefix matching is longest-prefix; /api and / both match /api/x and the more specific wins. If using regex, you need pathType: ImplementationSpecific + use-regex annotation.
  • Large uploads fail at ~1 MB — default proxy-body-size. Raise it via annotation.
  • cert-manager: "no configured challenge solvers" — the solvers selector in the issuer doesn't match; simplest is one solver with no selector.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.