Migration

Upgrade Sockguard through v2.0, including the signed-policy trust split, or migrate from another Docker socket proxy.

From Sockguard v1.4 to v1.5

v1.5 keeps the v1.x YAML, CLI, environment-variable, admin API, and Prometheus metric compatibility contract. Most existing configurations load unchanged, but four intentional enforcement/default changes deserve review before upgrading:

  1. Finite upstream requests now time out after 60 seconds by default. Streaming and long-running endpoints remain exempt. If your deployment intentionally needs unlimited finite requests, set upstream.request_timeout: "off" (or SOCKGUARD_UPSTREAM_REQUEST_TIMEOUT=off).
  2. Cross-owner container:<ref> namespace joins are denied by default when ownership.owner is configured. Same-owner joins continue to work. Set ownership.allow_cross_owner_namespace_sharing: true only if you deliberately accept the cross-tenant namespace-sharing risk.
  3. Host cgroup-namespace mode is denied by default. A create body with HostConfig.CgroupnsMode: host now needs request_body.container_create.allow_host_cgroupns: true, matching the existing opt-ins for the other host namespace modes.
  4. Create-time endpoint configuration now follows the existing network-connect policy. Static IPs, custom MAC addresses, endpoint driver options, and links inside POST /containers/create's NetworkingConfig.EndpointsConfig require request_body.network.allow_endpoint_config: true, just as they already did on POST /networks/*/connect. Compose endpoint aliases remain allowed without that opt-in.

The other v1.5 policy additions are opt-in: restrict_namespace_sharing, allowed_namespace_sharing_containers, deny_namespace_path_mode, require_cpu_limit_hard, and exec allowed_env_vars/denied_env_vars/allowed_env_values. See Configuration for their exact semantics and defaults. The bundled Drydock self-update preset now value-pins its loopback finalize callback at port 3000; if Drydock uses a different server port, update that exact preset value before deployment.

Before switching the image tag, validate the current configuration with sockguard validate --config /etc/sockguard/config.yaml or the admin listener's POST /admin/validate. Deployments using macvlan/static-IP recreation should pay particular attention to allow_endpoint_config; the bundled drydock presets document that compatibility case inline.

From Sockguard v1.6 to v1.7

allow_endpoint_config can now be narrowed to per-field gates (#186)

request_body.network.allow_endpoint_config: true still admits every EndpointSettings field exactly as before — no existing configuration needs to change. New deployments that only need one or two of those fields (Compose Aliases plus a static IP, say, but not MAC pinning) can opt into the narrower request_body.network.endpoint_config block instead: allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority each gate one field independently and default to false; allow_aliases defaults to true to preserve the existing unconditional-allow behavior for Compose aliases. endpoint_config is only consulted when allow_endpoint_config is false — setting both is a load-time validation error, since the legacy flag already admits everything the granular block could.

Links and DriverOpts have no granular gate. Neither field is covered by any endpoint_config.allow_* option, on either POST /networks/*/connect or POST /containers/create's NetworkingConfig.EndpointsConfig — they remain denied under the granular block regardless of which other fields you enable. A workflow that needs either must keep allow_endpoint_config: true and accept whole-object access instead of narrowing to endpoint_config.

See Configuration for the full field mapping.

Migrating off insecure_accept_opaque_buildkit_tunnels (#185)

insecure_accept_opaque_buildkit_tunnels: true is now deprecated: it still works — existing configs that set it keep running unchanged — but setting it logs a startup warning, and the flag will be removed in a future major release. Full BuildKit gRPC mediation (issue #185) supersedes it: request_body.buildkit inspects the Control/Solve/Status RPCs and the session Auth/Secrets/SSH/FileSync/FileSend/Upload callbacks per gRPC message, instead of admitting the whole POST /session/POST /grpc tunnel with zero inspection. The two are mutually exclusive — a config setting both fails validation with an error naming the conflict.

To migrate, replace the acknowledgment with a request_body.buildkit policy scoped to what your Dockerfiles actually need. This is the minimum for a plain docker build/docker compose build of a local Dockerfile and context, tagging the result into the local image store (no push, no secrets, no remote context):

-insecure_accept_opaque_buildkit_tunnels: true
+request_body:
+  buildkit:
+    control:
+      allow_info: true
+      allow_list_workers: true
+      allow_status: true
+      solve:
+        allow: true
+        allowed_exporters: [image]
+    session:
+      file_sync:
+        allow: true

 rules:
   - match: { method: POST, path: "/session" }
     action: allow
   - match: { method: POST, path: "/grpc" }
     action: allow

The rules admitting POST /session and POST /grpc don't change — only which acknowledgment or policy satisfies sockguard's startup admission check for those two rules changes. Widen from that minimum only as needed:

  • docker push / --push: add the destination registry hosts to control.solve.allowed_exporter_registries.
  • RUN --mount=type=secret / type=ssh: enable session.secrets / session.ssh with an exact ID allowlist.
  • Base-image pulls that need registry auth (most public registries included — Docker Hub issues anonymous pull tokens too — unless the daemon's own credentials already cover it): enable session.auth with the exact allowed_registries / allowed_realms / allowed_scopes your Dockerfiles need; these match exactly, not by pattern, so there's no single generic value that covers arbitrary images.
  • A remote (git/HTTP) build context: set request_body.build.allow_remote_context: true. A stdin or remote context that BuildKit streams back over the session also needs session.upload.allow: true.
  • -o type=local / -o type=tar output: add local / tar to control.solve.allowed_exporters and set session.file_send.allow: true. The exporter allowlist is empty by default and denies both.

See the drydock-with-mediated-build.yaml / portwing-with-mediated-build.yaml presets for a complete, validated starting point, and Security / Configuration for the full field reference.

From Sockguard v1.7.5 to v2.0.0

Split signed-policy trust from the signed candidate

Existing signed-policy deployments must move trust roots out of the candidate before upgrading. Create a second bootstrap file that reuses the existing policy_bundle fields for enabled, allowed_signing_keys, allowed_keyless, require_rekor_inclusion, and verify_timeout. Leave only signature_path in the signed candidate:

policy_bundle:
  enabled: true
  allowed_signing_keys:
    - pem: |
        -----BEGIN PUBLIC KEY-----
        ...
        -----END PUBLIC KEY-----
  require_rekor_inclusion: true
  verify_timeout: 10s
policy_bundle:
  signature_path: /etc/sockguard/sockguard.yaml.bundle

Changing the candidate invalidates its old bundle. Sign the exact updated bytes on a trusted workstation with the private key matching allowed_signing_keys, deploy only the candidate and bundle, then add the required serve flag. Never mount or deploy the private signing key with Sockguard:

cosign sign-blob --key ./policy-signing.key --bundle ./sockguard.yaml.bundle ./sockguard.yaml
sockguard serve --config /etc/sockguard/sockguard.yaml --policy-bundle-trust-config /etc/sockguard/policy-bundle-trust.yaml

For keyless signing, replace allowed_signing_keys with an exact allowed_keyless issuer and subject pattern, then omit --key from the cosign sign-blob --yes command. A GitHub Actions signing job also needs permissions: id-token: write. Keyless trust fetches the public Sigstore root through TUF initially and refreshes it about every 24 hours, so ongoing egress is required. The loader uses no local cache, bounds each HTTP request to 15 seconds, and works with Sockguard's read-only container filesystem. An initial load failure aborts startup; a failed background refresh is logged and retains the last valid root.

policy_bundle.verify_timeout is a cooperative local-verification deadline. Sockguard checks it before beginning and after each synchronous Sigstore verification attempt, rejects late success, and stops signer fallback. It does not preempt an individual crypto call, and TUF downloads use their separate 15-second HTTP bound. Candidate and bootstrap YAML files are capped at 16 MiB, and bundle JSON at 4 MiB, before parsing or verification. Only regular files are accepted; FIFOs, devices, directories, and other non-regular paths are rejected without blocking.

Image trust also caps every registry GET response at 4 MiB, including redirect destinations; referrers at 32 descriptors; distinct signature images at 16; layers per signature manifest at 32; aggregate verification candidates at 16; aggregate annotation keys and values at 256 KiB; each simple-signing payload at 1 MiB; and all payload reads for one image at 16 MiB. Exceeding any limit aborts discovery, even when a valid sibling signature exists. enforce denies the request, while warn logs the failed discovery and forwards it. Signature references must be direct image manifests; OCI indexes are rejected instead of recursively traversed, and payload layers with alternate URLs are rejected before blob resolution. Legal media-type parameters on direct manifests are accepted. Discovery deadlines preserve their cancellation cause instead of being reported as an unsigned image.

The two paths must identify different files. Sockguard rejects equal paths, symlinks to the same target, and hardlinks to the same inode. Candidate copies of trust fields are ignored; policy_bundle.enabled: true in the candidate without the new flag is a startup error.

Signed-policy mode also rejects rule-generating Tecnativa variables, including section variables, POST, GRPC/SESSION, and granular ALLOW_* variables, even when set to false. Convert those generated grants into YAML rules before signing. All SOCKGUARD_* settings that affect a signed deployment, including listener settings, must be present in the signed YAML because environment overlays are discarded after verification. A variable added after startup makes reload record reject_compat and preserve the active policy.

Helm users must configure policyBundleTrust and policyBundleSignature together. The chart mounts the external signature object at /etc/sockguard-policy-bundle-signature/signature.json; set the candidate's policy_bundle.signature_path to that path before signing. The trust ConfigMap reference cannot name the chart-generated candidate ConfigMap, and trust plus signature data cannot share one Secret or ConfigMap because Kubernetes grants write authority at the object level. The chart also moves its default listener settings from unsigned environment variables into the candidate YAML. If you replace the chart's config value, keep an explicit listen block in the signed policy so the Service and probes can reach the pod.

Review tightened request handling

  • Native POST /libpod/build now uses request_body.build. A policy that allows this path must explicitly permit remote contexts, host networking, or Dockerfile RUN instructions if the workload needs them.
  • response.allow_attestation_statements: false now follows daemon query semantics. Empty, 0, no, false, and none are false; every other non-empty statement value is treated as a request for protected content.
  • Owner and visibility checks now cover resources named after collection actions such as create, prune, and json. No config change is required, but a cross-owner or hidden resource that previously slipped through by name now returns the configured denial.
  • Inspected bodies and attach/exec upgrade handshakes have effective 30-second deadlines. Long-lived upgraded streams are unchanged after a valid 101.
  • Mediated BuildKit refs and uploads are correlated by trusted caller principal, selected profile, and BuildKit session. Multiple simultaneous builds from one caller no longer share consumable session state. Persistent identifiers are limited to 256 bytes, one control tunnel can retain at most 256 BuildKit session IDs, and abandoned upload grants expire after one hour.
  • Prometheus method and route labels are finite. Custom HTTP methods appear as method="OTHER"; unknown path families appear as route="unknown". Update dashboards that previously expected raw custom values.

From Tecnativa/docker-socket-proxy

Sockguard accepts the current Tecnativa environment-variable surface, including section vars, ALLOW_RESTARTS, SOCKET_PATH, and LOG_LEVEL. Replace the image:

 services:
   socket-proxy:
-    image: tecnativa/docker-socket-proxy
+    image: codeswhat/sockguard
     volumes:
       - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - CONTAINERS=1
      - POST=0

That gets you current Tecnativa env-surface compatibility. If you intentionally want section-wide raw archive/export or log/attach streaming parity from CONTAINERS=1 or IMAGES=1, also set SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION=true; otherwise move to YAML and allow only the list/inspect endpoints you actually need.

Tecnativa compatibility mode and signed-policy mode are separate migration stages. Rule-generating compatibility variables are unsigned process state, so Sockguard rejects them whenever --policy-bundle-trust-config is active. Convert the generated allow surface to YAML first, validate it, then sign it.

When ready, migrate to YAML config for more control:

rules:
  - match: { method: GET, path: "/containers/json" }
    action: allow
  - match: { method: GET, path: "/containers/*/json" }
    action: allow
  - match: { method: GET, path: "/_ping" }
    action: allow
  - match: { method: GET, path: "/version" }
    action: allow
  - match: { method: GET, path: "/events" }
    action: allow
  - match: { method: "*", path: "/**" }
    action: deny

From LinuxServer/socket-proxy

Same process as Tecnativa. Additionally, granular operation env vars are supported:

ALLOW_START=1
ALLOW_STOP=1
ALLOW_RESTARTS=1  # or the legacy singular ALLOW_RESTART=1; both map to the same rules

From wollomatic/socket-proxy

wollomatic already does more than a basic path gate: regex allowlists per method, IP or hostname admission, per-container label allowlists, optional bind-mount restrictions, JSON logging, active upstream watchdog checks, and a filtered unix-socket endpoint. Sockguard's migration story is mostly direct: body policy, named profiles, mTLS selectors, owner isolation, Prometheus metrics, trace/log correlation, and read-side visibility are the main upgrades; hostname admission maps best to mTLS DNS/URI selectors, CIDRs, unix peers, or container labels.

wollomaticsockguard
-allowGET "^/(v[0-9.]+/)?containers/json$"{ method: GET, path: "/containers/json" }
-allowGET "^/(v[0-9.]+/)?events$"{ method: GET, path: "/events" }
-allowPOST "^/(v[0-9.]+/)?containers/[a-z0-9]+/start$"{ method: POST, path: "/containers/*/start" }
-allowfrom=172.18.0.0/16clients.allowed_cidrs: ["172.18.0.0/16"]
-allowfrom=traefik (hostname admission)clients.allowed_cidrs with the caller's IP, or clients.container_labels for name-based resolution. For cryptographic caller identity, clients.client_certificate_profiles (mTLS) or clients.unix_peer_profiles are a stronger upgrade.
socket-proxy.allow.get=.*clients.container_labels.label_prefix: socket-proxy.allow.
-allowbindmountfrom=/srv/data,/var/logrequest_body.container_create.allowed_bind_mounts: ["/srv/data", "/var/log"]
-proxysocketendpoint=/tmp/filtered.socklisten.socket: /tmp/filtered.sock

Key differences:

  • Sockguard auto-strips API version prefixes — no need for (v[0-9.]+/)? in patterns
  • Glob patterns (*, **) instead of regex
  • YAML config instead of CLI flags
  • Auto-anchoring built in (no need for ^ and $)
  • Wollomatic already supports hostname allowlists and a filtered unix-socket endpoint today; Sockguard is stronger on exec/pull/build inspection, named profiles, ownership isolation, and visibility-controlled reads

From 11notes/docker-socket-proxy

11notes/docker-socket-proxy is a fixed read-only proxy — it allows every Docker API GET except seven exfiltration-prone endpoints and blocks every write. There is no rule syntax to translate; the equivalent in Sockguard is a default-deny config that allows reads broadly while denying those same endpoints first (first-match-wins):

rules:
  # Deny the seven exfiltration-prone reads 11notes blocks
  - match: { method: GET, path: "/containers/*/attach/ws" }
    action: deny
  - match: { method: GET, path: "/containers/*/export" }
    action: deny
  - match: { method: GET, path: "/containers/*/archive" }
    action: deny
  - match: { method: GET, path: "/secrets" }
    action: deny
  - match: { method: GET, path: "/configs" }
    action: deny
  - match: { method: GET, path: "/swarm/unlockkey" }
    action: deny
  - match: { method: GET, path: "/images/*/get" }
    action: deny
  # Allow everything else read-only — writes fall through to default-deny
  - match: { method: GET, path: "/**" }
    action: allow

The 11notes environment variables configure the process, not the policy, so they map to Sockguard's listener and upstream settings rather than to rules:

11notes env varSockguard equivalent
SOCKET_PROXY_DOCKER_SOCKETupstream.socket (env: SOCKGUARD_UPSTREAM_SOCKET)
SOCKET_PROXY_VOLUMElisten.socket (env: SOCKGUARD_LISTEN_SOCKET)
SOCKET_PROXY_HTTP_LISTEN_IPlisten.address — mTLS is required for any non-loopback bind
SOCKET_PROXY_UID / SOCKET_PROXY_GIDSockguard's image already runs as UID 65532; set container runtime user: / group_add only when you need to match the host Docker socket's numeric UID/GID
DEBUGlog.level: debug (env: SOCKGUARD_LOG_LEVEL)

Once migrated, tighten the broad GET /** allow into the specific list/inspect endpoints your client actually needs, then re-enable individual writes with body inspection instead of leaving the whole API read-only.

From hectorm/cetusguard

CetusGuard is the closest in spirit to Sockguard — a zero-dependency, default-deny proxy with method + path rules. Its rules use a METHOD[,METHOD] PATTERN grammar with regex patterns and %VARIABLE% placeholders; Sockguard expresses the same intent as YAML glob rules:

CetusGuard ruleSockguard rule
GET %API_PREFIX_CONTAINERS%/json{ method: GET, path: "/containers/json" }
GET %API_PREFIX_CONTAINERS%/%CONTAINER_ID_OR_NAME%/json{ method: GET, path: "/containers/*/json" }
POST %API_PREFIX_CONTAINERS%/%CONTAINER_ID_OR_NAME%/start{ method: POST, path: "/containers/*/start" }

%API_PREFIX_*% placeholders become literal path segments (/containers, /images, …), id placeholders such as %CONTAINER_ID_OR_NAME% become a * glob, and a GET,POST method list becomes two rules. CetusGuard's flags and env vars map to Sockguard's listener and upstream config:

CetusGuardSockguard
-frontend-addr / CETUSGUARD_FRONTEND_ADDRlisten.address (TCP) or listen.socket (unix)
-backend-addr / CETUSGUARD_BACKEND_ADDRupstream.socket
-frontend-tls-cert / -frontend-tls-keylisten.tls.cert_file / listen.tls.key_file
-frontend-tls-cacertlisten.tls.client_ca_file (enables mTLS)
-rules / -rules-file / CETUSGUARD_RULES*The YAML rules: block
-log-level / CETUSGUARD_LOG_LEVELlog.level
builtin /_ping, /info, /version rulesAllow them explicitly — Sockguard has no implicit allows

Key differences:

  • Sockguard auto-strips Docker API version prefixes (/v1.45/…) before matching, so patterns never account for them
  • Glob patterns (*, **) with auto-anchoring instead of regex
  • CetusGuard filters on method + path only; Sockguard adds request-body inspection, named per-client profiles, owner isolation, read-side visibility/redaction, rate limits, and metrics on top
  • Both tools support remote daemon upstreams; Sockguard uses upstream.endpoints (YAML) or the DOCKER_HOST/DOCKER_TLS_VERIFY/DOCKER_CERT_PATH drop-in — see Remote Upstreams & Failover for the full guide.