← Back to blog

Before You Migrate: Five Surprising Ingress-NGINX Behaviors You Need to Know

kubernetesk8sdevopsorchestrationcloud

In the hyper-converged, multi-cloud Kubernetes landscapes of 2026, the humble Ingress controller remains an unsung hero. For many, ingress-nginx is the default choice, a workhorse that has proven its mettle across countless production environments. Its robustness, flexibility, and mature feature set make it the go-to solution for routing external traffic into our clusters.

But let’s be real: “default” doesn’t mean “simple.” As our applications grow more complex, demanding zero-downtime deployments, sophisticated traffic management, and stringent security, the subtle nuances of ingress-nginx can quickly become major headaches. Especially when you’re planning a critical migration – be it an upgrade, a cluster re-platform, or even just adopting new traffic policies – overlooking these behaviors can turn a smooth transition into a production incident.

Today, we’re diving deep into five surprising (and often overlooked) ingress-nginx behaviors that, if misunderstood, can cause significant operational friction. We’ll explore why they matter, how they work under the hood, and how to tame them with actionable best practices and code examples, ensuring your Kubernetes migrations are resilient and predictable.


1. The Double-Edged Sword of server-snippet and configuration-snippet

Ah, snippets. The ultimate escape hatch. Need a quick NGINX directive that isn’t exposed by an annotation? Just drop it into a server-snippet or configuration-snippet annotation, and ingress-nginx will inject it directly into the generated NGINX configuration. Powerful, right? Absolutely. But also potentially catastrophic.

The Problem: These annotations allow you to insert arbitrary NGINX configuration into the server or http block respectively. While incredibly flexible, they bypass the controller’s logic and validation. This means you can easily:

  • Introduce Syntax Errors: Malformed NGINX directives will crash the NGINX worker processes upon reload.
  • Override Critical Directives: Accidentally overwrite essential NGINX settings that the controller relies on, leading to unexpected routing, broken TLS, or performance degradation.
  • Create Security Vulnerabilities: Improperly configured directives can expose sensitive information or weaken security postures.
  • Increase Maintenance Burden: Snippets are bespoke. They don’t benefit from the controller’s upgrade paths or standard validation, becoming a unique snowflake you’ll have to manually re-evaluate with every major ingress-nginx version bump.

The Behavior: The controller builds an NGINX configuration file based on your Ingress resources, ConfigMap parameters, and its internal templates. Snippets are injected at specific points in this generated configuration. Their placement means they can override default values or introduce new logic, often after the controller has made its own decisions.

Best Practice & Mitigation:

  • Use Sparingly and Document Relentlessly: Only use snippets when there’s absolutely no other way. Clearly document why a snippet is needed and what it does.
  • Understand NGINX Configuration Hierarchy: Before adding a snippet, know exactly where it will be injected and what NGINX directives it might interact with or override.
  • Prefer Dedicated Annotations and ConfigMap Parameters: Always check if an existing annotation or a global ConfigMap parameter (e.g., enable-h2c, proxy-read-timeout) can achieve your goal. These are validated and maintained by the ingress-nginx project.
  • Test Thoroughly: Any Ingress with a snippet should be subjected to rigorous testing, especially during migrations or upgrades.

Example: Custom Error Page with server-snippet (Use with Caution!)

Let’s say you desperately need a custom 503 error page for a specific application’s Ingress.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/server-snippet: |
      error_page 503 /custom-503.html;
      location = /custom-503.html {
          internal;
          return 200 "<h1>Service Temporarily Unavailable - Please try again soon.</h1>";
      }
spec:
  ingressClassName: nginx
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-service
            port:
              number: 80

This snippet injects custom error_page and location directives into the server block for myapp.example.com. While it works, it’s a perfect example of a behavior that’s outside the standard Ingress API and needs careful management. For custom error pages, consider a global default-backend-service or dedicated error page services where possible.


2. Backend Protocol Auto-Detection and TLS Passthrough Misconceptions

When ingress-nginx talks to your backend services, it generally assumes HTTP. But what if your service speaks HTTPS internally? Or what if you want true TLS passthrough, where the Ingress controller doesn’t even decrypt the traffic? This is where auto-detection and common misconceptions often collide.

