RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
Nginx Docker Socket Permission Denied Fix
02. Root Cause Vector

Nginx cannot connect to the Docker socket because the container process lacks read/write permission on /var/run/docker.sock, blocking all...

03. Rapid Prescription

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

Zero-Trust Client IDE/ Security

Nginx Docker Socket Permission Denied Fix

Nginx cannot connect to the Docker socket because the container process lacks read/write permission on /var/run/docker.sock, blocking all proxied Docker API calls.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(56L)
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
version: '3.8'

# WARNING: This is the legacy docker-compose for the PCI-secure-zone reverse proxy.
# TODO: Migrate to Kubernetes by Q4, but the Helm charts are a mess.
# TEMPORARY WORKAROUND: Using nginx to proxy docker.sock for the old monitoring system.

services:
  nginx-proxy:
    image: my-registry.internal/legacy-nginx:1.18-alpine
    container_name: pci-proxy-nginx
    # VULNERABILITY: The user mismatch between container and host socket ownership causes permission denied.
    user: "1001:1001"  # Hardcoded non-root user for "security compliance" but breaks socket access.
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro  # Host socket mounted read-only
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs/internal-ca.pem:/etc/ssl/certs/internal-ca.pem:ro
    environment:
      - AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
      - AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
      - STRIPE_LIVE_SECRET_KEY=sk_live_51Np9j2KqN3xAmP9LmV8gHjF7tYzXcVbAsDfGhJkLpO0QwErTyUi
      - DB_CONNECTION_STRING=postgresql://legacy_user:V3ryS3cur3P@ssw0rd!@pci-db.internal:5432/transaction_db?sslmode=require
    networks:
      - legacy-backend
    # OLD config that used to work with root, commented out during "security hardening"
    # user: root
    # privileged: true

  legacy-monitor:
    image: my-registry.internal/old-monitoring:v2.1
    container_name: pci-monitor
    environment:
      - DOCKER_HOST=unix:///var/run/docker.sock
      - MONITOR_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - legacy-backend
    depends_on:
      - nginx-proxy
    # TODO: This service fails because nginx can't proxy the socket due to permissions.

networks:
  legacy-backend:
    driver: bridge
    # Internal only, but still needs socket access... ugh.

# nginx.conf snippet reference (not mounted correctly in this mess):
# upstream docker_sock {
#   server unix:/var/run/docker.sock;
# }
# server {
#   listen 2375;
#   location / {
#     proxy_pass http://docker_sock;
#   }
# }
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

How to Fix Nginx 'connect() to unix:/var/run/docker.sock failed (13: Permission denied)' in Docker Socket Proxy

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

TL;DR

  • What broke: The Nginx process (or its container) is running as a non-root user without membership in the docker group, so the kernel rejects the Unix socket connection with EACCES (errno 13).
  • How to fix it: Add the container to the host docker GID, or—better—front the socket with Tecnativa/docker-socket-proxy and never expose the raw socket to Nginx at all.
  • Fastest path: Use our Client-Side Sandbox above to auto-refactor your docker-compose.yml—paste it in, get the hardened output back without sending your config to a third-party server.

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

The Incident (What does the error mean?)

Raw error from Nginx error log:

2024/01/15 03:42:11 [crit] 29#29: *1 connect() to unix:/var/run/docker.sock failed
(13: Permission denied) while connecting to upstream, client: 172.18.0.1,
server: _, request: "GET /containers/json HTTP/1.1", upstream:
"http://unix:/var/run/docker.sock:/containers/json", host: "localhost"

What is immediately broken:

  • Every upstream request Nginx proxies to the Docker API returns a 502.
  • Any dashboard, reverse-proxy autodiscovery, or container introspection feature (Traefik-style label reads, Portainer agents, custom health UIs) is completely dead.
  • If this is in a readiness probe path, your pod/container enters a crash loop.

