Skip to content

CodesWhat/sockguard

sockguard

sockguard

Control what gets through. A security-first Docker socket proxy built in Go.

Release GHCR Docker Hub pulls Quay.io
Multi-arch Image size License Apache-2.0

Stars Forks Issues Last commit Commit activity
Discussions Repo size

CI Go Report Card Go Reference
OpenSSF Scorecard


📑 Contents


Warning

Pre-release software. Sockguard is in active development. APIs, rule formats, and CLI flags may change before v1.0.

🚀 Quick Start

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
      - 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:2375

By 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 with SOCKGUARD_LISTEN_INSECURE_ALLOW_PLAIN_TCP=true so migration from tecnativa/docker-socket-proxy and linuxserver/socket-proxy still works on a private Docker network. Do not publish that plaintext listener to the host or Internet.

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: true
  • cap_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
      - CONTAINERS=1

Non-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.

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
      - 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:

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.


🤔 Why Sockguard

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.

Existing socket proxies (Tecnativa, LinuxServer) filter by URL path only. Sockguard goes further: granular operation control, structured audit logging, and a default-deny posture out of the box.


✨ Features

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. 10 bundled presets.
📊 Structured Logging JSON access logs with method, path, decision, matched rule, latency, client info.
🔐 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 and client-container label ACLs let one proxy differentiate TCP callers before the global rule set runs.
🔍 Request Body Inspection POST /containers/create bodies are inspected to block privileged containers, host networking, and non-allowlisted bind mounts before Docker sees the request.
🏷️ Owner Label Isolation A proxy instance can stamp containers, networks, volumes, and build-produced images with an owner label, auto-filter list/prune/events calls, and deny cross-owner access to labeled resources.
🧱 Body-Blind Write Guardrail Remaining body-sensitive write endpoints such as exec, build, and Swarm writes still require explicit unsafe opt-in until their request bodies are inspected.
🔄 Tecnativa Compatible Drop-in replacement using the same env vars. CONTAINERS=1, POST=0, ALLOW_START=1 all work.
🪶 Minimal Attack Surface Wolfi-based image, ~12MB. 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 /health endpoint with cached upstream reachability probes.
🧪 Battle-Tested ~99% statement coverage, race-detector clean, fuzz testing on filter, config, proxy, and hijack paths.

⚖️ Comparison

How sockguard stacks up against other Docker socket proxies:

Feature Tecnativa LinuxServer wollomatic Sockguard
Method + path filtering
Granular POST ops Partial Via regex
Request body inspection ✅ (/containers/create)
Per-client policies CIDR + client labels ✅ (CIDR + client labels)
Response filtering 🕒 Planned
Structured audit log
YAML config
Tecnativa env compat N/A

⚙️ Configuration

Environment Variables (Tecnativa-compatible)

CONTAINERS=1    # Allow GET /containers/**
IMAGES=0        # Deny /images/**
EVENTS=1        # Allow GET /events
POST=0          # Read-only mode

# Granular (requires POST=1)
ALLOW_START=1
ALLOW_STOP=1
ALLOW_CREATE=0
ALLOW_EXEC=0

Compat 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.

YAML Config (recommended)

listen:
  address: 127.0.0.1:2375
  insecure_allow_plain_tcp: false
  tls:
    cert_file: /run/secrets/sockguard/server-cert.pem
    key_file: /run/secrets/sockguard/server-key.pem
    client_ca_file: /run/secrets/sockguard/client-ca.pem

insecure_allow_body_blind_writes: false

response:
  deny_verbosity: minimal  # recommended for production; verbose adds method/path/reason for debugging

request_body:
  container_create:
    allowed_bind_mounts:
      - /srv/containers
      - /var/lib/app-data

clients:
  allowed_cidrs:
    - 172.18.0.0/16
  container_labels:
    enabled: true
    label_prefix: com.sockguard.allow.

ownership:
  owner: ci-job-123
  label_key: com.sockguard.owner

