Tecnativa / Tecnativa/docker-socket-proxy

CONTAINERS=1 grants unintended filesystem read access to all containers

Open
#182 2 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
2.8k
Forks
207
PR merge metrics
No merged PRs in 30d

Description

Summary

When CONTAINERS=1 is set (the standard configuration recommended by every Traefik and monitoring guide), the proxy allows arbitrary file reads from any container and full container filesystem exports via GET-only Docker API endpoints. POST=0 does not prevent this.

An attacker who compromises a service behind the proxy (Traefik, Portainer, a monitoring agent) can silently exfiltrate the entire filesystem of every container on the host - including credentials, private keys, application source code, and environment variables - using only GET requests through the "read-only" proxy.

Affected configuration

# This is the universally recommended "secure" Traefik configuration.
# Every deployment guide reviewed uses this pattern.
docker-socket-proxy:
  image: tecnativa/docker-socket-proxy
  environment:
    CONTAINERS: 1   # ← enables the attack
    POST: 0          # ← does NOT prevent it
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro

Root cause

haproxy.cfg line 60 gates the entire /containers namespace with a single prefix-match regex:

http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers } { env(CONTAINERS) -m bool }

This allows ALL sub-paths, including:

Endpoint Method What it does
/containers/{id}/archive?path=/etc/shadow GET Read any file from any container
/containers/{id}/export GET Download entire container filesystem as tar
/containers/{id}/top GET Process listing with full command-line arguments
/containers/{id}/logs?stdout=1 GET Container stdout/stderr logs

These are all GET endpoints - the POST=0 read-only toggle is irrelevant.

The project already has sub-path controls for container operations (ALLOW_START, ALLOW_STOP, ALLOW_RESTARTS, ALLOW_PAUSE, ALLOW_UNPAUSE), demonstrating awareness that blanket /containers access is too broad. But the most dangerous sub-paths - /export, /archive, /logs, /top - have no equivalent controls.

Impact

Confidentiality: High. An attacker with access to the proxy can:

  1. Read arbitrary files from any container - /etc/shadow, /app/.env, private keys, TLS certificates, database credential files
  2. Export full container filesystems - complete tar archive of every container on the host
  3. Extract secrets from process arguments - database passwords, API keys, and tokens visible in /top output
  4. Read application logs - which frequently contain tokens, session IDs, and PII

Scope: Every deployment using CONTAINERS=1 is affected. This is the standard recommended configuration for Traefik, Portainer read-only, Prometheus Docker SD, and other monitoring/routing tools.

No mitigation exists. An operator who needs container listing (the core use case for this proxy) cannot block filesystem reads without modifying the HAProxy configuration manually. There is no ALLOW_EXPORT=0 or ALLOW_ARCHIVE=0 flag.

Proof of concept

See the PoC files here: https://gist.github.com/nedlir/e4f52f88a757f02c67db1fd5dd70d732

The PoC creates a self-contained lab environment and demonstrates the attack. It is fully non-destructive - no containers are modified, only read.

# 1. Start the lab
docker compose -f findings/docker-compose.yml up -d
# Wait a few seconds for containers to start

# 2. Run the PoC
bash findings/poc.sh

# 3. Cleanup
docker compose -f findings/docker-compose.yml down -v

See poc.sh for the full test, or run individual probes:

PROXY="http://localhost:2375"

# List containers
curl -s "$PROXY/containers/json" | jq '.[].Names'

# Read /etc/passwd from any container (GET only, no POST needed)
CID=$(curl -s "$PROXY/containers/json" | jq -r '.[0].Id')
curl -s "$PROXY/containers/$CID/archive?path=/etc/passwd" | tar -xO

# Export entire filesystem
curl -s "$PROXY/containers/$CID/export" | tar -t | head -20

# Process listing with command-line arguments
curl -s "$PROXY/containers/$CID/top" | jq '.Processes'

Attack scenario

                    ┌────────────────────────────┐
                    │      Docker Host           │
                    │                            │
                    │  ┌──────────┐  ┌────────┐  │
                    │  │ Postgres │  │  App    │  │
                    │  │ (secrets │  │ (source │  │
                    │  │  in env) │  │  code)  │  │
                    │  └──────────┘  └────────┘  │
                    │        ▲            ▲       │
                    │        │            │       │
                    │  ┌─────┴────────────┴────┐  │
                    │  │    Docker daemon       │  │
                    │  └───────────┬────────────┘  │
                    │              │                │
                    │  ┌───────────┴────────────┐  │
                    │  │  docker-socket-proxy    │  │
                    │  │  CONTAINERS=1, POST=0   │  │
                    │  │  (intended: read-only   │  │
                    │  │   container metadata)   │  │
                    │  └───────────┬────────────┘  │
                    │              │                │
                    │  ┌───────────┴────────────┐  │
                    │  │       Traefik           │  │
                    │  │  (compromised via SSRF, │  │
                    │  │   plugin supply-chain,  │  │
                    │  │   or CVE)               │  │
                    │  └────────────────────────┘  │
                    └────────────────────────────┘

Attacker compromises Traefik → uses proxy to:
  GET /containers/{postgres-id}/archive?path=/var/lib/postgresql/data
  GET /containers/{app-id}/export
  GET /containers/{app-id}/top  (shows: "node app.js --db-password=s3cret")

Suggested fix

Add deny rules for dangerous container sub-paths before the blanket CONTAINERS allow, matching the pattern already used for ALLOW_START/ALLOW_STOP:

# Deny dangerous container sub-paths unless explicitly allowed (place BEFORE the CONTAINERS rule)
http-request deny if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/export } !{ env(ALLOW_EXPORT) -m bool }
http-request deny if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/archive } !{ env(ALLOW_ARCHIVE) -m bool }
http-request deny if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/logs } !{ env(ALLOW_LOGS) -m bool }

With corresponding Dockerfile defaults:

ENV ALLOW_EXPORT=0 \
    ALLOW_ARCHIVE=0 \
    ALLOW_LOGS=0

This preserves backward compatibility for the safe /containers/json and /containers/{id}/json endpoints while blocking the dangerous ones by default.

Environment

  • docker-socket-proxy: latest (HAProxy 3.4.2-alpine base, commit 2e118a0)
  • Docker Engine: 29.4.3
  • Tested: 2026-08-15
  • All testing performed against live proxy instances, not code snippets

Related

  • #114 - reports that POST=1 enables all methods (related but different finding)
  • #176 - proposes ALLOW_EXEC, ALLOW_KILL, ALLOW_DELETE (same pattern, doesn't cover /export or /archive)
  • #20 - earlier attempt at per-endpoint controls (closed 2020)

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with haproxy.cfg around line 60 and compare the blanket CONTAINERS rule with the existing ALLOW_START and ALLOW_STOP controls. Review Dockerfile defaults and run findings/docker-compose.yml with findings/poc.sh to verify that dangerous GET endpoints are denied by default while container metadata access remains available.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker
Domain
infrastructure, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.