Getting Started

Install sockguard with Docker Compose, Docker Run, Homebrew, or a release binary, and point your apps at the proxy socket.

Installation

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.yaml:/etc/sockguard/sockguard.yaml:ro
      - sockguard-socket:/var/run/sockguard
    environment:
      - SOCKGUARD_LISTEN_SOCKET=/var/run/sockguard/sockguard.sock

volumes:
  sockguard-socket:

Docker Run

docker run -d \
  --name sockguard \
  --read-only \
  --cap-drop=ALL \
  --security-opt no-new-privileges:true \
  --group-add <docker-socket-gid> \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v ./sockguard.yaml:/etc/sockguard/sockguard.yaml:ro \
  -v sockguard-socket:/var/run/sockguard \
  -e SOCKGUARD_LISTEN_SOCKET=/var/run/sockguard/sockguard.sock \
  codeswhat/sockguard:latest

Sockguard runs as UID 65532 (Chainguard nonroot) inside the container. Set DOCKER_SOCK_GID before starting Compose (export DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock) on Linux; use stat -f '%g' on macOS), or replace <docker-socket-gid> in the docker run command with that numeric value. The hardening knobs that matter in practice are --read-only, --cap-drop=ALL, --security-opt no-new-privileges:true, 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 of that socket must also use UID 65532; root consumers can connect as well. If an application must keep another UID, run Sockguard with that matching UID against a pre-owned bind-mounted directory, set listen.socket_mode: "0660" with an explicit listen.socket_gid so a named group can reach the socket, or use an authenticated TCP listener. 0600 and 0660 are the only modes Sockguard accepts, and 0660 without socket_gid is rejected, so a group-shared socket is always a deliberate choice rather than a default.

Homebrew

Install the latest stable native binary on macOS or Linux from the CodesWhat tap:

brew install --cask codeswhat/tap/sockguard
sockguard version

Stable releases update the cask automatically. Release candidates and other prereleases remain available from GitHub Releases without moving the stable Homebrew channel.

The cask installs only the sockguard command. It does not create a service, grant Docker socket access, or generate a policy file. Run it under your own service manager after configuring the upstream Docker socket and an explicit policy; Docker Compose remains the recommended production deployment.

The macOS binary is not yet signed and notarized with an Apple Developer ID. After Homebrew verifies the release archive's SHA-256 checksum, the cask removes the quarantine attribute from its staged sockguard binary so Gatekeeper does not block it. This bypass applies only to that installed file. That checksum proves the archive wasn't corrupted or tampered with in transit, it does not prove who published it, so it is not a substitute for Apple Developer ID signing/notarization. If your policy requires Apple notarization, or you don't want the quarantine bypass at all, use the container image or verify the GitHub Releases binary with cosign instead.

Upgrade or remove the cask with:

brew upgrade --cask sockguard
brew uninstall --cask sockguard

Binary

Download from GitHub Releases:

sockguard serve -c /etc/sockguard/sockguard.yaml

This guide uses a unix socket because it is the simplest secure deployment. If you expose Sockguard on non-loopback TCP, configure listen.tls for mutual TLS. Plaintext remote TCP requires the two-flag legacy opt-in — both SOCKGUARD_LISTEN_INSECURE_ALLOW_PLAIN_TCP=true and SOCKGUARD_LISTEN_INSECURE_ALLOW_UNAUTHENTICATED_CLIENTS=true.

Connecting Your Apps

Point your Docker consumers at the proxy socket instead of docker.sock:

services:
  traefik:
    volumes:
      - sockguard-socket:/var/run/sockguard:ro
    environment:
      - DOCKER_HOST=unix:///var/run/sockguard/sockguard.sock

  drydock:
    volumes:
      - sockguard-socket:/var/run/sockguard:ro
    environment:
      - DD_WATCHER_LOCAL_SOCKET=/var/run/sockguard/sockguard.sock

CLI Commands

# Start the proxy (default command)
sockguard serve

# Start with a signed policy and separate pinned trust
sockguard serve --config /etc/sockguard/sockguard.yaml --policy-bundle-trust-config /etc/sockguard/policy-bundle-trust.yaml

# Validate configuration and print the compiled rule table
sockguard validate -c /etc/sockguard/sockguard.yaml

# Offline dry-run: evaluate a single request against the rules,
# show which rule fires and why, without starting the proxy.
sockguard match -c /etc/sockguard/sockguard.yaml \
  -X GET --path /v1.45/containers/json

# Runtime self-check: is the upstream daemon answering, is this
# sockguard's own health endpoint up, does the TLS material load
sockguard verify -c /etc/sockguard/sockguard.yaml

# Print version
sockguard version

sockguard match reads the same config, applies the same path normalization as the running proxy, and reports the decision in text (default) or JSON (-o json). Use it to sanity-check a ruleset before any traffic hits the proxy.

sockguard verify is the runtime counterpart to sockguard validate. Where validate is an offline check on a config file, verify loads the config the way serve does (flags, environment, file) and then checks that the things it names are reachable right now. It runs five checks and prints one line each:

CheckWhat it does
configLoads and validates the effective config, without touching the filesystem paths it names.
upstreamBuilds the upstream endpoints (loading their TLS material), runs the same reachability probe serve runs at startup, asks the Docker API through the same readiness probe /health uses, and resolves the engine flavor.
listenerIssues GET <health.path> against each configured listener.
tlsOpens the cert, key, and client CA that each mutual-TLS listener names — the filesystem half of validation that POST /admin/validate deliberately skips.
image-trustReports whether image trust is configured and, when it uses keyless identities, whether the Sigstore trust root loads.

Each check reports ok, fail, or skip. A skip is a check that does not apply to this deployment — an opt-in feature that is off, a listener that is not running — and never changes the exit code; any fail exits non-zero, so verify works as a container healthcheck or a deploy gate. Add --json for a machine-readable report with the same five checks in the same order. --listen-socket and --upstream-socket override the config the same way they do on serve.

Verify does not need the proxy to be running: with no listener up, the listener check skips and everything else still runs, so it is also a pre-flight check on a host before the first start.

Signed-policy mode requires the candidate and bootstrap trust config to be different files, including after symlink or hardlink resolution. The trust file uses policy_bundle.enabled, signing identities, Rekor posture, and the verification timeout. The signed candidate carries policy_bundle.signature_path. Rule-generating Tecnativa variables cannot be combined with this mode; migrate those grants to YAML before signing. See Signed Policy Bundles for the complete layout.

Next Steps

  • Configuration — YAML config and environment variables
  • Presets — Ready-made configs for drydock, Traefik, and more
  • Migration — Migrate from Tecnativa or LinuxServer