Migration

Upgrade Sockguard through v2.1, including the signed-policy trust split and the v2.0-to-v2.1 TLS-verification deprecation, 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/sockguard.yaml or the admin listener's POST /admin/validate. Deployments using macvlan/static-IP recreation should pay particular attention to the granular endpoint-config gates — endpoint_config.allow_static_addressing, allow_link_local_ips, allow_mac_pinning, and allow_gw_priority — which cover recreation's actual needs individually; request_body.network.allow_endpoint_config remains the whole-object escape hatch for DriverOpts/Links recreation, which have no granular gate of their own. 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 Sockguard v2.0 to v2.1

upstream.endpoints[].insecure_skip_tls_verify is deprecated in v2.1 and will be removed in v3.0.0. It remains accepted in v2.1 and its wire behavior has not changed, so upgrading does not break an existing endpoint. Startup now logs the removal version and replacement metadata.

Replace the flag with the CA that issued the daemon certificate:

 upstream:
   endpoints:
     - address: tcp://dockerd.internal:2376
       tls:
+        ca_file: /certs/ca.pem
         cert_file: /certs/cert.pem
         key_file: /certs/key.pem
-      insecure_skip_tls_verify: true

The Docker environment fallback follows the same schedule. A non-empty DOCKER_TLS with DOCKER_TLS_VERIFY empty or unset selects unverified TLS; set DOCKER_TLS_VERIFY=1 before v3.0.0. Any non-empty verification value, including 0 or false, enables verification and takes precedence over DOCKER_TLS, matching the Docker CLI's environment semantics. When TLS is enabled, sockguard requires ca.pem from DOCKER_CERT_PATH, otherwise DOCKER_CONFIG, otherwise ~/.docker; it presents cert.pem / key.pem only when both exist. DOCKER_CERT_PATH alone only locates material and does not enable TLS. Bare and portless TCP hosts default to port 2375; a TCP URL path is preserved as the API prefix across proxy, inspection, readiness, attach/exec, BuildKit, and failover traffic; and valid custom Unix socket names retain their literal bytes after Docker-compatible URL validation. Absolute Unix paths preserve percent escapes and raw ? or #, while relative host-form escapes such as %2F and %3F are invalid. An invalid explicit DOCKER_HOST fails startup instead of falling through to upstream.socket.

The five sections below were checked against these upstream versions on 2026-10-02, re-checked at every release cut: Tecnativa docker-socket-proxy v0.5.0, LinuxServer docker-socket-proxy 3.4.6-r0-ls100, wollomatic socket-proxy 1.13.1, 11notes docker-socket-proxy v2.2.0, and hectorm cetusguard v1.1.4.

From Tecnativa/docker-socket-proxy

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

Compatibility mode only generates rules while the effective ruleset is still the built-in default. The check is on the rules themselves, not on whether you supplied a config file, so an explicit rules: block that happens to be byte-identical to the default still activates it. That's why a drop-in works untouched: the config the image ships at /etc/sockguard/sockguard.yaml is literally the default. Write a ruleset that differs from it and every section var and ALLOW_* stops generating anything at all, with no error; SOCKET_PATH and LOG_LEVEL keep applying either way.

Values parse as 1, true, or yes, and 0, false, or no, case-insensitively. Anything else logs ignoring compat env var with unparseable boolean value and reads as off. That bites hardest on PING, VERSION, and EVENTS, which are on by default whenever compatibility mode is active: PING=on is not PING=1, it drops the /_ping allow rule and startup still succeeds.

Replace the image, and add the socket's group. Sockguard runs as UID 65532, so it can't open a stock 0660 root:docker socket without one:

 services:
   socket-proxy:
-    image: tecnativa/docker-socket-proxy
+    image: codeswhat/sockguard
+    group_add:
+      - "${DOCKER_SOCK_GID:?set to the GID of /var/run/docker.sock}"
     volumes:
       - /var/run/docker.sock:/var/run/docker.sock:ro
     environment:
+      - SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION=true
       - CONTAINERS=1
       - POST=0

The acknowledgement is required, not optional. CONTAINERS=1 generates allow GET,HEAD /containers/**, and that admits GET /containers/*/top, /archive, /export, /logs, and /attach/ws, so without SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION=true Sockguard exits 1 at startup with an error naming those endpoints. IMAGES=1, SERVICES=1, and TASKS=1 open the same class of surface. Unlike GRPC=1/SESSION=1, which auto-set their own acknowledgement when nothing else mediates that tunnel, compatibility mode never sets this one for you: the whole point is that a section-wide read grant should be a decision you made. Drop the acknowledgement once you move to YAML and allow only the list/inspect endpoints you actually need.

