NGINX and Angie on Kubernetes: Helm, Ingress, WAF, HTTP/3 and TLS Guide

If your team has finally pushed everything into Kubernetes and you are wondering how to put a sensible web server in front of it, this is your guide. NGINX and Angie on Kubernetes with Helm charts, an Ingress controller, ModSecurity WAF, HTTP/3 over QUIC, cert-manager TLS, and proper autoscaling — this is the production stack most modern teams actually run, and it is much less terrifying than it looks the first time you read about it.

This is not “hello world Kubernetes.” This is “how do I run a real, hardened web server in front of my services without spending a weekend reading YAML.” The NGINX Kubernetes Ingress controller setup walked through here works on any Kubernetes 1.28+ cluster, on EKS, GKE, AKS, k3s, or bare metal. The Angie variant is identical for the bits that matter — Angie is a drop-in NGINX fork from the original developers, with a few extra features that are particularly nice in a Kubernetes context.

Why NGINX (or Angie) for Kubernetes Ingress?

Kubernetes ships with an “ingress” resource type, but it does not ship with an ingress controller. You pick one. The two big choices in 2026 are NGINX (or Angie) and Envoy/Istio. NGINX wins for almost every team that is not running a full service mesh:

  • You already understand it. If you have ever written an nginx.conf, the NGINX Ingress controller’s annotations and ConfigMap will feel familiar in fifteen minutes.
  • Single binary, single config layer. No sidecars, no Envoy admin API, no Istio control plane to operate.
  • Memory footprint that fits on a 1 GB node. Envoy with full Istio is more like 4 GB minimum once you turn on the bells and whistles.
  • Battle-tested ModSecurity WAF. The same OWASP Core Rule Set you run on a bare VPS works inside a pod with no changes.
  • HTTP/3 over QUIC support at the ingress layer — terminating HTTP/3 from clients and proxying HTTP/2 to your services internally.

If you are running 200 microservices with strict mTLS between every pod, you want a service mesh. If you are running fifteen services and you want a fast, secure, easy-to-debug front door, you want NGINX or Angie.

Installing the NGINX Ingress Controller With Helm

Helm is the package manager for Kubernetes. If you do not have it installed, the install is two commands. The NGINX Ingress controller has an official Helm chart maintained by the Kubernetes project, and that is the one to use:

# Add the ingress-nginx chart repository
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

# Create a dedicated namespace for it
kubectl create namespace ingress-nginx

# Install with sensible defaults for production
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --set controller.replicaCount=2 \
  --set controller.metrics.enabled=true \
  --set controller.podAnnotations."prometheus\.io/scrape"=true \
  --set controller.podAnnotations."prometheus\.io/port"=10254

Two replicas is the minimum sensible production deployment. The controller is stateless, so HPA (Horizontal Pod Autoscaler) can scale it horizontally as traffic grows. Metrics enabled means Prometheus can scrape latency and connection counters out of the box.

A Sane values.yaml for Production

The default Helm values are fine for “I’m just trying it out” — they are not fine for production. Drop the following into a values.yaml and pass it with helm install -f values.yaml:

controller:
  replicaCount: 3
  minAvailable: 2

  # Resource requests so the cluster schedules sensibly
  resources:
    requests:
      cpu: 200m
      memory: 256Mi
    limits:
      cpu: 1
      memory: 1Gi

  # Autoscale on CPU and request rate
  autoscaling:
    enabled: true
    minReplicas: 3
    maxReplicas: 12
    targetCPUUtilizationPercentage: 70

  # Service annotations for AWS NLB (adjust per cloud)
  service:
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-type: nlb
      service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"

  # Sensible config for high-traffic sites
  config:
    use-http2: "true"
    enable-brotli: "true"
    brotli-level: "6"
    brotli-types: "text/html text/css application/javascript application/json"
    proxy-body-size: "50m"
    keep-alive: "75"
    keep-alive-requests: "1000"
    use-forwarded-headers: "true"
    enable-real-ip: "true"
    log-format-escape-json: "true"

  metrics:
    enabled: true
    serviceMonitor:
      enabled: true
      namespace: monitoring