rules:
  - match: { method: GET, path: "/_ping" }
    action: allow
  - match: { method: GET, path: "/containers/**" }
    action: allow
  - match: { method: POST, path: "/containers/*/start" }
    action: allow
  - match: { method: "*", path: "/**" }
    action: deny

Trailing /** matches both the base path and any deeper path. For example, /containers/** matches /containers and /containers/abc/json.

listen.tls is only needed when you expose Sockguard on non-loopback TCP. Plaintext non-loopback TCP is rejected unless you set listen.insecure_allow_plain_tcp: true, which is intended only for legacy compatibility on a private, trusted network.

Allowed POST /containers/create requests are inspected by default. Unless you opt out, Sockguard blocks HostConfig.Privileged=true, HostConfig.NetworkMode=host, and any bind mount source outside request_body.container_create.allowed_bind_mounts. Named volumes still work without allowlist entries because they are not host bind mounts.

clients.allowed_cidrs is a coarse TCP-client gate. Requests whose source IP falls outside every configured CIDR are denied before /health or the global rule set runs.

When clients.container_labels.enabled is true, Sockguard resolves bridge-network callers by source IP through the Docker API and looks for per-client allow labels on the calling container. Each clients.container_labels.label_prefix + <method> label is interpreted as a comma-separated Sockguard glob allowlist for that HTTP method. For example, com.sockguard.allow.get=/containers/**,/events allows only GET /containers/** and GET /events for that client. If you are migrating from wollomatic, set clients.container_labels.label_prefix: socket-proxy.allow. to reuse existing labels.

Set ownership.owner to turn on per-proxy resource ownership isolation. Sockguard will add ownership.label_key=ownership.owner labels to container, network, and volume creates, add the same label to POST /build, inject owner label filters into list/prune/events requests, and deny direct access to labeled resources owned by some other proxy instance. Unowned images are still readable by default so shared base images can be pulled and inspected without relabeling.

insecure_allow_body_blind_writes is off by default. If your rule set allows the remaining body-sensitive Docker write endpoints such as POST /containers/*/exec, POST /exec/*/start, POST /build, or Swarm service creation/update, validation fails unless you explicitly set this flag to true. That opt-in acknowledges that Sockguard is still enforcing method+path only for those writes.

response.deny_verbosity defaults to minimal so 403 responses carry only a generic deny message and never leak the request method, path, or matched rule reason back to the caller. Set it to verbose explicitly during rule authoring if you need to see which rule denied a request — verbose is still useful in dev but should never run in production because it echoes request details in the response body. Even in verbose mode, Sockguard redacts denied /secrets/* and /swarm/unlockkey paths before returning them.

Preset configs included for drydock, Traefik, Portainer, Watchtower, Homepage, Homarr, Diun, Autoheal, and read-only.


🔧 CLI

sockguard serve                           # Start proxy (default)
sockguard validate -c sockguard.yaml      # Validate config
sockguard version                         # Print version

🔄 Migrating from Tecnativa

Replace the image — your env vars work as-is:

 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
       - CONTAINERS=1
       - POST=0

🗺️ Roadmap

Version Theme Status
0.1.0 MVP — drop-in replacement with granular control, YAML config, structured logging ✅ shipped
0.2.0 mTLS for remote TCP, TLS 1.3 minimum, loopback-by-default listener, body-blind write guardrail ✅ shipped
0.3.0 Request-body inspection for /containers/create, per-proxy owner labels, per-client CIDR + container-label ACLs ✅ shipped
0.4.0 Named per-client policy profiles, body inspection for /build and exec, response filtering 🕒 planned
0.5.0 Observability — Prometheus metrics, audit log persistence, OTel trace/span IDs in log records 🕒 planned
0.6.0 Rate limiting, policy safety rails, security enforcement 🕒 planned

🛠️ Built With

Go Cobra Viper Wolfi Cosign
Next.js Nextra Tailwind Turborepo Biome


🤝 Contributing

See CONTRIBUTING.md. Issues, ideas, and pull requests welcome.


🔒 Security

  • 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

About

Docker socket proxy. Filter API requests by method and path with default-deny posture, structured audit logging, and Tecnativa drop-in compatibility.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors