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:
- 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"(orSOCKGUARD_UPSTREAM_REQUEST_TIMEOUT=off). - Cross-owner
container:<ref>namespace joins are denied by default whenownership.owneris configured. Same-owner joins continue to work. Setownership.allow_cross_owner_namespace_sharing: trueonly if you deliberately accept the cross-tenant namespace-sharing risk. - Host cgroup-namespace mode is denied by default. A create body with
HostConfig.CgroupnsMode: hostnow needsrequest_body.container_create.allow_host_cgroupns: true, matching the existing opt-ins for the other host namespace modes. - 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'sNetworkingConfig.EndpointsConfigrequirerequest_body.network.allow_endpoint_config: true, just as they already did onPOST /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: allowThe 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 tocontrol.solve.allowed_exporter_registries.RUN --mount=type=secret/type=ssh: enablesession.secrets/session.sshwith 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.authwith the exactallowed_registries/allowed_realms/allowed_scopesyour 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 needssession.upload.allow: true. -o type=local/-o type=taroutput: addlocal/tartocontrol.solve.allowed_exportersand setsession.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: 10spolicy_bundle:
signature_path: /etc/sockguard/sockguard.yaml.bundleChanging 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.yamlsockguard serve --config /etc/sockguard/sockguard.yaml --policy-bundle-trust-config /etc/sockguard/policy-bundle-trust.yamlFor 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/buildnow usesrequest_body.build. A policy that allows this path must explicitly permit remote contexts, host networking, or DockerfileRUNinstructions if the workload needs them. response.allow_attestation_statements: falsenow follows daemon query semantics. Empty,0,no,false, andnoneare false; every other non-emptystatementvalue is treated as a request for protected content.- Owner and visibility checks now cover resources named after collection
actions such as
create,prune, andjson. 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 asroute="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=0That 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: denyFrom 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 rulesFrom 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.
| wollomatic | sockguard |
|---|---|
-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/16 | clients.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/log | request_body.container_create.allowed_bind_mounts: ["/srv/data", "/var/log"] |
-proxysocketendpoint=/tmp/filtered.sock | listen.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: allowThe 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 var | Sockguard equivalent |
|---|---|
SOCKET_PROXY_DOCKER_SOCKET | upstream.socket (env: SOCKGUARD_UPSTREAM_SOCKET) |
SOCKET_PROXY_VOLUME | listen.socket (env: SOCKGUARD_LISTEN_SOCKET) |
SOCKET_PROXY_HTTP_LISTEN_IP | listen.address — mTLS is required for any non-loopback bind |
SOCKET_PROXY_UID / SOCKET_PROXY_GID | Sockguard'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 |
DEBUG | log.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 rule | Sockguard 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:
| CetusGuard | Sockguard |
|---|---|
-frontend-addr / CETUSGUARD_FRONTEND_ADDR | listen.address (TCP) or listen.socket (unix) |
-backend-addr / CETUSGUARD_BACKEND_ADDR | upstream.socket |
-frontend-tls-cert / -frontend-tls-key | listen.tls.cert_file / listen.tls.key_file |
-frontend-tls-cacert | listen.tls.client_ca_file (enables mTLS) |
-rules / -rules-file / CETUSGUARD_RULES* | The YAML rules: block |
-log-level / CETUSGUARD_LOG_LEVEL | log.level |
builtin /_ping, /info, /version rules | Allow 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 theDOCKER_HOST/DOCKER_TLS_VERIFY/DOCKER_CERT_PATHdrop-in — see Remote Upstreams & Failover for the full guide.