The Problem: By default, ingress-nginx will attempt to connect to your backend services using HTTP. If your service is listening on HTTPS, this will result in failed connections (often 502 Bad Gateway errors). Many users also confuse “Ingress-NGINX terminates TLS and re-encrypts to the backend” with “TLS passthrough.” True TLS passthrough means the Ingress controller passes encrypted TCP streams directly to the backend without decryption.

The Behavior:

  • Auto-Detection: ingress-nginx has some heuristics to guess the backend protocol, but it’s not foolproof. It primarily relies on the Service port name if it starts with https-.
  • Re-Encryption (Default for HTTPS Backends): If you use nginx.ingress.kubernetes.io/backend-protocol: "HTTPS", the Ingress controller will terminate TLS from the client, decrypt the request, and then re-encrypt it before sending it to your backend service. This is standard and secure, but not TLS passthrough.
  • True TLS Passthrough: Requires a specific configuration via the tcp-services ConfigMap and often a separate port on your LoadBalancer. The Ingress controller itself acts as a Layer 4 proxy, effectively bypassing its Layer 7 (HTTP) features for that specific port/service.

Best Practice & Mitigation:

  • Explicitly Define Backend Protocol: Always use nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" if your backend service serves traffic over TLS. This makes your configuration explicit and avoids auto-detection issues.
  • Understand Re-encryption vs. Passthrough: If you need client-certificate authentication or end-to-end encryption where the Ingress controller cannot decrypt traffic (e.g., specific gRPC scenarios, databases), you need true TLS passthrough. For this, ingress-nginx’s tcp-services ConfigMap is your friend.
  • Consider Dedicated L4 Load Balancers: For complex TLS passthrough scenarios, sometimes it’s simpler and more robust to use a dedicated L4 load balancer or a specialized L4 proxy in front of your Ingress.

Example: Explicit HTTPS Backend Protocol

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: secure-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" # Crucial for HTTPS backends
    nginx.ingress.kubernetes.io/proxy-ssl-verify: "on"   # Optional: Verify backend cert
spec:
  ingressClassName: nginx
  rules:
  - host: secureapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-secure-app-service
            port:
              number: 443 # Backend service listening on 443

For True TLS Passthrough (via tcp-services ConfigMap):

First, define your ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: tcp-services
  namespace: ingress-nginx
data:
  9000: "default/my-passthrough-service:9000" # Maps LB port 9000 to my-passthrough-service port 9000

Then, ensure your ingress-nginx service (of type LoadBalancer) exposes port 9000. This configuration bypasses HTTP handling completely for traffic on port 9000.


3. The Peculiar Order of Rewrites and Redirects

URL manipulation is a cornerstone of modern web applications. ingress-nginx offers several powerful annotations for this: rewrite-target, permanent-redirect, temporal-redirect, and the all-encompassing server-snippet for rewrite directives. But their order of evaluation is critical and often misunderstood.

The Problem: Applying multiple rewrite or redirect annotations, or mixing them with snippets, can lead to unexpected behavior, infinite redirect loops, or incorrect path resolution. rewrite-target behaves very differently from an NGINX rewrite directive or an HTTP 301/302 redirect.

The Behavior:

  • nginx.ingress.kubernetes.io/rewrite-target: This annotation modifies the URI before NGINX performs location matching. It’s a path transformation that happens internally to the NGINX processing, effectively changing what path NGINX tries to match against its location blocks. It often works best with regex capture groups in the path field.
  • nginx.ingress.kubernetes.io/permanent-redirect (301) and temporal-redirect (302): These trigger an external HTTP redirect response to the client. NGINX will process the initial request, then send a redirect header back to the client, telling it to go to a different URL. This happens after location matching and is generally the last thing NGINX does for a request before proxying.
  • NGINX rewrite directive (via server-snippet or location-snippet): A raw rewrite directive behaves according to NGINX’s internal processing order, which can be complex. Depending on where it’s placed (server context vs. location context), it can change the URI multiple times, and even trigger internal sub-requests.

