Skip to content

CodesWhat/sockguard

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

188 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
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.
πŸ«₯ Visibility-Controlled Reads Redacts env, mount, and network-topology metadata by default and can hide list/events/inspect results behind per-client label visibility rules.
🧱 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 ❌ ❌ ❌ βœ… (visibility + redaction)
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
  redact_container_env: true
  redact_mount_paths: true
  redact_network_topology: true

request_body:
  container_create:
    allowed_bind_mounts:
      - /srv/containers
      - /var/lib/app-data
  exec:
    allowed_commands:
      - ["/usr/local/bin/pre-update", "--check"]
  image_pull:
    allow_official: true
    allowed_registries:
      - ghcr.io
  build:
    allow_remote_context: false
    allow_host_network: false
    allow_run_instructions: false

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.

Allowed POST /containers/*/exec and POST /exec/*/start requests are inspected when request_body.exec.allowed_commands is non-empty. Sockguard denies non-allowlisted argv vectors, denies privileged execs unless request_body.exec.allow_privileged: true, denies root-user execs unless request_body.exec.allow_root_user: true, and re-inspects POST /exec/*/start against Docker's stored exec metadata before letting it run.

Allowed POST /images/create requests are inspected by default. Sockguard denies fromSrc image imports unless request_body.image_pull.allow_imports: true and only allows Docker Hub official images unless you set request_body.image_pull.allow_all_registries: true or list explicit request_body.image_pull.allowed_registries.

Allowed POST /build requests are inspected by default. Sockguard denies remote build contexts, networkmode=host, and Dockerfiles containing RUN instructions unless you explicitly allow those behaviors under request_body.build.*.

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.

For multi-consumer setups, define named client profiles and assign them by source IP or mTLS client certificate common name. Root-level rules and request_body remain the fallback policy unless clients.default_profile points at one of the named profiles:

clients:
  default_profile: readonly
  source_ip_profiles:
    - profile: watchtower
      cidrs:
        - 172.18.0.0/16
  client_certificate_profiles:
    - profile: portainer
      common_names:
        - portainer-admin
  profiles:
    - name: readonly
      response:
        visible_resource_labels:
          - com.sockguard.visible=true
      rules:
        - match: { method: GET, path: "/containers/**" }
          action: allow
        - match: { method: GET, path: "/events" }
          action: allow
        - match: { method: "*", path: "/**" }
          action: deny
    - name: watchtower
      response:
        visible_resource_labels:
          - com.sockguard.client=watchtower
      request_body:
        image_pull:
          allow_all_registries: true
        exec:
          allowed_commands:
            - ["/usr/local/bin/pre-update"]
      rules:
        - match: { method: GET, path: "/containers/**" }
          action: allow
        - match: { method: POST, path: "/containers/*/exec" }
          action: allow
        - match: { method: POST, path: "/exec/*/start" }
          action: allow
        - match: { method: POST, path: "/images/create" }
          action: allow
        - match: { method: "*", path: "/**" }
          action: deny

Client-certificate profile assignment requires listen.tls mutual TLS. Profile rules and request-body policies are compiled and validated at startup just like the root policy, and sockguard validate now prints the configured client-profile sections too.

response.visible_resource_labels and clients.profiles[].response.visible_resource_labels add read-side visibility control on top of allow rules. Sockguard injects those label selectors into GET /containers/json, /images/json, /networks, /volumes, and /events, and returns 404 for hidden targets on inspect-style reads such as GET /containers/*/json, /images/*/json, /networks/*, /volumes/*, and GET /exec/*/json. Selectors use Docker label syntax (key or key=value), are ANDed together, and profile selectors are additive with the root response selectors.

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. Validation still fails unless you explicitly set it to true when your rules allow body-sensitive writes Sockguard cannot safely constrain yet, such as arbitrary POST /containers/*/exec / POST /exec/*/start without a request_body.exec.allowed_commands allowlist, POST /services/create, POST /services/*/update, or POST /swarm/init.

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.

response.redact_container_env, response.redact_mount_paths, and response.redact_network_topology also default to true. Sockguard scrubs Config.Env on GET /containers/*/json, redacts HostConfig.Binds host paths plus Mounts[*].Source on container list/inspect responses, redacts volume Mountpoint on volume list/inspect responses, and strips container/network address topology from GET /containers/json, GET /containers/*/json, GET /networks, and GET /networks/*. Disable those toggles only for trusted admin clients that genuinely need raw Docker metadata.

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 + 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 version

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


πŸ”„ 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 Profile inheritance, unix peer creds, container/image pattern visibility πŸ•’ planned
0.5.0 Operator auditability β€” Prometheus metrics, dedicated audit log schema, stable request IDs, explicit deny reason codes πŸ•’ planned
0.6.0 Secure container enforcement β€” readonly rootfs, resource limits, approved seccomp/AppArmor/SELinux, restricted CapAdd/Devices, image signature verification πŸ•’ planned
0.7.0 Abuse controls β€” per-client rate limits, burst controls, concurrency caps πŸ•’ planned
0.8.0 Dynamic configuration β€” hot reload, admin API, config validation, policy versioning πŸ•’ 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