Securing Kubernetes Ingress#

The ingress controller is the front door to your cluster. Every request from the internet passes through it, making it both the most exposed component and the best place to enforce security controls. Most teams deploy an ingress controller and stop at basic routing. That leaves the door wide open.

TLS Termination and HTTPS Enforcement#

Every ingress should terminate TLS. Never serve production traffic over plain HTTP. With nginx-ingress, force HTTPS redirects and add HSTS headers:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/hsts: "true"
    nginx.ingress.kubernetes.io/hsts-max-age: "31536000"
    nginx.ingress.kubernetes.io/hsts-include-subdomains: "true"
    nginx.ingress.kubernetes.io/hsts-preload: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - app.example.com
      secretName: app-tls-cert
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-service
                port:
                  number: 8080

Use cert-manager to automate certificate issuance and renewal. Manually managing TLS secrets is a guaranteed path to expired certificates in production.

Rate Limiting#

Rate limiting at the ingress prevents abuse before requests reach your application. Nginx-ingress supports rate limiting through annotations:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/limit-rps: "10"
    nginx.ingress.kubernetes.io/limit-rpm: "300"
    nginx.ingress.kubernetes.io/limit-connections: "5"
    nginx.ingress.kubernetes.io/limit-burst-multiplier: "3"
    nginx.ingress.kubernetes.io/limit-whitelist: "10.0.0.0/8"

limit-rps sets requests per second per client IP. limit-burst-multiplier allows short bursts (3x the rate limit). limit-whitelist exempts internal subnets from rate limiting – use this for health checks and internal services that call the endpoint heavily.

For login or authentication endpoints, apply stricter limits:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/limit-rps: "3"
    nginx.ingress.kubernetes.io/limit-burst-multiplier: "1"

WAF Integration with ModSecurity#

Nginx-ingress supports ModSecurity as a built-in WAF. Enable it globally in the ingress controller ConfigMap, then control it per-Ingress resource:

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  enable-modsecurity: "true"
  enable-owasp-modsecurity-crs: "true"
  modsecurity-snippet: |
    SecRuleEngine On
    SecAuditEngine On
    SecAuditLogType Serial
    SecAuditLog /var/log/modsec_audit.log

To disable ModSecurity for a specific ingress (for example, a file upload endpoint that triggers false positives), use per-Ingress annotations:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/enable-modsecurity: "false"

For fine-grained rule tuning on a specific ingress:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/modsecurity-snippet: |
      SecRuleRemoveById 932100 932110

IP Whitelisting#

Restrict access to admin or internal endpoints by source IP:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8,192.168.1.0/24,203.0.113.50/32"

This annotation accepts a comma-separated list of CIDR ranges. Requests from IPs outside these ranges receive a 403. Apply this to admin panels, internal APIs, and monitoring dashboards that should never be publicly accessible.

Authentication at the Ingress Level#

Basic Auth#

For simple internal tools, basic authentication at the ingress avoids modifying the application:

# Create the htpasswd file and secret
htpasswd -c auth admin
kubectl create secret generic basic-auth --from-file=auth -n production
metadata:
  annotations:
    nginx.ingress.kubernetes.io/auth-type: basic
    nginx.ingress.kubernetes.io/auth-secret: basic-auth
    nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"

OAuth2 Proxy#

For production authentication, deploy OAuth2 Proxy and use external auth:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/auth-url: "https://oauth2.example.com/oauth2/auth"
    nginx.ingress.kubernetes.io/auth-signin: "https://oauth2.example.com/oauth2/start?rd=$scheme://$host$escaped_request_uri"
    nginx.ingress.kubernetes.io/auth-response-headers: "X-Auth-Request-User,X-Auth-Request-Email"

The ingress controller sends a subrequest to the auth URL before forwarding to the backend. If the auth service returns a 401 or 403, the user is redirected to sign in. Authenticated user identity is passed to the backend in headers.

Request Size Limits and Security Headers#

Block oversized requests and add security headers:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "1m"
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "X-Content-Type-Options: nosniff";
      more_set_headers "X-Frame-Options: DENY";
      more_set_headers "X-XSS-Protection: 1; mode=block";
      more_set_headers "Referrer-Policy: strict-origin-when-cross-origin";
      more_set_headers "Content-Security-Policy: default-src 'self'";

Setting proxy-body-size to 1m prevents large payload attacks. Adjust this for endpoints that legitimately accept file uploads.

Securing the Ingress Controller Itself#

The ingress controller runs with significant cluster privileges. Isolate it:

  1. Dedicated namespace. Deploy the controller in ingress-nginx, not in kube-system or an application namespace.

  2. Network policies. Restrict what the controller can reach:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ingress-controller-egress
  namespace: ingress-nginx
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: ingress-nginx
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: TCP
          port: 8080
        - protocol: TCP
          port: 443
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
  1. RBAC. The controller needs to watch Ingress and Service resources. Use the Helm chart’s default RBAC, which is already scoped appropriately. Do not grant it cluster-admin.

  2. Keep it updated. Ingress controllers are internet-facing and regularly have CVEs. Pin versions in your Helm values and update deliberately.