Best Practice & Mitigation:

  • Prioritize rewrite-target for Internal Path Adjustments: Use rewrite-target when you want to map a user-facing URL to a different internal path on your backend service, often in conjunction with regex.
  • Use permanent-redirect for External URL Changes: When you want to tell clients (and search engines) that a resource has moved permanently or temporarily, use the redirect annotations.
  • Avoid Mixed Use Unless Fully Understood: Don’t combine rewrite-target with permanent-redirect on the same Ingress path unless you’ve thoroughly tested and understand the exact NGINX configuration generated.
  • Be Wary of server-snippet for Rewrites: If you must use server-snippet for rewrite directives, understand the last and break flags and their implications on NGINX’s request processing cycle.

Example: rewrite-target for API Versioning

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2 # Captures the second group
spec:
  ingressClassName: nginx
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /v1(/|$)(.*) # Matches /v1/path or /v1
        pathType: Prefix
        backend:
          service:
            name: my-api-v1-service
            port:
              number: 80
      - path: /v2(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: my-api-v2-service
            port:
              number: 80

Here, /v1/users becomes /users for the my-api-v1-service. The rewrite happens before the backend receives the request.


4. The Default Backend and the Elusive 404

Every ingress-nginx deployment has a “default backend.” This is the service that catches all requests that don’t match any Ingress rule. It’s often a simple 404 page, but its behavior can be surprisingly complex, especially in multi-tenancy or multi-Ingress environments.

The Problem:

  • Generic 404s: The default 404 page provided by ingress-nginx is, by design, generic. This might not align with your branding or provide helpful context for users.
  • Unexpected Routing: If a request’s Host header doesn’t match any Ingress rule, or if the path doesn’t match within a host’s rules, it falls to the default backend. This can lead to sensitive information being inadvertently served by the default backend if it’s not strictly controlled.
  • Multiple Ingresses for Same Host: What happens if two Ingress resources claim the same host, but with different paths? NGINX’s internal matching order (longest prefix match, then alphabetical, etc.) can lead to unexpected routing if not explicitly managed.

The Behavior: The ingress-nginx controller is started with a --default-backend-service argument, pointing to a service (e.g., default/default-http-backend). This service handles all requests that don’t have a matching Host header and path rule within any defined Ingress resource.

Best Practice & Mitigation:

  • Deploy a Custom Default Backend: Always deploy your own custom 404/default backend service. This allows you to control the branding, provide helpful links, or even log unroutable requests.
  • Strict Host Matching: Ensure all your Ingress rules are as specific as possible regarding Host headers. Avoid catch-all Ingress rules (host: "*") unless explicitly needed for a custom default.
  • Order of Precedence: While ingress-nginx attempts to resolve conflicts, rely on specific path types (e.g., Exact over Prefix) and distinct Host rules to avoid ambiguous routing.
  • Monitoring: Monitor traffic hitting your default backend. A high volume often indicates misconfigured Ingress rules or malicious probing.

Example: Deploying a Custom Default Backend

# 1. Custom 404 Page Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: custom-default-backend
  namespace: ingress-nginx # Deploy in the same namespace as your controller
spec:
  replicas: 1
  selector:
    matchLabels:
      app: custom-default-backend
  template:
    metadata:
      labels:
        app: custom-default-backend
    spec:
      containers:
      - name: custom-default-backend
        image: nginxdemos/hello:latest # Or a custom image serving your 404 HTML
        ports:
        - containerPort: 8080
        env:
        - name: NGINX_PORT
          value: "8080"
---
# 2. Service for the Custom 404 Backend
apiVersion: v1
kind: Service
metadata:
  name: custom-default-backend-service
  namespace: ingress-nginx
spec:
  selector:
    app: custom-default-backend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

Then, you would pass --default-backend-service=ingress-nginx/custom-default-backend-service to your ingress-nginx controller deployment arguments.


5. Connection Draining and Graceful Reloads During Controller Upgrades

In the quest for operational excellence, zero-downtime deployments are paramount. While ingress-nginx boasts rapid configuration reloads, the graceful handling of active connections during controller upgrades or pod restarts is a nuanced area that frequently trips up even experienced teams.

The Problem: When an ingress-nginx controller pod terminates (e.g., during a rolling update, scaling down, or node drain), active connections to that specific pod will be abruptly cut if not handled gracefully. This leads to client-side connection errors, incomplete requests, and a degraded user experience, especially for long-lived connections like WebSockets, server-sent events, or file uploads.

The Behavior: NGINX handles configuration reloads (nginx -s reload) by starting new worker processes with the updated configuration and gracefully shutting down old ones after they finish processing existing requests. However, when the entire NGINX pod is terminated by Kubernetes, the process is different:

  1. Kubernetes sends a SIGTERM to the pod’s containers.
  2. The ingress-nginx controller’s TERM handler starts gracefully shutting down its NGINX master process. This involves telling NGINX workers to stop accepting new connections and finish existing ones.
  3. Kubernetes waits for terminationGracePeriodSeconds. If the processes haven’t exited by then, it sends SIGKILL.

If terminationGracePeriodSeconds is too short, NGINX won’t have enough time to drain connections, leading to abrupt disconnects.

Best Practice & Mitigation:

  • Increase terminationGracePeriodSeconds: For your ingress-nginx controller deployment, increase terminationGracePeriodSeconds to a value that allows long-lived connections to drain. 30-60 seconds is a common starting point, but adjust based on your application’s typical connection duration.
  • Monitor NGINX Metrics: Keep an eye on active connections and connection churn during deployments. Metrics like nginx_connections_active, nginx_connections_accepted, and nginx_connections_handled are crucial.
  • PreStop Hook (Advanced): For critical applications, you can add a preStop hook to your controller deployment that explicitly signals NGINX to gracefully shut down (e.g., nginx -s quit or a more sophisticated script to monitor active connections) before Kubernetes terminates the pod.
  • Consider externalTrafficPolicy: Local: If your LoadBalancer service for ingress-nginx is set to Local, it ensures that client IP addresses are preserved. More importantly for draining, it means that connections arriving at a node will only be routed to ingress-nginx pods on that same node. This can simplify connection draining during node-level operations.

Example: Extended terminationGracePeriodSeconds

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  # ... other deployment specs ...
  template:
    metadata:
      # ...
    spec:
      terminationGracePeriodSeconds: 60 # Allow up to 60 seconds for graceful shutdown
      containers:
      - name: controller
        image: registry.k8s.io/ingress-nginx/controller:v1.9.0 # Replace with your target version
        args:
          - /nginx-ingress-controller
          - --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller
          - --election-id=ingress-controller-leader
          # ... other arguments ...

This subtle change can drastically improve the reliability of your service during controller rollouts, preventing those annoying “network error” pop-ups for your users.


Actionable Takeaways

Navigating ingress-nginx in a production environment means moving beyond the basics. These five behaviors highlight that even established tools have hidden depths. For your next migration or operational sprint:

  1. Be Skeptical of Snippets: While powerful, they introduce bespoke complexity. Exhaust all other configuration options first.
  2. Explicit is Best for Backends: Always declare backend-protocol for clarity and to avoid auto-detection surprises. Understand the difference between re-encryption and true TLS passthrough.
  3. Master Your Rewrites: Know the order of operations for rewrite-target vs. redirects to prevent routing nightmares.
  4. Own Your 404: A custom default backend is a must for branding, user experience, and security.
  5. Prioritize Graceful Shutdowns: Adjust terminationGracePeriodSeconds for ingress-nginx to ensure active connections are drained, not dropped.

By internalizing these nuances, you’re not just deploying ingress-nginx; you’re operating it with the precision and foresight required for 2026’s demanding cloud-native ecosystems.


Discussion Questions

  1. What’s the most unusual ingress-nginx snippet you’ve had to implement in a production environment, and what problem did it solve?
  2. With the rise of Gateway API, do you anticipate fewer “surprising behaviors” as the API matures, or will new complexities emerge with its expanded capabilities?

Comments

No comments yet. Be the first!

We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking "OK", you consent to our use of cookies and agree to our Terms of Service & Privacy Policy.