Introduction

Sockguard is a Docker socket proxy that inspects the bodies it proxies, not just the URLs.

A Docker socket proxy that actually inspects what it proxies.

Why Sockguard?

The Docker socket (/var/run/docker.sock) is effectively root access to your host. Any container with socket access can create privileged containers, mount the host filesystem, and escape containment entirely.

Most existing proxies stop at method/path or regex filtering. Sockguard goes further:

  • Request body inspection. Docker and native Podman write bodies across container create/update/exec/archive, image pull/load/build, volume/network/secret/config/service/swarm/node, and plugin endpoints are inspected before the daemon sees the request. Native POST /libpod/build shares the bounded request_body.build policy with Docker's classic builder.
  • Owner label isolation. Stamp label-capable creates and build images with an owner label, auto-filter labeled list/prune, and deny cross-owner access across workload and control-plane resources. Method-aware path handling keeps resources named create, prune, or similar action words inside the boundary.
  • Per-client policies — gate callers by source CIDR, certificate selectors including SPKI pins, unix peer credentials, or calling container labels, then apply named profiles
  • mTLS for remote TCP — non-loopback TCP requires mutual TLS 1.3 by default, plaintext TCP is an explicit legacy opt-in
  • Structured access logging — JSON logs with method, raw path, normalized policy path, decision, matched rule, latency, canonical request ID, preserved client request ID when present, and W3C trace correlation fields
  • Operator observability. Opt-in /metrics uses finite method and route labels, active upstream watchdog signals report Docker socket reachability, and trace/log correlation works without an OTLP exporter.
  • Abuse controls — per-profile token-bucket rate limits, concurrency caps, per-endpoint cost weighting, and a system-wide priority-aware fairness gate keep noisy callers from starving the rest
  • Dynamic policy delivery. Per-profile enforce|warn|audit rollout modes support staged rollouts, fsnotify + SIGHUP hot reload uses an immutable-field gate, and policy versions are visible at GET /admin/policy/version and on sockguard_policy_version. Cosign-signed candidates are verified at startup and every reload against a separate trust config selected with --policy-bundle-trust-config.
  • Default-deny — everything blocked unless explicitly allowed
  • Tecnativa compatible. Drop-in replacement for the current Tecnativa environment-variable surface in unsigned mode. Signed mode rejects rule-generating compatibility variables so environment state cannot modify verified rules.

Quick Start

services:
  sockguard:
    image: codeswhat/sockguard:latest
    restart: unless-stopped
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    group_add:
      - "${DOCKER_SOCK_GID:?set to the GID of /var/run/docker.sock}"
    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
      - IMAGES=1
      - EVENTS=1

  your-app:
    depends_on:
      - sockguard
    # Sockguard's filtered socket is 0600 and owned by its non-root UID.
    user: "65532:65532"
    volumes:
      - sockguard-socket:/var/run/sockguard:ro
    environment:
      - DOCKER_HOST=unix:///var/run/sockguard/sockguard.sock

volumes:
  sockguard-socket:

Before starting Compose, set DOCKER_SOCK_GID to the socket's numeric group owner (export DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock) on Linux; use stat -f '%g' on macOS).

The published image runs as UID 65532 (Chainguard nonroot) inside the container. The group_add above lets that user open the stock root:docker 0660 host socket without weakening the container's user identity. The meaningful hardening levers are the proxy policy, read_only, dropped capabilities, no-new-privileges, and your runtime's seccomp/AppArmor/SELinux defaults.

The filtered socket and its named-volume directory are owner-only and owned by UID/GID 65532. A non-root consumer must use UID 65532, as your-app does above; root can also connect. If the consumer must keep another UID, run Sockguard as that matching UID with a pre-owned bind mount, or use an authenticated TCP listener. Sockguard intentionally rejects broader socket modes.

SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION=true is only there to preserve broad CONTAINERS=1 / IMAGES=1 compatibility, including raw archive/export and log/attach streaming endpoints. Remove it once you switch to tighter YAML list/inspect rules.

If you choose TCP instead of a unix socket, Sockguard only allows loopback TCP by default. Non-loopback TCP requires mutual TLS unless you explicitly opt into legacy plaintext mode with both SOCKGUARD_LISTEN_INSECURE_ALLOW_PLAIN_TCP=true and SOCKGUARD_LISTEN_INSECURE_ALLOW_UNAUTHENTICATED_CLIENTS=true.