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: 8080Use 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.logTo 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 932110IP 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 productionmetadata:
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:
-
Dedicated namespace. Deploy the controller in
ingress-nginx, not inkube-systemor an application namespace. -
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-
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.
-
Keep it updated. Ingress controllers are internet-facing and regularly have CVEs. Pin versions in your Helm values and update deliberately.