Root cause in one line: /var/run/docker.sock is owned by root:docker with mode 660. The Nginx worker process is UID 101 (nginx user) and is not in the docker group, so the kernel denies the connect() syscall before a single byte is exchanged.


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

The Attack Vector / Blast Radius

This error is a symptom of a correct security boundary—but the wrong fix will create a critical vulnerability.

The naive fix developers reach for first:

chmod 777 /var/run/docker.sock   # DO NOT DO THIS

or

docker run --user root ...       # DO NOT DO THIS

Why that is catastrophic: The Docker socket is an unauthenticated root shell. Any process that can write to /var/run/docker.sock can:

  1. POST /containers/create with Binds: ["/:/host"] and Privileged: true.
  2. POST /containers/{id}/start
  3. POST /containers/{id}/exec → execute arbitrary commands as root on the host.

This is a full container escape to host root in three API calls. CVSSv3 score for unrestricted Docker socket exposure: 9.8 (Critical). If your Nginx container is internet-facing and you chmod 777 the socket, you have handed the host to the first person who finds an Nginx RCE or SSRF.

Blast radius of the permission denied error itself:

  • All Docker API proxy routes return 502.
  • Autodiscovery pipelines stall.
  • Monitoring agents lose container metadata.
  • CI/CD pipelines that query container state will fail their health gates.

How to Fix It (The Solution)

Basic Fix — Add Container to the Docker GID

Find the GID of the docker group on the host:

stat -c '%g' /var/run/docker.sock
# or
getent group docker | cut -d: -f3
# typical output: 999

Pass that GID into the container at runtime:

# docker-compose.yml
services:
  nginx:
    image: nginx:alpine
-   user: "101"
+   user: "101:999"   # 999 = host docker GID from stat above
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

⚠️ The GID must match the host. If you hardcode 999 and the host uses 998, it still fails. Use $(stat -c '%g' /var/run/docker.sock) in your entrypoint or compose override.

Dynamic GID in compose (production-safe):

services:
  nginx:
    image: nginx:alpine
-   volumes:
-     - /var/run/docker.sock:/var/run/docker.sock
+   group_add:
+     - "${DOCKER_GID}"
+   volumes:
+     - /var/run/docker.sock:/var/run/docker.sock:ro
# .env or CI environment
DOCKER_GID=$(stat -c '%g' /var/run/docker.sock)

Enterprise Best Practice — Docker Socket Proxy (Zero Direct Exposure)

Never mount the raw socket into Nginx. Use Tecnativa/docker-socket-proxy as a sidecar that whitelists only the API endpoints Nginx actually needs.

# docker-compose.yml
+services:
+  socket-proxy:
+    image: tecnativa/docker-socket-proxy:latest
+    restart: unless-stopped
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock:ro
+    environment:
+      CONTAINERS: 1       # allow GET /containers/json
+      SERVICES: 0
+      TASKS: 0
+      NETWORKS: 0
+      VOLUMES: 0
+      INFO: 0
+      EXEC: 0             # NEVER enable this
+      POST: 0             # block all write operations
+    networks:
+      - socket-proxy-net
+    # Do NOT expose port 2375 to host or internet

  nginx:
    image: nginx:alpine
-   volumes:
-     - /var/run/docker.sock:/var/run/docker.sock
+   environment:
+     - DOCKER_HOST=tcp://socket-proxy:2375
+   depends_on:
+     - socket-proxy
+   networks:
+     - socket-proxy-net
+     - public

+networks:
+  socket-proxy-net:
+    internal: true   # air-gapped from internet
+  public:

Nginx upstream config pointing to the proxy:

# nginx.conf upstream block
 upstream docker_api {
-    server unix:/var/run/docker.sock;
+    server socket-proxy:2375;
 }

Now Nginx has zero access to the Docker socket. The proxy enforces an allowlist at the HTTP layer. Even a full Nginx RCE cannot escape to the host.