One other difference from Tecnativa in that diff: Sockguard binds loopback 127.0.0.1:2375 by default rather than all interfaces, so a proxy other Compose services talk to over TCP also needs SOCKGUARD_LISTEN_ADDRESS plus both plaintext-TCP acknowledgements, or SOCKGUARD_LISTEN_SOCKET and a shared socket volume. See Getting Started for either block in full.

A third difference is a real permission change, not just a behavior parity gap: Sockguard's granular ALLOW_* vars (ALLOW_START, ALLOW_STOP, ALLOW_RESTARTS, and the rest) are honored independently of POST. Real Tecnativa's haproxy.cfg denies any non-GET request before it ever reaches the ALLOW_* rules, so ALLOW_RESTARTS=1 POST=0 on Tecnativa grants nothing. The same combination on Sockguard grants working restart access. Audit your ALLOW_* settings before cutover if you were relying on POST=0 to blanket-deny them.

CONTAINERS=1 with POST=1, or ALLOW_DELETE=1 independently, keeps Tecnativa's full container-delete behavior, including force removal, anonymous-volume removal, and slash-bearing legacy link names such as client/alias. To allow bare deletes while denying any of those query controls, migrate the delete rule to YAML and leave its matching request_body.container_remove field disabled.

Adding POST=1 needs a second acknowledgement whenever the widened section rule reaches a body-sensitive write endpoint. CONTAINERS=1 POST=1 generates allow POST /containers/**, which admits POST /containers/*/exec with no command allowlist, so Sockguard exits 1 with set insecure_allow_body_blind_writes=true to acknowledge this risk until SOCKGUARD_INSECURE_ALLOW_BODY_BLIND_WRITES=true is set too. EXEC=1, SWARM=1, and PLUGINS=1 with POST=1 hit the same gate, and so does ALLOW_EXEC=1 on its own. Sections whose widened rule reaches nothing body-sensitive don't: PING=1, VERSION=1, EVENTS=1, INFO=1, NETWORKS=1, and VOLUMES=1 with POST=1 all validate clean, as do POST=1 by itself and ALLOW_DELETE=1, ALLOW_START=1, and ALLOW_STOP=1.