Apply with helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx -f values.yaml. Helm handles the diff for you on subsequent runs.

cert-manager and Let’s Encrypt TLS

You do not want to manually rotate TLS certificates in Kubernetes. cert-manager is the de facto add-on that watches Ingress resources, requests Let’s Encrypt certificates over ACME, stores them as Kubernetes secrets, and renews them automatically. Install it once, never think about it again:

# Install cert-manager (one-liner)
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml

# Wait for it to come up
kubectl -n cert-manager rollout status deploy/cert-manager-webhook

Then create a ClusterIssuer that talks to Let’s Encrypt:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: you@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-account
    solvers:
      - http01:
          ingress:
            class: nginx

From this point on, every Ingress resource that references cert-manager.io/cluster-issuer: letsencrypt-prod automatically gets a valid certificate. No Certbot, no cron, no human in the loop.

If you would prefer to skip cert-manager entirely, our Angie complete guide covers Angie’s native ACME client — Angie issues Let’s Encrypt certificates itself, which is genuinely useful for non-Kubernetes workloads.

Your First Real Ingress Resource

An Ingress is just a YAML object that says “route this hostname to this service.” Here is one with TLS, automatic certificate issuance, and a couple of sensible production annotations:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: default
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts: [app.example.com]
      secretName: app-example-com-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80

Apply with kubectl apply -f ingress.yaml. Within about thirty seconds, cert-manager has a valid certificate, NGINX has reloaded its config, and the hostname is live with proper HTTPS. The first time you watch this happen end-to-end is a quiet, “oh that’s nice” moment.

Enabling ModSecurity WAF Inside the Ingress Pod

The NGINX Ingress controller ships with ModSecurity v3 and the OWASP Core Rule Set built in — you just enable it. ModSecurity in Kubernetes is the same WAF you would deploy on a regular server, packaged ready-to-go. Add to your values.yaml:

controller:
  config:
    enable-modsecurity: "true"
    enable-owasp-modsecurity-crs: "true"
    modsecurity-snippet: |
      # Detection mode until you have tuned out false positives
      SecRuleEngine DetectionOnly
      SecRequestBodyAccess On
      SecRequestBodyLimit 13107200
      SecAuditEngine RelevantOnly
      SecAuditLog /var/log/modsec_audit.log

Then enable it per-Ingress when you are ready to actually block traffic:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/enable-modsecurity: "true"
    nginx.ingress.kubernetes.io/enable-owasp-core-rules: "true"
    nginx.ingress.kubernetes.io/modsecurity-snippet: |
      SecRuleEngine On

This pattern — cluster-wide ModSecurity in detection mode, then per-Ingress enforcement once tuned — is the safe way to roll out a WAF without breaking your apps. Our standalone ModSecurity and OWASP CRS guide covers the paranoia level tuning in detail; the same knobs apply inside Kubernetes.

HTTP/3 and QUIC at the Ingress Layer

HTTP/3 over QUIC is genuinely faster on mobile and lossy networks — it removes head-of-line blocking, halves the connection setup round trips, and survives IP-address changes when users switch from Wi-Fi to cellular. The NGINX Ingress controller supports HTTP/3 termination, you just need to expose UDP/443 in addition to TCP/443:

controller:
  service:
    enableHttp3: true
    ports:
      http: 80
      https: 443
    targetPorts:
      http: http
      https: https

  config:
    http3: "true"
    quic-listen: "0.0.0.0:443"
    use-quic: "true"

On most managed Kubernetes services (EKS, GKE, AKS) you also need to ensure the cloud load balancer in front of NGINX passes UDP/443 through. AWS Network Load Balancer supports this natively; classic ELB does not. Check your cloud’s docs — this is the single most common reason HTTP/3 silently fails in a cluster.

Autoscaling: HPA and Cluster Autoscaler

The NGINX Ingress controller is stateless, so it autoscales beautifully. Two layers of autoscaling work together:

  • Horizontal Pod Autoscaler (HPA) — scales the number of NGINX pods based on CPU or custom metrics. Already set up in the values.yaml above.
  • Cluster Autoscaler — adds Kubernetes nodes when there are not enough resources for the pods HPA wants to spin up.

