Configuration
Configure sockguard via YAML or environment variables — listeners, TLS, request-body inspection, client profiles, ownership, and structured access plus audit logging.
Sockguard supports two configuration methods: YAML config files and environment variables. Every YAML configuration file is capped at 16 MiB before parsing.
YAML Config (recommended)
listen:
address: 127.0.0.1:2375
insecure_allow_plain_tcp: false
insecure_allow_unauthenticated_clients: 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
dns_names:
- portainer.internal
uri_sans:
- spiffe://sockguard.test/workload/portainer
server:
shutdown_grace: "30s" # how long to wait for in-flight requests to finish on SIGTERM/SIGINT before force-closing every listener (Go duration); default "30s"; 0 closes immediately
insecure_allow_body_blind_writes: false
insecure_allow_read_exfiltration: false
insecure_accept_opaque_buildkit_tunnels: false # ack required to allow POST /session, POST /grpc, or a moby.buildkit.v1.Control method path
upstream:
socket: /var/run/docker.sock
request_timeout: "60s" # total per-request deadline (Go duration); default "60s"; set "off" (or "") to disable
flavor: auto # which engine is behind the socket: auto | docker | podman. "auto" probes GET /version once at startup
hijack_inactivity_timeout: "10m" # idle deadline for attach/exec-start connections (Go duration); default "10m"
# Remote TCP endpoints with mTLS — when set, socket is ignored.
# List endpoints in priority order; first healthy wins (active/passive failover).
# See Remote Upstreams & Failover for the full guide.
# endpoints:
# - address: tcp://dockerd-a:2376
# tls: { ca_file: /certs/ca.pem, cert_file: /certs/cert.pem, key_file: /certs/key.pem }
# - address: tcp://dockerd-b:2376
# tls: { ca_file: /certs/ca.pem, cert_file: /certs/cert.pem, key_file: /certs/key.pem }
# failover:
# health_interval: "5s"
# health_timeout: "2s"
log:
level: info # debug, info, warn, error
format: json # json, text
output: stderr
access_log: true
response:
deny_verbosity: minimal # minimal (default, production) or verbose (dev/rule-authoring)
redact_container_env: true
redact_mount_paths: true
redact_network_topology: true
redact_sensitive_data: true
redact_host_topology: false # redact GET /info container-runtime plumbing fields (Containerd/FirewallBackend/DiscoveredDevices/NRI); opt-in
allow_attestation_statements: false # deny a truthy statement query unless explicitly allowed
request_body:
container_remove:
allow_force: false # deny force-killing a running container (default)
allow_remove_volumes: false # deny deleting attached anonymous volumes, libpod `depend`, pod removal and kube down (default)
allow_remove_links: false # deny Docker's legacy link-removal mode (default)
container_create:
allow_privileged: false # deny HostConfig.Privileged=true (default)
allow_host_network: false # deny HostConfig.NetworkMode=host and ns:<path> (default)
allow_host_pid: false # deny HostConfig.PidMode=host and ns:<path> (default)
allow_host_ipc: false # deny HostConfig.IpcMode=host and ns:<path> (default)
allowed_bind_mounts: # host paths allowed as /src:/dst bind mounts
- /srv/containers
- /var/lib/app-data
allow_all_devices: false # allow every HostConfig.Devices PathOnHost
allowed_devices: # HostConfig.Devices PathOnHost allowlist
- /dev/dri
allow_device_requests: false # deny HostConfig.DeviceRequests by default (escape hatch: skips all inspection)
allowed_device_requests: # structured DeviceRequests allowlist; each entry must match driver + capabilities + count
- driver: nvidia
allowed_capabilities:
- ["gpu", "compute"]
max_count: -1 # -1 = all devices; omit to allow any count
allow_device_cgroup_rules: false
allowed_device_cgroup_rules: # structured cgroup-device allowlist (empty = deny all)
- "c 1:3 rwm" # /dev/null (char major 1, minor 3)
- "c 226:* rwm" # /dev/dri/* GPU class (char major 226, any minor)
allow_tmpfs_privileged_options: false # deny privilege-escalating tmpfs Mount.TmpfsOptions.Options (dev, suid, exec) by default
require_no_new_privileges: false # require HostConfig.SecurityOpt to include "no-new-privileges:true"
require_non_root_user: false # require Config.User to be a non-zero UID / non-root name
require_readonly_rootfs: false # require HostConfig.ReadonlyRootfs=true
require_drop_all_capabilities: false # require HostConfig.CapDrop to contain "ALL"
allow_all_capabilities: false # opt out of the CapAdd allowlist below (default-deny CapAdd entries)
allowed_capabilities: [] # CapAdd allowlist (CAP_ prefix stripped, case-insensitive)
require_memory_limit: false # require HostConfig.Memory > 0
require_cpu_limit: false # require one of NanoCpus, CpuQuota, CpuPeriod, CpuShares > 0
require_cpu_limit_hard: false # require a genuine CPU-time cap (NanoCpus or CpuQuota); CpuShares alone does not satisfy this
require_pids_limit: false # require HostConfig.PidsLimit > 0
allowed_seccomp_profiles: [] # if non-empty, seccomp= profile must be in this list
deny_unconfined_seccomp: false # standalone toggle: deny seccomp=unconfined when no allowlist is set
allowed_apparmor_profiles: [] # if non-empty, apparmor= profile must be in this list
deny_unconfined_apparmor: false # standalone toggle: deny apparmor=unconfined when no allowlist is set
deny_selinux_disable: false # deny label=disable / label:disable SecurityOpt (turns off SELinux confinement)
deny_selinux_label_override: false # deny label=user:/role:/type:/level: SELinux context overrides
deny_unconfined_system_paths: false # deny systempaths=unconfined AND explicit empty MaskedPaths/ReadonlyPaths
allow_host_userns: false # deny HostConfig.UsernsMode=host and ns:<path> (default)
allow_host_cgroupns: false # deny HostConfig.CgroupnsMode=host and ns:<path> (default)
restrict_namespace_sharing: false # gate container:<ref> NetworkMode/PidMode/IpcMode/UTSMode/UsernsMode/CgroupnsMode joins, and refuse ns:<path> on them (default: pass through unchecked)
allowed_namespace_sharing_containers: [] # only consulted when restrict_namespace_sharing=true; empty denies every container:<ref>
deny_namespace_path_mode: false # deny NetworkMode=ns:<path> even with allow_host_network on
allow_sysctls: false # deny a non-empty HostConfig.Sysctls map (default)
allowed_runtimes: [] # allowlist for non-empty HostConfig.Runtime values (e.g. ["runsc", "kata-runtime"]); an empty/unset runtime selects the daemon default and is always permitted
required_labels: [] # Config.Labels keys that must be present with non-empty values
image_trust: # cosign signature verification (default: off)
mode: enforce # off | warn | enforce
allowed_signing_keys: # keyed: PEM-encoded ECDSA/RSA/ed25519 public keys
- pem: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
allowed_keyless: # keyless: Fulcio cert chains with issuer + SAN matching
- issuer: "https://token.actions.githubusercontent.com"
subject_pattern: "^https://github.com/my-org/my-repo/.github/workflows/release\\.yml@refs/heads/main$"
require_rekor_inclusion: true # additionally require a Rekor transparency log entry (default: true)
verify_timeout: 10s # end-to-end image-trust deadline (default: 10s)
libpod_container_create: # POST /libpod/containers/create — Podman's native SpecGenerator body, path-exclusive from container_create above
allow_privileged: false # deny top-level privileged=true (default)
allow_host_network: false # deny netns.nsmode=host and path (default)
allow_host_pid: false # deny pidns.nsmode=host and path (default)
allow_host_ipc: false # deny ipcns.nsmode=host and path (default)
allow_host_userns: false # deny userns.nsmode=host and path (default)
allowed_bind_mounts: # host paths allowed as mounts[] bind sources
- /srv/containers
allow_all_devices: false # allow every devices[] host path
allowed_devices: # devices[] host path allowlist (the part before the first ":")
- /dev/dri
restrict_namespace_sharing: false # gate {"nsmode":"container","value":"<ref>"} joins across netns/pidns/ipcns/userns/utsns/cgroupns, and refuse nsmode=path on them (default: pass through unchecked)
allowed_namespace_sharing_containers: [] # only consulted when restrict_namespace_sharing=true; empty denies every container:<ref> join
allow_all_capabilities: false # opt out of the cap_add allowlist below (default-deny cap_add entries)
allowed_capabilities: [] # cap_add allowlist (CAP_ prefix stripped, case-insensitive)
allowed_seccomp_profiles: [] # if non-empty, seccomp_profile_path must be in this list ("default" covers the empty/unset default)
deny_unconfined_seccomp: false # standalone toggle: deny seccomp_profile_path=unconfined when no allowlist is set
allowed_apparmor_profiles: [] # if non-empty, apparmor_profile must be in this list
deny_unconfined_apparmor: false # standalone toggle: deny apparmor_profile=unconfined when no allowlist is set
deny_selinux_disable: false # deny selinux_opts containing "disable" (turns off SELinux confinement)
require_non_root_user: false # require the "user" field to be a non-zero UID / non-root name
require_readonly_rootfs: false # require read_only_filesystem=true
require_memory_limit: false # require resource_limits.memory.limit > 0
require_cpu_limit: false # require any of resource_limits.cpu.{quota,period,shares} > 0
require_cpu_limit_hard: false # require a genuine CPU-time cap (cpu.quota); cpu.shares alone does not satisfy this
require_pids_limit: false # require resource_limits.pids.limit > 0
allow_sysctls: false # deny a non-empty sysctl map (default)
allow_systemd_mode: false # deny systemd values other than "false" (default) — SpecGenerator itself defaults systemd to "true" even when --systemd is never passed, so this is deliberately strict
allow_custom_id_mappings: false # deny non-default idmappings.uidMap/gidMap or --userns=auto (default)
image_trust: # cosign signature verification on the "image" field (default: off); same semantics as container_create.image_trust above
mode: off # off | warn | enforce
require_rekor_inclusion: true
exec:
allow_privileged: false
allow_root_user: false
allowed_commands:
- ["/usr/local/bin/pre-update", "--check"]
allowed_env_vars:
- CALLBACK_URL
allowed_env_values:
- CALLBACK_URL=http://127.0.0.1:3000/callback
image_pull:
allow_imports: false
allow_all_registries: false
allow_official: true
allowed_registries:
- ghcr.io
build:
allow_remote_context: false
allow_host_network: false
allow_run_instructions: false
# Mediated BuildKit gRPC policy (issue #185) — gates POST /session and
# POST /grpc once request_body.buildkit is configured, superseding the
# deprecated insecure_accept_opaque_buildkit_tunnels wholesale ack (the two
# are mutually exclusive). See the Compose/BuildKit Transport section of
# the security model docs and the *-with-mediated-build.yaml presets.
buildkit:
control:
allow_info: false # Control/Info — response filtered to the buildkit version
allow_list_workers: false # Control/ListWorkers — response filtered to worker id/platforms/version
allow_status: false # Control/Status — admitted only for a ref this session itself Solve'd
solve:
allow: false # Control/Solve — the actual build request
allowed_cache_import_types: [] # Cache.Imports[].Type allowlist; empty = deny
allowed_cache_export_types: [] # Cache.Exports[].Type allowlist; empty = deny
allowed_cache_registries: [] # registry host allowlist for "registry"-typed cache entries
allowed_exporters: [] # Exporters[].Type allowlist (e.g. image, oci, local); empty = deny
allowed_exporter_registries: [] # registry host allowlist for an "image" exporter with push=true
session:
health: false # passthrough grpc.health.v1.Health/{Check,Watch}
auth:
allow: false # moby.filesync.v1.Auth (Credentials/FetchToken/GetTokenAuthority/VerifyTokenAuthority)
allowed_registries: [] # exact registry-host allowlist
allowed_realms: [] # exact FetchToken realm allowlist
allowed_scopes: [] # exact FetchToken scope allowlist
secrets:
allow: false # moby.buildkit.secrets.v1.Secrets/GetSecret
allowed_ids: [] # exact secret-ID allowlist
ssh:
allow: false # moby.sshforward.v1.SSH/{CheckAgent,ForwardAgent}
allowed_ids: [] # exact SSH agent-ID allowlist
file_sync:
allow: false # moby.filesync.v1.FileSync/DiffCopy — required to sync the Dockerfile/context at all
max_files: 0 # 0 = buildkitproxy.Limits default
max_total_bytes: 0 # 0 = buildkitproxy.Limits default
max_path_length: 0 # 0 = buildkitproxy.Limits default
max_file_bytes: 0 # 0 = buildkitproxy.Limits default
file_send:
allow: false # moby.filesync.v1.FileSend/DiffCopy (local/tar exporter output)
max_bytes: 0 # 0 = buildkitproxy.Limits default
upload:
allow: false # moby.upload.v1.Upload/Pull (remote/stdin build context)
max_bytes: 0 # 0 = buildkitproxy.Limits default
service:
allow_host_network: false
allowed_bind_mounts:
- /srv/services
allow_official: true
allowed_registries:
- ghcr.io
require_non_root_user: false # require a non-root ContainerSpec.User (default off)
require_no_new_privileges: false # require ContainerSpec.Privileges.NoNewPrivileges (default off)
require_readonly_rootfs: false # require ContainerSpec.ReadOnly=true (default off)
require_drop_all_capabilities: false # require ContainerSpec.CapabilityDrop=["ALL"] (default off)
deny_unconfined_seccomp: false # deny ContainerSpec.Privileges.Seccomp.Mode=="unconfined" (default off)
deny_custom_seccomp_profiles: false # deny Seccomp.Mode=="custom" or a bare Profile blob (default off)
deny_unconfined_apparmor: false # deny ContainerSpec.Privileges.AppArmor.Mode=="disabled" (default off)
deny_selinux_disable: false # deny ContainerSpec.Privileges.SELinuxContext.Disable=true (default off)
deny_selinux_label_override: false # deny SELinuxContext.{User,Role,Type,Level} overrides (default off)
swarm:
allow_force_new_cluster: false
allow_external_ca: false
clients:
allowed_cidrs: # coarse TCP admission; empty means all allowed
- 172.18.0.0/16
container_labels:
enabled: true # resolve caller by source IP, enforce labels
label_prefix: com.sockguard.allow.
ownership:
owner: ci-job-123 # when set, stamps owner labels and denies cross-owner access on owned resources
label_key: com.sockguard.owner
allow_unowned_images: true
allow_cross_owner_namespace_sharing: false # default false (secure): deny container:<ref> namespace joins into a different owner's container
health:
enabled: true
path: /health
watchdog:
enabled: false
interval: 5s
readiness:
enabled: false # opt-in /ready probe against the Docker API
path: /ready # must differ from health.path, metrics.path, admin.path
interval: 10s # positive duration
timeout: 5s # positive duration; per-probe deadline
metrics:
enabled: false
path: /metrics
admin:
enabled: false
path: /admin/validate
policy_version_path: /admin/policy/version
max_request_bytes: 524288
rules:
- match: { method: GET, path: "/_ping" }
action: allow
- match: { method: HEAD, path: "/_ping" }
action: allow
- match: { method: GET, path: "/version" }
action: allow
- match: { method: GET, path: "/events" }
action: allow
- match: { method: GET, path: "/containers/json" }
action: allow
- match: { method: GET, path: "/containers/*/json" }
action: allow
- match: { method: "*", path: "/**" }
action: deny
reason: "no matching allow rule"The default listener is loopback TCP 127.0.0.1:2375, which keeps the Docker API proxy off the network unless you explicitly choose otherwise.
- For the safest deployment, use
listen.socketand share a unix socket. - Sockguard hardens the unix socket to
0600owner-only permissions by default. The published image and fresh named-volume directory use UID/GID65532, so a non-root consumer must use UID65532too; root can also connect. For another consumer UID, run Sockguard under that matching UID with a pre-owned bind mount, or use authenticated TCP.listen.socket_modeaccepts one other value:0660, group-readable, which also requireslisten.socket_gid(the group ID that should own the socket).listen.socket_uidoptionally sets the socket's owning UID for either mode. Any mode other than0600or0660(withsocket_gidset) is rejected at startup. - For remote or container-network TCP, configure
listen.tlsso Sockguard requires mutual TLS. Sockguard's mTLS server minimum is TLS 1.3, so callers must support TLS 1.3. (The TLS 1.3 floor applies to the listener — the side your clients connect to. The upstream client side, where Sockguard dials a remote Docker daemon, floors at TLS 1.2 for daemon compatibility; see Remote Upstreams & Failover.) listen.tls.client_ca_filestill defines the issuing trust root. To avoid trusting every cert that CA can mint, optionally narrow the accepted verified client leaf with the selectors described under mTLS Client Selectors below. Different selector fields are ANDed, while entries inside one field are ORed.- Plaintext non-loopback TCP is rejected unless you set both
listen.insecure_allow_plain_tcp: trueandlisten.insecure_allow_unauthenticated_clients: true. Both acknowledgments are required — one without the other is rejected — so a single fat-fingered flag cannot expose the listener. That mode is only for legacy compatibility on a private, trusted network. server.shutdown_gracebounds how long sockguard waits for in-flight requests to finish, on both the main listener(s) and the admin listener, after a shutdown signal (SIGTERM/SIGINT) is received, as a Go duration string (e.g."30s"). A request still running when the grace period elapses has its listener force-closed out from under it rather than being waited on indefinitely. Defaults to"30s", unchanged from sockguard's behavior before this field existed. Unlikehijack_inactivity_timeout,0is a valid value — it means "close immediately" — so every value, including an empty string, must parse as a non-negative Go duration, or config load fails validation; only a negative duration or an unparseable value is rejected. The wholeserver.*block is immutable across hot reload — changing it requires a restart.health.watchdog.enabledstarts an active upstream socket monitor that checks Docker everyhealth.watchdog.interval, logs reachable/unreachable state transitions, and lets/healthanswer from the latest watchdog state instead of waiting for a scrape or probe to discover an outage. The watchdog dials the socket — a liveness signal that only proves the socket accepts connections.health.readiness.enabledadds an opt-in/readyendpoint (default path/ready) that goes one step further than the watchdog: instead of dialing the socket, it issues a realGET /containers/json?limit=1against the upstream Docker API everyhealth.readiness.interval(per-probe deadlinehealth.readiness.timeout). It returns200only when the daemon actually answers, and503on any transport error or non-2xx — catching the failure mode where the socket stays connectable but request handling has wedged. Point a Kubernetes / load-balancer readiness check at/readyand a liveness check at/health. The path must start with/and must not collide withhealth.path,metrics.path, oradmin.path;intervalandtimeoutmust be positive durations. The wholehealth.*block (readiness included) is immutable across hot reload — changing it requires a restart.upstream.endpointsis the ordered list of remote daemon addresses for TCP+mTLS or unix-socket connections. When non-empty,upstream.socketis ignored and sockguard uses the first healthy endpoint in the list (active/passive failover).endpointsandupstream.failover.*are reload-immutable — changing them requires a restart. See Remote Upstreams & Failover for the full guide including HA failover, mTLS setup, insecure opt-ins, and theDOCKER_*drop-in path.upstream.endpoints[].insecure_skip_tls_verifyis deprecated in v2.1 and scheduled for removal in v3.0.0. Existing v2.1 configurations remain accepted and keep the same encrypted but unauthenticated wire behavior, with a startup warning namingtls.ca_fileas the replacement. The Docker environment equivalent (a non-emptyDOCKER_TLSwhileDOCKER_TLS_VERIFYis empty or unset) has the same schedule and warns withDOCKER_TLS_VERIFY=1as its replacement. Configure the daemon's issuing CA or enable Docker TLS verification before v3.0.0.upstream.request_timeoutbounds the total lifetime of a single proxied request as a Go duration string (e.g."30s"); it defaults to"60s".ResponseHeaderTimeoutonly caps the wait for response headers; a daemon that sends headers and then hangs the body — or hangs a heavy read likeGET /containers/json— can otherwise pin a request indefinitely. When active, an expired finite request aborts its upstream connection. If the deadline fires before the daemon sends response headers, the caller gets504 Gateway Timeout(reason_code=upstream_request_timeout), distinct from the502an unreachable socket yields. If headers were already committed — the hung-body case above — HTTP does not allow replacing a status that has been sent, so the caller keeps the committed status (typically200) and the response body is truncated mid-stream. The request is still aborted rather than pinned, which is the guarantee this setting provides; the504is not available once streaming has begun. Long-lived endpoints are exempt so the deadline never severs a legitimately long response: event streams, follow/stream logs and stats (including swarm service and task log following), truthy Podman top streams on both the native routes and Docker-compatible container route, image pull/build/push/load, image export, plugin create/pull/push/upgrade, container export, image get, container archive (docker cp, both directions), websocket attach, the BuildKit tunnel endpointsPOST /sessionandPOST /grpc, and the blockingGET /containers/{id}/wait; hijacked attach/exec-start connections already bypass it. Docker daemon top and Podman top without a truthystreamvalue remain finite and bounded. Setrequest_timeout: "off"to explicitly disable the deadline — the legacy empty string""is also still accepted for backward compatibility. Prefer"off"when disabling via the env var:SOCKGUARD_UPSTREAM_REQUEST_TIMEOUT=(an explicitly empty value) is treated as unset and silently falls back to the60sdefault, whileoffalways works; an explicitrequest_timeout: ""in YAML does correctly disable it. Unlikehealth.*, this field is reload-mutable (upstream.socket,upstream.endpoints, andupstream.failoverare immutable), so it takes effect on hot reload. Any other value must be a positive duration.upstream.flavornames the container engine behind the upstream:auto(the default),docker, orpodman. It exists for one reason. Podman serves the Docker-compatGET /eventsand its ownGET /libpod/eventsfrom a single handler, and that handler evaluates several values under one filter key disjunctively — Podman's own source comments it as "Filters under the same key are disjunctive while each key must match". dockerd ANDs them. Sockguard enforces a visibility policy on/eventsby injectinglabelfilter values, so on Podman two or moreresponse.visible_resource_labelsselectors would become an OR and stream events the policy hides. Knowing the engine is what lets sockguard write a filter the daemon will read the way it meant it. See Podman.autoissues oneGET /versionto the daemon at startup, before any listener binds, and classifies it from the engine component name the response carries (Podman EngineversusEngine). Nothing is probed afterwards; the result is fixed for the process.- An explicit
dockerorpodmanis taken as given. NoGET /versionflavor probe runs, so nothing in that response can move an operator's stated answer. Set it explicitly when the upstream is itself a proxy that does not serve/version. Startup still performs the mandatory upstream reachability dial before resolving the configured flavor; the explicit value removes the HTTP flavor-probe dependency, not the requirement that the upstream be reachable. - A probe that fails, times out (5 seconds), returns a non-200, or names an engine sockguard does not recognize fails startup with an error naming this field. There is deliberately no fallback: defaulting to
dockerwould silently reopen the disclosure above on a Podman host, and defaulting topodmanwould silently start refusing/eventson a Docker host that was working — and because the answer is cached for the life of the process, one transient failure at boot would carry the wrong answer until the next restart. The remedy is one config line. Either outcome is logged asupstream flavor resolved(withprobed=true|false) orupstream flavor probe failed. sockguard validateand every other config-only path never probe, so validation works with no daemon present.- Reload-immutable, alongside
upstream.socketandupstream.endpoints: it describes the same daemon they address, and the resolved value is bound into the handler chain at startup. - The setting has to match the engine that is really behind the socket, because owner-isolation checks branch on it and a wrong value does not fail startup. For example, an image retag (
POST /images/{name}/tag) under owner isolation checks one target name when the flavor isdocker, and both{repo}:{tag}andlocalhost/{repo}:{tag}when it ispodman. A Podman upstream configured asdockergets the one-name check and leaves thelocalhost/name unchecked. Leave it onautounless the upstream is a proxy that does not serve/version.
upstream.hijack_inactivity_timeoutbounds how long a hijacked connection (docker attach,docker execstart) may sit idle — no bytes read or written in either direction — before sockguard tears it down, as a Go duration string (e.g."10m"). Activity in either direction resets the connection-wide timer, so continuous output keeps the input side usable and vice versa. It never caps the total length of an active session, only how long a forgotten one is left open. Hijacked connections are exempt fromupstream.request_timeoutentirely — this is their only deadline. Defaults to"10m", unchanged from sockguard's behavior before this field existed. Unlikerequest_timeoutthere is no"off"spelling: every value, including an empty string, must parse as a positive duration, or config load fails validation. An idle teardown logs atwarn("hijack: idle connection closed after inactivity timeout"), so it is visible at the defaultinfolog level instead of only atdebug. Reload-mutable, same asrequest_timeout.- Bounded request-body inspectors set a 30-second body-read deadline before decoding. The access-log and metrics wrappers both preserve the response-controller interface used to set and clear it, so enabling either layer does not turn a slow body into an unlimited request. Attach and exec-start use a separate 30-second handshake deadline for the client body and upstream request/response headers; it is cleared only after a valid
101 Switching Protocolsresponse so the upgraded stream can remain long lived. metrics.enabledis opt-in and serves Prometheus text metrics atmetrics.pathon the same listener. The endpoint is local to Sockguard, is never forwarded to Docker, bypasses Docker API allow rules like/health, and remains behind listener security plusclients.allowed_cidrs. Every scrape also exports asockguard_build_info{version,commit,build_date,go_version}gauge and asockguard_start_time_secondsgauge for version panels and uptime alerts. Request labels are finite: known methods keep their uppercase name, all other methods useOTHER, known route families use bounded templates, and every unknown family usesunknown. When the active watchdog is enabled, metrics also includesockguard_upstream_socket_upandsockguard_upstream_watchdog_checks_total; when the readiness probe is enabled, they also includesockguard_upstream_api_upandsockguard_upstream_readiness_checks_total.admin.enabledis opt-in and exposes a singlePOST <admin.path>endpoint (default/admin/validate) that runs the same parse + validate + compile pipeline as the offlinesockguard validatecommand against a YAML body in the request payload. Useful as a CI gate before promoting a candidate config to production. Running policy is never mutated. The endpoint rides the main listener, so the listener's CIDR allowlist, mTLS posture, and per-profile rate-limit / concurrency caps all apply. Bodies are hard-capped atadmin.max_request_bytes(default 512 KiB) viahttp.MaxBytesReaderand return413on overflow. Non-POST methods return405withAllow: POST. The response body is a structured JSON report:{"ok": bool, "rules": int, "profiles": int, "compat_active": bool, "errors": [...]?}. A failing candidate returns422with the validator's per-issue error list; a passing candidate returns200.admin.pathmust start with/and must not collide withhealth.pathormetrics.pathwhen those endpoints are also enabled.reload.enabledis opt-in and turns on hot reload at runtime. Sockguard watches the candidate throughfsnotifyand also reloads onSIGHUP;reload.debouncecollapses a burst of editor events. The pipeline parses, validates, compiles, and atomically swaps the handler chain on success. In-flight requests finish on the previous chain and new requests use the new one. Any load, validation, immutable-field, signature, or signed-compatibility failure preserves the active policy. Unsigned mode applies Tecnativa rule generation normally. Signed mode verifies with pinned bootstrap trust and rejects rule-generating compatibility variables withreject_compat. Listener, upstream endpoint, log, health, metrics, and admin fields require a restart; the signed candidate'spolicy_bundle.signature_pathremains reloadable while bootstrap trust cannot be changed in process. Reload metrics use the finite result setok|reject_load|reject_validation|reject_immutable|reject_signature|reject_compat. Enabling reload changesSIGHUPfrom process termination to reload.- W3C trace/log correlation is always on and has no config knob. Sockguard preserves valid incoming
traceparenttrace IDs and sampled flags, replaces the parent span with a proxy-local span ID for the forwarded request, and generates fresh local context when the caller does not send valid trace context. DELETE /containers/**query parameters are inspected by default, including slash-bearing names used for Docker's legacy link-removal route. A bare container removal is allowed whenever its method/path rule allows it, whileforce,v(anonymous-volume removal), andlinkeach require their own explicit opt-in underrequest_body.container_remove. Matching follows dockerd exactly: after trimming and case-folding, only an empty value,0,no,false, ornoneis false; every other value is true.force,vandlinkare each read once under their exact lowercase spelling. While the matching gate is closed, a request that repeats one of them or spells it in another case (Force=1,force=0&force=1) is refused with a403, because dockerd takes the first value of the exact key while Podman matches the key in any case and takes the last, so the two would disagree about whether the removal is forced. Percent-encoded keys and values are decoded before inspection, and malformed query encoding is rejected with400before it can reach the daemon. Podman'sDELETE /libpod/containers/**is inspected the same way, except that it reads the volumes flag fromvolumesand never readslink. On that pathallow_forcegatesforce, andallow_remove_volumesgatesvolumes,v(documented for the route, ignored by Podman 5.8.6) anddepend, because removing dependents can remove a whole pod and Podman deletes a removed pod's anonymous volumes whatevervolumessays.timeoutandignorearen't gated there. Podman'sDELETE /libpod/pods/{name}always deletes the anonymous volumes of the containers it removes, so every pod removal needsallow_remove_volumes, andforce, which stops running containers first, needsallow_forcetoo. Kube down (DELETE /libpod/play/kubeandDELETE /libpod/kube/play) stops and force-removes every pod its YAML names whatever its query says, so every kube down needs both. See the Podman guide for what each route removes.POST /containers/createbodies are inspected by default. Sockguard blocksHostConfig.Privileged=true,HostConfig.NetworkMode=host,HostConfig.PidMode=host,HostConfig.IpcMode=host,HostConfig.UsernsMode=host, a non-emptyHostConfig.Sysctlsmap (unlessallow_sysctls: true), a non-emptyHostConfig.Runtimevalue not present inallowed_runtimes(an empty/unset runtime selects the daemon default and is always permitted — only callers that explicitly request an alternate OCI runtime need an entry), bind mount sources outsiderequest_body.container_create.allowed_bind_mounts(aMountsentry of typevolumecounts as a bind source when itsVolumeOptions.DriverConfigasks the local driver for one, e.g.{"type":"none","o":"bind","device":"/host/path"}, because that reaches the same host path, and so does the same entry handing the local driver a real filesystem type over a/devnode, e.g.{"type":"ext4","device":"/dev/sda1"}),HostConfig.Deviceshost paths outsiderequest_body.container_create.allowed_devices,HostConfig.DeviceRequests(unless explicitly allowed viaallow_device_requestsorallowed_device_requests),HostConfig.DeviceCgroupRules(unless explicitly allowed viaallow_device_cgroup_rulesorallowed_device_cgroup_rules), and anyHostConfig.CapAddentry that isn't covered byallow_all_capabilitiesorallowed_capabilities. Named volumes still work without allowlist entries because they are not host bind mounts.allowed_device_requestsis the structured opt-in for GPU passthrough and similar device request policy — each entry must specify adriver(exact match, case-insensitive), anallowed_capabilitieslist of capability sets (each request capability set must be a subset of at least one allowlisted set), and an optionalmax_countbound (-1means all devices; requestCount: -1is only allowed whenmax_countis also-1); setallow_device_requests: trueonly when you need unrestricted device request access.allowed_device_cgroup_rulesis the structured opt-in for cgroup device policy — it accepts Docker cgroup rule strings (<type> <major>:<minor> <perms>) with*wildcards for major or minor, and denies request wildcards unless the matching allowlist entry also uses a wildcard at that position; setallow_device_cgroup_rules: trueonly when you need unrestricted cgroup device access. Optional opt-in rails enforceno-new-privileges, non-rootConfig.User,HostConfig.ReadonlyRootfs=true,HostConfig.CapDrop=["ALL"], memory / CPU / PIDs limits, allowlisted seccomp and AppArmor profiles, and requiredConfig.Labelskeys — all default to off, so a configuration that does not set them keeps prior behavior except for the CapAdd allowlist and host-userns default-deny noted above. Three further opt-in rails cover the remainingSecurityOptdirectives:deny_selinux_disabledenieslabel=disable(and the legacylabel:disablecolon form), which turns off SELinux confinement;deny_selinux_label_overridedenieslabel=user:/role:/type:/level:SELinux context customization; anddeny_unconfined_system_pathsdeniessystempaths=unconfinedand requests that setMaskedPathsorReadonlyPathsto an explicit empty array — the Docker CLI converts--security-opt systempaths=unconfinedintoMaskedPaths: []client-side, so a direct API caller can clear the masked-path protections without ever sending the SecurityOpt string; both vectors are blocked. All three default to off (pass-through), preserving existing behavior.image_trustadds cosign-backed signature verification: setmode: enforceto deny containers whose image lacks a valid signature from one of yourallowed_signing_keys(PEM public keys) orallowed_keylessidentities (Fulcio cert chain matched by issuer URL and SAN regex); setmode: warnto log failures and allow the request through instead.require_rekor_inclusion: trueadditionally requires a Rekor transparency log entry from the supplied bundle; verification is local and does not query Rekor.verify_timeoutis the end-to-end image-trust deadline (default 10s), including registry discovery. Registry deadline and cancellation errors retain their cause instead of being reported as an unsigned image. Sockguard checks cancellation before beginning and after each synchronous Sigstore verification attempt, rejects late success, and stops later signatures or signer fallbacks. Sigstore-go's local verification API cannot preempt an individual crypto call. Verification resolves the image to its registry manifest digest, fetches the cosign signatures attached to it (the classicsha256-<digest>.sigtag and OCI 1.1 referrers are both checked), and only accepts a signature whose simple-signing payload binds to that exact digest. Signature references must resolve directly to image manifests; OCI indexes are rejected instead of recursively traversed. Registry-controlled input stops at 4 MiB for every registry GET response, including redirect destinations; 32 referrer descriptors; 16 distinct signature images; 32 layers per signature manifest; 16 aggregate verification candidates; 256 KiB of aggregate annotation keys and values; 1 MiB per simple-signing payload; and 16 MiB across all payload reads for one image. Exceeding any limit aborts discovery, even when a valid sibling signature exists. Inenforcemode a limit breach denies the request; inwarnmode it logs the failed discovery and forwards. The registry must be reachable from the proxy, and private registries require ambient credentials (a mounted Dockerconfig.json). Keyless verification additionally fetches the Sigstore trust root through the same bounded, no-local-cache loader used by signed policy bundles. It loads initially and refreshes about every 24 hours, so ongoing egress is required. A failed initial load fails closed; a failed background refresh is logged and retains the last valid root. TUF downloads use their own 15-second HTTP bound rather thanverify_timeout. The loader works with the read-only runtime filesystem. Keyed (PEM) verification makes no TUF request. When image trust is configured, an empty image reference is denied at Sockguard before any registry request; Docker would reject it too.- Image-trust signature payload layers with alternate URLs are rejected before blob resolution can fan out. Legal media-type parameters on direct manifests are accepted after normalization.
POST /containers/create'sHostConfig.Mountsentries are validated further: a mount whoseTypeis not one ofbind/volume/tmpfs/imageis denied fail-closed (an unrecognized mount type has no reviewed policy, so it is refused rather than passed through);VolumeOptions.SubpathandImageOptions.Subpath(Engine API 1.45+/1.55+) must be empty or a clean relative path that does not escape the mount root via..; andTmpfsOptions.Optionsentries that re-enableexec,dev, orsuidon a tmpfs mount are denied unlessallow_tmpfs_privileged_options: true— Docker's tmpfs default isnoexec,nodev,nosuid, and these options exist specifically to override that.POST /containers/createalso denies fiveHostConfigfields unconditionally — norequest_bodysetting opts back in:VolumesFrom,UTSMode=host, a non-emptyCgroupParent,GroupAdd, andExtraHosts. Each one opens a namespace-escape or privilege-escalation path, so it is blocked regardless of policy.allow_host_network/allow_host_pid/allow_host_ipc/allow_host_userns/allow_host_cgroupnsgate two values onHostConfig.NetworkMode/PidMode/IpcMode/UsernsMode/CgroupnsMode: the literal"host", andns:<path>. Podman's Docker-compatible create readsns:<path>on every one of those fields and joins whatever namespace the path names on the daemon host, soPidMode: "ns:/proc/1/ns/pid"is the host PID namespace under another name. A path needs the same gatehostneeds.UTSMode: "ns:<path>"is always refused, likeUTSMode: host. dockerd has nons:mode: it rejects the value onPidMode,IpcMode,UTSModeandCgroupnsMode, treats it as not-hostonUsernsMode, and reads it as a network name onNetworkMode. It lets a network be named anything, so a Docker network whose name starts withns:can't be used asNetworkModeunlessallow_host_networkis on. Acontainer:<id-or-name>value (join another container's namespace) passes through the host gates unchecked, and always has. Two opt-in filter-tier knobs cover that:restrict_namespace_sharing(+allowed_namespace_sharing_containers) deniescontainer:<ref>joins unless the target is on the allowlist (empty allowlist = deny all), acrossNetworkMode,PidMode,IpcMode,UTSMode,UsernsModeandCgroupnsMode(dockerd refuses acontainer:value onCgroupnsMode, and Podman's Docker-compatible create joins that container's cgroup namespace), and while it's on it refusesns:<path>on those fields too, host gate or not, because a path can name another container's namespace and no list of names can vouch for it;deny_namespace_path_modedenies aNetworkModevalue with anns:<path>prefix even whenallow_host_networkis on. Both default off/empty (pass-through), so enabling them is a deliberate, independent choice from theallow_host_*flags. Separately, whenownership.owneris configured, sockguard resolves everycontainer:<ref>target onPOST /containers/createand denies the request if that container belongs to a different owner — see Owner Label Isolation below; that check runs regardless of whether the filter-tier knobs above are enabled.POST /libpod/containers/create— Podman's native SpecGenerator create endpoint — is inspected by a separate, path-exclusive body inspector underrequest_body.libpod_container_create, independent fromcontainer_createabove; the two never read each other's body shape (Podman's libpod API is structurally distinct from the Docker-compat API, e.g. top-levelprivileged/netns/mounts/volumes/devices/cap_add/user/sysctlfields instead of nestedHostConfig/Configobjects). Sockguard blocksprivileged=true,netns/pidns/ipcns/usernsobjects whosensmodeishostorpath(a path joins whatever namespace it names, and/proc/1/ns/pidis the host's), bind-mountedmounts[]sources outsideallowed_bind_mounts(namedvolumes[]entries are never bind-gated — they reference a volume by name, not a host path),devices[]host paths outsideallowed_devices, and anycap_addentry not covered byallow_all_capabilities/allowed_capabilities. While one of those four gates is off, its namespace only takes the modes Podman has that keep the container out of the host's, so annsmodesockguard doesn't know, or one spelled in another case, is refused too. The Podman page lists them.{"nsmode":"container","value":"<ref>"}namespace-sharing objects (netns/pidns/ipcns/userns/utsns/cgroupns) are gated byrestrict_namespace_sharing/allowed_namespace_sharing_containers, mirroringcontainer_create's equivalent knob, and while it's onnsmode: pathis refused on those fields whatever the host gates say. Optional opt-in rails require a non-rootuser,read_only_filesystem=true, memory/CPU/PIDs limits fromresource_limits, allowlistedseccomp_profile_path/apparmor_profilevalues, and denyselinux_optscontainingdisable— all default to off.image_trustreuses the same cosign verification ascontainer_create.image_trust, applied to theimagefield. Two libpod-only rails have no Docker-compat analog:allow_systemd_mode(SpecGenerator's own default for thesystemdfield is"true"even when--systemdis never passed on the client, so sockguard denies anything but an explicit"false"unless this is set) andallow_custom_id_mappings(denies a non-defaultidmappings.uidMap/gidMapor--userns=auto, independent ofallow_host_userns, which only coversuserns.nsmode=host).POST /containers/*/execandPOST /exec/*/startare inspected whenrequest_body.exec.allowed_commandsis non-empty. Sockguard denies argv vectors that match no allowlist entry — each entry is an argv template whose tokens are sockguard globs (*matches a run of non-slash characters,**matches any sequence), and a command matches when its token count equals an entry's and every token matches the glob at that position, so an exec carrying a variable argument can be allowlisted without enumerating every literal form. It also denies privileged exec unlessallow_privileged: true, denies root-user exec unlessallow_root_user: true, and re-checksPOST /exec/*/startagainst Docker's stored exec metadata before execution. Docker exposes exec inspect and exec start as separate API calls, so this start-time check has an unavoidable time-of-check/time-of-use window; keep exec allowlists and client profile assignments narrow.request_body.execis the single config surface for exec, covering both the Docker-compat paths above and Podman's libpod-nativePOST /libpod/containers/*/execandPOST /libpod/exec/*/start— the two API families expose exec create/start bodies with identical field names, so there is no separatelibpod_execblock; the sameallowed_commands/allow_privileged/allow_root_user/env-var settings apply to a request regardless of which path family carried it, and the libpod exec-start re-check queriesGET /libpod/exec/{id}/json(the libpod equivalent of the Docker-compat exec-inspect call) instead of the compat endpoint.POST /images/createis inspected by default. Sockguard blocks imports unlessrequest_body.image_pull.allow_imports: trueand only allows Docker Hub official images unless you setallow_all_registries: trueor list explicitallowed_registries. The two checks are independent: a request that carries bothfromSrcandfromImageis a pull to dockerd and to Podman, so itsfromImagehas to pass the registry allowlist even when imports are allowed, andfromImageandfromSrcare each read once under their exact spelling. A request that repeats either or spells it in another case is refused with a403while the check that reads it is enabled, because dockerd takes the first value of the exact key while Podman matches the key case-insensitively and keeps the last. Podman's nativePOST /libpod/images/pullshares the samerequest_body.image_pullblock, so one registry allowlist governs both surfaces and there is no separatelibpod_image_pullgroup to configure and forget. libpod's query shape differs and is read on its own terms: the reference arrives inreference, notfromImage/tag, Podman matches that key case-insensitively and takes the last value of a repeated key, and it accepts adocker://transport prefix. Sockguard readsreferenceonce under its exact spelling, refuses a request that repeats it or spells it in another case, and strips the transport prefix before matching the registry host. A libpod pull carrying no usablereferenceis denied rather than forwarded unlessallow_all_registries: true(Podman rejects such a request itself). NativePOST /libpod/images/importsharesimage_pull.allow_imports: every request to that route is an import, whether Podman reads its archive from the body or fetches the caller-selectedURL. Body-form imports are spooled through a 512 MiB limit and return413 Payload Too Largeon overflow; URL imports retain the coarseallow_importsgate without reading a request body. NativePOST /libpod/images/loadlikewise sharesrequest_body.image_loadwith Docker's archive-load endpoint, with format-aware checks for Dockermanifest.jsonrepo tags and OCIindex.jsonref-name annotations.GET /distribution/{name}/jsonmakes the daemon reach out to whatever registry the reference names to fetch a manifest descriptor, so it is checked against the samerequest_body.image_pull.allowed_registriesallowlist as a pull. It reads the registry host from the path reference (allow_officialstill exempts Docker Hub official images). This check is gated on an explicitly configured allowlist: with noallowed_registriesset, or withallow_all_registries: true, the route is left exactly as open as before, so turning on the pull inspector's defaultallow_officialposture never starts denying distribution queries you did not opt into restricting. Credentials in anX-Registry-Authheader are not inspected here because the registry the daemon contacts is the one named in the path.PUT /containers/*/archiveand native Podman'sPUT /libpod/containers/*/archiveare inspected by default through the samerequest_body.container_archiveblock, andPOST /containers/*/updateand native Podman'sPOST /libpod/containers/*/updatethrough the samerequest_body.container_updateblock. There is no separatelibpod_container_archiveorlibpod_container_updategroup to configure and forget. Podman routes both archive spellings to onecompat.Archivehandler, so one inspector reads the shared tar body and case-insensitive query fields. It requires one nonempty, unambiguouspath; refuses relative targets whenallowed_pathsis configured because Podman resolves them against the container WorkingDir; and refuses a nonempty or ambiguousrename, which Podman applies after reading the tar headers. Update needs its own shape handling: libpod's body isUpdateEntities, whose embedded OCIspecs.LinuxResourcesflattens to root-levelmemory/cpu/pids/blockIO/devices/unifiedrather than Docker's PascalCase field names, and its restart policy arrives in therestartPolicy/restartRetriesquery string rather than the body, soallow_restart_policyis enforced against the folded query there.devicesridesallow_all_devices; the OCI resource keys, the per-device IO throttle lists andr_limitsrideallow_resource_updates;health_on_failure,no_healthcheckandhealth_startup_retriesrideallow_restart_policy.allow_privilegedandallow_capabilitiesare inert on the libpod path becauseUpdateEntitiescarries no such field.health_cmd,health_startup_cmd,Env/UnsetEnv, andhealth_log_destinationare denied outright. Health timing and log-retention fields can create high-frequency checks or infinite daemon logs, so they require the globalinsecure_allow_body_blind_writes: trueacknowledgment; the outright denials remain in force even when it is set. The post-ownershiprequire_memory_limit,require_cpu_limit,require_cpu_limit_hard, andrequire_pids_limiteffective-state ratchets cover both update paths, applying each daemon's own merge semantics while forwarding the exact submitted body.POST /buildand native Podman'sPOST /libpod/buildare inspected by default through the samerequest_body.buildpolicy. Sockguard blocks primary remote contexts, Podmanadditionalbuildcontextsbacked by a URL or image,networkmode=host, and Dockerfiles that containRUNinstructions unless you explicitly allow those behaviors underrequest_body.build.*.networkmode,remote,dockerfileandrusagelogfileare read once under their exact spelling, and a request that repeats one or spells it in another case is refused while the check that reads it is enabled.additionalbuildcontextsandrusagemay repeat under their exact spelling and every value is validated. Because their value is always validated, another spelling (/build?Rusage=1) is refused whatever the gates are, while the parameters above are refused in another spelling only while the check that reads them is enabled. The volume controls are recognized in any spelling, so without the blind-write acknowledgment below aVolumeis denied just like avolume. Malformed, empty, unsupported, or duplicate additional-context definitions fail closed. Podman'svolume/volumes/transientRunMounts,localpath:additional contexts, multipart local contexts, and resource-usage host-file output (rusage/rusagelogfile) have no narrower policy and require the globalinsecure_allow_body_blind_writesacknowledgment. Direct and version-prefixed libpod paths use the same inspector and body-sensitive startup validation. WhileRUNis restricted, sockguard inspects every file the engine could build, because the two engines pick it differently and aPOST /builddoesn't say which one will answer. With nodockerfileparameter,POST /buildinspectsDockerfileand the lowercasedockerfiledockerd falls back to, andPOST /libpod/buildinspectsContainerfileandDockerfile, since Podman builds the first of those it finds. A build is refused when any file it inspects carriesRUNor can't be read, and when the context has none of them, so a libpod build whose unusedDockerfilehas aRUNis refused even though Podman would build itsContainerfile. Adockerfilevalue is read the way Podman reads it as well as the way dockerd does: a JSON array of paths, which is how podman-remote sends-f, inspects every file in it. A value Podman would read from somewhere other than the uploaded context is refused before the body is read, on both paths: anhttp://orhttps://URL, an absolute path (Podman reads it from the daemon host when the host has that file), a path that climbs out of the context with../, and a name ending in.in, which Podman runs through the C preprocessor first. podman-remote sends an absolute path for a Containerfile outside the build context, for-f -, and for a relative-frun from a symlinked directory, so those builds are refused whileRUNis restricted (see Podman). A body that is neither a tar nor gzip tar but opens with a gzip, bzip2, xz or zstd header is refused as uninspectable, since the engines decompress it and sockguard doesn't. The tar scan also refuses a context that could change an inspected file after the scan read it: a second entry at the same name, an entry written through a symlink onto it, or a non-directory replacing the context root or a directory on the way to it. An inspected file that is itself a symlink or hard link is refused rather than followed.POST /volumes/createis inspected by default. Sockguard blocks non-local volume drivers and driver options unless you explicitly allow them underrequest_body.volume.*.PUT /volumes/{name}, the Swarm cluster-volume (CSI) update, is inspected by default through the samerequest_body.volumeblock. Its body is a singleSpecobject, and every field in it is denied unless you opt in.Spec.Secretshas its own flag,allow_cluster_volume_secrets, because each entry names a Swarm secret the daemon then hands to the CSI plugin, so rewriting the list points the plugin at a secret the caller was never granted.allow_cluster_volume_updatescovers the rest —Availability,Group,AccessMode,CapacityRange,AccessibilityRequirements— and deliberately does not admitSpec.Secrets. A body with noSpec, or an explicitly null one, is a no-op the daemon applies nothing from and is allowed. Owner isolation covers the same request: the update runs through the owned-resource check like every other named-resource write, so a client cannot update a volume it does not own regardless of these flags. Matching follows moby's route,PUT /volumes/{name:.*}, so slash-bearing names and thecreate/prunespellings (which have noPUTroute of their own and resolve to the update handler) are inspected too.POST /secrets/createandPOST /configs/createare inspected by default. Sockguard blocks custom and template drivers unless you explicitly allow them underrequest_body.secret.*andrequest_body.config.*.Driveris read as the{"Name", "Options"}object both engines take, andDriver.Optionsneedsallow_custom_driversthe same as a driver name does. On this route every driver name is custom,fileincluded, because dockerd resolves any name as a secrets plugin. A client that wants the built-in store, or Podman's file default, leavesDriverout, which is what the Docker CLI does.POST /services/createandPOST /services/*/updateare inspected by default. Sockguard blocks services that attach thehostnetwork, blocks bind mounts outsiderequest_body.service.allowed_bind_mounts(aContainerSpecmount of typevolumecounts as a bind source when itsVolumeOptions.DriverConfigasks the local driver for one, e.g.{"type":"none","o":"bind","device":"/host/path"}, exactly as it does onPOST /containers/create), constrains service images to Docker Hub official images unless you setrequest_body.service.allow_all_registries: trueor list explicitrequest_body.service.allowed_registries, enforces the sameallow_all_capabilities/allowed_capabilitiescapability allowlist andallow_sysctlsgate as container-create, and appliesimage_trustcosign verification to the service's ContainerSpec image. Opt-in hardening rails mirror the container-create knobs onto the swarmContainerSpec:require_non_root_user(denies a root or emptyContainerSpec.User),require_no_new_privileges(requiresContainerSpec.Privileges.NoNewPrivileges: true),require_readonly_rootfs(requiresContainerSpec.ReadOnly: true), andrequire_drop_all_capabilities(requiresContainerSpec.CapabilityDropto includeALL) — all default off, so services that do not set them keep prior behavior. Confinement-mode rails complete the parity:deny_unconfined_seccompdeniesContainerSpec.Privileges.Seccomp.Mode: "unconfined",deny_custom_seccomp_profilesdeniesMode: "custom"and the fail-closed case of aSeccompobject carrying aProfileblob with noMode(an inline profile the proxy cannot vet), anddeny_unconfined_apparmordeniesContainerSpec.Privileges.AppArmor.Mode: "disabled"(swarm's equivalent of unconfined AppArmor). Note that a custom seccomp profile can encode an allow-everything policy, so operators settingdeny_unconfined_seccompshould usually setdeny_custom_seccomp_profilesas well. SELinux rails complete the parity with container-create:deny_selinux_disabledeniesContainerSpec.Privileges.SELinuxContext.Disable: true(the swarm equivalent of the container-createlabel=disable), anddeny_selinux_label_overridedenies anySELinuxContext.{User,Role,Type,Level}context customization — both default off. (Swarm has no privileged mode, no per-service namespace sharing, and no runtime/device knobs, so those container-create rails have no service equivalent; swarm's seccomp/AppArmor settings are mode enums, so the named-profile allowlists from container-create do not apply.)POST /swarm/init,POST /swarm/join, andPOST /swarm/updateare inspected by default. Sockguard blocksForceNewCluster, external CA configuration, non-allowlisted join targets, token rotations, manager unlock-key rotations, manager autolock, and signing-CA updates unless you explicitly allow them underrequest_body.swarm.*.POST /networks/create,POST /networks/*/connect, andPOST /networks/*/disconnectare inspected by default. Sockguard blocks custom drivers, swarm/ingress/attachable/config-only controls, custom IPAM, driver options, and forced disconnects unless explicitly allowed underrequest_body.network.*.allow_endpoint_configadditionally gates endpoint static IP, MAC address, links, driver options, andGwPriority(Engine API 1.45+, which network provides the default route when a container is attached to more than one) — enforced on bothPOST /networks/*/connectandPOST /containers/create'sNetworkingConfig.EndpointsConfig(see thecontainer_createrow below), since the two carry the identical attack surface. EndpointAliasesare always allowed at both, independent of this flag.POST /networks/createalso denies an explicitEnableIPv4: false(Engine API 1.48+; the field defaults totruewhen absent) unlessallow_disable_ipv4: true— disabling IPv4 on a network is a deliberate topology choice, not a bypass, but it changes how the network's traffic can be observed and is gated the same as other network-shape controls.endpoint_confignarrowsallow_endpoint_configinto independent per-field gates (#186).allow_endpoint_config: true(defaultfalse) remains the whole-object escape hatch and, when set, keeps admitting everyEndpointSettingsfield exactly as before — the granular block below is not consulted at all in that case, and a config that sets both is rejected at load time (pick one). That rejection applies per group:request_body.networkandrequest_body.libpod_networkeach carry the block and each are checked on their own, so writing both keys under either one fails startup with a message naming that group. Whenallow_endpoint_configis left at its defaultfalse,request_body.network.endpoint_configgates each field independently:allow_static_addressing(IPAMConfig.IPv4Address/IPv6Addressand the deprecated top-levelGateway/IPAddress/IPPrefixLen/IPv6Gateway/GlobalIPv6Address/GlobalIPv6PrefixLenfields),allow_link_local_ips(IPAMConfig.LinkLocalIPs, independent of static addressing),allow_mac_pinning(MacAddress, shared withcontainer_create's deprecated top-levelMacAddressfield), andallow_gw_priority(GwPriority) all defaultfalse.allow_aliasesdefaultstrue, reproducingallow_endpoint_config's long-standing unconditional-allow behavior forAliases— set it tofalseexplicitly to deny Aliases under the granular form (there is no equivalent opt-out underallow_endpoint_config: true).LinksandDriverOptshave no granular field of their own: under the granular form they are always denied, fail-closed, regardless of the other settings — onlyallow_endpoint_config: truecan admit them. Denial reasons name the specific offending field (e.g. "endpoint link-local IP addresses are not allowed"). Applies identically whereverallow_endpoint_configapplies today —POST /networks/*/connectandPOST /containers/create'sNetworkingConfig.EndpointsConfig/legacyMacAddressfield — and, under thelibpod_networkkey, to Podman's nativePOST /libpod/networks/{name}/connect, whose top-levelstatic_ips/static_mac/aliases/optionsfields map onto the same gates (allow_link_local_ipsandallow_gw_priorityhave no libpod analog and never fire there). See the Podman guide for the libpod wire shape.POST /containers/*/updateis inspected by default. Sockguard blocks restart-policy changes, resource controls, privileged mode, device changes, and capability/security-profile fields unless explicitly allowed underrequest_body.container_update.*.POST /containers/*/update, nativePOST /libpod/containers/*/update, andPOST /services/create/POST /services/*/updateadditionally carry an opt-in resource-limit guard (request_body.container_update.require_memory_limit/require_cpu_limit/require_cpu_limit_hard/require_pids_limit,request_body.service.require_cpu_limit/require_cpu_limit_hard) that runs as its own policy layer after ownership, not inside the inspectors above. All six defaultfalse. For container update, the guard fetches current state from the daemon and applies the target endpoint's merge semantics before validating the effective post-update state: Docker-compatible updates treat omitted or zero scalarHostConfigfields as unchanged while pointerPidsLimitvalues including0/-1clear; native updates overlay lowercasememory.limit,cpu.{shares,quota,period}, andpids.limit, where an explicit zero clears the submitted native field.require_cpu_limit_hardaccepts only a positive CPU quota/cap; shares alone do not satisfy it. Container-update require_* flags are enforced only whenallow_resource_updates: trueis also set; sockguard warns when a require_* flag is set while that gate stays off. For services,ServiceSpecis a full replacement rather than a merge, so an ordinary create/update is validated directly against the submitted body;POST /services/*/update?rollback=previousis instead validated against the daemon's storedPreviousSpec, with a409 resource_limit_policy_state_changedif the inspected version no longer matches the caller'sversionquery; an automatic-rollback-capable update additionally validates the currentSpec. The original body is forwarded byte-for-byte after a successful check on both container API spellings.- The resource-limit guard fails closed: a malformed request body (
400 resource_limit_request_invalid), a failed or malformed daemon lookup (502 resource_limit_policy_lookup_failed), and a rollback version mismatch (409 resource_limit_policy_state_changed) are hard errors in every rollout mode, includingwarn/auditprofiles — only a genuine policy violation (403 resource_limit_policy_denied) honors the profile's rollout mode. Denial messages are static and parameter-free (e.g. "container update denied: a memory limit is required") and never echo submitted values, container/service IDs, or daemon response content. - Migration note (the "ratchet" edge): because the guard validates effective state, enabling a require_* flag against a container or service that already carries a legacy, non-compliant resource configuration blocks that resource's next guarded write — even one that doesn't touch resources at all — until a compliant patch supplies the missing limit. Roll a require_* flag out under
warnorauditbefore switching it toenforcefleet-wide, and expect to remediate long-lived legacy containers/services with one compliant update before enabling it inenforcemode. PUT /containers/*/archiveand nativePUT /libpod/containers/*/archiveare inspected by default. Sockguard blocks unsafe target paths, tar traversal, setuid/setgid entries, device nodes, and escaping symlinks/hardlinks unless explicitly allowed underrequest_body.container_archive.*.POST /images/loadand native Podman'sPOST /libpod/images/loadare inspected by default through the same policy. Docker archives are checked throughmanifest.jsonrepo tags; OCI archives are checked through the byte-exact effectiveindex.jsonname, withio.containerd.image.nametaking precedence overorg.opencontainers.image.ref.nameexactly as it does in Podman. On the native route, bare names are normalized tolocalhost. SHA-256, SHA-384, and SHA-512 OCI graphs are supported. Tagged references must satisfyrequest_body.image_load.*even whenallow_untaggedis enabled, and every Docker manifest member is checked separately for an untagged image. Podman's permissive handling of advisory layout, index schema-version, and descriptor media-type metadata is mirrored. Mixed OCI and Docker archives require both reference sets to be inspectable and pass policy because Podman can reject OCI config or layer content after metadata inspection and then fall back to Docker. The exception is an OCI half no daemon selects a single image from. A multi-manifestindex.jsonloads with themanifest.jsonrepo tags and every index annotation name checked together, since Podman falls back to the Docker archive while a containerd-store dockerd imports each descriptor and records the name it carries; a missing or undecodableindex.jsoncarries no names, so those archives are judged on themanifest.jsonset alone. Either way the multi-image tarballdocker save a:1 b:2writes on Docker's containerd image store still loads. Canonically duplicate controls, malformed mixed-format controls, and outer archive symlink or hardlink entries fail closed. An archive with a top-levelrepositoriesfile and nomanifest.jsonis refused onPOST /images/loadunlessallow_all_registriesis set. Docker's classic image store, before Docker 29, loads that shape in its pre-1.10 layout and takes the image names fromrepositorieswhatever else the archive holds, and Sockguard does not read that file, so its names would skip the allowlist. Everydocker savesince 1.10 writes amanifest.jsonand is unaffected, and Podman never reads the file, so its native route is left alone.POST /swarm/unlockandPOST /nodes/*/updateare inspected by default. Swarm unlock is denied unlessrequest_body.swarm.allow_unlock: true; node updates block role, availability, name, and unapproved label mutations unless allowed underrequest_body.node.*.POST /plugins/pull,POST /plugins/*/upgrade,POST /plugins/*/set, andPOST /plugins/createare inspected by default. Sockguard constrains plugin registries, privileges, assignment prefixes, local tarconfig.json, host mounts, device exposure, and capabilities unless you explicitly allow them underrequest_body.plugin.*.POST /plugins/createis inspected as the raw tar upload Docker reads. Amultipart/form-dataupload is refused withrequest_form_body_refusedbefore it gets that far, because Docker ignores the framing and untars the raw body.POST /images/create(pull),POST /libpod/images/pull, andPOST /images/*/pushall decode the base64X-Registry-Authheader (standard, URL-safe, and unpadded variants all accepted) under the same 8 KiB bound as other inspectors; an oversized, non-base64, or non-JSON header is denied fail-closed. Theimage_pull.allowed_registriescross-check against the decodedserveraddressapplies to the two pull paths only —POST /images/*/pushis not inspected against it, so a client with push access can push to any registry its own credentials reach, gated only byinsecure_allow_read_exfiltration.POST /builddecodes the analogousX-Registry-Configmulti-registry header the same bounded way but does not cross-check hosts against a build-time allowlist. Decoded credential fields (username/password/identity token) are never inspected, logged, or reflected in a deny reason.POST /libpod/pods/create(Podman's libpod-native pod creation) is inspected by default underrequest_body.libpod_pod_create. Sockguard denies a pod-level host network namespace (netns: {nsmode: "host"}), a network namespace joined by path (netns: {nsmode: "path"}) and anetnsmode it doesn't know unlessallow_host_network: true, denies a shared PID namespace ("pid"present inshared_namespaces) unlessallow_shared_pid_namespace: true, and denies an infra image whose registry isn't inallowed_infra_image_registries— reusing the same registry-allowlist shape asimage_pull.allowed_registries— wheneverinfra_imageis set explicitly. Whenownership.owneris configured, cross-owner pod membership is also denied on both sides of the relationship (#148):POST /libpod/pods/createjoining another owner's host namespaces, andPOST /libpod/containers/createtargeting another owner's pod — reusing the samecontainer:<ref>namespace-sharing checksPOST /containers/createalready applies. See Owner Label Isolation below for the full picture.POST /libpod/volumes/create,POST /libpod/networks/create,POST /libpod/networks/{name}/connect,POST /libpod/networks/{name}/disconnect,POST /libpod/networks/{name}/update, andPOST /libpod/secrets/create(Podman's libpod-native equivalents ofPOST /volumes/create/POST /networks/create/POST /networks/*/connect/POST /networks/*/disconnect/POST /secrets/create) are inspected by default underrequest_body.libpod_volume,request_body.libpod_network, andrequest_body.libpod_secretrespectively — these reuse the samevolume/network/secretconfig fields documented above rather than introducing separate knobs, since the policy question (custom drivers? driver options? custom IPAM/subnets?) is identical even though libpod's wire shapes differ from the Docker-compat API: volume driver options live under a top-levelOptionskey (notDriverOpts), network options are snake_case (ipam_options,subnets) with no swarm/ingress/attachable/config-only fields (libpod has no swarm mode), and secret creation readsdriverfrom the query string rather than a JSON body, so the libpod secret inspector never touchesr.Bodyat all. On that routedriver=file, the containers.conf default podman-remote sends on every create, counts as the default driver, and anydriveroptsneedsallow_custom_driverstoo: the file driver'spathoption is the directory on the daemon host the secret data is written to, so choosing it is the same step as choosing a driver.POST /libpod/containers/*/restore,POST /libpod/play/kube(and its identically-handledPOST /libpod/kube/playalias),POST /libpod/kube/apply, andPOST/PUT /libpod/manifests/*have no request-body inspector. Admitting any of them requiresinsecure_allow_body_blind_writes: trueexactly like uninspected exec. Treatplay/kubewith particular care: a single request can carry a multi-container (or multi-pod) Kubernetes manifest, so one allowed call can provision an arbitrary number of privileged containers from a document sockguard never parses, a materially larger blast radius than the single-container blind writes this flag otherwise covers. With?import=1, restore creates a container from an opaque CRIU checkpoint archive whose spec never passes through either container-create inspector.- Oversized bodies on bounded JSON/tar inspectors are rejected with
413 Payload Too Largebefore the inspector decodes them, so a misbehaving or hostile client cannot tie up the filter or the Docker daemon with oversized payloads. Underwarnorauditthe filter still reads no more than the limit, but the body is forwarded whole, so the daemon does see it. insecure_allow_body_blind_writesis now reserved for body-bearing controls Sockguard still cannot safely constrain, chiefly arbitrary exec without anallowed_commandsallowlist,POST /swarm/joinwithoutrequest_body.swarm.allowed_join_remote_addrs, plugin setting writes without allowed assignment prefixes, Podman build host/local/multipart or resource-usage host-file controls, the daemon-hostPOST /libpod/local/buildandPOST /libpod/local/images/loadroutes, the SSH image transferPOST /libpod/images/scp/*,POST /libpod/containers/*/restore, and the uninspected libpodplay/kube/kube/apply/manifest surface described above. For exec and Podman build, setting it changes request-time behavior, not just startup validation: an exec with noallowed_commandsis admitted instead of denied, and Podman's host volume/local/multipart controls orrusage/rusagelogfileoutput is admitted. The remaining exec and build checks still apply. The native libpod image load and import routes are inspected and do not use this acknowledgment. For the daemon-host local routes, the SSH image transfer, container restore, and theplay/kube/kube/apply/manifest writes there is no equivalent partial protection, so setting the flag admits the request with no body inspection at all.POST /libpod/images/scp/*needs both acknowledgments, one per direction: this one for the image it creates from an archive fetched off a caller-named SSH host, outsideimage_pull.allowed_registries, andinsecure_allow_read_exfiltrationfor the copy it sends to one. Scope any allow rule for these paths as narrowly as possible.insecure_allow_read_exfiltrationstaysfalseby default and must be set explicitly before broad read rules can expose process-list, raw archive/export, or stream-style endpoints such asGET /containers/*/top,GET /containers/*/archive,GET /containers/*/export,GET /containers/*/logs,GET /containers/*/attach/ws,POST /containers/*/attach,GET /services/*/logs,GET /tasks/*/logs,GET /images/get, orGET /images/*/get— as well as their libpod-native counterpartsGET /libpod/containers/*/top,GET /libpod/pods/*/top,GET /libpod/containers/*/archive,GET /libpod/containers/*/export,GET /libpod/containers/*/logs,POST /libpod/containers/*/attach,POST /libpod/containers/*/checkpoint,POST /libpod/containers/*/mount,GET /libpod/containers/showmounted,GET /libpod/images/export,GET /libpod/images/*/get,POST /libpod/images/*/push,POST /libpod/images/scp/*(an image copy to a caller-named SSH host, which also takesinsecure_allow_body_blind_writesfor the ingest half),POST /libpod/manifests/*/registry/*,POST /libpod/manifests/*/push, andGET /libpod/generate/kube(a read despite the "generate" name: Podman serves it as aGETthat dumps an existing pod's or container's definition to YAML, which can include environment variables and other resource data). Two entries in that list are gated twice over:GET /images/getandGET /images/*/getare refused by owner isolation and by visibility policy regardless of this flag, so setting the acknowledgment does not make either reachable for a caller that either layer applies to (see Owner Label Isolation). The libpod-nativeGET /libpod/images/exportandGET /libpod/images/*/getstay available under isolation, each behind its own owner and visibility preflight. Container and pod top are not redacted metadata reads: Docker-compatible caller-selectedps_argschoose the daemon host'spsoutput, including process command lines and arguments, while both native Podman routes expose the same process-list class and support streaming responses. Version prefixes normalize away before rule matching and before the request-time gate alike. Image names in Podman's{name:.*}/pushroute may contain slashes; startup validation audits the whole dynamic route language, including constrained wildcard rules below any literal name prefix rather than only one representative path. Setting the acknowledgment does not make the risk go quiet: sockguard logs a warning naming the exfiltration endpoints the current rules actually admit, once per process, on the first filter-chain build (startup or hot-reload). Startup validation audits the authored rule literals against the catalog's whole route language, so an exact-name process-list allow and an ordered deny that shadows the representative path are both refused at load time without the acknowledgment. With the acknowledgment set, the warning names an evaluator-confirmed concrete path where it has one, and otherwise the catalog spelling as a conservative stand-in for an endpoint whose concrete path could not be confirmed. The top-level rules and each named client profile are reported separately, since the acknowledgment is global and a profile can be the only reason it has to be set. The fields name one reachable path per cataloged endpoint rather than every path the rules admit, so two empty lists mean no cataloged endpoint is reachable and the flag is buying nothing.insecure_accept_opaque_buildkit_tunnelsstaysfalseby default and must be set explicitly before startup validation will accept a rule that admitsPOST /sessionorPOST /grpc— the opaque BuildKit session/gRPC tunnel Compose uses fordocker compose build/up --buildby default (Engine API 1.53 deprecated both, but current Buildx still uses them). (A rule naming a literalmoby.buildkit.v1.Controlmethod path is held to the same startup acknowledgment, but that path stays denied at runtime regardless — there is no hijack-capable h2c tunnel there for any mediator to terminate, so it is never actually reachable end-to-end.) UnlikePOST /build's single inspectable JSON body, this transport carries secrets, SSH agent forwarding, and file sync as an ongoing binary conversation. This flag is now deprecated:request_body.buildkit(issue #185) mediates the same two endpoints with full per-message policy instead of admitting the tunnel wholesale, and setting the flag totruenow logs a startup deprecation warning steering operators toward that surface; it will be removed in a future major release. The flag andrequest_body.buildkitare mutually exclusive — mediation supersedes the acknowledgment — so a config cannot set both, and the deprecation warning never fires alongside that validation error. Tecnativa'sGRPC=1/SESSION=1compat env vars still auto-set this flag with their own compat-specific deprecation warning so existing drop-in configs keep working, unlessrequest_body.buildkitis configured at the top level or on any client profile: the two are mutually exclusive and compat runs before validation, so there it leaves the flag alone. With a top-level policy the compat/grpcand/sessionrules are then admitted under mediation and startup succeeds. With a policy on a client profile only, startup still refuses, because those compat rules are top-level and a profile's policy cannot mediate them; the refusal names the env vars that generated the rules and the top-level cure. See the Compose/BuildKit Transport section of the security model docs, Migration for the step-by-step move off this flag, and thedrydock-with-build.yaml/portwing-with-build.yaml(classicDOCKER_BUILDKIT=0builder) ordrydock-with-mediated-build.yaml/portwing-with-mediated-build.yaml(mediated BuildKit) presets for fully-inspectable alternatives.request_body.buildkit(issue #185) configures per-gRPC-message mediation ofPOST /session/POST /grpc, gating the same two endpointsinsecure_accept_opaque_buildkit_tunnelsadmits wholesale.control.solve.allowgates theControl/SolveRPC — the actual build request — withsecurity.insecurealways denied (no enabling knob),network.hostrequiringrequest_body.build.allow_host_network, a remote build context requiringrequest_body.build.allow_remote_context, and RUN-instruction inspection on the Dockerfile synced oversession.file_syncrequiringrequest_body.build.allow_run_instructions(the same three siblingrequest_body.buildflags used for classic/build, reused verbatim rather than duplicated).control.solve's five allowlists (allowed_cache_import_types,allowed_cache_export_types,allowed_cache_registries,allowed_exporters,allowed_exporter_registries) gate cache import/export types, the registry host of aregistry-typed cache entry, exporter types, and the destination registry of animage-typed exporter that setspush: true— each empty by default (deny).control.allow_info/allow_list_workersgate two worker/version metadata RPCs whose requests carry no policy-relevant field but whose responses are rewritten before relay — a worker record reaches the client with only its id, platforms and buildkit version, never the daemon host's labels, garbage-collection policy or CDI device inventory;control.allow_statusgatesControl/Status, admitted only for a ref this same client/profile actuallySolved. Undersession,auth(exact registry/realm/scope match for themoby.filesync.v1.Authcredential RPCs),secrets/ssh(exact-ID allowlists),file_sync/file_send(byte/file/path caps,file_sync.allowrequired to sync any Dockerfile or build context at all), andupload(a one-use token bound to an admittedSolve) each stay denied until their ownallow/allowlist fields are set — see the Request Body Policy Reference table below for the full field list.response.allow_attestation_statementsstaysfalseby default. A truthystatementquery returns the full in-toto attestation payload, which may have been generated by a different and less-trusted pipeline than the image itself, so it is denied unless explicitly allowed. Empty,0,no,false, andnoneare false after trimming and case-folding; every other non-empty value is true. A request without a truthy statement value still lists available attestation manifests.response.redact_container_env,response.redact_mount_paths,response.redact_network_topology, andresponse.redact_sensitive_datadefault totrue. Sockguard redacts workload env arrays across container/service/task/plugin reads, redacts host-path-bearing mount and device metadata across container/volume/task/service/plugin/system-usage reads, strips container/network/swarm/node topology from container/network/service/task/node/swarm/info/system-usage responses, and redacts higher-risk payload material such as configSpec.Data, service secret/config references, swarm join/unlock material, and swarm/node TLS metadata. Disable them only for trusted admin clients that truly need Docker's raw metadata.response.redact_host_topologydefaults tofalse(opt-in): when enabled, Sockguard redacts container-runtime plumbing fields onGET /info—Containerd(socket path and namespace layout),FirewallBackend,DiscoveredDevices, andNRI— independent of Swarm mode (the redaction runs whether or not the daemon has aSwarmsection in its response). Enable it when/inforeads are exposed to a caller that should be able to confirm the daemon is reachable without learning host build details.
Multiple Listeners
listeners: runs more than one main (Docker-API) listener from a single Sockguard process, each with its own bind target and its own admission scope, instead of requiring one process per audience.
listeners:
- name: ci
socket: /var/run/sockguard-ci.sock
socket_mode: "0600"
allowed_profiles: [ci]
- name: ops
socket: /var/run/sockguard-ops.sock
socket_mode: "0600"
allowed_profiles: [ops]listeners:and the legacy top-levellisten:block are mutually exclusive — set one or the other, never both. Migrating means moving thelisten:block's fields into a single-entrylisteners:list.- Each entry's
nameidentifies the listener for logs, metrics, health, audit, and reload diagnostics. It must be unique withinlisteners, match the listener-name pattern, and cannot beadmin(reserved for the admin listener). allowed_profilesscopes a listener to a subset ofclients.profilesand is required and non-empty on every explicit entry. A client that resolves (via mTLS cert, unix peer UID, or source IP) to a profile outside a listener'sallowed_profilesis denied withlistener_profile_not_allowed, even if the base policy would otherwise allow it — the listener boundary is evaluated after normal profile resolution and never falls back to a weaker selector. The single-element list["*"]preserves legacy behavior: every resolved profile, plus the unprofiled/default-policy path, is admitted.- Each entry otherwise takes the same fields as the top-level
listen:block (socket/address,socket_mode,tls, and so on) — see the listener bullets above for what each field does. Unlike the legacylisten:block, an explicitlisteners[*]unix-socket entry does not defaultsocket_mode; set it explicitly on every entry. admin.mount_onnames the single listener that carries/admin/*traffic when in-band admin (admin.enabled: truewith no dedicatedadmin.listen) runs alongside two or more effective listeners. It is required in that case — startup validation rejects a config with 2+ listeners, in-band admin enabled, and noadmin.mount_on— and the admin endpoints are unreachable from any other listener. Configuring a dedicatedadmin.listeninstead sidestepsmount_onentirely.
See configs/multi-listener.yaml for a complete working example, including a TCP+mTLS variant and disjoint per-listener client profiles.
Request Body Policy Reference
All request-inspection policy fields under request_body default to the safest value unless noted. List fields default to empty lists.
| Group | Fields | Default behavior |
|---|---|---|
container_create | allow_privileged, allow_host_network, allow_host_pid, allow_host_ipc, allowed_bind_mounts, allow_all_devices, allowed_devices, allow_device_requests, allowed_device_requests, allow_device_cgroup_rules, allowed_device_cgroup_rules, allow_tmpfs_privileged_options, require_no_new_privileges, require_non_root_user, require_readonly_rootfs, require_drop_all_capabilities, allow_all_capabilities, allowed_capabilities, require_memory_limit, require_cpu_limit, require_cpu_limit_hard, require_pids_limit, allowed_seccomp_profiles, deny_unconfined_seccomp, allowed_apparmor_profiles, deny_unconfined_apparmor, deny_selinux_disable, deny_selinux_label_override, deny_unconfined_system_paths, allow_host_userns, allow_host_cgroupns, restrict_namespace_sharing, allowed_namespace_sharing_containers, deny_namespace_path_mode, allow_sysctls, allowed_runtimes, required_labels, image_trust | Denies privileged containers, host network/PID/IPC/user/cgroup namespaces, kernel sysctls, non-allowlisted bind sources, non-allowlisted device mappings, device requests, device cgroup rules, and non-allowlisted CapAdd entries. A Mounts entry with an unrecognized Type is denied fail-closed; VolumeOptions.Subpath/ImageOptions.Subpath must not escape the mount root via ..; VolumeOptions.DriverConfig options that ask the local driver for a bind have their device checked against allowed_bind_mounts like any other bind source, and so does a /dev device carried under a real filesystem type, which mounts a host block device without ever being a bind; allow_tmpfs_privileged_options gates tmpfs exec/dev/suid option re-enablement (default deny). The allow_host_* fields gate host and ns:<path> on their namespace mode, since a path can name the host's namespace. restrict_namespace_sharing (+ allowed_namespace_sharing_containers) gates joining another container's namespace via container:<id> values across NetworkMode/PidMode/IpcMode/UTSMode/UsernsMode and refuses ns:<path> on them while it's on, and deny_namespace_path_mode blocks ns:<path> on NetworkMode even when allow_host_network is on; both default to pass-through until explicitly enabled. allowed_runtimes is an allowlist for non-empty HostConfig.Runtime values; an empty/unset runtime selects the daemon default and is always permitted, so only callers that select an alternate runtime (e.g. runsc, kata-runtime) need an entry. allowed_device_requests provides per-driver structured policy for HostConfig.DeviceRequests (GPU passthrough, etc.) — each entry restricts by driver (exact match), allowed capability sets (request sets must be subsets), and optional max_count. allowed_device_cgroup_rules provides per-class device cgroup policy without blanket allow. Opt-in rails additionally require no-new-privileges, non-root execution, read-only rootfs, dropped capabilities, memory / CPU / PIDs limits, approved seccomp/AppArmor profiles, and required Config.Labels keys. require_cpu_limit_hard narrows require_cpu_limit to accept only a genuine CPU-time cap (NanoCpus or CpuQuota) — CpuShares alone (a relative scheduling weight, not a cap) satisfies require_cpu_limit but not this stricter, independent check. image_trust adds cosign signature verification (keyed PEM keys or keyless Fulcio+Rekor) with warn (log + allow) or enforce (deny on failure) modes; require_rekor_inclusion defaults to true. Opt-in deny_selinux_disable / deny_selinux_label_override / deny_unconfined_system_paths rails cover the SELinux label= and systempaths= SecurityOpt directives (the last also blocks the direct-API MaskedPaths: [] / ReadonlyPaths: [] equivalent). VolumesFrom, host UTSMode, a custom CgroupParent, GroupAdd, and ExtraHosts are denied unconditionally with no opt-out field. NetworkingConfig.EndpointsConfig (every network entry, not just the first) and the deprecated top-level MacAddress create field are gated by the network group's allow_endpoint_config and its endpoint_config.* granular fields (#186) — there is no separate container_create field for either, see the network row below. |
libpod_container_create | allow_privileged, allow_host_network, allow_host_pid, allow_host_ipc, allow_host_userns, allowed_bind_mounts, allow_all_devices, allowed_devices, restrict_namespace_sharing, allowed_namespace_sharing_containers, allow_all_capabilities, allowed_capabilities, allowed_seccomp_profiles, deny_unconfined_seccomp, allowed_apparmor_profiles, deny_unconfined_apparmor, deny_selinux_disable, require_non_root_user, require_readonly_rootfs, require_memory_limit, require_cpu_limit, require_cpu_limit_hard, require_pids_limit, allow_sysctls, allow_systemd_mode, allow_custom_id_mappings, image_trust | Podman's POST /libpod/containers/create counterpart to container_create above — a separate, path-exclusive inspector reading libpod's own top-level field shape (privileged, netns/pidns/ipcns/userns, mounts/volumes, devices, cap_add, user, sysctl, resource_limits, read_only_filesystem, selinux_opts), never the Docker-compat HostConfig/Config shape. Gate semantics mirror container_create field-for-field where a libpod equivalent exists: privileged, host netns/pidns/ipcns/userns (nsmode host or path, and while the gate is off any mode sockguard doesn't know), bind-mount and device allowlists (named volumes[] entries are never bind-gated), cap_add allowlist, restrict_namespace_sharing for {"nsmode":"container","value":"<ref>"} joins, non-root user, read-only rootfs, memory/CPU/PIDs limits from resource_limits, seccomp/AppArmor allowlists, and deny_selinux_disable for selinux_opts containing disable. image_trust reuses the same cosign verification as container_create.image_trust, applied to the image field. Two fields have no Docker-compat analog: allow_systemd_mode denies any systemd value other than the explicit "false" — SpecGenerator's own default is "true" even when --systemd is never passed, so this is a deliberately strict default; allow_custom_id_mappings denies a non-default idmappings.uidMap/gidMap or --userns=auto, independent of allow_host_userns (which only covers userns.nsmode=host). |
exec | allow_privileged, allow_root_user, allowed_commands, allowed_env_vars, denied_env_vars, allowed_env_values | Denies privileged/root exec and requires an argv allowlist before broad exec rules pass blind-write validation — an empty allowed_commands denies every exec at request time too, unless the top-level insecure_allow_body_blind_writes: true is also set, in which case the argv-allowlist gate is lifted but every other exec control still applies. allowed_env_vars/denied_env_vars restrict exec-create Env entries by name; allowed_env_values pins selected names to exact NAME=VALUE entries. All are create-time only and empty by default. Values are compared but never logged or returned in denial reasons. Covers both POST /containers/*/exec/POST /exec/*/start and their libpod-native counterparts POST /libpod/containers/*/exec/POST /libpod/exec/*/start — there is no separate libpod_exec group. |
image_pull | allow_imports, allow_all_registries, allow_official, allowed_registries | Denies Docker fromSrc and native Podman /libpod/images/import imports unless allow_imports is set, and allows Docker Hub official pulls by default (allow_official: true). Covers both POST /images/create and Podman's libpod-native POST /libpod/images/pull; there is no separate libpod_image_pull group. The registry allowlist, allow_official, and allow_all_registries apply identically on both pull surfaces. Body-form native imports are capped at 512 MiB; URL imports remain a coarse opt-in. |
build | allow_remote_context, allow_host_network, allow_run_instructions | Governs Docker's POST /build and native Podman's POST /libpod/build. Denies primary remote contexts, Podman URL/image additional contexts, host-network builds, and Dockerfiles containing RUN, checking every file either engine could build (Containerfile too on the libpod path, and each path in a JSON-array dockerfile). Podman host/local/multipart and resource-usage host-file controls require the separate global blind-write acknowledgment. |
buildkit | control.allow_info, control.allow_list_workers, control.allow_status, control.solve.allow, control.solve.allowed_cache_import_types, control.solve.allowed_cache_export_types, control.solve.allowed_cache_registries, control.solve.allowed_exporters, control.solve.allowed_exporter_registries, session.health, session.auth.allow, session.auth.allowed_registries, session.auth.allowed_realms, session.auth.allowed_scopes, session.secrets.allow, session.secrets.allowed_ids, session.ssh.allow, session.ssh.allowed_ids, session.file_sync.allow, session.file_sync.max_files, session.file_sync.max_total_bytes, session.file_sync.max_path_length, session.file_sync.max_file_bytes, session.file_send.allow, session.file_send.max_bytes, session.upload.allow, session.upload.max_bytes | Mediates POST /session/POST /grpc (issue #185) per gRPC message once configured — presence, not an enabled flag, is what matters; an absent block denies both endpoints internally exactly like today, even when an outer HTTP rule allows them. security.insecure on Control/Solve is always denied with no enabling knob; network.host/remote build contexts/RUN-instruction inspection reuse the sibling build group's three flags verbatim rather than duplicating them here. Every allowlist and every *.allow field defaults empty/false (deny), the standard request_body.* convention; the four file_sync/file_send/upload byte/file/path caps default 0, which selects buildkitproxy.Limits' hardcoded ceiling rather than disabling the cap. Mutually exclusive with the deprecated insecure_accept_opaque_buildkit_tunnels — a config setting both fails validation. |
container_update | allow_privileged, allow_all_devices, allow_capabilities, allow_resource_updates, allow_restart_policy, require_memory_limit, require_cpu_limit, require_cpu_limit_hard, require_pids_limit | Denies privileged/device/capability-like edits, resource-control changes, and restart-policy changes. require_* fields are enforced by the post-ownership resource-limit guard against the effective merged state, only when allow_resource_updates: true is also set — see the resource-limit guard bullets above. Covers Podman's libpod-native POST /libpod/containers/*/update too — there is no separate libpod_container_update group. libpod's body and query shape are read on their own terms; see the libpod bullet above for which gate each field rides and which four fields are refused outright. |
container_remove | allow_force, allow_remove_volumes, allow_remove_links | Allows a bare DELETE /containers/{id} while denying force removal, anonymous-volume deletion, and legacy link deletion unless each behavior is explicitly enabled. Uses dockerd's exact first-value and boolean parsing semantics and rejects malformed query encoding. Covers Podman's libpod-native DELETE /libpod/containers/{id} too, where allow_remove_volumes gates volumes, v and depend, and Podman's pod removal and kube down, which need allow_remove_volumes on every request; see the bullet above. |
container_archive | allowed_paths, allow_setid, allow_device_nodes, allow_escaping_links | Denies missing, ambiguous, unsafe, or unallowlisted target paths, tar traversal, setuid/setgid entries, device nodes, escaping links, and Podman's post-inspection rename transformation. Relative targets are denied when allowed_paths is configured because Podman resolves them against the container WorkingDir. Covers Podman's libpod-native PUT /libpod/containers/*/archive too; there is no separate libpod_container_archive group. |
image_load | allow_all_registries, allow_official, allowed_registries, allow_untagged | Applies the same registry gate to Docker manifest.json tags and OCI index.json names: Docker Hub official references are allowed by default (allow_official: true), and untagged Docker or OCI archive entries are denied unless opted in. On native Podman loads, bare refs in either archive format resolve to localhost rather than Docker Hub. |
volume | allow_custom_drivers, allow_driver_opts, allow_cluster_volume_secrets, allow_cluster_volume_updates | Denies non-local drivers and driver options on POST /volumes/create, and every ClusterVolumeSpec field on the PUT /volumes/{name} Swarm cluster-volume update. When allow_driver_opts: true lifts the second gate, local-driver options that request a bind (o containing a bind/rbind token, or type of none/bind, together with a device) still have that device checked against container_create.allowed_bind_mounts — the volume group has no allowed_bind_mounts key of its own, because a bind volume reaches the same host path a HostConfig.Binds entry would. The same allowlist covers the other way those options reach the host, which is not a bind and so was invisible to that check: a type naming a real kernel filesystem (ext4, xfs, btrfs, vfat — anything outside nfs/nfs4/cifs/smb3/tmpfs — and the case where the request omits type entirely) over a device under /dev mounts the host's own block device, which reaches the host's storage as completely as a bind of / does. The device is normalized before the comparison, so //dev/sda1, /dev/./sda1 and /dev/../dev/sda1 all compare as /dev/sda1, and a relative dev/sda1 is rooted the way mount(2) resolves it against the daemon's working directory; a device= entry inside o counts as a device too, because btrfs takes the extra members of a multi-device filesystem there. The denial reads local volume device %q is not allowlisted, distinct from the bind wording, and an operator who genuinely needs one lists the device path in that same allowed_bind_mounts. Non-bind local options (tmpfs, nfs, cifs, a size quota) and third-party drivers are untouched. allow_cluster_volume_secrets governs Spec.Secrets on its own; allow_cluster_volume_updates governs the rest and does not admit Secrets. |
network | allow_custom_drivers, allow_swarm_scope, allow_ingress, allow_attachable, allow_config_only, allow_config_from, allow_custom_ipam_drivers, allow_custom_ipam_config, allow_ipam_options, allow_driver_options, allow_endpoint_config, endpoint_config.* (allow_static_addressing, allow_link_local_ips, allow_mac_pinning, allow_gw_priority, allow_aliases), allow_disconnect_force, allow_disable_ipv4 | Denies custom drivers, swarm/ingress/attachable/config-only controls, custom IPAM, driver options, forced disconnects, and (allow_disable_ipv4) an explicit EnableIPv4: false on network create. allow_endpoint_config gates endpoint static IP, MAC address, links, driver options, and GwPriority on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig (plus create's deprecated top-level MacAddress field) — one flag, two enforcement points. When allow_endpoint_config is false/unset, endpoint_config.* (#186) gates the same fields individually instead — see the narrative bullet above for the field mapping, precedence, and Links/DriverOpts fail-closed behavior. Endpoint Aliases are always allowed under allow_endpoint_config: true, and default-allowed (opt-outable) under the granular form. |
secret / config | allow_custom_drivers, allow_template_drivers | Denies custom and template drivers. allow_custom_drivers covers both Driver.Name and Driver.Options. |
service | allow_host_network, allowed_bind_mounts, allow_all_registries, allow_official, allowed_registries, allow_all_capabilities, allowed_capabilities, allow_sysctls, require_non_root_user, require_no_new_privileges, require_readonly_rootfs, require_drop_all_capabilities, deny_unconfined_seccomp, deny_custom_seccomp_profiles, deny_unconfined_apparmor, deny_selinux_disable, deny_selinux_label_override, require_cpu_limit, require_cpu_limit_hard, image_trust | Denies host-network services and non-allowlisted bind mounts, ContainerSpec mounts of type volume whose VolumeOptions.DriverConfig asks the local driver for a bind included, and a /dev device under a real filesystem type with it — the device is checked against service.allowed_bind_mounts like any other bind source; allows Docker Hub official images by default (allow_official: true). Enforces the same allowed_capabilities / allow_all_capabilities capability gate, allow_sysctls sysctl gate, and image_trust cosign verification on the ContainerSpec image as container-create. Opt-in rails mirror the container-create hardening knobs onto the swarm ContainerSpec: require_non_root_user (ContainerSpec.User), require_no_new_privileges (ContainerSpec.Privileges.NoNewPrivileges), require_readonly_rootfs (ContainerSpec.ReadOnly), and require_drop_all_capabilities (ContainerSpec.CapabilityDrop includes ALL) — all default off. Confinement-mode rails: deny_unconfined_seccomp (Privileges.Seccomp.Mode: "unconfined"), deny_custom_seccomp_profiles (Mode: "custom", or a Profile blob with no Mode — fail-closed), deny_unconfined_apparmor (Privileges.AppArmor.Mode: "disabled"), deny_selinux_disable (Privileges.SELinuxContext.Disable: true), and deny_selinux_label_override (SELinuxContext.{User,Role,Type,Level} overrides) — all default off. require_cpu_limit/require_cpu_limit_hard are enforced by the post-ownership resource-limit guard against Resources.Limits.NanoCPUs on create/update, the stored PreviousSpec on ?rollback=previous, and the current Spec when UpdateConfig.FailureAction: "rollback" is set — Swarm has no CpuShares/CpuPeriod-style split, so require_cpu_limit_hard alone is sufficient and does not depend on require_cpu_limit. |
swarm | allow_force_new_cluster, allow_external_ca, allowed_join_remote_addrs, allow_token_rotation, allow_manager_unlock_key_rotation, allow_auto_lock_managers, allow_signing_ca_update, allow_unlock | Denies unsafe init/join/update controls and swarm unlock. POST /swarm/join needs allowed_join_remote_addrs before broad join rules pass blind-write validation. |
node | allow_name_change, allow_role_change, allow_availability_change, allow_label_mutation, allowed_label_keys | Denies name, role, availability, and arbitrary label mutations while allowing the configured owner-label key for controlled claims. |
plugin | allow_host_network, allow_host_ipc, allow_host_pid, allow_all_devices, allowed_bind_mounts, allowed_devices, allow_all_capabilities, allowed_capabilities, allow_all_registries, allow_official, allowed_registries, allowed_set_env_prefixes | Denies host namespaces, non-allowlisted mounts/devices/capabilities, and non-official registries by default (allow_official: true). POST /plugins/*/set needs allowed_set_env_prefixes before broad set rules pass blind-write validation. |
libpod_pod_create | allow_host_network, allow_shared_pid_namespace, allowed_infra_image_registries | Governs POST /libpod/pods/create only (Podman's libpod-native pod creation; no Docker-compat equivalent). Denies a pod-level host network namespace (netns with nsmode host or path, or a mode sockguard doesn't know) and a shared PID namespace ("pid" in shared_namespaces) by default; denies an explicitly set infra_image whose registry isn't allowlisted. allowed_infra_image_registries uses the same registry-allowlist shape as image_pull.allowed_registries. When ownership.owner is configured, cross-owner pod-membership checks are also enforced (#148): a pod-create request cannot join another owner's host namespaces, and POST /libpod/containers/create cannot target another owner's pod. |
libpod_volume | (reuses volume's fields) | Governs POST /libpod/volumes/create with the same allow_custom_drivers/allow_driver_opts semantics as the volume group above, decoded against libpod's wire shape (Options, not DriverOpts). Local-driver bind options, and a /dev device under a real filesystem type, are checked the same way, against libpod_container_create.allowed_bind_mounts rather than the Docker-compat list. The two cluster-volume flags have no libpod analog — Podman has no swarm mode, no CSI cluster volumes, and no PUT volume route — and are not consulted. |
libpod_network | (reuses network's fields) | Governs POST /libpod/networks/create with the same driver/IPAM/driver-options semantics as the network group above, decoded against libpod's wire shape (snake_case, no swarm/ingress/attachable/config-only fields — libpod has no swarm mode). Also governs POST /libpod/networks/{name}/connect and .../disconnect via the same allow_endpoint_config/endpoint_config/allow_disconnect_force gates the network group applies to the Docker-compat spellings, and POST /libpod/networks/{name}/update via allow_dns_servers, which is libpod-only in the other direction (Docker has no per-network resolver field and no update route). |
libpod_secret | (reuses secret's fields) | Governs POST /libpod/secrets/create with the same allow_custom_drivers semantics as the secret group above; libpod reads driver and driveropts from the query string, so this inspector never reads the request body. With the flag off, driver must be empty or file (the containers.conf default) and driveropts must be absent, empty, {} or null, since options go to whichever driver stores the secret, the default included, and the file driver's path option picks where on the daemon host it is written. allow_template_drivers has no libpod analog (no template-driver concept) and is not consulted. |
mTLS Client Selectors
listen.tls.client_ca_file is the issuing trust root. To avoid trusting every leaf that CA can mint, narrow admission with one or more of the selector fields below. Different fields are ANDed; entries inside one field are ORed. An empty selector means "any verified client certificate issued by the configured CA is accepted".
| Field | Type | Matches |
|---|---|---|
common_names | []string | Exact CN on the verified leaf certificate |
dns_names | []string | DNS SAN entries on the verified leaf |
ip_addresses | []string | IP SAN entries on the verified leaf (not the TCP source IP) |
uri_sans | []string | URI SAN entries on the verified leaf (for example spiffe://...) |
public_key_sha256_pins | []string | Lowercase hex SHA-256 of the leaf SubjectPublicKeyInfo, optionally prefixed with sha256: |
listen:
tls:
client_ca_file: /run/secrets/sockguard/client-ca.pem
common_names: ["portainer"]
dns_names: ["portainer.internal"]
ip_addresses: ["10.0.5.12"]
uri_sans: ["spiffe://sockguard.test/workload/portainer"]
public_key_sha256_pins:
- 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1bMalformed selectors (invalid IPs, malformed URIs, non-hex SPKI pins, pins that are not 32 bytes) are rejected at startup. The TLS handshake fails closed with no upstream connection when a CA-issued client certificate does not match the configured allowlist.
Per-Client ACLs And Profiles
clients.allowed_cidrs is a coarse TCP-client gate evaluated before the global rule set. Requests whose source IP is outside every configured CIDR are denied with 403 and never reach the health handler or the rule evaluator.
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:
com.sockguard.allow.get=/containers/json,/containers/*/json,/events
com.sockguard.allow.post=/containers/*/restartEvery comma-separated path in a label must start with /, exactly as a rule's match.path must. Label values are globs, not regexes, and the same dialect applies: * stops at a /, ** spans any number of segments, and /** is the catch-all. A path without the leading slash is refused rather than compiled, because the two families it splits into are both wrong and only one of them is obviously so. A rootless single-star path such as containers/* matches nothing at all, so the grant silently does nothing. A rootless path carrying ** does the opposite: ** alone spans every segment including the leading slash, so a label reading com.sockguard.allow.get=** would hand that client every GET the global policy allows instead of the narrow grant it looks like. Sockguard refuses the whole label set for that container, logs the offending label and path at error level, and answers 502 for its requests until the label is fixed. This is the same fail-closed path a label whose pattern cannot be compiled already takes; labels never pass through config validation, so it is the only place the shape is checked.
If you are migrating from wollomatic, set clients.container_labels.label_prefix: socket-proxy.allow. to keep the existing label namespace. Only the key prefix carries over: wollomatic label values are regexes, Sockguard reads them as globs, and every value has to be rewritten as a rooted glob starting with /. A leftover value like socket-proxy.allow.get=.* is not a glob Sockguard treats as a catch-all; it compiles to a literal match on the string .* and is refused for missing the leading slash, so requests from that container 502 until the label is rewritten. Callers that cannot be resolved by IP (for example, because they share the host network) fall through to the global rule set unchanged.
Security note — IP-based identity is soft isolation. IP-keyed admission (
clients.allowed_cidrs,clients.container_labels.enabled, and profilematch.source_cidrs) is adequate against configuration drift but not hard isolation against an attacker who can influence container-to-IP mapping on a shared bridge. For caller identity in the security boundary, prefer a unix socket withclients.unix_peer_profilesanduids/gids—SO_PEERCREDcannot be spoofed. See the Known Limitations section in the Security guide for the full caveat.
Security note — label grants assume sockguard is the only socket consumer. Allow labels are read from the calling container, and any workload that can reach the raw Docker socket directly can create a container carrying arbitrary
label_prefixpermission labels — self-granting access the policy never approved. Label ACLs are only trustworthy when every Docker API consumer goes through sockguard; the proxy cannot detect other socket consumers, so it logs a warning stating this invariant — once per process, on the first policy build (startup or hot-reload) that hasclients.container_labels.enabledset.
Named client profiles turn one Sockguard instance into a shared control plane for multiple consumers. Root-level rules and request_body remain the fallback policy unless clients.default_profile points at a named profile:
clients:
default_profile: readonly
source_ip_profiles:
- profile: watchtower
cidrs:
- 172.18.0.0/16
client_certificate_profiles:
- profile: portainer
dns_names:
- portainer.internal
spiffe_ids:
- spiffe://sockguard.test/workload/portainer
public_key_sha256_pins:
- 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b
unix_peer_profiles:
- profile: readonly
uids:
- 501
profiles:
- name: readonly
response:
visible_resource_labels:
- com.sockguard.visible=true
rules:
- match: { method: GET, path: "/containers/json" }
action: allow
- match: { method: GET, path: "/containers/*/json" }
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/json" }
action: allow
- match: { method: GET, path: "/containers/*/json" }
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: denyclients.source_ip_profilesmatches the caller's remote IP against CIDRs in config order.clients.client_certificate_profilesmatches the verified mTLS leaf certificate in config order. Each assignment can matchcommon_names,dns_names,ip_addresses,uri_sans,spiffe_ids, andpublic_key_sha256_pins; different selector fields on the same assignment are ANDed, while entries inside one field are ORed.clients.unix_peer_profilesmatches unix-socket callers by peeruids,gids, andpids. Different selector fields on the same assignment are ANDed, while entries inside one field are ORed.clients.default_profileis the fallback when no specific assignment matches.- Each profile has its own
rulesandrequest_bodypolicy, so one proxy can safely host a read-only dashboard, a container updater, and an admin UI at the same time. clients.client_certificate_profilesrequireslisten.tlsmutual TLS.clients.unix_peer_profilesrequireslisten.socket.response.visible_resource_labelsandclients.profiles[].response.visible_resource_labelsenforce read-side visibility on labeled list/events/inspect paths plus selected service/task log paths. Selectors use Docker label syntax (keyorkey=value), are ANDed together, and profile selectors are additive with the root response selectors. Whenownership.owneris configured, a selector may not use the ownership label key: both layers write the samelabelfilter upstream, that key is proxy-enforced, and Docker's Swarm control-plane lists collapse repeated keys nondeterministically. Sockguard refuses to start on that combination. Podman'sGET /eventsis stricter: because its handler ORs repeated label values, a request under both owner isolation and any visibility label selector is refused with403(reason_code=owner_visibility_podman_events_unscopeable) rather than forwarded with a wider scope.response.name_patternsandresponse.image_patternsadd glob-based selector axes that operate alongside label selectors (AND semantics across all axes). Within each axis, at least one pattern must match (OR semantics).name_patternsis matched againstNames[0]with the leading/stripped for containers, and against eachRepoTagsshort name (the part after the last/) for images.image_patternsis matched against the container'sImagefield and against each fullRepoTagsreference for images. Patterns use the same glob dialect as rule path patterns (*= no slash,**= any depth). Both knobs are supported at the per-profile level viaclients.profiles[*].response.name_patterns/image_patterns. Pattern filtering buffers the upstream list response in memory under an 8 MiB cap; a larger response is rejected with a502rather than buffered unbounded. Example — expose only traefik containers and images from a private registry to a specific client profile:
The resolved caller identity also scopes mediated BuildKit state. Sockguard uses the verified client certificate on mTLS, kernel-supplied Unix peer credentials on a Unix listener, or the normalized remote host for unauthenticated TCP. It pairs that principal with the selected profile and BuildKit's session ID, so /grpc and /session connections from one build correlate even when TCP source ports differ, while simultaneous builds and unrelated Unix peers remain isolated. Session, ref, and upload identifiers have a 256-byte persistence bound, each control tunnel can admit at most 256 distinct BuildKit session IDs, and unconsumed upload grants expire after one hour.
For a single container or image read that combines label selectors with name
or image patterns, Sockguard decodes both axes from one bounded upstream
inspect response. It does not cache that result across requests. Visibility
path matching treats write-only action words such as create and prune as
valid resource names on GET and HEAD, so a resource cannot escape a hidden
view because its name resembles a collection operation.
response:
name_patterns:
- "traefik"
clients:
profiles:
- name: registry-reader
response:
image_patterns:
- "ghcr.io/myorg/**"- Hidden resources disappear from
GET /containers/json,/images/json,/networks,/volumes,/services,/tasks,/secrets,/configs,/nodes, andGET /events, while hidden inspect/log-style targets such asGET /services/*,GET /services/*/logs,GET /tasks/*,GET /tasks/*/logs,GET /secrets/*,GET /configs/*,GET /nodes/*, andGET /swarmreturn404instead of exposing a policy-specific deny body.
Rate Limiting and Concurrency Caps
Each named profile can carry a limits block that enforces two independent
mechanisms. Both are per-profile and disabled by default — omitting limits
entirely (or omitting either sub-block) leaves that profile unthrottled.
Token-bucket rate limiting
limits.rate enforces a maximum sustained request rate using a token bucket.
The bucket refills continuously at tokens_per_second; its capacity is burst
(peak burst size). When the bucket is empty a request is denied immediately with
429 Too Many Requests, a JSON body containing retry_after_seconds, and a
Retry-After header.
tokens_per_second(required): refill rate; must be> 0.burst(optional, default= tokens_per_second): bucket capacity. Must be>= tokens_per_secondwhen explicitly set. Set to0to accept the default (no burst allowance beyond one token per interval). Upper bound:65535— the validator rejects larger values (the token bucket packs its state into a single atomic word with a 16-bit integer token field). Sinceburst >= tokens_per_second, the refill rate is implicitly bounded at 65535 tokens/second as well.
clients:
profiles:
- name: interactive-operator
limits:
rate:
tokens_per_second: 8
burst: 16 # allow short bursts; sustain at 8 req/s
rules:
- match: { method: GET, path: "/containers/**" }
action: allow
- match: { method: "*", path: "/**" }
action: deny
- name: ci-agent
limits:
rate:
tokens_per_second: 100
burst: 200 # CI workflows issue large list calls at startup
rules:
- match: { method: "*", path: "/**" }
action: allowGuidance on starting values:
| Consumer type | Suggested tokens_per_second | Suggested burst | Rationale |
|---|---|---|---|
| Interactive operator | 8 | 16 | Human clicks; a burst allows rapid multi-request workflows |
| Monitoring / metrics scraper | 4 | 8 | Periodic polls; low sustained rate is sufficient |
| CI agent | 100 | 200 | Build pipelines issue large batches at startup |
| Read-only dashboard | 20 | 40 | Page loads trigger a handful of list calls together |
Endpoint cost weighting
By default every request withdraws one token from the bucket. limits.rate.endpoint_costs
lets you assign a higher per-request cost to expensive Docker operations
(build, image pull, exec) so they consume the budget proportionally to the
work they actually do. The base rate stays comfortable for normal traffic
while abusive POST /build floods are bounded.
Each entry has:
path(required): glob matched against the normalized request path (Docker API version prefix is stripped). Same dialect as filter rules.methods(optional): list of HTTP methods to restrict the rule to. Case-insensitive. Empty matches all methods.cost(required): number of tokens withdrawn on match. Must be>= 1and<= effective burst(a cost greater than burst can never be satisfied; startup fails closed if you misconfigure it).
Rules are evaluated in declaration order; first match wins. Unmatched
requests fall back to the default cost of 1.
clients:
profiles:
- name: ci-agent
limits:
rate:
tokens_per_second: 100
burst: 200
endpoint_costs:
# Image pull is the single most expensive operation Docker exposes;
# registry round-trips can pin upstream for minutes.
- path: /images/create
methods: [POST]
cost: 20
# Build context uploads + multi-stage assembly are similarly heavy.
- path: /build
methods: [POST]
cost: 10
# Exec creation is cheap but exec start spawns a process — tax it.
- path: /containers/*/exec
methods: [POST]
cost: 5
rules:
- match: { method: "*", path: "/**" }
action: allowWhen a request is throttled because of its weighted cost, the structured
audit record (sampled once per (client, reason) per second) carries the
cost attribute alongside the existing rate-limit fields so operators can
distinguish expensive-endpoint denials from base-rate denials.
Concurrency caps
limits.concurrency.max_inflight caps the number of simultaneously in-flight
requests per client. Once the cap is reached, additional requests are denied
immediately with 429 Too Many Requests and a {"reason":"concurrency_cap"}
body (no Retry-After, since release timing depends on concurrent traffic).
The cap is decremented when the response completes or the connection is
hijacked/closed — a request that is denied as rate-limited or by policy never
counts against the cap.
clients:
profiles:
- name: streaming-client
limits:
concurrency:
max_inflight: 16 # allow up to 16 simultaneous streaming connections
rules:
- match: { method: GET, path: "/events" }
action: allow
- match: { method: "*", path: "/**" }
action: denyCombining both mechanisms
Rate limiting and concurrency caps can coexist on the same profile. The rate check runs first; a request that passes the rate check but hits the concurrency cap is denied with a concurrency reason (not a rate reason). A request denied by either mechanism is never counted as in-flight.
limits:
rate:
tokens_per_second: 50
burst: 100
concurrency:
max_inflight: 32Priority / fairness controls
A noisy low-priority client can saturate the per-profile concurrency cap on its own profile, but it should not be able to starve unrelated higher-priority profiles. The system-wide priority-aware fairness gate solves this.
Enable it by setting a global cap and tagging each profile with a priority tier:
clients:
global_concurrency:
max_inflight: 100 # system-wide ceiling, shared across all profiles
profiles:
- name: admin
limits:
priority: high # admits up to 100% of the global cap
concurrency:
max_inflight: 50
- name: ci
limits:
priority: normal # admits up to 80% of the global cap (default)
concurrency:
max_inflight: 30
- name: scraper
limits:
priority: low # admits up to 50% of the global cap
concurrency:
max_inflight: 20Each priority tier has a hardcoded share of the global cap:
| Priority | Share | Floor at max_inflight: 100 |
|---|---|---|
low | 50% | denied above 50 in-flight |
normal (default) | 80% | denied above 80 in-flight |
high | 100% | denied above 100 in-flight |
When total in-flight crosses the floor for a request's priority, the request
is denied with 429 Too Many Requests and {"reason":"priority_floor"}.
Per-profile concurrency caps still apply on top — a low profile with
max_inflight: 20 is bounded by both its own cap and the 50% global floor.
The gate is checked before the per-profile concurrency cap, so a low-priority
request that hits the global floor never occupies a per-profile slot. Profiles
configured without limits.priority or without any limits block default to
normal, so a single client cannot evade the gate by skipping per-profile
configuration. priority is only honored when clients.global_concurrency is
set; otherwise it has no effect.
Reload resets limiter state
A hot reload rebuilds the rate-limit and concurrency machinery
from scratch, even when the change had nothing to do with limits or
global_concurrency. The replacement chain starts empty: every client's
consumed token-bucket quota and every in-flight concurrency count begin at
zero, so a bucket that was nearly drained comes back full and a profile
sitting at its max_inflight cap opens back up to new requests.
Only the new chain starts empty. Requests already admitted when the swap
lands keep running on the old chain and hold its concurrency slot until they
finish, so the old and new caps overlap for the length of that drain window:
a profile capped at max_inflight: 20 can have up to 20 pre-swap requests
still draining while the new chain admits another 20. The same goes for
clients.global_concurrency. The overlap clears on its own as the old
requests return.
State is not carried across the old and new chains, and there is no way to
preserve it, since a profile can change shape between reloads and there is
no principled way to remap old counters onto a different limit. Sockguard
logs a warning when a reload replaces a chain that actually held counters (a
token bucket, a per-profile concurrency cap, or the global one), so this
shows up as an expected reload side effect rather than an unexplained gap in
your throttling data. A profile that sets only limits.priority has no
counters to discard, so reloading it stays quiet.
Validation
Sockguard fails at startup with a clear error if any of these invariants are violated (fail-closed, never silent-disable):
limits.rate.tokens_per_secondmust be> 0limits.rate.burstmust be>= tokens_per_secondor0(default)limits.rate.endpoint_costs[].pathmust be non-empty and compile as a valid globlimits.rate.endpoint_costs[].costmust be>= 1and<= effective burstlimits.rate.endpoint_costs[].methods[]must not contain empty stringslimits.concurrency.max_inflightmust be> 0clients.global_concurrency.max_inflightmust be> 0when setlimits.prioritymust be one oflow,normal, orhigh
Observability
When Prometheus metrics are enabled (metrics.enabled: true), two additional
series appear:
sockguard_throttle_requests_total{profile, reason_code, mode}— counter incremented on every denial by this subsystem.reason_codeis one ofrate_limit_exceeded,concurrency_cap, orpriority_floor.sockguard_inflight_requests{profile}— gauge tracking the current in-flight count for profiles that havemax_inflightconfigured.
Audit log events for throttle denials are sampled to the first occurrence of
each (client, reason) pair per second to avoid log-volume blowout under load.
The Prometheus counters are not sampled. priority_floor audit records
additionally carry priority, current_global_inflight, priority_threshold,
and global_max_inflight so operators can attribute denials to specific tiers.
Owner Label Isolation
Setting ownership.owner turns on per-proxy resource ownership isolation. Sockguard will:
- Add
ownership.label_key=ownership.ownertoPOST /containers/create,/networks/create,/volumes/create,/services/create,/services/*/update,/secrets/create,/configs/create,/nodes/*/update, and/swarm/update - Stamp service writes at both
LabelsandTaskTemplate.ContainerSpec.Labelsso downstream tasks inherit the same owner identity - Add the same label to
POST /buildvia thelabelsquery parameter so build-produced images can carry the owner identity too. A Podman build cannot shed that stamp: a request whoseunsetlabelnames the owner key is refused (Podman's builder appliesunsetlabelafter the label it sets, so it would strip the stamp), and aLabelssent in any other letter case is collapsed into the single lowercaselabelsthe proxy writes, so a case variant cannot race the stamp on a Podman upstream that reads the key case-insensitively - Inject
label=<owner>filters into list, prune, and events requests, including/services,/tasks,/secrets,/configs, and/nodes(node.label=<owner>there), so responses only reveal resources owned by this proxy instance - Inspect target resources on individual
GET,POST,PUT, andDELETEpaths and deny cross-owner access to owned containers, images, networks, volumes, services, tasks, secrets, configs, nodes, and swarm state - Resolve every
container:<ref>namespace-sharing target onPOST /containers/create(HostConfig.NetworkMode/PidMode/IpcMode/UTSMode/UsernsMode/CgroupnsMode) and deny the request if that container belongs to a different owner - Authorize resources embedded in workload payloads before forwarding them: container
Image, namedHostConfig.Binds/volume mounts, customNetworkMode, and everyNetworkingConfig.EndpointsConfignetwork; plus serviceContainerSpec.Image, named volume mounts, secrets, configs, and attached networks
Ownership authorization always performs a fresh Docker inspect. It deliberately does not reuse the short-lived visibility cache because Docker names and image tags are mutable: after a delete/recreate or retag, a cached positive label result could otherwise authorize a different resource. Repeated references inside one payload are deduplicated before inspection.
Docker reuses words such as create, prune, and json for collection actions, but those words are also legal resource names. Ownership path classification is method-aware and reserves a word only for the exact method and path that performs the collection action. A resource action such as POST /containers/create/start still treats create as the container identifier and applies the normal owner check.
Unlabeled nodes and swarm state are denied on reads once ownership is enabled. They can still be claimed through POST /nodes/*/update and POST /swarm/update, where Sockguard stamps the current owner label into the outgoing update body before forwarding it to Docker.
allow_unowned_images defaults to true so shared base images that Docker can inspect but that lack the ownership label can still be referenced by a container or service. A labeled image owned by somebody else is always denied, and an image reference that Docker cannot resolve is not treated as “unlabeled.” Set the option to false in tighter multi-tenant setups. The option also covers the reference an image retag creates: POST /images/{name}/tag and POST /libpod/images/{name}/tag are denied when repo and tag name a reference another owner's image holds, and a reference held by an unlabeled image can be repointed only while this option is true. The same holds for every other route that names an image: a commit's repo and tag, a build's t, an import, a load and a pull. With the option false, an owner cannot build, load or pull over a tag an unlabeled image currently holds, which includes refreshing an image it pulled earlier unless that image carries its owner label. Unlabeled or unresolved volumes, networks, secrets, and configs remain untrusted; create those resources through their owner-stamping endpoints before referencing them from a workload.
allow_cross_owner_namespace_sharing defaults to false. Joining another container's network/PID/IPC namespace via container:<id> shares that container's sockets, process visibility, and /dev/shm — a full cross-tenant compromise, strictly worse than the access ownership already gates on every other endpoint. As of v1.5, sockguard denies a container:<ref> target that belongs to a different owner by default whenever ownership.owner is set; same-owner refs still pass, but an unlabeled target is treated as untrusted and denied too — consistent with every other container-targeting ownership check — so only a same-owner join is allowed by default. This is a genuine behavior change for the narrow case of an ownership.owner deployment that was already relying on cross-owner container: sharing — set allow_cross_owner_namespace_sharing: true to restore the old unchecked behavior. This check is independent of the filter-tier restrict_namespace_sharing knob described above: it runs whenever ownership.owner is configured, regardless of whether restrict_namespace_sharing is enabled.
Owner isolation and visibility policy also cover the libpod (Podman-native) surface (#148). POST /libpod/containers/create, /pods/create, /networks/create, and /volumes/create get the owner label stamped into their labels field (libpod's own lowercase convention, except /libpod/volumes/create which serializes Labels capitalized — Podman's VolumeCreateOptions carries no JSON tag on that field); /libpod/secrets/create has no JSON body (the body is the raw secret payload), so the label is stamped into its labels query parameter instead, a JSON-encoded map, the same way a build's labels is. Pods have no Docker-compat equivalent, so they get a dedicated KindLibpodPod resource kind with its own list/inspect owner-filtering and visibility coverage; every other libpod list and inspect/action path (containers, networks, volumes, secrets) is owner-filtered and visibility-filtered too, so switching a client from Docker-compat to libpod-native paths is not a way around isolation. Cross-owner pod membership is denied on both sides of the relationship: POST /libpod/pods/create joining another owner's host namespaces, and POST /libpod/containers/create targeting another owner's pod (SpecGenerator.pod) — reusing the same container:<ref> namespace-sharing and embedded-resource-reference checks described above, extended to libpod's {"nsmode":"container","value":"<ref>"} namespace shape. Denial reasons for libpod-family requests are prefixed libpod , matching the convention the libpod body inspectors already use.
Admission Mutations
mutations.rules[] declaratively injects fixed labels or remaps an image reference on a matched request body before the corresponding container_create/service body inspection above runs, image-trust verification, and owner-label stamping — so the mutated bytes get the exact same downstream policy scrutiny a client-authored body would. It is a fail-closed, deliberately narrow DSL: exactly two operations (inject_labels, remap_image), no JSON-patch, no arbitrary field paths, no templating.
Unlike request_body.* and rules, mutations is not part of clients.profiles — there is a single, global mutation authority applied identically regardless of which client profile matched the request. This is a deliberate v1 scope decision, not an oversight: per-profile mutation overrides introduce a merge-order question (which profile's rule wins when both match?) that the feature does not need to answer yet.
mutations:
rules:
- id: tag-managed-by
mode: enforce # enforce | warn | audit
surfaces: [container_create, service_create]
inject_labels:
labels:
com.sockguard.managed-by: "sockguard"
- id: pin-internal-registry
mode: warn # observe would_apply before enforcing
surfaces: [container_create, service_create, service_update]
remap_image:
match: prefix # exact | prefix
from: "docker.io/"
to: "registry.internal.example.com/"
- id: pin-internal-registry-unqualified-alpine
mode: warn
surfaces: [container_create, service_create, service_update]
remap_image:
match: exact # unqualified refs need one exact rule each
from: "alpine:3.21"
to: "registry.internal.example.com/alpine:3.21"remap_image matches the literal image string a client sent, byte for byte — it never expands Docker's own unqualified-reference conventions (no implicit docker.io/library/ prefix, no case folding). The pin-internal-registry rule above only catches requests that already spell out docker.io/...; a request for alpine:3.21 or nginx:1.27 does not start with that prefix and passes through unmodified. To also pin unqualified references, add one match: exact rule per expected bare image, as shown in the second rule above.
| Field | Type | Notes |
|---|---|---|
id | string | Required, unique, ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$. Identifies the rule in logs and audit records. |
mode | string | enforce (default), warn, or audit — see Rollout Modes below; the same semantics apply per rule. warn/audit rules are evaluated against an independent cloned copy of the request body and never affect what is actually forwarded. |
surfaces | []string | One or more of container_create, service_create, service_update. No duplicates. |
inject_labels.labels | map[string]string | Unconditionally sets/replaces these keys in the target label map(s). Valid only on container_create (Config.Labels) and service_create (both Labels and TaskTemplate.ContainerSpec.Labels) — service_update has no label field to mutate. Up to 32 labels per rule, 256 across all rules; keys ≤128 bytes, values ≤4096 bytes, no control characters. Keys and values must both be non-empty (whitespace-only values are rejected too). |
remap_image.match | string | exact (the whole image reference must equal from) or prefix (from must prefix the reference; the remainder is preserved after to). Matches the literal request-body string — no docker.io/library alias expansion or case folding. Valid on all three surfaces. |
remap_image.from / .to | string | ≤4096 bytes, no control characters. For match: exact, both must parse as valid image references. |
Exactly one of inject_labels/remap_image is required per rule — never both, never neither. A rule that unconditionally injects the ownership label key (when ownership.owner is configured) is rejected at config load, since that key must stay under sockguard's exclusive control. Two rules that could both match the same label key or overlapping image from pattern on the same surface are also rejected at config load — which rule would win is otherwise silently order-dependent.
The mutations block uses a stricter config decode than the rest of this schema: unknown keys and YAML type-coercion (e.g. an unquoted id: 0 silently becoming the string "0") are both load-time errors here, not silently accepted.
Rollout Modes
Each named profile can set a mode field that controls how Sockguard applies
denials from that profile's rules, request-body inspectors, ownership
isolation, visibility checks, client-ACL label policy, and rate-limit /
concurrency throttle gates:
| Mode | Behavior |
|---|---|
enforce | Default. Denied requests return 403; throttled requests return 429. |
warn | Requests that would be denied are allowed upstream. The structured audit record carries decision=would_deny. Deny and throttle counters fire with a mode label. |
audit | Same as warn — the request is served — but the log record is tagged decision=would_deny rather than producing a warning-level event. |
warn and audit exist for staged rollouts: add a tighter rule to a profile
in warn or audit mode, observe would_deny in dashboards and logs until
you are confident the deny rate is correct, then flip to enforce.
A request a body inspector refuses, for what the body holds or for its size,
is forwarded with every byte the client sent. Sockguard still reads no more of
it than the inspector's byte limit, but under warn and audit that limit
doesn't stop an oversized body reaching the daemon.
Pre-auth admission gates —
clients.allowed_cidrsCIDR checks and identity-lookup failures — stayenforceregardless of the profile's mode. Unacknowledged Docker-compatible and native libpod process-list reads also stayenforce;warnandauditcannot replace the globalinsecure_allow_read_exfiltration: trueacknowledgment. These gates are unsafe to relax through rollout mode.
clients:
profiles:
- name: ci-agent
mode: warn # observe would_deny before enforcing
rules:
- match: { method: GET, path: "/containers/**" }
action: allow
- match: { method: POST, path: "/containers/*/exec" }
action: allow
- match: { method: "*", path: "/**" }
action: denyOnce the would-deny rate in dashboards matches expectations, set mode: enforce (or remove the field — enforce is the default).
The mode label on deny/throttle counters lets you compare blocked vs.
would-have-been-blocked volume side by side:
sum by (mode) (rate(sockguard_http_denied_requests_total[5m]))Policy Discovery Workflow
mode: audit has a second use beyond staging a tighter rule on an existing
profile: bootstrapping a policy for a client you don't already have rules
for. Point a deny-everything profile at the real client, run its normal
traffic through it, and every profile denial comes back as a would_deny
record carrying the exact method and normalized path — the answer to "what
does this thing actually call" without reading its source or guessing from
vendor docs. Pre-auth admission failures, including allowed-CIDR and identity
lookup failures, remain enforced and are not converted to would_deny.
1. Prerequisites. Access and audit records both carry would_deny and
normalized_path, but the dedicated audit log is off by default
(log.audit.enabled defaults to false — see Logging And
Audit). The discovery.yaml preset in app/configs/
turns it on for you: a discovery profile in mode: audit with a single
deny-all rule, applied to every caller of a dedicated owner-only unix socket
via clients.default_profile, with the audit stream on stdout (separate
from the operational log, which stays on its stderr default) so it's
trivial to capture on its own.
2. Start with the discovery preset.
sockguard serve --config app/configs/discovery.yamlThe preset binds /var/run/sockguard-discovery.sock with mode 0600 instead
of relying on Sockguard's default TCP listener. Mount that dedicated socket
only into the client being observed. If the client must use authenticated TCP
or unix-peer identity instead, copy the production listener settings into a
private discovery config and keep that listener equally isolated; do not put
the discovery profile on a shared listener.
3. Exercise the client normally. Let it run through its actual startup
and steady-state behavior: list, inspect, create, whatever it does on a
normal day. mode: audit forwards requests denied by the resolved profile;
pre-auth admission gates still enforce. The observed client therefore has
unfiltered Docker API access through the dedicated discovery listener while
the preset is active, so give that socket only to a client you already trust
with the daemon. Every profile denial that was forwarded is logged as
would_deny.
4. Harvest the events. Every audit event the client's traffic produced
carries decision: "would_deny", plus the request's method and
normalized_path. A denied POST /v1.51/containers/create looks like:
{"event_type":"http_request","timestamp":"2026-08-29T14:32:07.118453Z","request_id":"a1b2c3d4e5f67890","client_request_id":"","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","trace_parent_id":"00f067aa0ba902b7","trace_span_id":"7f1a3c9e2b6d4f80","trace_sampled":true,"method":"POST","raw_path":"/v1.51/containers/create","normalized_path":"/containers/create","decision":"would_deny","reason_code":"matched_deny_rule","reason":"discovery preset — logged only, not enforced","matched_rule":0,"selected_profile":"discovery","rollout_mode":"audit","status":201,"actor_remote_addr":"@","actor_source_ip":"","transport_listener":"unix","transport_scheme":"http","transport_protocol":"HTTP/1.1","listener_name":"discovery","ownership":{"enabled":false,"owner":"","label_key":""}}trace_id, trace_span_id, trace_parent_id, and trace_sampled ride every audit event (and every access log line and upstream-error log — see Logging And Audit) with no omitempty, so a real event always carries all four; trace_sampled renders false rather than vanishing when the incoming request carried no sampled trace context.
status: 201 is the daemon's real response — the request was actually
forwarded and actually succeeded; would_deny only describes what the
profile would have done. Pull the distinct method/path pairs out of the
stream with jq:
docker logs sockguard 2>/dev/null \
| jq -r 'select(.decision == "would_deny") | "\(.method) \(.normalized_path)"' \
| sort -uA morning of traffic from, say, a Traefik-style consumer might come back as:
GET /_ping
GET /containers/4f8a1c2e9b7d/json
GET /containers/json
GET /events
GET /version
POST /containers/4f8a1c2e9b7d/start5. Turn that into rules. normalized_path carries the real resource ID
from the traffic that happened to run, not a wildcard — generalize per-
resource segments (4f8a1c2e9b7d) to * by hand before writing the rule,
the same way every other preset in app/configs/ does. The harvested output
above becomes:
clients:
default_profile: traefik-live
profiles:
- name: traefik-live
rules:
- match: { method: GET, path: "/_ping" }
action: allow
- match: { method: GET, path: "/version" }
action: allow
- match: { method: GET, path: "/events" }
action: allow
- match: { method: GET, path: "/containers/json" }
action: allow
- match: { method: GET, path: "/containers/*/json" }
action: allow
- match: { method: POST, path: "/containers/*/start" }
action: allow
- match: { method: "*", path: "/**" }
action: denyNote the new profile has no mode field — that's step 6, not this one.
Leaving discovery.yaml's own deny-all rule in mode: audit while you
edit is fine; nothing is enforced either way until you switch.
6. Switch off shadow mode and verify. Replace the discovery config with
the rules you just wrote — mode: audit omitted (or set to enforce,
which is the same thing) so denials are real again — restart sockguard, and
run the client through the same paths one more time. Confirm two things:
the client still works exactly as it did in step 3, with no new errors; and
the audit log shows no decision: "deny" events for it (would_deny itself
can't appear anymore — that decision only exists under warn/audit). If
you see a real deny, you missed a rule; add it and repeat this step before
calling the profile done.
Hot Reload
Sockguard can reload policy at runtime without dropping connections.
reload:
enabled: true # default false; opt-in
debounce: 250ms # collapse burst of fs events into one reload (default "250ms")
poll_interval: "" # "" = off; opt-in stat fallback for inotify-unreliable backendsWhen reload.enabled: true:
- fsnotify file watch — Sockguard watches the loaded config file via Linux
inotify / macOS kqueue. A burst of editor-save events (vim's chmod + write +
rename + create sequence, for example) is debounced by
reload.debounceinto a single reload. - SIGHUP — sending
SIGHUPtriggers a reload. Withoutreload.enabled, SIGHUP terminates the process (Go's default); with reload on, SIGHUP never terminates. On Synology / DSM and other btrfs bind-mount backends,SIGHUPis the canonical reload trigger — inotify events on the host filesystem don't always propagate into the container, so the fsnotify watch may miss otherwise-valid edits. - Stat-based poll fallback — set
reload.poll_interval(typical5s–15s) to have Sockguard periodically re-stat the config file and fire a reload when its size, modification time, or inode have moved. Use this on filesystems where fsnotify drops events (Synology btrfs bind-mounts, some FUSE backends, NFS). The poll is off by default because regular Linux inotify and macOS kqueue cover the common cases reliably. - Atomic swap — on success, the running handler chain is replaced atomically. In-flight requests at the swap moment complete on the previous chain; new requests immediately route through the new one. No connections are dropped.
- Rollback on failure — if the new file is unreadable, the YAML is
malformed, the validator rejects the config, or a signature check fails, the
running policy is preserved untouched. The failure is logged with a
structured
result=reject_load|reject_validation|reject_immutable|reject_signature|reject_compatkey that matches the metric label exactly, so SIEM grep against either surface produces the same set of events.
Immutable fields
Some config fields are bound to long-lived sockets and goroutines that cannot
be replaced from within a running process. A reload that would mutate any of
these is refused — the running policy stays in place and the failure is logged
with changed_fields=...:
listen.*— listener address, TLS material, and socket pathserver.*— currently justshutdown_grace, read once at startup into the seamshutdownServersconsults; a reload has no way to make a changed value take effect before the next shutdownupstream.socket— upstream Docker socket pathupstream.endpointsandupstream.failover— remote endpoint list and health-probe loop parameters (bound to the long-lived Resolver at startup)upstream.flavor— which engine the upstream is (theautoprobe runs once at startup and the resolved value is bound into the handler chain)log.*— log level, format, and output sinkhealth.*— health endpoint path, watchdog, and readiness probe configmetrics.*— metrics endpoint and pathadmin.*— admin config (includingadmin.listen.*)- signed-policy trust material selected by
--policy-bundle-trust-configis pinned outside the candidate and cannot be changed by hot reload
The signed candidate's policy_bundle.signature_path is reload-mutable so
an operator can re-sign the same YAML without a restart. Any candidate copies
of enabled, allowed_signing_keys, allowed_keyless,
require_rekor_inclusion, or verify_timeout are ignored and replaced by the
pinned bootstrap values.
Everything else — rules, clients.*, response.*, request_body.*,
ownership.*, insecure_allow_*, and upstream.request_timeout /
upstream.hijack_inactivity_timeout (of the upstream block, only those two
fields are mutable; socket, endpoints, failover, and flavor are pinned)
— is rebuilt and atomically applied on every successful reload.
Reload outcomes
When metrics.enabled: true, two series track reload outcomes:
sockguard_config_reload_total{result}— counter with labelsok,reject_load,reject_validation,reject_immutable,reject_signature,reject_compat.sockguard_config_reload_last_success_timestamp_seconds— gauge; omitted from scrape output until the first successful reload.
Admin Listener
The admin endpoints (POST /admin/validate and GET /admin/policy/version)
can run on a separate listener, keeping admin traffic entirely off the main
Docker-API data plane.
admin:
enabled: true
listen:
socket: /var/run/sockguard-admin/admin.sock # operator-only permsOr loopback TCP:
admin:
enabled: true
listen:
address: 127.0.0.1:2376
tls:
cert_file: /run/secrets/sockguard/admin-cert.pem
key_file: /run/secrets/sockguard/admin-key.pem
client_ca_file: /run/secrets/sockguard/admin-ca.pemWhen admin.listen is unset, admin endpoints are served on the main listener
and inherit its CIDR allowlist, mTLS posture, and per-profile rate-limit /
concurrency caps. When set, Docker-API traffic and admin traffic are fully
isolated. See the Admin API page for the full endpoint reference and
recommended posture.
The admin.* block including admin.listen.* is immutable across hot reload
— restart is required to change the listener binding.
Security note — a wide-open admin listener is rejected at startup. A non-loopback plaintext
admin.listen.addresswith noclients.allowed_cidrswould let any host that can reach the port submit candidate YAML and read policy metadata with no authentication and no IP backstop — unlike the main listener, whose unauthenticated requests still face the full policy filter chain, the admin surface is gated only by CIDRs. Validation therefore rejects that combination. Resolve it by settingclients.allowed_cidrs, configuringadmin.listen.tls(mutual TLS), or — only on a private, trusted network — explicitly acknowledging the exposure withadmin.listen.insecure_allow_wide_open: true.
Signed Policy Bundles
Signed-policy mode has two files with separate jobs:
- The bootstrap trust config defines whether signature verification is
enabled, who may sign, whether Rekor inclusion is required, and the
verification timeout. It is selected out of band with the required
serve --policy-bundle-trust-config <path>flag. - The signed candidate contains the operational policy plus
policy_bundle.signature_path, which points to the cosign bundle covering that candidate's exact bytes.
The candidate and bootstrap YAML files inherit the 16 MiB configuration cap. The referenced Sigstore bundle JSON is capped at 4 MiB. Startup and reload fail closed on any oversized file before parsing or verification. Only regular files are accepted; inputs are opened nonblocking and FIFOs, devices, directories, and other non-regular paths are rejected.
The bootstrap file reuses the existing policy_bundle schema:
policy_bundle:
enabled: true
allowed_signing_keys:
- pem: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
allowed_keyless:
- issuer: "https://token.actions.githubusercontent.com"
subject_pattern: "^https://github.com/my-org/my-repo/.github/workflows/release\\.yml@refs/heads/main$"
require_rekor_inclusion: true
verify_timeout: 10sThe signed policy carries the mutable bundle location, not its own trust decision:
policy_bundle:
signature_path: /etc/sockguard/sockguard.yaml.bundle
rules:
# ...the signed policy...For the keyed entry above, sign the candidate on a trusted workstation with the matching private key, deploy only the candidate and bundle, and start Sockguard with the runtime paths. Never mount or deploy the private signing key with the proxy:
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.yamlFor the keyless entry, omit --key so cosign uses its OIDC identity:
cosign sign-blob --yes --bundle ./sockguard.yaml.bundle ./sockguard.yamlFor the GitHub Actions issuer and subject shown above, the signing job also
needs permissions: id-token: write so cosign can obtain the OIDC identity.
Keyless trust loads the public Sigstore root initially and refreshes it about
every 24 hours through a process-wide memoized TUF client, so ongoing outbound
access is required. It does not use a local TUF cache and bounds each TUF HTTP
request to 15 seconds. An initial load failure aborts startup before a listener
opens; a failed background refresh is logged and retains the last valid root.
Read-only container filesystems need no cache volume. A keyed-only bootstrap
does not load TUF material and needs no policy-bundle network access.
The Helm chart exposes the same separation without copying either external
object into the chart-generated candidate ConfigMap. Configure
policyBundleTrust and policyBundleSignature together, using either a Secret
or ConfigMap reference for each. The signature object is mounted at
/etc/sockguard-policy-bundle-signature/signature.json, so the candidate must
use that exact policy_bundle.signature_path. The trust reference cannot name
the chart-generated candidate ConfigMap or the same Secret/ConfigMap that holds
the rotatable signature bundle. Kubernetes authorization applies to the whole
object, not individual keys, so co-locating them would give signature rotators
authority over trust. Rotate either external object by updating it and
restarting the DaemonSet; bootstrap trust is process-immutable.
policyBundleTrust:
secretRef:
name: sockguard-policy-trust
key: trust.yaml
policyBundleSignature:
secretRef:
name: sockguard-policy-signature
key: signature.json
config: |
listen:
address: ":2375"
insecure_allow_plain_tcp: true
insecure_allow_unauthenticated_clients: true
policy_bundle:
signature_path: /etc/sockguard-policy-bundle-signature/signature.json
# ...the rest of the signed policy...Signed mode intentionally ignores every SOCKGUARD_* environment overlay.
Keep listener and other operational settings in the signed config value, as
the chart defaults do, so verification cannot silently replace them with
built-in defaults.
The candidate and bootstrap trust config must resolve to different files.
Sockguard rejects the same path, two symlinks to the same file, and hardlinks
to the same inode. A candidate cannot authenticate itself: setting
policy_bundle.enabled: true without --policy-bundle-trust-config is a
startup error. Copies of bootstrap fields placed in the candidate are ignored
and replaced with the pinned bootstrap values.
Verification runs before the operational logger is constructed, before any
rule compiles, and before a listener opens. Early diagnostics use a fixed
stderr bootstrap logger, so an unverified log.output path is never opened.
A missing, malformed, or wrong-key bundle aborts startup with a wrapped
policy bundle: error. On hot reload, a signature failure records
reject_signature and leaves the running policy untouched.
Two verification paths are supported and can coexist:
- Keyed: PEM-encoded ECDSA, RSA, or ed25519 public keys under
allowed_signing_keys. Verification uses the key's algorithm directly and needs no network round trip. - Keyless (Fulcio + Rekor):
allowed_keylessentries specify the exact OIDC issuer URL and a subject SAN regex. The signing certificate chain is verified against Fulcio. Whenrequire_rekor_inclusion: true, a Rekor transparency-log entry is also required. Startup loads the public Sigstore trust root through a memoized TUF client and fails closed if it cannot.verify_timeoutsupplies a cooperative deadline for local verification. Sockguard checks it before beginning and after each synchronous Sigstore verification attempt, rejects late success, and stops later signer fallbacks. Sigstore-go cannot preempt an individual crypto call. Rekor proof verification is local; TUF root downloads use the separate 15-second HTTP bound described below.
The verified signer fingerprint (keyed:<spki-fingerprint> or
keyless:<issuer>:<san>) and the YAML's SHA-256 digest are stamped onto the
policy-version snapshot returned by GET /admin/policy/version in the
bundle_signer, bundle_digest, and bundle_source fields.
Signed mode rejects the presence of every rule-generating Tecnativa variable,
even if its value is false. That includes section variables, POST,
GRPC/SESSION, and granular ALLOW_* variables. Startup fails before rule
compilation. If one appears after startup, the next reload records
reject_compat and keeps the active policy. Convert compatibility grants to
YAML rules before signing. Unsigned compatibility mode remains available.
The feature uses github.com/sigstore/sigstore-go, the same verification stack
as container image trust (request_body.container_create.image_trust).
Logging And Audit
Sockguard has two operator-facing logging streams:
log.access_log(defaulttrue) controls the structured request log written through the normal logger output.log.audit.enabled(defaultfalse) enables a dedicated JSON audit stream with a stable schema: request ID, client request ID, trace ID, trace parent/span IDs, sampled flag, raw and normalized path, decision,reason_code, reason, matched rule, selected profile, flattened actor and transport identity fields, ownership context, and final HTTP status.
Access logs intentionally carry both path and normalized_path. path is the raw client URL path as received, including any Docker API version prefix, percent-encoded separators, or other client-controlled shape; keep it for forensic replay. normalized_path is the canonical path after Sockguard's policy normalization and is the field to use for SIEM grouping, alerting, rule dashboards, and allow/deny analysis. Audit events use the same split as raw_path and normalized_path.
Every request also carries trace correlation fields. If a caller sends a valid W3C traceparent, Sockguard preserves trace_id and trace_sampled, records the incoming parent as trace_parent_id, forwards a new proxy-local trace_span_id, and emits the same fields in access logs, audit events, and upstream reverse-proxy error logs. Invalid or absent trace context starts a fresh local trace and drops stale tracestate.
Audit logs use a separate sink from the access logger:
log.audit.format: currently onlyjsonis accepted.log.audit.output: same sink options as the main logger (stderr,stdout, orfile:/absolute/path).
Every audit event includes an ownership object with enabled, owner, and label_key. When ownership.owner is set, that owner identifier appears in every audit event, including requests that do not touch owned resources. Treat it as an operator-visible tenant or workload identifier, not a secret.
Audit events intentionally preserve both the proxy-generated canonical request_id and any caller-supplied client_request_id. Upstream proxy failures rewrite the audit reason_code to bounded terminal values such as upstream_socket_unreachable or upstream_response_rejected_by_policy so the final outcome is explicit even after a request was policy-allowed.
Rule Matching
Rules are evaluated in order. First match wins. If no rule matches, the request is denied.
Path Patterns
| Pattern | Matches | Does Not Match |
|---|---|---|
/containers/json | /containers/json, /v1.45/containers/json | /containers/abc |
/containers/* | /containers/json, /containers/abc123 | /containers/abc/start |
/containers/** | /containers/json, /containers/abc/start, /containers/abc/logs/stream | /images/json |
/** | Everything | Nothing |
Every pattern must start with /. Config validation rejects one that does not, naming the rule and the rooted spelling to write instead. The two ways a rootless pattern goes wrong are why it is the shape that is refused rather than the outcome. A rootless single-star pattern (containers/*, */json, a bare *) matches nothing: request paths are always rooted and * stops at a /, so the rule silently never fires. A rootless pattern carrying ** goes the other way, because ** spans any sequence including the leading slash: ** on its own is a catch-all, **/json matches /containers/json, and */** matches everything. Neither is what the spelling suggests. Use /**, not **, for a catch-all. The same rule and the same reasoning apply to container-label ACL paths, which are refused in clientacl because they never reach config validation.
A trailing slash is a real, empty final segment, not punctuation: a pattern matches only a path with exactly as many /-separated segments as the pattern spends, so /containers/* matches /containers/abc but not /containers/abc/, and /*/*/* matches /a/b/. On every Docker route this is invisible, because canonicalization strips the trailing slash before matching. The one place it shows is Podman's image-SCP route, where Sockguard matches the path gorilla/mux actually routes on, trailing slash included — see the Podman guide. ** is unaffected either way, since it already spans any number of segments including an empty one.
Before rule evaluation, Sockguard canonicalizes the request path: it strips any /vN.NN/ Docker API version prefix (Podman's own /vN.N.N[-suffix]/ semver prefixes included), percent-decodes the path (so %2F, %2E, and mixed-case escapes cannot smuggle separators past a literal allowlist), and resolves . / .. segments via path.Clean. A request for /v1.45/containers/%2e%2e/images/json therefore matches as /images/json. Because the path is decoded before matching, a rule whose match.path contains a literal % could never match a real request — such a pattern is rejected at config validation rather than silently never firing. The same applies to a match.path that itself begins with a version prefix (e.g. /v1.45/containers/json): it can never match the already-stripped request path, so config validation rejects it too, naming the offending rule and pattern — write the pattern without the /vN... prefix.
Methods
- Exact:
GET,POST,PUT,DELETE,HEAD - Wildcard:
*matches any method
Environment Variables
Most YAML fields can be set via env var by prefixing with SOCKGUARD_ and
replacing dots with underscores (metrics.enabled → SOCKGUARD_METRICS_ENABLED).
List-typed values accept comma-separated entries. Precedence is CLI flags >
env vars > YAML > built-in defaults.
The table below covers the most commonly used variables. Scalar and list-of-string fields at any nesting depth have an env-var equivalent even when they aren't enumerated here. Four things don't:
- The
rules:block. It's populated after unmarshal rather than from a default, so there is noSOCKGUARD_RULES. clients.global_concurrency, and a profile'slimits.rateandlimits.concurrency. These are pointer blocks that stay absent unless the YAML declares them, and binding them would turn "absent" into "present but empty".listen.socket_uidandlisten.socket_gid, and the admin listener's copies of them. They're pointer ints so that an omitted value is distinguishable from UID or GID 0, and the same absent-vs-empty rule applies. Set them in YAML.- Anything inside a list entry, because the list itself is the leaf. That covers
clients.profiles[],listeners[],upstream.endpoints[],mutations.rules[],request_body.container_create.allowed_device_requests[], and theimage_trust.allowed_signing_keys/allowed_keylesslists.
Rows marked (YAML-only) below are those cases.
A SOCKGUARD_* variable that binds to no config key is read by nothing, and
without a warning that silence looks exactly like a setting that took effect.
sockguard serve and sockguard validate both check the environment at
startup and log one line per unrecognized variable, naming it and, when the
spelling is within a couple of characters of a real one, the variable you
probably meant:
level=WARN msg="ignoring unrecognized SOCKGUARD_* environment variable: it maps to no configuration key, so the setting it looks like it carries is still at its config-file or default value" var=SOCKGUARD_LISTEN_SOCKT did_you_mean=SOCKGUARD_LISTEN_SOCKETOnly the name is logged, never the value, since a typo lands on a variable holding a credential as easily as on one holding a socket path. The warning is advisory and never fails startup: a variable nothing reads cannot make a config invalid.
Recognized means your configuration could actually bind it, which is the schema
plus whatever your YAML declares, rather than a fixed list. So a variable for
one of the four YAML-only cases above is reported unless the file declares the
block it sits in, because declaring it is what puts the key where an env var
can reach it. Tecnativa compatibility variables (CONTAINERS, POST,
ALLOW_START, SOCKET_PATH, LOG_LEVEL) carry no SOCKGUARD_ prefix and are
never flagged.
Listener and upstream
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_LISTEN_ADDRESS | listen.address | 127.0.0.1:2375 | TCP listener address. Loopback is allowed plaintext; non-loopback requires listen.tls or the two-flag legacy opt-in. |
SOCKGUARD_LISTEN_INSECURE_ALLOW_PLAIN_TCP | listen.insecure_allow_plain_tcp | false | First of two acknowledgments for plaintext non-loopback TCP (unencrypted transport). Must be paired with insecure_allow_unauthenticated_clients. |
SOCKGUARD_LISTEN_INSECURE_ALLOW_UNAUTHENTICATED_CLIENTS | listen.insecure_allow_unauthenticated_clients | false | Second acknowledgment for plaintext non-loopback TCP (any host that can reach the port can impersonate a client). Use only on private trusted networks; never expose to the host or Internet. |
SOCKGUARD_LISTEN_TLS_CERT_FILE | listen.tls.cert_file | (unset) | Path to the listener certificate. Required to enable mTLS. |
SOCKGUARD_LISTEN_TLS_KEY_FILE | listen.tls.key_file | (unset) | Path to the listener private key. Required to enable mTLS. |
SOCKGUARD_LISTEN_TLS_CLIENT_CA_FILE | listen.tls.client_ca_file | (unset) | CA bundle that verifies client certificates. Pair with selectors under listen.tls (common_names, dns_names, ip_addresses, uri_sans, public_key_sha256_pins) to narrow trust. |
SOCKGUARD_LISTEN_SOCKET | listen.socket | (unset) | Switches to a unix socket listener. Sockguard hardens the socket to mode 0600 and rejects broader modes. |
SOCKGUARD_SERVER_SHUTDOWN_GRACE | server.shutdown_grace | "30s" | How long to wait for in-flight requests to finish on SIGTERM/SIGINT, on both the main and admin listeners, before force-closing them (Go duration, e.g. 30s). 0 closes immediately — must be a non-negative duration. Reload-immutable — restart required. |
SOCKGUARD_UPSTREAM_SOCKET | upstream.socket | /var/run/docker.sock | Path to the real Docker daemon socket Sockguard proxies to. |
SOCKGUARD_UPSTREAM_REQUEST_TIMEOUT | upstream.request_timeout | "60s" | Total per-request deadline (Go duration, e.g. 30s). Set off (or "") to disable — prefer off here since an explicitly empty env var is treated as unset and falls back to the default. Finite requests over the deadline return 504; streaming and long-lived endpoints are exempt. Reload-mutable. |
SOCKGUARD_UPSTREAM_FLAVOR | upstream.flavor | auto | Which engine is behind the upstream: auto, docker, or podman. auto probes GET /version once at startup and fails startup if the answer is ambiguous; an explicit value never probes. Reload-immutable — restart required. |
SOCKGUARD_UPSTREAM_HIJACK_INACTIVITY_TIMEOUT | upstream.hijack_inactivity_timeout | "10m" | Idle deadline for hijacked docker attach/exec start connections (Go duration, e.g. 10m), refreshed on activity in either direction. No off spelling — must be a positive duration. Reload-mutable. |
SOCKGUARD_UPSTREAM_FAILOVER_HEALTH_INTERVAL | upstream.failover.health_interval | "" (resolver default: 5s) | Background health-probe interval per endpoint. Empty uses the resolver default of 5s; a negative value disables continuous probing (failures still detected at request time). Applies only when upstream.endpoints is set. Reload-immutable — restart required. |
SOCKGUARD_UPSTREAM_FAILOVER_HEALTH_TIMEOUT | upstream.failover.health_timeout | "" (resolver default: 2s) | Per-probe dial and TLS-handshake timeout. Empty uses the resolver default of 2s. Applies only when upstream.endpoints is set. Reload-immutable — restart required. |
Logging
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_LOG_LEVEL | log.level | info | Operational log level: debug, info, warn, error. |
SOCKGUARD_LOG_FORMAT | log.format | json | Operational log format: json or text. |
SOCKGUARD_LOG_ACCESS_LOG | log.access_log | true | Emit one structured access-log line per request with method, path, decision, latency, and trace fields. |
SOCKGUARD_LOG_AUDIT_ENABLED | log.audit.enabled | false | Emit dedicated audit events with stable schema, separate from the operational log. |
SOCKGUARD_LOG_AUDIT_FORMAT | log.audit.format | json | Audit event format. JSON is required for SIEM ingestion; text exists for local debug only. |
SOCKGUARD_LOG_AUDIT_OUTPUT | log.audit.output | stderr | Audit sink: stderr, stdout, or a file path. File paths must already exist with writable permissions. |
Health and observability
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_HEALTH_ENABLED | health.enabled | true | Serve the /health liveness endpoint. |
SOCKGUARD_HEALTH_PATH | health.path | /health | Health endpoint path. Must start with / and differ from metrics.path / admin.path when those are set. |
SOCKGUARD_HEALTH_WATCHDOG_ENABLED | health.watchdog.enabled | false | Start an active upstream socket monitor. Logs reachable/unreachable transitions and feeds /health. |
SOCKGUARD_HEALTH_WATCHDOG_INTERVAL | health.watchdog.interval | 5s | Watchdog probe interval. Must be a positive Go duration; 1s–30s is typical. |
SOCKGUARD_HEALTH_READINESS_ENABLED | health.readiness.enabled | false | Serve the opt-in /ready probe that issues a real GET /containers/json against the Docker API. |
SOCKGUARD_HEALTH_READINESS_PATH | health.readiness.path | /ready | Readiness endpoint path. Must start with / and differ from health.path / metrics.path / admin.path. |
SOCKGUARD_HEALTH_READINESS_INTERVAL | health.readiness.interval | 10s | Readiness probe interval. Must be a positive Go duration. |
SOCKGUARD_HEALTH_READINESS_TIMEOUT | health.readiness.timeout | 5s | Per-probe deadline for the readiness API call. Must be a positive Go duration. |
SOCKGUARD_METRICS_ENABLED | metrics.enabled | false | Serve Prometheus text metrics on the proxy listener. See the Observability page for the full metric reference. |
SOCKGUARD_METRICS_PATH | metrics.path | /metrics | Scrape path. Must start with / and differ from health.path when both endpoints are enabled. |
SOCKGUARD_ADMIN_ENABLED | admin.enabled | false | Serve the in-band POST <admin.path> candidate-config validation endpoint on the main listener. |
SOCKGUARD_ADMIN_PATH | admin.path | /admin/validate | Admin endpoint path. Must start with / and must differ from health.path and metrics.path when enabled. |
SOCKGUARD_ADMIN_POLICY_VERSION_PATH | admin.policy_version_path | /admin/policy/version | Path of the read-only policy-version endpoint. Must start with / and not collide with other endpoint paths. |
SOCKGUARD_ADMIN_MAX_REQUEST_BYTES | admin.max_request_bytes | 524288 | Hard cap on candidate-YAML body size. Bodies above this return 413. |
SOCKGUARD_ADMIN_LISTEN_SOCKET | admin.listen.socket | (unset) | Serve admin endpoints on a dedicated unix socket instead of the main listener. |
SOCKGUARD_ADMIN_LISTEN_ADDRESS | admin.listen.address | (unset) | Serve admin endpoints on a dedicated TCP listener (mTLS via admin.listen.tls.*). |
SOCKGUARD_ADMIN_LISTEN_INSECURE_ALLOW_WIDE_OPEN | admin.listen.insecure_allow_wide_open | false | Acknowledge a non-loopback plaintext admin listener with no clients.allowed_cidrs. Without this (or a CIDR allowlist / mTLS) such a config is a validation error. Private trusted networks only. |
SOCKGUARD_RELOAD_ENABLED | reload.enabled | false | Watch the config file via fsnotify and reload on SIGHUP. Enabling this changes SIGHUP from "terminate" to "reload". |
SOCKGUARD_RELOAD_DEBOUNCE | reload.debounce | "250ms" | Coalesce a burst of fsnotify events into a single reload. Must be a valid Go duration string >= 0. |
SOCKGUARD_RELOAD_POLL_INTERVAL | reload.poll_interval | "" | Opt-in stat-based fallback for inotify-unreliable filesystems (Synology btrfs bind-mounts, some FUSE / NFS backends). Periodically re-stats the config file and fires a reload when size, mtime, or inode changes. Empty string disables polling. Must be a valid Go duration string >= 0. |
Response controls
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_RESPONSE_DENY_VERBOSITY | response.deny_verbosity | minimal | minimal returns only the generic deny message. verbose echoes method, path, and reason — dev only. |
SOCKGUARD_RESPONSE_REDACT_CONTAINER_ENV | response.redact_container_env | true | Replace workload env arrays with empty arrays on container/service/task/plugin reads. |
SOCKGUARD_RESPONSE_REDACT_MOUNT_PATHS | response.redact_mount_paths | true | Redact mount and host-device source paths on container/volume/task/service/plugin//system/df reads. |
SOCKGUARD_RESPONSE_REDACT_NETWORK_TOPOLOGY | response.redact_network_topology | true | Redact network IDs, attached addresses, remote managers, and node reachability on relevant reads. |
SOCKGUARD_RESPONSE_REDACT_SENSITIVE_DATA | response.redact_sensitive_data | true | Redact config payloads, swarm join/unlock and CA material, and node/swarm TLS metadata. |
SOCKGUARD_RESPONSE_REDACT_HOST_TOPOLOGY | response.redact_host_topology | false | Redact GET /info container-runtime plumbing fields (Containerd, FirewallBackend, DiscoveredDevices, NRI). Opt-in. |
SOCKGUARD_RESPONSE_ALLOW_ATTESTATION_STATEMENTS | response.allow_attestation_statements | false | Allow a truthy statement query on GET /images/{name}/attestations to return full content. Empty, 0, no, false, and none are false; every other non-empty value is true. |
Insecure opt-ins
These flags loosen Sockguard's defaults. Do not enable them without an explicit operational reason and a plan to revisit.
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_INSECURE_ALLOW_BODY_BLIND_WRITES | insecure_allow_body_blind_writes | false | Allow controls Sockguard cannot inspect narrowly, including arbitrary exec without an allowlist, plugin set without allowed prefixes, Podman build host/local/multipart or resource-usage file controls, and the documented uninspected libpod writes. |
SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION | insecure_allow_read_exfiltration | false | Allow rules that match container or pod process-list, raw archive/export, log/attach, image-tarball-get, or image/plugin registry-push endpoints. Top accepts caller-selected ps_args and returns process arguments without response redaction; native container and pod top can also stream. The legacy option name includes “read,” but outbound pushes also read local artifact content and can exfiltrate it. Tighten the rules instead whenever possible. |
SOCKGUARD_INSECURE_ACCEPT_OPAQUE_BUILDKIT_TUNNELS | insecure_accept_opaque_buildkit_tunnels | false | Deprecated — allow rules that match POST /session, POST /grpc, or a moby.buildkit.v1.Control method path with zero inspection. Setting this to true now logs a startup deprecation warning. Prefer request_body.buildkit (issue #185) for full per-message mediation of the same endpoints, or the classic-builder (DOCKER_BUILDKIT=0 + POST /build) presets. Mutually exclusive with request_body.buildkit. |
Container-create body inspection
POST /containers/create is inspected by default; bodies that violate any of
the gates below are denied before the request reaches Docker.
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_PRIVILEGED | request_body.container_create.allow_privileged | false | Allow HostConfig.Privileged=true. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_NETWORK | request_body.container_create.allow_host_network | false | Allow HostConfig.NetworkMode=host and ns:<path>. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_PID | request_body.container_create.allow_host_pid | false | Allow HostConfig.PidMode=host and ns:<path>. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_IPC | request_body.container_create.allow_host_ipc | false | Allow HostConfig.IpcMode=host and ns:<path>. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_BIND_MOUNTS | request_body.container_create.allowed_bind_mounts | empty | Comma-separated host-path prefixes allowed as bind sources, including the device of a local-driver volume whose options request a bind. Named volumes are otherwise always allowed. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_ALL_DEVICES | request_body.container_create.allow_all_devices | false | Allow any HostConfig.Devices host path. Prefer the allowlist below instead. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_DEVICES | request_body.container_create.allowed_devices | empty | Comma-separated host device paths allowed for HostConfig.Devices. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_DEVICE_REQUESTS | request_body.container_create.allow_device_requests | false | Escape hatch: allow all HostConfig.DeviceRequests without inspection. Prefer allowed_device_requests for least-privilege access. |
| (YAML-only) | request_body.container_create.allowed_device_requests | empty | Structured HostConfig.DeviceRequests allowlist. Each entry has driver (required), allowed_capabilities (list of capability sets; request sets must each be a subset of at least one), and optional max_count (-1 = all devices). Empty = deny all (default). Not settable via env var due to nested structure; configure in YAML. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_DEVICE_CGROUP_RULES | request_body.container_create.allow_device_cgroup_rules | false | Allow all HostConfig.DeviceCgroupRules without inspection. Prefer allowed_device_cgroup_rules for least-privilege access. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_DEVICE_CGROUP_RULES | request_body.container_create.allowed_device_cgroup_rules | empty | Comma-separated Docker cgroup rule strings (<type> <major>:<minor> <perms>) that HostConfig.DeviceCgroupRules entries must match. Wildcards (*) in major/minor are allowed in allowlist entries and match any value; request wildcards are only permitted when the allowlist entry also uses a wildcard at that position. Empty list = deny all (default). Example: c 1:3 rwm,c 226:* rwm. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_NO_NEW_PRIVILEGES | request_body.container_create.require_no_new_privileges | false | Require HostConfig.SecurityOpt to include no-new-privileges:true. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_NON_ROOT_USER | request_body.container_create.require_non_root_user | false | Require Config.User to be a non-zero UID or non-root username. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_READONLY_ROOTFS | request_body.container_create.require_readonly_rootfs | false | Require HostConfig.ReadonlyRootfs=true. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_DROP_ALL_CAPABILITIES | request_body.container_create.require_drop_all_capabilities | false | Require HostConfig.CapDrop to contain "ALL". |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_ALL_CAPABILITIES | request_body.container_create.allow_all_capabilities | false | Skip the HostConfig.CapAdd allowlist. With this off, only entries in allowed_capabilities are permitted. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_CAPABILITIES | request_body.container_create.allowed_capabilities | empty | HostConfig.CapAdd allowlist (case-insensitive, optional CAP_ prefix). |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_MEMORY_LIMIT | request_body.container_create.require_memory_limit | false | Require HostConfig.Memory > 0. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_CPU_LIMIT | request_body.container_create.require_cpu_limit | false | Require one of NanoCpus, CpuQuota, CpuPeriod, CpuShares > 0. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_CPU_LIMIT_HARD | request_body.container_create.require_cpu_limit_hard | false | Require NanoCpus or CpuQuota specifically; CpuShares (relative priority) does not satisfy this stricter, independent check. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_PIDS_LIMIT | request_body.container_create.require_pids_limit | false | Require HostConfig.PidsLimit > 0. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_SECCOMP_PROFILES | request_body.container_create.allowed_seccomp_profiles | empty | If non-empty, HostConfig.SecurityOpt seccomp=<profile> must be in the list. Include default to allow the implicit Docker default. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_DENY_UNCONFINED_SECCOMP | request_body.container_create.deny_unconfined_seccomp | false | When no allowlist is set, deny seccomp=unconfined specifically. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_APPARMOR_PROFILES | request_body.container_create.allowed_apparmor_profiles | empty | If non-empty, HostConfig.SecurityOpt apparmor=<profile> must be in the list. Include docker-default to accept the implicit default. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_DENY_UNCONFINED_APPARMOR | request_body.container_create.deny_unconfined_apparmor | false | When no allowlist is set, deny apparmor=unconfined specifically. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_DENY_SELINUX_DISABLE | request_body.container_create.deny_selinux_disable | false | Deny label=disable / label:disable SecurityOpt (turns off SELinux confinement). |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_DENY_SELINUX_LABEL_OVERRIDE | request_body.container_create.deny_selinux_label_override | false | Deny label=user:/role:/type:/level: SELinux context overrides. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_DENY_UNCONFINED_SYSTEM_PATHS | request_body.container_create.deny_unconfined_system_paths | false | Deny systempaths=unconfined and explicit empty MaskedPaths/ReadonlyPaths arrays (the CLI-translated direct-API form). |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_USERNS | request_body.container_create.allow_host_userns | false | Allow HostConfig.UsernsMode=host and ns:<path>. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_RESTRICT_NAMESPACE_SHARING | request_body.container_create.restrict_namespace_sharing | false | Gate container:<ref> joins on NetworkMode/PidMode/IpcMode/UTSMode/UsernsMode/CgroupnsMode against allowed_namespace_sharing_containers, and refuse ns:<path> on them. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_NAMESPACE_SHARING_CONTAINERS | request_body.container_create.allowed_namespace_sharing_containers | empty | Comma-separated container IDs/names permitted as container:<ref> targets when restrict_namespace_sharing: true. Empty denies all. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_DENY_NAMESPACE_PATH_MODE | request_body.container_create.deny_namespace_path_mode | false | Deny NetworkMode values with an ns: prefix even when allow_host_network is on. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_SYSCTLS | request_body.container_create.allow_sysctls | false | Allow a non-empty HostConfig.Sysctls map (kernel parameter tuning). |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_RUNTIMES | request_body.container_create.allowed_runtimes | empty | Comma-separated non-empty HostConfig.Runtime values to allow (e.g. runsc,kata-runtime). An empty/unset runtime field selects the daemon default and is always permitted; only an explicit alternate runtime needs an entry. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRED_LABELS | request_body.container_create.required_labels | empty | Config.Labels keys that must be present with a non-empty value. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_IMAGE_TRUST_MODE | request_body.container_create.image_trust.mode | "off" | Cosign signature verification mode: off, warn, or enforce. |
| (YAML-only) | request_body.container_create.image_trust.allowed_signing_keys | empty | List of PEM public keys (ECDSA/RSA/ed25519) trusted to sign images. At least one key or keyless entry required when mode is not off. |
| (YAML-only) | request_body.container_create.image_trust.allowed_keyless | empty | List of Fulcio keyless identities; each entry has issuer (exact OIDC URL) and subject_pattern (regex matched against the cert SAN). |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_IMAGE_TRUST_REQUIRE_REKOR_INCLUSION | request_body.container_create.image_trust.require_rekor_inclusion | true | Require a Rekor transparency log entry for keyless bundles. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_IMAGE_TRUST_VERIFY_TIMEOUT | request_body.container_create.image_trust.verify_timeout | 10s | End-to-end image-trust deadline, including registry fetching, with cooperative checks around local crypto. Must be a positive Go duration string (e.g. 5s, 30s). |
libpod_container_create
Podman's native POST /libpod/containers/create counterpart to container_create above — see Request Body Policy Reference for the full field-by-field mapping to libpod's SpecGenerator body shape.
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_PRIVILEGED | request_body.libpod_container_create.allow_privileged | false | Allow top-level privileged=true. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_HOST_NETWORK | request_body.libpod_container_create.allow_host_network | false | Allow netns.nsmode=host and path. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_HOST_PID | request_body.libpod_container_create.allow_host_pid | false | Allow pidns.nsmode=host and path. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_HOST_IPC | request_body.libpod_container_create.allow_host_ipc | false | Allow ipcns.nsmode=host and path. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_HOST_USERNS | request_body.libpod_container_create.allow_host_userns | false | Allow userns.nsmode=host and path. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_BIND_MOUNTS | request_body.libpod_container_create.allowed_bind_mounts | empty | Comma-separated host-path prefixes allowed as mounts[] bind sources. Named volumes[] entries always allowed. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_ALL_DEVICES | request_body.libpod_container_create.allow_all_devices | false | Allow any devices[] host path. Prefer the allowlist below instead. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_DEVICES | request_body.libpod_container_create.allowed_devices | empty | Comma-separated host device paths allowed for devices[]. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_RESTRICT_NAMESPACE_SHARING | request_body.libpod_container_create.restrict_namespace_sharing | false | Gate {"nsmode":"container","value":"<ref>"} joins on netns/pidns/ipcns/userns/utsns/cgroupns against allowed_namespace_sharing_containers, and refuse nsmode=path on them. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_NAMESPACE_SHARING_CONTAINERS | request_body.libpod_container_create.allowed_namespace_sharing_containers | empty | Comma-separated container IDs/names permitted as container:<ref> targets when restrict_namespace_sharing: true. Empty denies all. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_ALL_CAPABILITIES | request_body.libpod_container_create.allow_all_capabilities | false | Skip the cap_add allowlist. With this off, only entries in allowed_capabilities are permitted. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_CAPABILITIES | request_body.libpod_container_create.allowed_capabilities | empty | cap_add allowlist (case-insensitive, optional CAP_ prefix). |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_SECCOMP_PROFILES | request_body.libpod_container_create.allowed_seccomp_profiles | empty | If non-empty, seccomp_profile_path must be in the list. Include default to allow the implicit/empty default. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_DENY_UNCONFINED_SECCOMP | request_body.libpod_container_create.deny_unconfined_seccomp | false | When no allowlist is set, deny seccomp_profile_path=unconfined specifically. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_APPARMOR_PROFILES | request_body.libpod_container_create.allowed_apparmor_profiles | empty | If non-empty, apparmor_profile must be in the list. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_DENY_UNCONFINED_APPARMOR | request_body.libpod_container_create.deny_unconfined_apparmor | false | When no allowlist is set, deny apparmor_profile=unconfined specifically. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_DENY_SELINUX_DISABLE | request_body.libpod_container_create.deny_selinux_disable | false | Deny selinux_opts containing disable (turns off SELinux confinement). |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_NON_ROOT_USER | request_body.libpod_container_create.require_non_root_user | false | Require the user field to be a non-zero UID or non-root username. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_READONLY_ROOTFS | request_body.libpod_container_create.require_readonly_rootfs | false | Require read_only_filesystem=true. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_MEMORY_LIMIT | request_body.libpod_container_create.require_memory_limit | false | Require resource_limits.memory.limit > 0. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_CPU_LIMIT | request_body.libpod_container_create.require_cpu_limit | false | Require any of resource_limits.cpu.{quota,period,shares} > 0. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_CPU_LIMIT_HARD | request_body.libpod_container_create.require_cpu_limit_hard | false | Require resource_limits.cpu.quota specifically; cpu.shares (relative priority) does not satisfy this stricter, independent check. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_PIDS_LIMIT | request_body.libpod_container_create.require_pids_limit | false | Require resource_limits.pids.limit > 0. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_SYSCTLS | request_body.libpod_container_create.allow_sysctls | false | Allow a non-empty sysctl map (kernel parameter tuning). |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_SYSTEMD_MODE | request_body.libpod_container_create.allow_systemd_mode | false | Allow a systemd value other than the explicit "false". SpecGenerator itself defaults systemd to "true" even when --systemd is never passed by the client, so leaving this off denies the common case. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_CUSTOM_ID_MAPPINGS | request_body.libpod_container_create.allow_custom_id_mappings | false | Allow a non-default idmappings.uidMap/gidMap or --userns=auto. Independent of allow_host_userns, which only covers userns.nsmode=host. |
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_IMAGE_TRUST_* | request_body.libpod_container_create.image_trust.* | see container_create.image_trust above | Cosign signature verification on the image field. Identical semantics and sub-fields to container_create.image_trust. The three scalar sub-fields take env vars; allowed_signing_keys and allowed_keyless are YAML-only. |
Other request-body inspectors
Comma-separated list values (ALLOWED_*, ALLOWED_BIND_MOUNTS, etc.) accept
multiple entries via the env var; e.g.
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOWED_REGISTRIES=ghcr.io,quay.io.
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_REQUEST_BODY_EXEC_ALLOW_PRIVILEGED | request_body.exec.allow_privileged | false | Allow privileged exec sessions. |
SOCKGUARD_REQUEST_BODY_EXEC_ALLOW_ROOT_USER | request_body.exec.allow_root_user | false | Allow exec sessions running as root. |
SOCKGUARD_REQUEST_BODY_EXEC_ALLOWED_ENV_VARS | request_body.exec.allowed_env_vars | empty | Comma-separated allowlist of exec Env variable names (name-only match; empty = no restriction). |
SOCKGUARD_REQUEST_BODY_EXEC_DENIED_ENV_VARS | request_body.exec.denied_env_vars | empty | Comma-separated denylist of exec Env variable names; wins over the allowlist (name-only match; empty = nothing blocked). |
SOCKGUARD_REQUEST_BODY_EXEC_ALLOWED_ENV_VALUES | request_body.exec.allowed_env_values | empty | Comma-separated exact NAME=VALUE entries for selected exec variables; values are compared but never logged. |
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOW_IMPORTS | request_body.image_pull.allow_imports | false | Allow Docker fromSrc and native Podman image imports. |
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOW_ALL_REGISTRIES | request_body.image_pull.allow_all_registries | false | Allow image pulls from any registry. Prefer ALLOWED_REGISTRIES instead. |
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOW_OFFICIAL | request_body.image_pull.allow_official | true | Allow Docker Hub official images (single-segment names). |
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOWED_REGISTRIES | request_body.image_pull.allowed_registries | empty | Comma-separated allowed pull registries. |
SOCKGUARD_REQUEST_BODY_BUILD_ALLOW_REMOTE_CONTEXT | request_body.build.allow_remote_context | false | Allow a primary remote build context and Podman URL/image additional contexts. |
SOCKGUARD_REQUEST_BODY_BUILD_ALLOW_HOST_NETWORK | request_body.build.allow_host_network | false | Allow POST /build with networkmode=host. |
SOCKGUARD_REQUEST_BODY_BUILD_ALLOW_RUN_INSTRUCTIONS | request_body.build.allow_run_instructions | false | Allow Dockerfiles containing RUN instructions. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_ALLOW_INFO | request_body.buildkit.control.allow_info | false | Allow the Control/Info RPC (buildkit version metadata; the response is filtered before relay). |
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_ALLOW_LIST_WORKERS | request_body.buildkit.control.allow_list_workers | false | Allow the Control/ListWorkers RPC (worker metadata; the response is filtered to worker id, platforms and buildkit version before relay). |
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_ALLOW_STATUS | request_body.buildkit.control.allow_status | false | Allow the mediated Control/Status RPC — admitted only for a ref this same client/profile actually Solved. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOW | request_body.buildkit.control.solve.allow | false | Allow the mediated Control/Solve RPC — the actual build request. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOWED_CACHE_IMPORT_TYPES | request_body.buildkit.control.solve.allowed_cache_import_types | empty | Comma-separated Cache.Imports[].Type allowlist (e.g. registry,local,gha). Empty = deny all. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOWED_CACHE_EXPORT_TYPES | request_body.buildkit.control.solve.allowed_cache_export_types | empty | Comma-separated Cache.Exports[].Type allowlist. Empty = deny all. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOWED_CACHE_REGISTRIES | request_body.buildkit.control.solve.allowed_cache_registries | empty | Comma-separated registry-host allowlist for a registry-typed cache import/export entry's ref attribute. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOWED_EXPORTERS | request_body.buildkit.control.solve.allowed_exporters | empty | Comma-separated Exporters[].Type allowlist (e.g. image,oci,local). Empty = deny all. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOWED_EXPORTER_REGISTRIES | request_body.buildkit.control.solve.allowed_exporter_registries | empty | Comma-separated registry-host allowlist consulted only when an image-typed exporter's push attr parses true; a plain local tag/load never checks this list. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_HEALTH | request_body.buildkit.session.health | false | Allow the passthrough grpc.health.v1.Health/{Check,Watch} RPCs. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_AUTH_ALLOW | request_body.buildkit.session.auth.allow | false | Allow moby.filesync.v1.Auth's Credentials/FetchToken/GetTokenAuthority/VerifyTokenAuthority RPCs, subject to the three allowlists below. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_AUTH_ALLOWED_REGISTRIES | request_body.buildkit.session.auth.allowed_registries | empty | Comma-separated exact registry-host allowlist every Auth RPC's Host must match. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_AUTH_ALLOWED_REALMS | request_body.buildkit.session.auth.allowed_realms | empty | Comma-separated exact FetchToken Realm allowlist. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_AUTH_ALLOWED_SCOPES | request_body.buildkit.session.auth.allowed_scopes | empty | Comma-separated exact FetchToken scope allowlist; every requested scope must be a member. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_SECRETS_ALLOW | request_body.buildkit.session.secrets.allow | false | Allow moby.buildkit.secrets.v1.Secrets/GetSecret, subject to allowed_ids. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_SECRETS_ALLOWED_IDS | request_body.buildkit.session.secrets.allowed_ids | empty | Comma-separated exact secret-ID allowlist. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_SSH_ALLOW | request_body.buildkit.session.ssh.allow | false | Allow moby.sshforward.v1.SSH's CheckAgent/ForwardAgent RPCs, subject to allowed_ids. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_SSH_ALLOWED_IDS | request_body.buildkit.session.ssh.allowed_ids | empty | Comma-separated exact SSH agent-ID allowlist. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_FILE_SYNC_ALLOW | request_body.buildkit.session.file_sync.allow | false | Allow moby.filesync.v1.FileSync/DiffCopy — required to sync the Dockerfile/build context to buildkitd at all. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_FILE_SYNC_MAX_FILES | request_body.buildkit.session.file_sync.max_files | 0 | Cap the number of files/dirs a single FileSync/DiffCopy stream may declare. 0 uses buildkitproxy.Limits.MaxFileSyncFiles. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_FILE_SYNC_MAX_TOTAL_BYTES | request_body.buildkit.session.file_sync.max_total_bytes | 0 | Cap cumulative bytes relayed across a FileSync/DiffCopy stream. 0 uses buildkitproxy.Limits.MaxFileSyncTotalBytes. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_FILE_SYNC_MAX_PATH_LENGTH | request_body.buildkit.session.file_sync.max_path_length | 0 | Cap the byte length of any single file path or symlink target. 0 uses buildkitproxy.Limits.MaxFileSyncPathLength. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_FILE_SYNC_MAX_FILE_BYTES | request_body.buildkit.session.file_sync.max_file_bytes | 0 | Cap the bytes belonging to any one file, including the Dockerfile hold-and-inspect buffer. 0 uses buildkitproxy.Limits.MaxFileSyncFileBytes. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_FILE_SEND_ALLOW | request_body.buildkit.session.file_send.allow | false | Allow moby.filesync.v1.FileSend/DiffCopy (a local/tar exporter's output back to the client). Content is capped, never decoded. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_FILE_SEND_MAX_BYTES | request_body.buildkit.session.file_send.max_bytes | 0 | Cap cumulative bytes relayed for a FileSend/DiffCopy stream. 0 uses buildkitproxy.Limits.MaxFileSendBytes. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_UPLOAD_ALLOW | request_body.buildkit.session.upload.allow | false | Allow moby.upload.v1.Upload/Pull (a remote/stdin build context), bound to a one-use token from an admitted Solve. |
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_UPLOAD_MAX_BYTES | request_body.buildkit.session.upload.max_bytes | 0 | Cap cumulative bytes relayed for an Upload/Pull stream. 0 uses buildkitproxy.Limits.MaxUploadBytes. |
SOCKGUARD_REQUEST_BODY_CONTAINER_UPDATE_ALLOW_RESTART_POLICY | request_body.container_update.allow_restart_policy | false | Allow POST /containers/*/update to change the restart policy. |
SOCKGUARD_REQUEST_BODY_CONTAINER_UPDATE_REQUIRE_MEMORY_LIMIT | request_body.container_update.require_memory_limit | false | Require an effective HostConfig.Memory > 0 after the update merges with current state. Enforced only when allow_resource_updates: true. |
SOCKGUARD_REQUEST_BODY_CONTAINER_UPDATE_REQUIRE_CPU_LIMIT | request_body.container_update.require_cpu_limit | false | Require an effective CPU limit (NanoCpus, CpuQuota, CpuPeriod, or CpuShares > 0) after merge. Enforced only when allow_resource_updates: true. |
SOCKGUARD_REQUEST_BODY_CONTAINER_UPDATE_REQUIRE_CPU_LIMIT_HARD | request_body.container_update.require_cpu_limit_hard | false | Require an effective NanoCpus or CpuQuota specifically; CpuShares alone does not satisfy this stricter check. Enforced only when allow_resource_updates: true. |
SOCKGUARD_REQUEST_BODY_CONTAINER_UPDATE_REQUIRE_PIDS_LIMIT | request_body.container_update.require_pids_limit | false | Require an effective HostConfig.PidsLimit > 0 after merge; a submitted PidsLimit of 0 or -1 is an explicit clear, not a no-op. Enforced only when allow_resource_updates: true. |
SOCKGUARD_REQUEST_BODY_CONTAINER_REMOVE_ALLOW_FORCE | request_body.container_remove.allow_force | false | Allow DELETE /containers/** and DELETE /libpod/containers/** to force-kill a running container before removal, DELETE /libpod/pods/* to send force, and kube down. |
SOCKGUARD_REQUEST_BODY_CONTAINER_REMOVE_ALLOW_REMOVE_VOLUMES | request_body.container_remove.allow_remove_volumes | false | Allow DELETE /containers/** and DELETE /libpod/containers/** to delete attached anonymous volumes, including libpod depend, and allow DELETE /libpod/pods/* and kube down. |
SOCKGUARD_REQUEST_BODY_CONTAINER_REMOVE_ALLOW_REMOVE_LINKS | request_body.container_remove.allow_remove_links | false | Allow Docker's legacy container-link removal mode, including slash-bearing link names. |
SOCKGUARD_REQUEST_BODY_CONTAINER_ARCHIVE_ALLOWED_PATHS | request_body.container_archive.allowed_paths | empty | Comma-separated container paths allowed for PUT /containers/*/archive and PUT /libpod/containers/*/archive. |
SOCKGUARD_REQUEST_BODY_IMAGE_LOAD_ALLOW_UNTAGGED | request_body.image_load.allow_untagged | false | Allow Docker or OCI load archives containing untagged images. |
SOCKGUARD_REQUEST_BODY_VOLUME_ALLOW_DRIVER_OPTS | request_body.volume.allow_driver_opts | false | Allow POST /volumes/create with custom DriverOpts. Local-driver options that request a bind, or that name a /dev device under a real filesystem type, still have that device checked against request_body.container_create.allowed_bind_mounts. |
SOCKGUARD_REQUEST_BODY_VOLUME_ALLOW_CLUSTER_VOLUME_SECRETS | request_body.volume.allow_cluster_volume_secrets | false | Allow PUT /volumes/{name} to rewrite Spec.Secrets, the Swarm secrets the daemon hands to the CSI plugin. Not covered by allow_cluster_volume_updates. |
SOCKGUARD_REQUEST_BODY_VOLUME_ALLOW_CLUSTER_VOLUME_UPDATES | request_body.volume.allow_cluster_volume_updates | false | Allow PUT /volumes/{name} to change the rest of Spec: Availability, Group, AccessMode, CapacityRange, AccessibilityRequirements. |
SOCKGUARD_REQUEST_BODY_NETWORK_ALLOW_ENDPOINT_CONFIG | request_body.network.allow_endpoint_config | false | Allow endpoint static IP/MAC/links/driver options/GwPriority on POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig. Aliases are always allowed regardless of this flag. Mutually exclusive with an explicit endpoint_config block below (#186) — setting both is a config validation error. |
SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_ALLOW_STATIC_ADDRESSING | request_body.network.endpoint_config.allow_static_addressing | false | Narrows allow_endpoint_config (#186): allow IPAMConfig.IPv4Address/IPv6Address and the deprecated top-level Gateway/IPAddress/IPPrefixLen/IPv6Gateway/GlobalIPv6Address/GlobalIPv6PrefixLen fields. Only consulted when allow_endpoint_config is false. |
SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_ALLOW_LINK_LOCAL_IPS | request_body.network.endpoint_config.allow_link_local_ips | false | Allow IPAMConfig.LinkLocalIPs, independent of allow_static_addressing. Only consulted when allow_endpoint_config is false. |
SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_ALLOW_MAC_PINNING | request_body.network.endpoint_config.allow_mac_pinning | false | Allow MacAddress — shared with container_create's deprecated top-level MacAddress field. Only consulted when allow_endpoint_config is false. |
SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_ALLOW_GW_PRIORITY | request_body.network.endpoint_config.allow_gw_priority | false | Allow GwPriority (Engine API 1.55+). Only consulted when allow_endpoint_config is false. |
SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_ALLOW_ALIASES | request_body.network.endpoint_config.allow_aliases | true | Allow Aliases. Defaults true to reproduce allow_endpoint_config's historical unconditional-allow behavior for Aliases; set false to deny them under the granular form. Only consulted when allow_endpoint_config is false. |
SOCKGUARD_REQUEST_BODY_NETWORK_ALLOW_DISABLE_IPV4 | request_body.network.allow_disable_ipv4 | false | Allow POST /networks/create with an explicit EnableIPv4: false (Engine API 1.48+; the field defaults to true when absent). |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_TMPFS_PRIVILEGED_OPTIONS | request_body.container_create.allow_tmpfs_privileged_options | false | Allow HostConfig.Mounts[].TmpfsOptions.Options entries that re-enable exec, dev, or suid on a tmpfs mount. |
SOCKGUARD_REQUEST_BODY_SECRET_ALLOW_TEMPLATE_DRIVERS | request_body.secret.allow_template_drivers | false | Allow POST /secrets/create with a template driver. |
SOCKGUARD_REQUEST_BODY_CONFIG_ALLOW_TEMPLATE_DRIVERS | request_body.config.allow_template_drivers | false | Allow POST /configs/create with a template driver. |
SOCKGUARD_REQUEST_BODY_SERVICE_ALLOWED_BIND_MOUNTS | request_body.service.allowed_bind_mounts | empty | Comma-separated host-path prefixes allowed as bind sources for POST /services/create. |
SOCKGUARD_REQUEST_BODY_SERVICE_REQUIRE_NON_ROOT_USER | request_body.service.require_non_root_user | false | Require a non-root ContainerSpec.User on swarm service create/update. |
SOCKGUARD_REQUEST_BODY_SERVICE_REQUIRE_NO_NEW_PRIVILEGES | request_body.service.require_no_new_privileges | false | Require ContainerSpec.Privileges.NoNewPrivileges: true on service writes. |
SOCKGUARD_REQUEST_BODY_SERVICE_REQUIRE_READONLY_ROOTFS | request_body.service.require_readonly_rootfs | false | Require ContainerSpec.ReadOnly: true on service writes. |
SOCKGUARD_REQUEST_BODY_SERVICE_REQUIRE_DROP_ALL_CAPABILITIES | request_body.service.require_drop_all_capabilities | false | Require ContainerSpec.CapabilityDrop to include ALL on service writes. |
SOCKGUARD_REQUEST_BODY_SERVICE_DENY_UNCONFINED_SECCOMP | request_body.service.deny_unconfined_seccomp | false | Deny ContainerSpec.Privileges.Seccomp.Mode: "unconfined" on service writes. |
SOCKGUARD_REQUEST_BODY_SERVICE_DENY_CUSTOM_SECCOMP_PROFILES | request_body.service.deny_custom_seccomp_profiles | false | Deny Seccomp.Mode: "custom", or a Profile blob with no Mode (fail-closed). |
SOCKGUARD_REQUEST_BODY_SERVICE_DENY_UNCONFINED_APPARMOR | request_body.service.deny_unconfined_apparmor | false | Deny ContainerSpec.Privileges.AppArmor.Mode: "disabled" on service writes. |
SOCKGUARD_REQUEST_BODY_SERVICE_DENY_SELINUX_DISABLE | request_body.service.deny_selinux_disable | false | Deny ContainerSpec.Privileges.SELinuxContext.Disable: true on service writes. |
SOCKGUARD_REQUEST_BODY_SERVICE_DENY_SELINUX_LABEL_OVERRIDE | request_body.service.deny_selinux_label_override | false | Deny SELinuxContext.{User,Role,Type,Level} overrides on service writes. |
SOCKGUARD_REQUEST_BODY_SERVICE_REQUIRE_CPU_LIMIT | request_body.service.require_cpu_limit | false | Require Resources.Limits.NanoCPUs > 0 on service create/update, the stored PreviousSpec on manual rollback, and the current Spec on automatic rollback. |
SOCKGUARD_REQUEST_BODY_SERVICE_REQUIRE_CPU_LIMIT_HARD | request_body.service.require_cpu_limit_hard | false | Same evaluation surface as require_cpu_limit; kept as a separate flag for parity with container_create/container_update, since Swarm has no CpuShares/CpuPeriod-style soft limit to distinguish from a hard cap. |
SOCKGUARD_REQUEST_BODY_SWARM_ALLOWED_JOIN_REMOTE_ADDRS | request_body.swarm.allowed_join_remote_addrs | empty | Comma-separated host:port join targets allowed for POST /swarm/join. |
SOCKGUARD_REQUEST_BODY_NODE_ALLOWED_LABEL_KEYS | request_body.node.allowed_label_keys | empty | Comma-separated label keys allowed in POST /nodes/*/update. |
SOCKGUARD_REQUEST_BODY_PLUGIN_ALLOWED_SET_ENV_PREFIXES | request_body.plugin.allowed_set_env_prefixes | empty | Comma-separated KEY= prefixes allowed for POST /plugins/*/set. |
SOCKGUARD_REQUEST_BODY_LIBPOD_POD_CREATE_ALLOW_HOST_NETWORK | request_body.libpod_pod_create.allow_host_network | false | Allow POST /libpod/pods/create with a pod-level host network namespace (netns with nsmode host or path). |
SOCKGUARD_REQUEST_BODY_LIBPOD_POD_CREATE_ALLOW_SHARED_PID_NAMESPACE | request_body.libpod_pod_create.allow_shared_pid_namespace | false | Allow "pid" in a pod's shared_namespaces. |
SOCKGUARD_REQUEST_BODY_LIBPOD_POD_CREATE_ALLOWED_INFRA_IMAGE_REGISTRIES | request_body.libpod_pod_create.allowed_infra_image_registries | empty | Comma-separated allowlist of registries an explicit infra_image may come from. |
SOCKGUARD_REQUEST_BODY_LIBPOD_VOLUME_ALLOW_CUSTOM_DRIVERS | request_body.libpod_volume.allow_custom_drivers | false | Allow POST /libpod/volumes/create with a Driver other than local. Reuses the volume group's fields under the libpod key. |
SOCKGUARD_REQUEST_BODY_LIBPOD_VOLUME_ALLOW_DRIVER_OPTS | request_body.libpod_volume.allow_driver_opts | false | Allow POST /libpod/volumes/create with a non-empty Options map (libpod's driver-opts field; the wire key is Options, not DriverOpts). Local-driver options that request a bind, or that name a /dev device under a real filesystem type, still have that device checked against request_body.libpod_container_create.allowed_bind_mounts. |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ALLOW_CUSTOM_DRIVERS | request_body.libpod_network.allow_custom_drivers | false | Allow POST /libpod/networks/create with a non-builtin driver. Reuses the network group's fields under the libpod key. |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ALLOW_DRIVER_OPTIONS | request_body.libpod_network.allow_driver_options | false | Allow POST /libpod/networks/create with a non-empty options map or a non-empty top-level network_interface (the macvlan/ipvlan parent NIC, or the bridge interface name — Podman's equivalent of a driver-option parent, carried as its own field on the wire instead of nested under options). |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ALLOW_CUSTOM_IPAM_CONFIG | request_body.libpod_network.allow_custom_ipam_config | false | Allow POST /libpod/networks/create with custom static subnets. |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ALLOW_IPAM_OPTIONS | request_body.libpod_network.allow_ipam_options | false | Allow POST /libpod/networks/create with a non-empty ipam_options map. The network group's allow_swarm_scope/allow_ingress/allow_attachable/allow_config_only/allow_config_from/allow_custom_ipam_drivers/allow_disable_ipv4 fields have no libpod analog and are not consulted (libpod predates and is independent of Docker's swarm mode); allow_endpoint_config/endpoint_config/allow_disconnect_force are consulted, but on POST /libpod/networks/{name}/connect and .../disconnect rather than on create. |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ALLOW_ENDPOINT_CONFIG | request_body.libpod_network.allow_endpoint_config | false | Allow every native connect endpoint option (static_ips, static_mac, aliases, and options) on POST /libpod/networks/{name}/connect. Mutually exclusive with an explicit request_body.libpod_network.endpoint_config block. |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ENDPOINT_CONFIG_ALLOW_STATIC_ADDRESSING | request_body.libpod_network.endpoint_config.allow_static_addressing | false | Allow native connect's static_ips field when allow_endpoint_config is false. |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ENDPOINT_CONFIG_ALLOW_LINK_LOCAL_IPS | request_body.libpod_network.endpoint_config.allow_link_local_ips | false | Not consulted on native libpod connect because PerNetworkOptions has no link-local-IP field; retained as part of the reused network config shape. |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ENDPOINT_CONFIG_ALLOW_MAC_PINNING | request_body.libpod_network.endpoint_config.allow_mac_pinning | false | Allow native connect's static_mac field when allow_endpoint_config is false. |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ENDPOINT_CONFIG_ALLOW_GW_PRIORITY | request_body.libpod_network.endpoint_config.allow_gw_priority | false | Not consulted on native libpod connect because PerNetworkOptions has no gateway-priority field; retained as part of the reused network config shape. |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ENDPOINT_CONFIG_ALLOW_ALIASES | request_body.libpod_network.endpoint_config.allow_aliases | true | Allow native connect's aliases field. Set false to deny aliases under the granular form; only consulted when allow_endpoint_config is false. |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ALLOW_DISCONNECT_FORCE | request_body.libpod_network.allow_disconnect_force | false | Allow Force: true on POST /libpod/networks/{name}/disconnect. |
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ALLOW_DNS_SERVERS | request_body.libpod_network.allow_dns_servers | false | Allow a request to set or remove a network's own DNS resolvers: network_dns_servers on POST /libpod/networks/create and adddnsservers/removednsservers on POST /libpod/networks/{name}/update. Libpod-only — Docker networks have no per-network resolver field and the Engine API has no network-update route — so the network group never consults it. Denies by default because repointing an existing network's resolver changes what names resolve to inside every container already attached to it. |
SOCKGUARD_REQUEST_BODY_LIBPOD_SECRET_ALLOW_CUSTOM_DRIVERS | request_body.libpod_secret.allow_custom_drivers | false | Allow POST /libpod/secrets/create with a driver query parameter other than file, or with a driveropts that names any option. libpod reads both from the query string, not a JSON body. The secret group's allow_template_drivers has no libpod analog (no template-driver concept) and is not consulted by this inspector. |
Clients and ownership
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_CLIENTS_ALLOWED_CIDRS | clients.allowed_cidrs | empty | Comma-separated CIDR allowlist applied to all client transports including /metrics. |
SOCKGUARD_CLIENTS_CONTAINER_LABELS_ENABLED | clients.container_labels.enabled | false | Resolve client containers' labels via the upstream Docker socket for label-driven rules. |
SOCKGUARD_CLIENTS_CONTAINER_LABELS_LABEL_PREFIX | clients.container_labels.label_prefix | com.sockguard.allow. | Label-key prefix Sockguard considers when matching container-label rules. |
SOCKGUARD_OWNERSHIP_OWNER | ownership.owner | empty | Owner identifier stamped onto every audit event and used for ownership-scoped rules. |
SOCKGUARD_OWNERSHIP_LABEL_KEY | ownership.label_key | com.sockguard.owner | Label key Sockguard reads from container/image metadata to determine ownership. |
SOCKGUARD_OWNERSHIP_ALLOW_UNOWNED_IMAGES | ownership.allow_unowned_images | true | Allow operations on images that lack the ownership label. |
SOCKGUARD_OWNERSHIP_ALLOW_CROSS_OWNER_NAMESPACE_SHARING | ownership.allow_cross_owner_namespace_sharing | false | Allow container:<ref> namespace-sharing targets owned by a different owner (default denies them). |
response.deny_verbosity controls how much metadata Sockguard includes in its own 403 JSON deny responses:
minimal(default): returns only the genericmessage. Never echoes the request method, path, or matched rule reason.verbose: returnsmessage,method,path(with/secrets/*and/swarm/unlockkeypaths redacted), andreason. Intended for rule authoring and dev work only — never a production default because it can leak request details to denied callers.
response.redact_container_env, response.redact_mount_paths, response.redact_network_topology, response.redact_sensitive_data, and response.redact_host_topology control response redaction for known protected Docker JSON response shapes. The redaction layer runs on successful body-bearing 2xx responses across request methods, not only GET 200, and fails closed with a generic 502 if a protected successful response cannot be parsed or sanitized safely. Non-success responses, HEAD responses, no-body statuses, non-protected paths, and streaming endpoints (logs, attach, events) pass through untouched. Streaming-style endpoints are gated by request-side rules and the read-side exfiltration guardrail instead. The toggles:
redact_container_env(defaulttrue): replaces workload env arrays with empty arrays on container, service, task, and plugin reads.redact_mount_paths(defaulttrue): redacts mount and host-device source paths on container, volume, task, service, plugin, and/system/dfreads.redact_network_topology(defaulttrue): redacts container, network, task, service, node, swarm,/info, and/system/dftopology details such as network IDs, attached addresses, remote managers, and node reachability addresses.redact_sensitive_data(defaulttrue): redacts config payload material, service secret/config references, swarm join/unlock and CA material, and node/swarm TLS metadata.redact_host_topology(defaultfalse, opt-in): redactsGET /infocontainer-runtime plumbing fields —Containerd,FirewallBackend,DiscoveredDevices, andNRI— independent of whether the daemon is in Swarm mode.
response.allow_attestation_statements (default false) is a narrower, unconditional gate rather than a redaction toggle: it controls whether GET /images/{name}/attestations requests that ask for the statement are denied outright rather than redacted, since the field being gated is the entire attestation statement payload, not a specific key within it. Query booleans match daemon semantics: an empty value, 0, no, false, and none are false after trimming and case-folding; every other non-empty value, including 1, yes, and true, requests the statement. The gate applies even when every redaction toggle above is left at its default.
Security note — the redaction toggles do not cover stream bodies. These toggles operate only on structured JSON Docker responses. Hijacked / streaming endpoints (
GET /containers/*/logs,POST /containers/*/attach,GET /services/*/logs,GET /events, exec attach, image-build progress) are forwarded byte-for-byte; secrets a workload writes to its own stdout will reach an allowed caller. Gate those paths by rule, not by redaction. See the Known Limitations section in the Security guide for the full caveat.
Tecnativa Compatibility
For drop-in migration, sockguard accepts Tecnativa-style env vars:
CONTAINERS=1 # Allow /containers/** (GET/HEAD when POST=0)
IMAGES=1 # Allow /images/** (GET/HEAD when POST=0)
SERVICES=1 # Allow /services/** (GET/HEAD when POST=0)
SECRETS=0 # Deny /secrets/**
EVENTS=1 # Allow /events (default)
PING=1 # Allow /_ping (default)
VERSION=1 # Allow /version (default)
POST=0 # Read-only mode for section vars (default)
SOCKET_PATH=/var/run/docker.sock
LOG_LEVEL=warningCompat env vars only generate rules while the effective ruleset still matches Sockguard's built-in defaults. 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; a rules: block that differs from the default wins outright and no compat rules are generated.
Broad compat reads such as CONTAINERS=1, IMAGES=1, or POST=0 with section-wide GET access require SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION=true if you intentionally want process-list, raw archive/export, or log/attach streaming parity. Safer YAML configs should allow only the list/inspect endpoints a client needs.
CONTAINERS=1 with POST=1 preserves the compatibility proxy's full
container-write surface, including force removal, anonymous-volume removal,
and slash-bearing legacy link removal. Move the delete rule to YAML and
configure request_body.container_remove to narrow those controls.
Signed-policy mode cannot safely apply unsigned rule generation after verifying
the candidate. When --policy-bundle-trust-config is set, the presence of any
section variable, POST, GRPC/SESSION, or granular ALLOW_* variable is a
startup error, even when the variable's value is false. A variable introduced
after startup makes the next reload fail with reject_compat; the running
policy stays active. Convert compatibility rules to YAML before signing.
Granular Operations (LinuxServer compatible)
Granular container-write flags still work even when POST=0:
ALLOW_START=1 # Allow POST /containers/{id}/start
ALLOW_STOP=1 # Allow POST /containers/{id}/stop
ALLOW_RESTARTS=1 # Allow POST /containers/{id}/stop|restart|kill
ALLOW_CREATE=0 # Deny POST /containers/create (default)
ALLOW_EXEC=0 # Deny POST /containers/{id}/exec (default)
ALLOW_PAUSE=0 # Deny POST /containers/{id}/pause (default)
ALLOW_UNPAUSE=0 # Deny POST /containers/{id}/unpause (default)
ALLOW_KILL=0 # Deny POST /containers/{id}/kill (default; ALLOW_RESTARTS also grants it)
ALLOW_DELETE=0 # Deny DELETE /containers/{id} (default)
ALLOW_PRUNE=0 # Deny POST /containers/prune (default)Sockguard also accepts the legacy singular ALLOW_RESTART=1 alias, but ALLOW_RESTARTS is the upstream Tecnativa/LinuxServer name.
ALLOW_DELETE=1 independently preserves the same historical behavior by
allowing force removal, anonymous-volume removal, and slash-bearing legacy link
removal as well as a bare delete. Move the rule to YAML and configure
request_body.container_remove when you want those three controls separated.
Precedence
CLI flags > environment variables > config file > defaults
sockguard serve also registers five flags that override specific config keys directly, independent of the SOCKGUARD_* env var precedence above:
| Flag | Overrides |
|---|---|
--listen-socket | listen.socket |
--upstream-socket | upstream.socket |
--log-level | log.level |
--log-format | log.format |
--deny-verbosity | response.deny_verbosity (verbose or minimal) |
Each only takes effect when explicitly set on the command line; an unset flag leaves the config-file/env-var value in place.
Getting Started
Install sockguard with Docker Compose, Docker Run, Homebrew, or a release binary, and point your apps at the proxy socket.
Remote Upstreams & Failover
Connect sockguard to a remote Docker daemon over TCP+mTLS, or configure two endpoints for active/passive HA failover with automatic health probing.