- 📖 Documentation
- 🌐 Website
- 🚀 Quick Start
- 🤔 Why Sockguard
- ✨ Features
- ⚖️ Comparison
- ⚙️ Configuration
- 📈 Observability
- 🔧 CLI
- 🔄 Migrating from Tecnativa
- 🗺️ Roadmap
- 🛠️ Built With
- 🤝 Contributing
- 🔒 Security
Note
v1.0 soak. The v1.0 contract is locked — YAML schema, CLI flags, env vars, admin endpoints, and Prometheus metric names will not change before v1.0 ships. Active 0.x releases are bug fixes and operational polish only; the v1.0 tag will be cut once the locked surface has soaked in production deployments. See the changelog for the latest breaking changes (collapsed into the Unreleased section).
Drop sockguard in front of any Docker API consumer. The proxy filters requests, your app stays unchanged.
# docker-compose.yml
services:
sockguard:
image: codeswhat/sockguard:latest
restart: unless-stopped
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- SOCKGUARD_LISTEN_ADDRESS=:2375
- SOCKGUARD_LISTEN_INSECURE_ALLOW_PLAIN_TCP=true
- SOCKGUARD_LISTEN_INSECURE_ALLOW_UNAUTHENTICATED_CLIENTS=true
- SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION=true
- CONTAINERS=1
- IMAGES=1
- EVENTS=1
# Your app talks to tcp://sockguard:2375 over the compose network
# instead of mounting /var/run/docker.sock.
drydock:
image: codeswhat/drydock:latest
depends_on:
- sockguard
environment:
- DD_WATCHER_LOCAL_SOCKET=tcp://sockguard:2375By default sockguard listens on loopback TCP 127.0.0.1:2375, not on all interfaces. Non-loopback TCP now requires mutual TLS via listen.tls by default.
The compose example above opts into legacy plaintext TCP so migration from tecnativa/docker-socket-proxy and linuxserver/socket-proxy still works on a private Docker network. A non-loopback plaintext listener requires two deliberate acknowledgments — SOCKGUARD_LISTEN_INSECURE_ALLOW_PLAIN_TCP=true (unencrypted transport) and SOCKGUARD_LISTEN_INSECURE_ALLOW_UNAUTHENTICATED_CLIENTS=true (any host that can reach the port can impersonate a client) — so a single fat-fingered flag cannot expose it. It also opts into SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION=true because broad CONTAINERS=1 / IMAGES=1 compatibility includes raw archive/export and log/attach streaming endpoints. Do not publish that plaintext listener to the host or Internet, and remove the read-exfil opt-in once you migrate to tighter YAML list/inspect rules.
If you run sockguard directly on a host, keep SOCKGUARD_LISTEN_ADDRESS=127.0.0.1:2375, configure listen.tls for remote TCP, or switch to SOCKGUARD_LISTEN_SOCKET to avoid a network listener entirely.
Container runtime hardening
Sockguard runs as root inside the container by default so it can open /var/run/docker.sock on stock Docker hosts without user or group_add overrides. For this class of tool, the meaningful hardening levers are the proxy policy, a read-only root filesystem, dropped capabilities, no-new-privileges, and the host runtime's seccomp/AppArmor/SELinux confinement.
The examples in this README already opt into the container-level controls sockguard actually benefits from:
read_only: truecap_drop: [ALL]security_opt: ["no-new-privileges:true"]
Keep Docker's default seccomp profile or replace it with a stricter custom profile via security_opt. On AppArmor or SELinux hosts, keep the runtime's default confinement enabled or replace it with a stricter host policy. If the host runs rootless dockerd, a compromised Docker API client inherits the daemon's reduced authority instead of full host root.
mTLS TCP mode (recommended for remote TCP)
services:
sockguard:
image: codeswhat/sockguard:latest
restart: unless-stopped
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./certs:/certs:ro
environment:
- SOCKGUARD_LISTEN_ADDRESS=:2376
- SOCKGUARD_LISTEN_TLS_CERT_FILE=/certs/server-cert.pem
- SOCKGUARD_LISTEN_TLS_KEY_FILE=/certs/server-key.pem
- SOCKGUARD_LISTEN_TLS_CLIENT_CA_FILE=/certs/client-ca.pem
- SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION=true
- CONTAINERS=1Non-loopback TCP without listen.tls fails startup unless you explicitly set SOCKGUARD_LISTEN_INSECURE_ALLOW_PLAIN_TCP=true.
Sockguard's server-side TLS minimum for listen.tls is TLS 1.3, so remote clients must support TLS 1.3.
If one client CA issues multiple workloads, narrow the trusted set further in YAML with listen.tls.common_names, dns_names, ip_addresses, uri_sans, and/or public_key_sha256_pins so any CA-issued client cert is not automatically accepted.
Unix socket mode (filesystem-bounded access)
If you prefer to expose sockguard as a unix socket (no network surface at all), opt in by setting SOCKGUARD_LISTEN_SOCKET and sharing the socket via a named volume:
services:
sockguard:
image: codeswhat/sockguard:latest
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- sockguard-socket:/var/run/sockguard
environment:
- SOCKGUARD_LISTEN_SOCKET=/var/run/sockguard/sockguard.sock
- SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION=true
- CONTAINERS=1
drydock:
image: codeswhat/drydock:latest
depends_on:
- sockguard
volumes:
- sockguard-socket:/var/run/sockguard:ro
environment:
- DD_WATCHER_LOCAL_SOCKET=/var/run/sockguard/sockguard.sock
volumes:
sockguard-socket:Sockguard hardens its own unix socket to 0600 owner-only permissions. listen.socket_mode remains in the config surface as a guardrail and must stay 0600; broader modes are rejected at startup instead of being applied.
To run fully unprivileged with a unix socket, pre-create a host directory with the uid/gid you want and bind-mount it in place of the named volume.
The Docker socket is root access to your host. Every container with socket access can escape containment, mount the host filesystem, and pivot to other containers. Yet tools like Traefik, Portainer, and drydock need socket access to function.
Most existing socket proxies stop at method/path or regex filtering. Tecnativa and LinuxServer gate broad Docker API sections, wollomatic adds regex allowlists, hostname/IP admission, per-container label allowlists, optional bind-mount restrictions, JSON logging, an active upstream watchdog, and a filtered unix-socket endpoint, 11notes ships a fixed read-only proxy that blocks all writes plus seven exfiltration-prone GET endpoints, and CetusGuard pairs zero-dependency default-deny regex rules with mTLS. We go further on body-aware policy enforcement, per-client profile selection, ownership isolation, and read-side visibility/redaction.
| Feature | Description | |
|---|---|---|
| 🛡️ | Default-Deny Posture | Everything blocked unless explicitly allowed. No match means deny. |
| 🎛️ | Granular Control | Allow start/stop while blocking create/exec. Per-operation POST controls with glob matching. |
| 📋 | YAML Configuration | Declarative rules, glob path patterns, first-match-wins evaluation, and canonical path matching that strips API versions, collapses dot segments, and decodes escaped separators before policy evaluation. 12 bundled workload presets (including CIS Docker Benchmark, self-hosted GitHub Actions runners, and GitLab Runner) plus the default config. |
| 📊 | Structured Access Logging | JSON access logs with method, raw path, normalized path, decision, matched rule, latency, canonical request ID, W3C traceparent correlation fields, and client info. Use normalized_path for SIEM correlation and policy analysis; raw path is preserved for forensic replay. Canonical request IDs are generated from a buffered pool so request logging does not block on a fresh entropy read per request. |
| 🔐 | mTLS for Remote TCP | Non-loopback TCP listeners require mutual TLS by default. Plaintext TCP is explicit legacy mode only. |
| 🌐 | Client ACL Primitives | Optional source-CIDR admission checks, client-container label ACLs, listener certificate selectors (CN/DNS/IP/URI SAN/SPKI), profile certificate selectors (CN/DNS/IP/URI/SPIFFE/SPKI), and unix peer credentials let one proxy differentiate callers before the global rule set runs. When mTLS is enabled, certificate selectors follow the verified client leaf certificate rather than an unverified peer slice entry. |
| 🗃️ | Bounded Inspect Cache | Ownership and visibility checks reuse a short-lived singleflight cache for upstream Docker inspect metadata so bursts of repeated reads do not fan out into duplicate synchronous inspect calls. |
| 🔍 | Request Body Inspection | POST /containers/create, /containers/*/update, /containers/*/exec, /exec/*/start, PUT /containers/*/archive, /images/create, /images/load, /build, /volumes/create, /networks/create, /networks/*/connect, /networks/*/disconnect, /secrets/create, /configs/create, /services/create, /services/*/update, /swarm/init, /swarm/join, /swarm/update, /swarm/unlock, /nodes/*/update, /plugins/pull, /plugins/*/upgrade, /plugins/*/set, and /plugins/create are inspected before Docker sees the request. Sockguard blocks privileged or host-bound workloads, non-allowlisted mounts/devices/commands/remotes, unsafe network/service/swarm/node controls, image archive imports outside registry policy, and unsafe container filesystem archives. POST /plugins/create is inspected whether the tar upload arrives as a raw body or multipart/form-data. Oversized bodies on bounded JSON/tar inspectors are rejected with 413 Payload Too Large before any upstream call. These inspectors intentionally decode the policy-relevant subset of Docker's schema and still defer full-schema validation to Docker itself. |
| 🏷️ | Owner Label Isolation | A proxy instance can stamp label-capable creates plus build-produced images with an owner label, auto-filter labeled list/prune/events calls, and deny cross-owner access across containers, images, networks, volumes, services, tasks, secrets, configs, nodes, and swarm state. |
| 🫥 | Visibility-Controlled Reads | Redacts env, mount, network, config, plugin, and swarm-sensitive metadata by default, can hide labeled list/inspect plus selected service/task log reads behind per-client visibility rules, and keeps raw archive/export and stream-style reads behind explicit opt-in. |
| 🧱 | Body-Blind Write Guardrail | Any remaining write Sockguard cannot safely constrain stays behind explicit insecure_allow_body_blind_writes opt-in instead of being silently exposed. Today that guardrail chiefly covers arbitrary exec without request_body.exec.allowed_commands, POST /swarm/join without request_body.swarm.allowed_join_remote_addrs, and plugin setting writes without explicit allowed assignment prefixes. |
| 🔄 | Tecnativa Compatible | Drop-in replacement for the current Tecnativa env surface, including section vars, ALLOW_RESTARTS, SOCKET_PATH, and LOG_LEVEL. |
| 🎚️ | Rollout Modes | Per-profile mode: enforce|warn|audit lets operators stage a tighter policy without breaking callers. warn/audit pass-through with decision=would_deny on the audit record and a mode label on the deny/throttle counters, so dashboards compare blocked vs. would-have-been-blocked volume side by side. |
| 🔁 | Hot-Reload + Policy Versioning | reload.enabled: true watches the config file via fsnotify (Linux inotify / macOS kqueue) and accepts SIGHUP. The new policy goes through the full validator + rule compiler and is atomically swapped behind the running handler; immutable fields (listeners, log, health, metrics, admin, policy-bundle trust material) refuse the reload. A monotonic generation counter is exposed at GET /admin/policy/version and via the sockguard_policy_version gauge. |
| 🧪 | Admin API | Opt-in POST /admin/validate accepts a candidate YAML body and returns the same verdict the offline sockguard validate command would — perfect for a CI gate before promoting a config. GET /admin/policy/version reports {version, loaded_at, rules, profiles, source, config_sha256, bundle_signer?}. Both endpoints can ride the main listener or move to a dedicated admin.listen.* (socket or TCP, mTLS-aware) firewalled from Docker-API consumers. |
| ✍️ | Signed Policy Bundles | policy_bundle.enabled: true requires a cosign sigstore bundle to vouch for the YAML config bytes. Keyed (PEM) and keyless (Fulcio + Rekor) trust paths reuse the same sigstore-go stack as image trust. Verification runs at startup before any rule compiles and again on every hot reload — unsigned or tampered bundles abort startup and reject reloads with reject_signature on sockguard_config_reload_total. The verified signer and YAML digest are stamped on the policy-version snapshot. |
| 🪶 | Minimal Attack Surface | Wolfi-based image. Cosign-signed with SBOM and build provenance. |
| ⚡ | Streaming-Safe | Preserves Docker streaming endpoints (logs, attach, events) without breaking timeouts, while reaping idle TCP keep-alive connections after 120s. |
| 🩺 | Health Check + Watchdog | /health endpoint with cached upstream reachability probes and an opt-in active Docker socket watchdog that logs state transitions. |
| 📈 | Prometheus Metrics | Opt-in /metrics endpoint with low-cardinality request counters, deny counters, latency histograms, active request gauge, upstream watchdog state/check metrics, plus sockguard_build_info and sockguard_start_time_seconds gauges for version panels and uptime alerts. |
| 🔗 | Trace/Log Correlation | Preserves valid W3C traceparent context or generates local context, forwards a proxy-local span ID, and records trace fields in access, audit, and upstream error logs without an OTLP exporter. |
| 🧪 | Battle-Tested | 96%+ statement coverage, race-detector clean, monthly Gremlins mutation testing, and fuzz testing on filter, config, proxy, and hijack paths. |
How we stack up against other Docker socket proxies:
| Feature | Tecnativa | LinuxServer | wollomatic | 11notes | CetusGuard | Sockguard |
|---|---|---|---|---|---|---|
| Method + path filtering | ✅ | ✅ | ✅ (regex) | Fixed read-only | ✅ (regex) | ✅ |
| Granular container write ops | ❌ | Partial (ALLOW_*) |
Via regex | ❌ (read-only) | Via regex | ✅ |
| Request body inspection | ❌ | ❌ | Partial (bind-mount source restrictions) | ❌ | ❌ | ✅ (container create/update/exec/archive, image pull/load, build, volume, network create/connect/disconnect, secret, config, service, swarm init/join/update/unlock, node update, plugin) |
| Per-client admission / policy selection | ❌ | ❌ | Partial (IP/hostname + per-container labels) | ❌ | ❌ | ✅ (CIDR + labels + cert selectors incl. SPKI + unix peer profiles) |
| Read-side visibility / redaction | ❌ | ❌ | ❌ | Partial (blocks 7 risky GETs) | ❌ | ✅ (visibility + protected JSON redaction) |
| Remote TCP mTLS (listener) | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ (TLS 1.3) |
| Remote daemon upstream (TLS) | ❌ | ❌ | ❌ | ❌ | ✅ | Roadmap (v1.1) |
| Structured access logs | ❌ | ❌ | ✅ (JSON option) | ❌ | ❌ | ✅ (request + trace correlation) |
| Dedicated audit log schema | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (JSON schema + reason codes) |
| Rate limits / concurrency caps | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (per-profile token-bucket + global priority gate) |
| Rollout modes (audit/warn/enforce) | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (per-profile shadow + would_deny audit) |
| Hot-reload + policy versioning | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (fsnotify + SIGHUP, /admin/policy/version) |
| Signed policy bundles | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (sigstore keyed + keyless) |
| YAML config | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Tecnativa env compat | N/A | ✅ | ❌ | ❌ | ❌ | ✅ |
11notes/docker-socket-proxy takes a deliberately narrow stance: a fixed read-only proxy that allows every Docker API GET except seven exfiltration-prone endpoints (container attach/ws, export, archive, secrets/configs listing, swarm/unlockkey, images/{name}/get) and blocks all writes, shipped as a non-root distroless image — we match its read-side blocking with finer-grained per-field redaction and visibility rules and run non-root ourselves, but we additionally allow scoped writes instead of refusing them outright. hectorm/cetusguard is the closest in spirit to us: a zero-dependency, default-deny proxy with method + regex path rules and mTLS on both the frontend and backend — but it has no request-body inspection, no per-client policies, no owner isolation, no read-side filtering, no metrics, and no hot-reload. Where we go further is body inspection breadth (every body-bearing Docker write path we can safely constrain), named profiles, ownership isolation, and read-side visibility/redaction. CetusGuard, in turn, can dial a remote Docker daemon over backend TLS today — our upstream is the local socket, with remote TCP upstreams on the v1.1 roadmap.
CONTAINERS=1 # Allow /containers/** (GET/HEAD when POST=0)
IMAGES=0 # Deny /images/**
SERVICES=1 # Allow /services/** (GET/HEAD when POST=0)
EVENTS=1 # Allow /events (default)
POST=0 # Read-only mode
# Granular container writes still work even when POST=0
ALLOW_START=1
ALLOW_STOP=1
ALLOW_RESTARTS=1
# Compat aliases
SOCKET_PATH=/var/run/docker.sock
LOG_LEVEL=warningCompat env vars only generate rules when no explicit rules: are configured. If you provide rules: in YAML, those rules win even when they happen to match the built-in defaults exactly. Broad compat reads (CONTAINERS=1, IMAGES=1, POST=0) that pull in raw archive/export and log/attach streaming also need SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION=true — see the configuration reference for the full env-var surface.
listen:
address: 127.0.0.1:2375 # loopback TCP; use listen.socket or listen.tls for anything else
rules:
- match: { method: GET, path: "/_ping" }
action: allow
- match: { method: GET, path: "/containers/json" }
action: allow
- match: { method: GET, path: "/containers/*/json" }
action: allow
- match: { method: POST, path: "/containers/*/start" }
action: allow
- match: { method: "*", path: "/**" } # default-deny backstop
action: denyTrailing /** matches both the base path and any deeper path. For example, /containers/** matches /containers and /containers/abc/json.
Sockguard inspects the body of allowed write requests — containers/create, containers/*/update, exec, build, images/create, services/create, swarm/init, and the rest of the body-bearing write paths — and blocks privileged or host-bound workloads, non-allowlisted mounts, devices, and registries, and unsafe swarm/network controls. Response bodies are redacted (env, mount paths, topology, secrets) by default. None of that needs configuration to switch on.
Beyond these essentials, every knob is documented in full on the docs site rather than duplicated here:
- Configuration reference — full YAML schema, request-body inspection, mTLS client selectors, per-client ACLs and profiles, rate limiting and concurrency caps, owner-label isolation, rollout modes, hot-reload, signed policy bundles,
insecure_*opt-ins, response redaction, and config precedence (CLI flags > env vars > config file > defaults). - Admin API — the
POST /admin/validateCI gate andGET /admin/policy/version. - Observability — Prometheus metrics, access/audit log fields, and trace/log correlation.
- Security model — the defense-in-depth layers and known limitations.
Preset configs included for drydock, Traefik, Portainer, Watchtower, Homepage, Homarr, Diun, Autoheal, read-only, CIS Docker Benchmark (admission-gates CIS Section 5 runtime controls — see the dedicated guide), GitHub Actions self-hosted runner, and GitLab Runner (Docker executor).
Ready-to-run compose stacks pairing sockguard with a downstream consumer live under examples/compose/ — one directory per stack with its own docker-compose.yml, sockguard.yaml overlay, and README.md (audience, exposed surface, security tradeoffs). Currently shipped: drydock, Traefik, Portainer, Watchtower, GitHub Actions self-hosted runner, GitLab Runner, and a generic CIS Docker Benchmark gate you can drop in front of any Docker consumer.
sockguard serve # Start proxy (default)
sockguard validate -c sockguard.yaml # Validate + print compiled rule table
sockguard match -c sockguard.yaml -X GET --path /v1.45/containers/json
# Dry-run a single request through the rules
sockguard version # Print versionsockguard match is the offline rule-evaluation probe — point it
at a config and a <method, path> and it prints which rule fires,
what the normalized path looks like, and the reason (if any), so
you can sanity-check a ruleset before any traffic hits the proxy.
Output is text by default or JSON via -o json.
Replace the image — your current Tecnativa env surface maps over directly, with two explicit security acknowledgements for the non-loopback plaintext TCP listener plus a third for broad archive/export or log/attach streaming parity:
services:
socket-proxy:
- image: tecnativa/docker-socket-proxy
+ image: codeswhat/sockguard
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- SOCKGUARD_LISTEN_ADDRESS=:2375
- SOCKGUARD_LISTEN_INSECURE_ALLOW_PLAIN_TCP=true
+ - SOCKGUARD_LISTEN_INSECURE_ALLOW_UNAUTHENTICATED_CLIENTS=true
- SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION=true
- CONTAINERS=1
- SERVICES=1
- POST=0v1.0 contract is locked — the YAML schema, CLI flags, env vars, admin endpoints, and Prometheus metric names visible today are what v1.0 will ship. Soak period is underway; tag follows once production deployments validate the surface. See CHANGELOG.md for the full per-release detail.
| Track | Surface |
|---|---|
| Foundation | Default-deny proxy, glob path rules, Tecnativa env compatibility, structured access + audit logging, health endpoint, hardened Wolfi image, multi-arch |
| Transport | Unix socket and mTLS-protected TCP listener, TLS 1.3 minimum, loopback by default, SPKI pins, plaintext non-loopback rejected without explicit opt-in |
| Body inspection | Every Docker write surface with a meaningful body shape — containers/create, exec, build, services, swarm, configs/secrets, volumes, plugins, networks, image load, container update, archive write, node update |
| Container enforcement | Privileged / host namespaces / CapAdd / device passthrough denied by default; no-new-privileges, non-root, readonly rootfs, drop-all-capabilities, memory / CPU / PIDs limits, seccomp + AppArmor allowlists; cosign image-trust verification (keyed + keyless via Fulcio + Rekor) |
| Per-client policy | Source-IP, mTLS (CN/DNS/IP/URI/SPIFFE/SPKI), unix SO_PEERCRED, container-label resolution; named profiles with rollout modes (enforce / warn / audit) |
| Read-side visibility | Response filtering across containers/services/tasks/configs/secrets/nodes/plugins/swarm/info/system-df with generic protected-JSON mediation |
| Abuse controls | Per-client token-bucket rate limits, burst budgets, concurrency caps, endpoint-cost weighting, system-wide priority-aware fairness gate |
| Observability | Prometheus /metrics, dedicated audit schema, trusted request IDs, deny-reason enums, W3C trace/log correlation, active upstream socket watchdog, lock-free hot path |
| Dynamic policy | POST /admin/validate CI gate, fsnotify + SIGHUP hot reload with immutable-field gate, monotonic policy versioning, optional dedicated admin listener, cosign-signed policy bundles |
| Tier | Theme |
|---|---|
| Security hardening (v1.x) | Continued mutation-test hardening of the rule-evaluation core and config validators |
| Policy refinement (v1.x) | Multiple frontend listeners on the main proxy, named rule path aliases |
| Compliance (v1.x) | CIS Docker Benchmark control mapping, audit-ready policy templates |
| Multi-host (v1.1) | Remote Docker TCP upstreams, multi-upstream fan-out, remote daemon health checking, connection pooling, automatic failover |
| Extensibility (v1.x+) | Optional plugin extension points (WASM or Go plugins), OPA/Rego policy integration |
See CONTRIBUTING.md. Issues, ideas, and pull requests welcome.
For local fuzz triage, run scripts/local-fuzz.sh --suite ci --fuzztime 2m. Use --suite ultra for every fuzzer, --timeout to set the Go watchdog explicitly, and --docker --platform linux/amd64 when you want closer GitHub Actions parity.
- Responsible disclosure — see SECURITY.md for scope, supported versions, and how to report a vulnerability privately.
- Image verification — every release is cosign-signed via GitHub Actions OIDC. Before running a sockguard image in production, verify it with the canonical invocation in the image verification guide.
Built by CodesWhat · Licensed under Apache-2.0