For traffic-based autoscaling (much more useful than CPU on a network-bound service), use a custom metric:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ingress-nginx-controller
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Pods
      pods:
        metric:
          name: nginx_ingress_controller_requests_per_second
        target:
          type: AverageValue
          averageValue: "500"

This scales up when each pod is handling more than 500 requests per second on average. Tune the target to your real traffic — start with a number low enough that you scale early under load.

Monitoring and Observability

The NGINX Ingress controller exports a generous set of Prometheus metrics on port 10254 by default. Once the metrics endpoint is scraped, you get latency histograms, connection counters, upstream response times, and per-host request rates. Three dashboards worth installing:

  • Grafana dashboard 9614 — the official ingress-nginx dashboard. Drop it in and most operational questions are one click away.
  • SLO tracker — measure 99th-percentile latency against your service-level objective; alert when burn rate gets uncomfortable.
  • ModSecurity events panel — block counts per CRS rule, useful for spotting attack waves and tuning false positives.

Angie on Kubernetes

Angie is a NGINX fork from the original developers with a few features that are particularly nice in a Kubernetes environment: a native ACME client (no cert-manager required for plain workloads), a JSON status API (no exporter sidecar needed), and dynamic upstream changes that propagate without a reload. The Angie Ingress controller follows the same Helm pattern; configuration syntax is virtually identical because Angie reads nginx.conf as-is.

For full coverage of what Angie adds over NGINX, see our Angie web server complete guide. The choice between NGINX and Angie inside Kubernetes is largely a matter of taste; both work, both have good Helm charts, both run the same OWASP CRS.

Frequently Asked Questions

Do I really need a Kubernetes Ingress controller — what is wrong with a plain LoadBalancer service?

A LoadBalancer service gives you one cloud load balancer per service, which gets expensive fast and burns IPv4 addresses you do not have. An Ingress controller multiplexes hostnames and paths through a single load balancer, terminates TLS in one place, runs a single WAF for the whole cluster, and gives you centralised logging. For more than two or three exposed services, an Ingress controller is the only sane choice.

Should I use NGINX, Angie, or Envoy for Kubernetes Ingress?

If you are not already running a service mesh, NGINX or Angie — they share the same configuration model your team already understands, run on a fraction of the memory, and ship with battle-tested ModSecurity. Envoy or Istio if you need full mTLS between every pod, multi-cluster routing, or a heavyweight policy engine.

Does the NGINX Ingress controller support HTTP/3 and QUIC?

Yes, modern versions of the ingress-nginx Helm chart can terminate HTTP/3 over QUIC at the ingress layer. You need to enable http3 in the controller config and ensure UDP/443 is forwarded through whichever cloud load balancer sits in front of NGINX (AWS NLB supports it; classic ELB does not).

How do I get a free SSL certificate for an Ingress?

Install cert-manager, create a ClusterIssuer that points at Let’s Encrypt, then add the cert-manager.io/cluster-issuer annotation to your Ingress resource. cert-manager issues, stores, and renews the certificate automatically — you never touch Certbot or a cron job.

Can I run ModSecurity WAF inside the NGINX Ingress controller?

Yes — the ingress-nginx image ships with ModSecurity v3 and the OWASP Core Rule Set already inside it. Enable it via Helm values (enable-modsecurity, enable-owasp-modsecurity-crs) and start in DetectionOnly mode; switch to enforcing once you have tuned out false positives in the audit log.

How does autoscaling work for the Ingress controller?

The controller is stateless, so HPA scales it horizontally based on CPU or — better — request rate metrics scraped by Prometheus. Cluster Autoscaler adds nodes when the pods HPA wants do not fit. Three replicas as a floor, a dozen or so as the ceiling, is a healthy starting range for a medium-traffic site.

Where do the Kubernetes ModSecurity audit logs go?

Inside the controller pod, the audit log is written to /var/log/modsec_audit.log by default. The clean pattern is to either mount a persistent volume for it or — much better — ship it to your central log aggregator (Loki, ELK, or your cloud’s logging service) using a sidecar or DaemonSet log collector.

Related Posts