💡 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. Checkov — Block Raw Socket Mounts in IaC

Checkov rule CKV_DOCKER_1 flags privileged containers. Add a custom policy for socket mounts:

# .checkov/custom_policies/no_raw_docker_socket.yaml
metadata:
  name: "NoRawDockerSocketMount"
  id: "CKV2_DOCKER_CUSTOM_1"
  severity: CRITICAL
definition:
  and:
    - cond_type: attribute
      resource_types: [docker_container, docker_service]
      attribute: volumes
      operator: not_contains
      value: "/var/run/docker.sock:/var/run/docker.sock"

Run in CI:

checkov -d . --check CKV2_DOCKER_CUSTOM_1 --soft-fail-on MEDIUM

2. OPA/Conftest — Enforce Socket Proxy Pattern

# policy/docker_socket.rego
package docker

deny[msg] {
  input.services[name].volumes[_] == "/var/run/docker.sock:/var/run/docker.sock"
  input.services[name].image != "tecnativa/docker-socket-proxy"
  msg := sprintf("Service '%v' mounts raw Docker socket. Use tecnativa/docker-socket-proxy.", [name])
}
conftest test docker-compose.yml --policy policy/

3. GitHub Actions Gate

# .github/workflows/security.yml
- name: Lint Docker Compose for socket exposure
  run: |
    if grep -rn 'docker.sock' docker-compose*.yml | grep -v 'socket-proxy'; then
      echo "ERROR: Raw docker.sock mount detected outside socket-proxy service."
      exit 1
    fi

4. Dockerfile — Never Run as Root

 FROM nginx:alpine
+RUN addgroup -g 999 docker 2>/dev/null || true && \
+    adduser nginx docker
 USER nginx

Summary of controls:

Layer Tool What it catches
IaC scan Checkov Raw socket mounts in compose/Terraform
Policy gate OPA/Conftest Enforces socket-proxy pattern
CI lint grep + bash Fast fail on any docker.sock reference
Runtime docker-socket-proxy Allowlists API endpoints, blocks exec/write
Image build Dockerfile USER Prevents accidental root process

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.Why does errno 13 (Permission Denied) appear for /var/run/docker.sock even when the volume is mounted correctly?
The volume mount is a filesystem operation that succeeds regardless of permissions. The error fires at connect() time—a kernel permission check against the socket file's ownership (root:docker, mode 660). If the Nginx process UID is not root and its supplementary GIDs do not include the docker group GID, the kernel rejects the connection. The fix is GID alignment, not volume remounting. This is a fundamental kernel-level access control failure that exposes a critical misconfiguration in your container identity and privilege model.
Q2.Is adding the container to the docker group safe enough for production?
It is the minimum viable fix, not the production standard. Any process in the docker group has effective root on the host via the API. For production, use Tecnativa/docker-socket-proxy on an internal-only network, allowlist only the specific API routes needed (e.g., CONTAINERS=1, everything else=0), and mount the raw socket only into that proxy container—never into Nginx directly. Manually crafting this configuration is highly error-prone and introduces a catastrophic security vulnerability (blast radius: host compromise). Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix with principle-of-least-privilege API filtering.
Q3.Will this error occur on Kubernetes with a hostPath socket mount?
Yes, and it is worse on Kubernetes because the node's docker GID may differ across node pools and cloud providers. The correct Kubernetes pattern is to use the CRI socket via a DaemonSet sidecar with a restricted securityContext (runAsGroup matching the socket GID, readOnlyRootFilesystem: true), or to replace direct socket access with the Kubernetes API server using a scoped ServiceAccount and RBAC ClusterRole limited to list/watch pods. Doing this correctly across large mono-repos or at scale is a compliance nightmare. Deploy the StackEngine CI/CD Automator (Team Edition) for on-prem, automated pull request generation with Maker-Checker review, policy enforcement, and drift detection across your entire infrastructure-as-code estate.