GRPC=1 and SESSION=1 are the one pair that acknowledges itself. They generate allow /grpc/** and allow /session/**, which open BuildKit's opaque session transport, so compatibility mode sets insecure_accept_opaque_buildkit_tunnels for you instead of exiting 1, and that is what keeps a Tecnativa drop-in working untouched. It stops setting it as soon as request_body.buildkit is configured, at the top level or on any client profile, because the acknowledgement and a mediation policy are mutually exclusive and compatibility mode runs before validation: setting it there would fail startup over a key you never wrote.

Where that policy sits decides what happens next. A top-level request_body.buildkit block starts clean, and the rules those two vars generate are admitted under mediation, so every gRPC message on POST /session and POST /grpc is checked against your policy instead of passing uninspected. A policy on a client profile only still refuses to start, and should: the generated rules are top-level, they apply to every client that matches no profile, and a profile's policy cannot mediate them. That refusal names GRPC/SESSION as where the rules came from and gives the two cures that work, which are adding a top-level request_body.buildkit block or dropping the env vars. Setting insecure_accept_opaque_buildkit_tunnels yourself alongside request_body.buildkit is refused either way, because that one you did write.

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. Sockguard reads ten granular operation env vars:

ALLOW_CREATE=1
ALLOW_DELETE=1
ALLOW_EXEC=1
ALLOW_KILL=1
ALLOW_PAUSE=1
ALLOW_PRUNE=1
ALLOW_RESTARTS=1  # or the legacy singular ALLOW_RESTART=1; both map to the same rules
ALLOW_START=1
ALLOW_STOP=1
ALLOW_UNPAUSE=1

Granular Operations lists the rule each one generates. ALLOW_RESTARTS covers more than one endpoint, generating the kill, restart, and stop rules together, and the legacy singular ALLOW_RESTART maps to exactly the same three. Every other var maps to one rule.

Ignored on purpose

LinuxServer's image documents several env vars Sockguard has no equivalent for. LOG_LEVEL and SOCKET_PATH are not on this list — both map, covered above.

  • DISABLE_IPV6: a container-network toggle (bind to IPv4 only) that has no Sockguard equivalent; pick the bind with listen.address instead.
  • ALLOW_ARCHIVE, ALLOW_CHANGES, ALLOW_EXPORT, ALLOW_LOGS, ALLOW_TOP: LinuxServer's own GET-only per-sub-resource gates for /containers/{id}/archive, /changes, /export, /logs, and /top. Sockguard has no matching granular var; CONTAINERS=1 (with the read-exfiltration acknowledgment) admits all of them at once, or write a YAML rule against the specific path.
  • LIBPOD_* (all fifteen: LIBPOD_CONTAINERS, LIBPOD_EVENTS, LIBPOD_EXEC, LIBPOD_GENERATE, LIBPOD_IMAGES, LIBPOD_INFO, LIBPOD_MANIFESTS, LIBPOD_NETWORKS, LIBPOD_PING, LIBPOD_PLAY, LIBPOD_PODS, LIBPOD_SECRETS, LIBPOD_SYSTEM, LIBPOD_VERSION, LIBPOD_VOLUMES): LinuxServer's compat vars for Podman's libpod-native API. Sockguard inspects several /libpod/** request bodies natively (build, container create, image load) but generates no rules from these vars — compatVars has no LIBPOD_* entries — so a Podman /libpod/** deployment needs hand-written YAML rules for that path family instead.
  • TZ: a generic container timezone setting, not an API-gating var; Sockguard's config has nothing that reads it.

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: { enabled: true, label_prefix: "socket-proxy.allow." }, and rewrite the label value to socket-proxy.allow.get=/**. Only the label key's prefix is reusable. Wollomatic label values are regexes; Sockguard reads them as globs, so .* is a literal dot followed by one segment and matches no real path. Every comma-separated path in a label must start with /, and a container carrying one that does not is refused rather than partly honored. The prefix has a default; enabled does not, and label ACLs stay off until you set it.
-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, in rule paths and in container-label values alike. A wollomatic regex copied across unchanged is read as a glob, so translate it: .* becomes /**, ^/containers/.*$ becomes /containers/**
  • That last translation is a slight widening, not an exact match: ^/containers/.*$ requires the literal /containers/ to appear, so it does not match the bare path /containers, while sockguard's /containers/** does match /containers on its own, with nothing after the trailing slash
  • Every path, in a rule and in a label, must start with /
  • 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 read-only by default: it allows every Docker API GET except seven targeted exfiltration-prone endpoints and blocks every write. Its opt-in SOCKET_PROXY_CONTAINERS_ONLY mode (v2.2.0) flips that: only POST container start and stop are allowed, and reads are refused. Its own regex for the image-get endpoint (images/get(/|)$) doesn't match the real ImageGet/ImageGetAll request shapes, so only six of the seven are reliably blocked. Separately, that same style of pattern misfired on the image-inspect endpoint in 11notes #12, which was fixed in v2.1.6. 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):

insecure_allow_read_exfiltration: true

rules:
  # Deny the seven exfiltration-prone reads 11notes targets
  # (11notes' own regex only reliably blocks six of these)
  - 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
  # ** so the inspect endpoints are covered too; a bare /secrets is a literal
  # match that leaves GET /secrets/{id} to the catch-all allow below
  - match: { method: GET, path: "/secrets/**" }
    action: deny
  - match: { method: GET, path: "/configs/**" }
    action: deny
  - match: { method: GET, path: "/swarm/unlockkey" }
    action: deny
  # ** covers both single-segment and nested (registry-qualified) image names
  - 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 acknowledgement at the top isn't optional. GET /** also admits container, service, and task logs, /libpod/** archive and export, and GET /libpod/generate/kube, so without insecure_allow_read_exfiltration: true Sockguard exits 1 at startup and names every one of them. It's the price of translating a proxy whose whole model is "allow all reads"; narrow the GET /** allow to the endpoints your client actually calls and you can drop the acknowledgement.

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 by default for any non-loopback bind; a legacy plaintext escape hatch exists behind two explicit acknowledgments, listen.insecure_allow_plain_tcp: true and listen.insecure_allow_unauthenticated_clients: true (one without the other is rejected). 11notes defaults this to 0.0.0.0 unauthenticated on its TCP listener; a default change to 127.0.0.1 (a9f6073) was reverted the next day after user complaints (79f6afb), and the maintainer flagged that reachable-by-default as the wrong call in issue #17
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) — repeat the flag for multiple frontends and it maps to Sockguard's listeners: array, where each entry also needs a unique name and a non-empty allowed_profiles (see Multiple Listeners), not the singular listen: block
-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)
-backend-tls-cacert / -backend-tls-cert / -backend-tls-keyupstream.endpoints[].tls.ca_file / cert_file / key_file
-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/DOCKER_TLS_VERIFY/DOCKER_CERT_PATH drop-in — see Remote Upstreams & Failover for the full guide.
  • Sockguard's listener enforces a TLS 1.3 floor with no override; CetusGuard's frontend and backend both floor at TLS 1.2. Reused mTLS keypairs still work, but a client that never negotiates above TLS 1.2 needs upgrading before cutover, or the handshake fails.