RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
Nginx IPv6 Upstream No Route Debugger
02. Root Cause Vector

Nginx fails with errno 113 (No route to host) when proxying to an IPv6-only upstream because the host kernel, Nginx worker, or network pa...

03. Rapid Prescription

Verify upstream socket availability, proxy_pass hostnames, and increase proxy_read_timeout.

Zero-Trust Client IDE/ Performance

Nginx IPv6 Upstream No Route Debugger

Nginx fails with errno 113 (No route to host) when proxying to an IPv6-only upstream because the host kernel, Nginx worker, or network path lacks a functional IPv6 forwarding stack.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(90L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: v1
kind: ConfigMap
metadata:
  name: legacy-nginx-upstreams
  namespace: pci-secure-zone
  annotations:
    owner: "team-legacy@my-registry.internal"
    # TODO: Migrate to service mesh by Q4, but bandwidth...
data:
  # Upstream block for IPv6-only backend (legacy compliance)
  upstream.conf: |
    upstream legacy_backend {
      # VULNERABILITY: IPv6 address configured, but host network stack lacks IPv6 routing or interface
      server [2001:db8:dead:beef::cafe]:8080;  # IPv6-only legacy API
      # Old IPv4 fallback commented out during 'migration'
      # server 10.10.10.5:8080;
      keepalive 32;
    }
    # Temporary workaround for PCI logging (ignore for now)
    # access_log /var/log/nginx/access.log json;
  nginx.conf: |
    user nginx;
    worker_processes auto;
    error_log /var/log/nginx/error.log warn;
    events {
      worker_connections 1024;
    }
    http {
      # Embedded secrets in config (rotated manually, I think)
      # AWS key for S3 logging (prod)
      set $aws_key "AKIAIOSFODNN7EXAMPLE";
      # Stripe test key from old integration
      set $stripe_sk "sk_test_4eC39HqLyjWDarjtT1zdp7dc";
      # DB URI for audit service
      set $audit_db "postgresql://audit_user:SuperSecret123!@audit-db.internal:5432/audit_prod";
      include /etc/nginx/conf.d/*.conf;
      server {
        listen 80;
        server_name api.legacy-app.internal;
        location /v1/ {
          proxy_pass http://legacy_backend;
          # TODO: fix this workaround for timeouts after IPv6 migration
          proxy_connect_timeout 10s;
          proxy_send_timeout 30s;
          proxy_read_timeout 30s;
          # Old health check path, might still be referenced elsewhere
          # health_check uri=/health;
        }
      }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-proxy
  namespace: pci-secure-zone
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-proxy
  template:
    metadata:
      labels:
        app: nginx-proxy
    spec:
      # Host network for 'performance' (breaks IPv6 routing if not enabled on node)
      hostNetwork: true  # VULNERABILITY: Implicitly inherits node's network config, which may lack IPv6
      containers:
      - name: nginx
        image: my-registry.internal/nginx:1.18-legacy-patched
        ports:
        - containerPort: 80
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx
        # Redundant env var from old deployment
        env:
        - name: DEPLOY_ENV
          value: "prod"
        - name: LOG_LEVEL
          value: "info"
        # TODO: Remove after migration
        - name: LEGACY_MODE
          value: "true"
      volumes:
      - name: nginx-config
        configMap:
          name: legacy-nginx-upstreams
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

Fixing Nginx 'connect() failed (113: No route to host)' for IPv6-Only Upstream Backends

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–45 mins

TL;DR

  • What broke: Nginx cannot open a TCP socket to an IPv6-only upstream — the kernel returns EHOSTUNREACH (113), meaning no route exists at the socket layer, not just DNS.
  • How to fix it: Confirm IPv6 is enabled on the host, wrap the upstream address in brackets ([::1]:8080), ensure the resolver returns AAAA records, and verify the upstream server is actually bound to its IPv6 address.
  • Fast path: Use our Client-Side Sandbox above to auto-refactor your failing Nginx upstream block — paste the config, get corrected diff output without sending your IPs or hostnames to any third-party server.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

Raw error from /var/log/nginx/error.log:

2024/07/15 03:42:17 [error] 1187#1187: *9831 connect() failed (113: No route to host)
  while connecting to upstream,
  client: 203.0.113.44, server: api.internal,
  request: "POST /v1/ingest HTTP/1.1",
  upstream: "http://[2001:db8::cafe:1]:9000/v1/ingest",
  host: "api.internal"

Errno 113 (EHOSTUNREACH) is not a DNS failure and not a firewall REJECT. It means the kernel's routing table has no path to the destination network at the time connect() is called. The TCP SYN never leaves the NIC. This is distinct from:

Errno Meaning Nginx log text
111 Connection refused (port closed) connect() failed (111: Connection refused)
110 Timeout (firewall DROP) upstream timed out (110: Connection timed out)
113 No route (kernel routing gap) connect() failed (113: No route to host)

Immediate consequence: Every proxied request to this upstream returns 502 Bad Gateway instantly. There is no retry delay — the failure is synchronous and hard. If this upstream is in an upstream {} block with other peers, Nginx marks it down after max_fails and stops sending traffic to it entirely until the fail_timeout window expires.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is a silent availability failure, not a security exploit — but the blast radius is severe in production:

1. Full upstream eviction from the load-balancing pool. Nginx's passive health check marks the peer unavailable. If this is your only upstream or all upstreams are IPv6-only, 100% of traffic 502s. There is no circuit-breaker grace period.

2. Cascading retry storms. Clients retrying on 502 multiply request rate. If a CDN or API gateway sits in front, it may retry 3× per origin request, tripling the error log volume and masking the root cause in noise.

3. Split-brain dual-stack environments. In Kubernetes or Docker with --ipv6 disabled at the daemon level, the container's network namespace has no default IPv6 route (ip -6 route returns empty). Nginx running inside that container will always get errno 113 for any IPv6 upstream, even ::1. This is the most common cause and the most commonly missed.

4. Resolver returning AAAA but kernel can't route it. If resolver 8.8.8.8 resolves a hostname to an IPv6 address but the host has no IPv6 default gateway (common in legacy bare-metal or certain AWS EC2 instance types with IPv6 not explicitly enabled on the ENI), every dynamic upstream resolution produces an unroutable address and errno 113 follows.


How to Fix It (The Solution)

Root Cause Checklist — Run These First

# 1. Is IPv6 enabled in the kernel at all?
cat /proc/sys/net/ipv6/conf/all/disable_ipv6
# 0 = enabled, 1 = DISABLED (this is your culprit if 1)

# 2. Does a default IPv6 route exist?
ip -6 route show default
# Empty output = no gateway = errno 113 guaranteed

# 3. Can the Nginx worker process reach the upstream directly?
ip netns exec <nginx-netns-if-containerized> ping6 -c3 2001:db8::cafe:1

# 4. Is the upstream actually listening on its IPv6 address?
ss -tlnp | grep 9000
# Must show [::]:9000 or 2001:db8::cafe:1:9000, NOT 0.0.0.0:9000

Fix 1 — Basic: Nginx Config Syntax (Missing Brackets)

The most trivial cause: IPv6 addresses in Nginx upstream blocks must be wrapped in square brackets. Without them, Nginx misparses the address.

 upstream backend_ipv6 {
-    server 2001:db8::cafe:1:9000;   # WRONG: Nginx parses ':9000' as part of address
+    server [2001:db8::cafe:1]:9000; # CORRECT: brackets isolate the address
 }

 server {
     listen 80;
-    listen 443 ssl;
+    listen [::]:80;
+    listen [::]:443 ssl;

     location /api/ {
         proxy_pass http://backend_ipv6;
     }
 }

Fix 2 — Kernel IPv6 Disabled at Runtime

 # /etc/sysctl.d/99-ipv6.conf
-net.ipv6.conf.all.disable_ipv6 = 1
-net.ipv6.conf.default.disable_ipv6 = 1
+net.ipv6.conf.all.disable_ipv6 = 0
+net.ipv6.conf.default.disable_ipv6 = 0
+net.ipv6.conf.lo.disable_ipv6 = 0

Apply without reboot: sysctl --system


Fix 3 — Enterprise Best Practice: Resolver + Upstream with Keepalive and Health Checks

In dynamic environments (service mesh, Consul, k8s headless services), hardcoded IPv6 addresses are fragile. Use Nginx Plus active health checks or the OSS ngx_http_upstream_hc_module workaround with a proper resolver.

 http {
+    # Force resolver to return AAAA records; set valid TTL to prevent stale routes
+    resolver [2001:4860:4860::8888] [2001:4860:4860::8844] valid=10s ipv6=on;
+    resolver_timeout 3s;

     upstream backend_ipv6 {
-        server backend.internal:9000;  # Resolves to IPv6 but no ipv6=on resolver set
+        server backend.internal:9000 resolve; # Requires resolver directive above
+        keepalive 32;
+        keepalive_requests 1000;
+        keepalive_timeout 75s;
     }

     server {
-        listen 80;
+        listen 80;
+        listen [::]:80 ipv6only=off;  # Single socket handles both IPv4 and IPv6

         location /api/ {
             proxy_pass         http://backend_ipv6;
             proxy_http_version 1.1;
-            proxy_set_header   Connection "";
+            proxy_set_header   Connection "";  # Required for keepalive upstream
+            proxy_connect_timeout 3s;           # Fail fast; don't wait 60s on bad route
+            proxy_next_upstream error timeout http_502 http_503;
+            proxy_next_upstream_tries 2;
         }
     }
 }

Fix 4 — Docker / Kubernetes: Enable IPv6 in the Container Runtime

 # /etc/docker/daemon.json
 {
+  "ipv6": true,
+  "fixed-cidr-v6": "fd00::/80",
   "log-driver": "json-file"
 }

For Kubernetes, patch the kubeadm config or CNI plugin (Calico, Cilium) to enable dual-stack. Cilium example:

 # cilium-config ConfigMap
-  enable-ipv6: "false"
+  enable-ipv6: "true"
+  ipv6-range: "fd00::/104"

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Nginx Config Linting in Pre-Commit / PR Pipeline

# .github/workflows/nginx-lint.yml
- name: Validate Nginx config
  run: |
    docker run --rm -v $(pwd)/nginx:/etc/nginx:ro nginx:alpine \
      nginx -t -c /etc/nginx/nginx.conf
    # Fails the pipeline if brackets are missing or directives are invalid

2. Checkov Policy — Detect Missing IPv6 Listen Directives

Checkov doesn't ship an Nginx IPv6 check out of the box. Write a custom policy:

# checkov/custom_checks/nginx_ipv6_upstream.py
from checkov.common.models.enums import CheckResult
from checkov.nginx.checks.base_nginx_check import BaseNginxCheck
import re

class NginxIPv6UpstreamBrackets(BaseNginxCheck):
    def __init__(self):
        super().__init__(
            name="Ensure IPv6 upstream addresses use bracket notation",
            check_id="CKV_NGINX_IPV6_UPSTREAM"
        )

    def check_resource_conf(self, conf):
        # Regex: bare IPv6 address (colon-separated hex) NOT wrapped in brackets before :port
        bare_ipv6 = re.compile(r'server\s+[0-9a-fA-F:]{3,}:[0-9]+')
        for line in conf.get('raw_lines', []):
            if bare_ipv6.search(line):
                return CheckResult.FAILED
        return CheckResult.PASSED

3. OPA / Conftest Policy for Kubernetes Nginx Ingress

# policy/nginx_ipv6.rego
package nginx.ipv6

deny[msg] {
  input.kind == "ConfigMap"
  input.metadata.name == "nginx-configuration"
  val := input.data["use-ipv6"]
  val == "false"
  msg := "Nginx Ingress must have use-ipv6 enabled when IPv6-only backends are in use"
}

4. Smoke Test in Staging — Validate Route Before Deploy

#!/usr/bin/env bash
# ci/check_ipv6_route.sh — run this as a pre-deploy gate
UPSTREAM_IPV6="2001:db8::cafe:1"
if ! ip -6 route get "${UPSTREAM_IPV6}" &>/dev/null; then
  echo "FATAL: No IPv6 route to ${UPSTREAM_IPV6}. Aborting deploy."
  exit 1
fi
echo "IPv6 route confirmed. Proceeding."

Add this as a required status check in your GitHub branch protection rules. A 2-second script that has saved hours of production debugging.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.What is the difference between Nginx errno 113 and errno 111 for upstream connections?
Errno 113 (EHOSTUNREACH) means the kernel has no routing table entry to reach the destination network — the TCP SYN packet never leaves the host. Errno 111 (ECONNREFUSED) means the packet was routed and delivered, but the remote host actively rejected it with a TCP RST, typically because nothing is listening on that port. For IPv6 upstreams, errno 113 almost always means either IPv6 is disabled on the host kernel, there is no default IPv6 gateway, the Nginx config is missing bracket notation around the address, or the container runtime has IPv6 networking disabled. This is a fundamental networking failure, and manually diagnosing which layer is broken is a time-consuming, expert-level task that often leads to misconfiguration and extended downtime.
Q2.Why does Nginx require square brackets around IPv6 addresses in upstream blocks?
IPv6 addresses contain colons as part of the address notation (e.g., 2001:db8::1). Nginx's config parser uses the colon as a delimiter between host and port. Without brackets, the parser cannot distinguish where the address ends and the port number begins, causing a parse error or silent misinterpretation. The correct syntax is `server [2001:db8::1]:9000;` — this is consistent with RFC 2732 and RFC 3986 URI syntax for IPv6 literals. Manually fixing this across hundreds of configs is a high-risk, error-prone process that can introduce security vulnerabilities and create a massive blast radius if a bracket is misplaced or omitted. Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix with a full security audit trail.
Q3.How do I enable IPv6 for Nginx running inside a Docker container connecting to an IPv6-only backend?
You must enable IPv6 at three layers: (1) the Docker daemon — set `"ipv6": true` and `"fixed-cidr-v6"` in `/etc/docker/daemon.json` and restart Docker; (2) the container network — ensure the bridge or overlay network was created with `--ipv6`; (3) the host kernel — verify `cat /proc/sys/net/ipv6/conf/all/disable_ipv6` returns `0`. If any layer has IPv6 disabled, the Nginx worker process inside the container will have no IPv6 routing table and will return errno 113 on every connect attempt to an IPv6 address, regardless of what the Nginx config says. Doing this manually across large mono-repos or at scale is a compliance and operational nightmare. Deploy the StackEngine CI/CD Automator (Team Edition) for on-prem, automated pull request generation with Maker-Checker review and enterprise-grade policy enforcement.