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.

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

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

  # 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 GET /images/{name}/attestations?statement=true unless explicitly allowed

request_body:
  container_create:
    allow_privileged: false      # deny HostConfig.Privileged=true (default)
    allow_host_network: false    # deny HostConfig.NetworkMode=host (default)
    allow_host_pid: false        # deny HostConfig.PidMode=host (default)
    allow_host_ipc: false        # deny HostConfig.IpcMode=host (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 (default)
    allow_host_cgroupns: false            # deny HostConfig.CgroupnsMode=host (default)
    restrict_namespace_sharing: false     # gate container:<ref> NetworkMode/PidMode/IpcMode/UsernsMode joins (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> (raw host namespace-file attachment)
    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                 # per-verification network timeout (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 (default)
    allow_host_pid: false        # deny pidns.nsmode=host (default)
    allow_host_ipc: false        # deny ipcns.nsmode=host (default)
    allow_host_userns: false     # deny userns.nsmode=host (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 (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          # passthrough Control/Info (worker/version metadata only)
      allow_list_workers: false  # passthrough Control/ListWorkers (worker capability metadata only)
      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.socket and share a unix socket.
  • Sockguard hardens the unix socket to 0600 owner-only permissions. The published image and fresh named-volume directory use UID/GID 65532, so a non-root consumer must use UID 65532 too; 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_mode must stay 0600; broader modes are rejected at startup.
  • For remote or container-network TCP, configure listen.tls so 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_file still 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: true and listen.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.
  • health.watchdog.enabled starts an active upstream socket monitor that checks Docker every health.watchdog.interval, logs reachable/unreachable state transitions, and lets /health answer 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.enabled adds an opt-in /ready endpoint (default path /ready) that goes one step further than the watchdog: instead of dialing the socket, it issues a real GET /containers/json?limit=1 against the upstream Docker API every health.readiness.interval (per-probe deadline health.readiness.timeout). It returns 200 only when the daemon actually answers, and 503 on 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 /ready and a liveness check at /health. The path must start with / and must not collide with health.path, metrics.path, or admin.path; interval and timeout must be positive durations. The whole health.* block (readiness included) is immutable across hot reload — changing it requires a restart.
  • upstream.endpoints is the ordered list of remote daemon addresses for TCP+mTLS or unix-socket connections. When non-empty, upstream.socket is ignored and sockguard uses the first healthy endpoint in the list (active/passive failover). endpoints and upstream.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 the DOCKER_* drop-in path.
  • upstream.request_timeout bounds the total lifetime of a single proxied request as a Go duration string (e.g. "30s"); it defaults to "60s". ResponseHeaderTimeout only caps the wait for response headers; a daemon that sends headers and then hangs the body — or hangs a heavy read like GET /containers/json — can otherwise pin a request indefinitely. When active, an expired finite request aborts its upstream connection and returns 504 Gateway Timeout (reason_code=upstream_request_timeout), distinct from the 502 an unreachable socket yields. Long-lived endpoints are exempt so the deadline never severs a legitimately long response: event streams, follow/stream logs and stats, image pull/build/push/load, plugin create/pull/push/upgrade, container export, image get, container archive (docker cp, both directions), websocket attach, and the blocking GET /containers/{id}/wait; hijacked attach/exec-start connections already bypass it. Set request_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 the 60s default, while off always works; an explicit request_timeout: "" in YAML does correctly disable it. Unlike health.*, this field is reload-mutable (upstream.socket, upstream.endpoints, and upstream.failover are immutable), so it takes effect on hot reload. Any other value must be a positive duration.
  • metrics.enabled is opt-in and serves Prometheus text metrics at metrics.path on 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 plus clients.allowed_cidrs. Every scrape also exports a sockguard_build_info{version,commit,build_date,go_version} gauge and a sockguard_start_time_seconds gauge for version panels and uptime alerts. When the active watchdog is enabled, metrics also include sockguard_upstream_socket_up and sockguard_upstream_watchdog_checks_total; when the readiness probe is enabled, they also include sockguard_upstream_api_up and sockguard_upstream_readiness_checks_total.
  • admin.enabled is opt-in and exposes a single POST <admin.path> endpoint (default /admin/validate) that runs the same parse + validate + compile pipeline as the offline sockguard validate command 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 at admin.max_request_bytes (default 512 KiB) via http.MaxBytesReader and return 413 on overflow. Non-POST methods return 405 with Allow: POST. The response body is a structured JSON report: {"ok": bool, "rules": int, "profiles": int, "compat_active": bool, "errors": [...]?}. A failing candidate returns 422 with the validator's per-issue error list; a passing candidate returns 200. admin.path must start with / and must not collide with health.path or metrics.path when those endpoints are also enabled.
  • reload.enabled is opt-in and turns on hot reload of policy at runtime. When on, sockguard watches the loaded config file via fsnotify (Linux inotify / macOS kqueue) and also reloads on SIGHUP. A burst of editor events (vim's chmod + write + rename + create save dance, for example) is debounced into a single reload by reload.debounce (default "250ms"). The reload pipeline parses the new file, applies the same Tecnativa-compat env expansion the startup path uses, runs the full validator + rule compiler, and atomically swaps the running handler chain on success. In-flight requests at the moment of the swap complete on the previous chain; new requests immediately route through the new one — no connections dropped. On any failure (file unreadable, YAML malformed, validator rejects, compile error) the running policy is preserved untouched. Hot reload is restricted to a reloadable subset of the config. The immutable fields — listen.*, upstream.socket, log.*, health.*, metrics.*, admin.*, and policy_bundle trust material — are bound at startup to long-lived sockets and goroutines that cannot be replaced from within a running process. A reload that would mutate any of those is refused (the running config stays in place, and the failure is logged with changed_fields=...); operators must restart sockguard to apply listener, upstream socket, log sink, health, metrics, or admin changes. Everything else — rules, clients.*, response.*, request_body.*, ownership.*, insecure_allow_* — is rebuilt and atomically applied on every successful reload. Reload outcomes are surfaced as Prometheus metrics: sockguard_config_reload_total{result="ok|reject_load|reject_validation|reject_immutable|reject_signature"} counter and a sockguard_config_reload_last_success_timestamp_seconds gauge (omitted from scrape output until the first successful reload). SIGHUP semantics change when hot reload is on: previously SIGHUP terminated sockguard (Go's default action for unhandled SIGHUP); with reload.enabled: true it triggers a reload and never terminates the process. Default is reload.enabled: false for backward compatibility — operators who script around SIGHUP-as-shutdown must update their tooling before enabling reload.
  • W3C trace/log correlation is always on and has no config knob. Sockguard preserves valid incoming traceparent trace 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.
  • POST /containers/create bodies are inspected by default. Sockguard blocks HostConfig.Privileged=true, HostConfig.NetworkMode=host, HostConfig.PidMode=host, HostConfig.IpcMode=host, HostConfig.UsernsMode=host, a non-empty HostConfig.Sysctls map (unless allow_sysctls: true), a non-empty HostConfig.Runtime value not present in allowed_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 outside request_body.container_create.allowed_bind_mounts, HostConfig.Devices host paths outside request_body.container_create.allowed_devices, HostConfig.DeviceRequests (unless explicitly allowed via allow_device_requests or allowed_device_requests), HostConfig.DeviceCgroupRules (unless explicitly allowed via allow_device_cgroup_rules or allowed_device_cgroup_rules), and any HostConfig.CapAdd entry that isn't covered by allow_all_capabilities or allowed_capabilities. Named volumes still work without allowlist entries because they are not host bind mounts. allowed_device_requests is the structured opt-in for GPU passthrough and similar device request policy — each entry must specify a driver (exact match, case-insensitive), an allowed_capabilities list of capability sets (each request capability set must be a subset of at least one allowlisted set), and an optional max_count bound (-1 means all devices; request Count: -1 is only allowed when max_count is also -1); set allow_device_requests: true only when you need unrestricted device request access. allowed_device_cgroup_rules is 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; set allow_device_cgroup_rules: true only when you need unrestricted cgroup device access. Optional opt-in rails enforce no-new-privileges, non-root Config.User, HostConfig.ReadonlyRootfs=true, HostConfig.CapDrop=["ALL"], memory / CPU / PIDs limits, allowlisted seccomp and AppArmor profiles, and required Config.Labels keys — 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 remaining SecurityOpt directives: deny_selinux_disable denies label=disable (and the legacy label:disable colon form), which turns off SELinux confinement; deny_selinux_label_override denies label=user:/role:/type:/level: SELinux context customization; and deny_unconfined_system_paths denies systempaths=unconfined and requests that set MaskedPaths or ReadonlyPaths to an explicit empty array — the Docker CLI converts --security-opt systempaths=unconfined into MaskedPaths: [] 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_trust adds cosign-backed signature verification: set mode: enforce to deny containers whose image lacks a valid signature from one of your allowed_signing_keys (PEM public keys) or allowed_keyless identities (Fulcio cert chain matched by issuer URL and SAN regex); set mode: warn to log failures and allow the request through instead. require_rekor_inclusion: true additionally requires a Rekor transparency log entry for keyless bundles. verify_timeout controls the per-verification network timeout (default 10s); set to a low value in air-gapped environments to fail fast. Verification resolves the image to its registry manifest digest, fetches the cosign signatures attached to it (the classic sha256-<digest>.sig tag and OCI 1.1 referrers are both checked), and only accepts a signature whose simple-signing payload binds to that exact digest — so the registry must be reachable from the proxy, and private registries require ambient credentials (a mounted Docker config.json). Keyless verification additionally fetches the Sigstore trust root via TUF at startup, which needs network access and a writable TUF cache; if that fetch fails the policy fails closed. Keyed (PEM) verification needs neither network at startup nor a writable cache. Either mode is a no-op when the image reference is empty — Docker refuses to create a container without an image anyway.
  • POST /containers/create's HostConfig.Mounts entries are validated further: a mount whose Type is not one of bind/volume/tmpfs/image is denied fail-closed (an unrecognized mount type has no reviewed policy, so it is refused rather than passed through); VolumeOptions.Subpath and ImageOptions.Subpath (Engine API 1.45+/1.55+) must be empty or a clean relative path that does not escape the mount root via ..; and TmpfsOptions.Options entries that re-enable exec, dev, or suid on a tmpfs mount are denied unless allow_tmpfs_privileged_options: true — Docker's tmpfs default is noexec,nodev,nosuid, and these options exist specifically to override that.
  • POST /containers/create also denies five HostConfig fields unconditionally — no request_body setting opts back in: VolumesFrom, UTSMode=host, a non-empty CgroupParent, GroupAdd, and ExtraHosts. 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 only ever match the literal "host" value on HostConfig.NetworkMode/PidMode/IpcMode/UsernsMode — a container:<id-or-name> value (join another container's namespace) passes through those checks unchecked, and always has. Two new opt-in filter-tier knobs close that gap structurally: restrict_namespace_sharing (+ allowed_namespace_sharing_containers) denies container:<ref> joins unless the target is on the allowlist (empty allowlist = deny all), across NetworkMode, PidMode, IpcMode, and UsernsMode; deny_namespace_path_mode denies a NetworkMode value with an ns:<path> prefix — Docker's raw host-namespace-file attachment form. Both default off/empty (pass-through), so enabling them is a deliberate, independent choice from the allow_host_* flags. Separately, when ownership.owner is configured, sockguard resolves every container:<ref> target on POST /containers/create and 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 under request_body.libpod_container_create, independent from container_create above; the two never read each other's body shape (Podman's libpod API is structurally distinct from the Docker-compat API, e.g. top-level privileged/netns/mounts/volumes/devices/cap_add/user/sysctl fields instead of nested HostConfig/Config objects). Sockguard blocks privileged=true, netns/pidns/ipcns/userns objects whose nsmode is host, bind-mounted mounts[] sources outside allowed_bind_mounts (named volumes[] entries are never bind-gated — they reference a volume by name, not a host path), devices[] host paths outside allowed_devices, and any cap_add entry not covered by allow_all_capabilities/allowed_capabilities. {"nsmode":"container","value":"<ref>"} namespace-sharing objects (netns/pidns/ipcns/userns) are gated by restrict_namespace_sharing/allowed_namespace_sharing_containers, mirroring container_create's equivalent knob. Optional opt-in rails require a non-root user, read_only_filesystem=true, memory/CPU/PIDs limits from resource_limits, allowlisted seccomp_profile_path/apparmor_profile values, and deny selinux_opts containing disable — all default to off. image_trust reuses the same cosign verification as container_create.image_trust, applied to the image field. Two libpod-only rails have no Docker-compat analog: allow_systemd_mode (SpecGenerator's own default for the systemd field is "true" even when --systemd is never passed on the client, so sockguard denies anything but an explicit "false" unless this is set) and 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).
  • POST /containers/*/exec and POST /exec/*/start are inspected when request_body.exec.allowed_commands is 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 unless allow_privileged: true, denies root-user exec unless allow_root_user: true, and re-checks POST /exec/*/start against 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.exec is the single config surface for exec, covering both the Docker-compat paths above and Podman's libpod-native POST /libpod/containers/*/exec and POST /libpod/exec/*/start — the two API families expose exec create/start bodies with identical field names, so there is no separate libpod_exec block; the same allowed_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 queries GET /libpod/exec/{id}/json (the libpod equivalent of the Docker-compat exec-inspect call) instead of the compat endpoint.
  • POST /images/create is inspected by default. Sockguard blocks fromSrc imports unless request_body.image_pull.allow_imports: true and only allows Docker Hub official images unless you set allow_all_registries: true or list explicit allowed_registries.
  • POST /build is inspected by default. Sockguard blocks remote contexts, networkmode=host, and Dockerfiles that contain RUN instructions unless you explicitly allow those behaviors under request_body.build.*.
  • POST /volumes/create is inspected by default. Sockguard blocks non-local volume drivers and driver options unless you explicitly allow them under request_body.volume.*.
  • POST /secrets/create and POST /configs/create are inspected by default. Sockguard blocks custom and template drivers unless you explicitly allow them under request_body.secret.* and request_body.config.*.
  • POST /services/create and POST /services/*/update are inspected by default. Sockguard blocks services that attach the host network, blocks bind mounts outside request_body.service.allowed_bind_mounts, constrains service images to Docker Hub official images unless you set request_body.service.allow_all_registries: true or list explicit request_body.service.allowed_registries, enforces the same allow_all_capabilities / allowed_capabilities capability allowlist and allow_sysctls gate as container-create, and applies image_trust cosign verification to the service's ContainerSpec image. Opt-in hardening rails mirror the container-create knobs onto the swarm ContainerSpec: require_non_root_user (denies a root or empty ContainerSpec.User), require_no_new_privileges (requires ContainerSpec.Privileges.NoNewPrivileges: true), require_readonly_rootfs (requires ContainerSpec.ReadOnly: true), and require_drop_all_capabilities (requires ContainerSpec.CapabilityDrop to include ALL) — all default off, so services that do not set them keep prior behavior. Confinement-mode rails complete the parity: deny_unconfined_seccomp denies ContainerSpec.Privileges.Seccomp.Mode: "unconfined", deny_custom_seccomp_profiles denies Mode: "custom" and the fail-closed case of a Seccomp object carrying a Profile blob with no Mode (an inline profile the proxy cannot vet), and deny_unconfined_apparmor denies ContainerSpec.Privileges.AppArmor.Mode: "disabled" (swarm's equivalent of unconfined AppArmor). Note that a custom seccomp profile can encode an allow-everything policy, so operators setting deny_unconfined_seccomp should usually set deny_custom_seccomp_profiles as well. SELinux rails complete the parity with container-create: deny_selinux_disable denies ContainerSpec.Privileges.SELinuxContext.Disable: true (the swarm equivalent of the container-create label=disable), and deny_selinux_label_override denies any SELinuxContext.{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, and POST /swarm/update are inspected by default. Sockguard blocks ForceNewCluster, external CA configuration, non-allowlisted join targets, token rotations, manager unlock-key rotations, manager autolock, and signing-CA updates unless you explicitly allow them under request_body.swarm.*.
  • POST /networks/create, POST /networks/*/connect, and POST /networks/*/disconnect are inspected by default. Sockguard blocks custom drivers, swarm/ingress/attachable/config-only controls, custom IPAM, driver options, and forced disconnects unless explicitly allowed under request_body.network.*. allow_endpoint_config additionally gates endpoint static IP, MAC address, links, driver options, and GwPriority (Engine API 1.45+, which network provides the default route when a container is attached to more than one) — enforced on both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig (see the container_create row below), since the two carry the identical attack surface. Endpoint Aliases are always allowed at both, independent of this flag. POST /networks/create also denies an explicit EnableIPv4: false (Engine API 1.48+; the field defaults to true when absent) unless allow_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_config narrows allow_endpoint_config into independent per-field gates (#186). allow_endpoint_config: true (default false) remains the whole-object escape hatch and, when set, keeps admitting every EndpointSettings field 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). When allow_endpoint_config is left at its default false, request_body.network.endpoint_config gates each field independently: allow_static_addressing (IPAMConfig.IPv4Address/IPv6Address and the deprecated top-level Gateway/IPAddress/IPPrefixLen/IPv6Gateway/GlobalIPv6Address/GlobalIPv6PrefixLen fields), allow_link_local_ips (IPAMConfig.LinkLocalIPs, independent of static addressing), allow_mac_pinning (MacAddress, shared with container_create's deprecated top-level MacAddress field), and allow_gw_priority (GwPriority) all default false. allow_aliases defaults true, reproducing allow_endpoint_config's long-standing unconditional-allow behavior for Aliases — set it to false explicitly to deny Aliases under the granular form (there is no equivalent opt-out under allow_endpoint_config: true). Links and DriverOpts have no granular field of their own: under the granular form they are always denied, fail-closed, regardless of the other settings — only allow_endpoint_config: true can admit them. Denial reasons name the specific offending field (e.g. "endpoint link-local IP addresses are not allowed"). Applies identically wherever allow_endpoint_config applies today — both POST /networks/*/connect and POST /containers/create's NetworkingConfig.EndpointsConfig/legacy MacAddress field — and has no libpod analog (not consulted by libpod_network, which has no libpod-native network-connect endpoint to gate).
  • POST /containers/*/update is inspected by default. Sockguard blocks restart-policy changes, resource controls, privileged mode, device changes, and capability/security-profile fields unless explicitly allowed under request_body.container_update.*.
  • POST /containers/*/update and POST /services/create/POST /services/*/update additionally 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 default false. For container update, the guard fetches the container's current resource state from the daemon and merges it with the submitted update the same way the daemon does — moby-faithful: an omitted or zero scalar HostConfig field (Memory, NanoCpus, CpuQuota, CpuPeriod, CpuShares) leaves the current value unchanged, while PidsLimit is a pointer at the daemon too, so any submitted value including 0 or -1 is an explicit clear — then validates the effective post-update state against the active require_* flags, not just the fields the request happens to touch. require_cpu_limit_hard accepts only NanoCpus or CpuQuota; CpuShares is a relative scheduling weight, not a cap, and does not satisfy it. Container-update require_* flags are enforced only when allow_resource_updates: true is also set (resource fields are otherwise already denied outright); sockguard logs a startup/reload warning — not an error — when a require_* flag is set while that gate stays off, since the combination is valid YAML but leaves the flag unenforced. For services, ServiceSpec is a full replacement rather than a merge, so an ordinary create/update is validated directly against the submitted body; POST /services/*/update?rollback=previous is instead validated against the daemon's stored PreviousSpec, because the daemon reactivates that document verbatim and ignores the submitted body — with a 409 resource_limit_policy_state_changed if the inspected service's Version.Index no longer matches the version query parameter the caller pinned; and a submitted UpdateConfig.FailureAction: "rollback" additionally validates the current (pre-update) Spec, since Docker can reactivate it automatically on task failure without another API call sockguard could intercept. Both close a real bypass: without them, rollback=previous or an automatic rollback could reactivate a resource-unconstrained document that never went through the guard.
  • 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, including warn/audit profiles — 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 warn or audit before switching it to enforce fleet-wide, and expect to remediate long-lived legacy containers/services with one compliant update before enabling it in enforce mode.
  • PUT /containers/*/archive is inspected by default. Sockguard blocks unsafe target paths, tar traversal, setuid/setgid entries, device nodes, and escaping symlinks/hardlinks unless explicitly allowed under request_body.container_archive.*.
  • POST /images/load is inspected by default. Sockguard denies image archive imports unless their manifest.json repo tags satisfy request_body.image_load.* registry policy, or untagged imports are explicitly allowed.
  • POST /swarm/unlock and POST /nodes/*/update are inspected by default. Swarm unlock is denied unless request_body.swarm.allow_unlock: true; node updates block role, availability, name, and unapproved label mutations unless allowed under request_body.node.*.
  • POST /plugins/pull, POST /plugins/*/upgrade, POST /plugins/*/set, and POST /plugins/create are inspected by default. Sockguard constrains plugin registries, privileges, assignment prefixes, local tar config.json, host mounts, device exposure, and capabilities unless you explicitly allow them under request_body.plugin.*. POST /plugins/create is inspected whether the upload arrives as a raw tar body or a multipart/form-data envelope — the multipart stream is parsed and the embedded config.json is extracted before policy evaluation.
  • POST /images/create (pull) and POST /images/*/push also decode the base64 X-Registry-Auth header (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, and when image_pull.allowed_registries is configured the decoded serveraddress must canonicalize to an allowlisted host. POST /build decodes the analogous X-Registry-Config multi-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 under request_body.libpod_pod_create. Sockguard denies a pod-level host network namespace (netns: {nsmode: "host"}) unless allow_host_network: true, denies a shared PID namespace ("pid" present in shared_namespaces) unless allow_shared_pid_namespace: true, and denies an infra image whose registry isn't in allowed_infra_image_registries — reusing the same registry-allowlist shape as image_pull.allowed_registries — whenever infra_image is set explicitly. When ownership.owner is configured, cross-owner pod membership is also denied on both sides of the relationship (#148): POST /libpod/pods/create joining another owner's host namespaces, and POST /libpod/containers/create targeting another owner's pod — reusing the same container:<ref> namespace-sharing checks POST /containers/create already applies. See Owner Label Isolation below for the full picture.
  • POST /libpod/volumes/create, POST /libpod/networks/create, and POST /libpod/secrets/create (Podman's libpod-native equivalents of POST /volumes/create/POST /networks/create/POST /secrets/create) are inspected by default under request_body.libpod_volume, request_body.libpod_network, and request_body.libpod_secret respectively — these reuse the same volume/network/secret config 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-level Options key (not DriverOpts), network options are snake_case (ipam_options, subnets) with no swarm/ingress/attachable/config-only fields (libpod has no swarm mode), and secret creation reads driver from the query string rather than a JSON body, so the libpod secret inspector never touches r.Body at all.
  • POST /libpod/play/kube (and its identically-handled POST /libpod/kube/play alias), POST /libpod/kube/apply, and POST/PUT /libpod/manifests/* have no request-body inspector — full Kubernetes-YAML/PodSpec modeling is out of scope for this release — so admitting any of them requires insecure_allow_body_blind_writes: true exactly like uninspected exec. Treat play/kube with 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.
  • Oversized bodies on bounded JSON/tar inspectors are rejected with 413 Payload Too Large before the inspector decodes them, so a misbehaving or hostile client cannot tie up the filter or the Docker daemon with oversized payloads.
  • insecure_allow_body_blind_writes is now reserved for the body-bearing writes Sockguard still cannot safely constrain, chiefly arbitrary exec without an allowed_commands allowlist, POST /swarm/join without request_body.swarm.allowed_join_remote_addrs, plugin setting writes without allowed assignment prefixes, and the uninspected libpod play/kube/kube/apply/manifest surface described above. For exec, setting it changes request-time behavior, not just startup validation: an exec with no allowed_commands is admitted instead of denied, while allow_privileged, allow_root_user, allowed_env_vars/denied_env_vars, and allowed_env_values continue to gate it exactly as configured. For play/kube/kube/apply/manifest writes there is no equivalent partial protection — setting the flag admits the request with no body inspection at all, so scope any allow rule for these paths as narrowly as possible.
  • insecure_allow_read_exfiltration stays false by default and must be set explicitly before broad read rules can expose raw archive/export or stream-style endpoints such as 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, or GET /images/*/get — as well as their libpod-native counterparts GET /libpod/containers/*/archive, GET /libpod/containers/*/export, GET /libpod/containers/*/logs, POST /libpod/containers/*/attach, GET /libpod/images/export, GET /libpod/images/*/get, POST /libpod/images/*/push, and GET /libpod/generate/kube (a read despite the "generate" name: Podman serves it as a GET that dumps an existing pod's or container's definition to YAML, which can include environment variables and other resource data).
  • insecure_accept_opaque_buildkit_tunnels stays false by default and must be set explicitly before startup validation will accept a rule that admits POST /session or POST /grpc — the opaque BuildKit session/gRPC tunnel Compose uses for docker compose build/up --build by default (Engine API 1.53 deprecated both, but current Buildx still uses them). (A rule naming a literal moby.buildkit.v1.Control method 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.) Unlike POST /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 to true now logs a startup deprecation warning steering operators toward that surface; it will be removed in a future major release. The flag and request_body.buildkit are mutually exclusive — mediation supersedes the acknowledgment — so a config cannot set both, and the deprecation warning never fires alongside that validation error. Tecnativa's GRPC=1/SESSION=1 compat env vars still auto-set this flag with their own compat-specific deprecation warning so existing drop-in configs keep working. See the Compose/BuildKit Transport section of the security model docs, Migration for the step-by-step move off this flag, and the drydock-with-build.yaml/portwing-with-build.yaml (classic DOCKER_BUILDKIT=0 builder) or drydock-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 of POST /session/POST /grpc, gating the same two endpoints insecure_accept_opaque_buildkit_tunnels admits wholesale. control.solve.allow gates the Control/Solve RPC — the actual build request — with security.insecure always denied (no enabling knob), network.host requiring request_body.build.allow_host_network, a remote build context requiring request_body.build.allow_remote_context, and RUN-instruction inspection on the Dockerfile synced over session.file_sync requiring request_body.build.allow_run_instructions (the same three sibling request_body.build flags 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 a registry-typed cache entry, exporter types, and the destination registry of an image-typed exporter that sets push: true — each empty by default (deny). control.allow_info/allow_list_workers gate two passthrough RPCs with no policy-relevant fields (worker/version metadata); control.allow_status gates Control/Status, admitted only for a ref this same client/profile actually Solved. Under session, auth (exact registry/realm/scope match for the moby.filesync.v1.Auth credential RPCs), secrets/ssh (exact-ID allowlists), file_sync/file_send (byte/file/path caps, file_sync.allow required to sync any Dockerfile or build context at all), and upload (a one-use token bound to an admitted Solve) each stay denied until their own allow/allowlist fields are set — see the Request Body Policy Reference table below for the full field list.
  • response.allow_attestation_statements stays false by default: GET /images/{name}/attestations?statement=true returns the full in-toto attestation payload (SBOM/provenance content, potentially generated by a different and less-trusted pipeline than the image itself), so it is denied at the response layer unless explicitly allowed. A request to the same path without ?statement=true (listing available attestation manifests) is unaffected.
  • response.redact_container_env, response.redact_mount_paths, response.redact_network_topology, and response.redact_sensitive_data default to true. 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 config Spec.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_topology defaults to false (opt-in): when enabled, Sockguard redacts container-runtime plumbing fields on GET /infoContainerd (socket path and namespace layout), FirewallBackend, DiscoveredDevices, and NRI — independent of Swarm mode (the redaction runs whether or not the daemon has a Swarm section in its response). Enable it when /info reads are exposed to a caller that should be able to confirm the daemon is reachable without learning host build details.

Request Body Policy Reference

All request-body policy fields default to the safest value unless noted. List fields default to empty lists.

GroupFieldsDefault behavior
container_createallow_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_trustDenies 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 ..; allow_tmpfs_privileged_options gates tmpfs exec/dev/suid option re-enablement (default deny). restrict_namespace_sharing (+ allowed_namespace_sharing_containers) gates joining another container's namespace via container:<id> values across NetworkMode/PidMode/IpcMode/UsernsMode, while deny_namespace_path_mode blocks the raw ns:<path> host-namespace-file form on NetworkMode only — a different vector from allow_host_network/pid/ipc/userns above, which only ever match the literal "host" value; both default to pass-through (unchanged behavior) 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_createallow_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_trustPodman'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, 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).
execallow_privileged, allow_root_user, allowed_commands, allowed_env_vars, denied_env_vars, allowed_env_valuesDenies 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_pullallow_imports, allow_all_registries, allow_official, allowed_registriesDenies fromSrc imports and allows Docker Hub official images by default (allow_official: true).
buildallow_remote_context, allow_host_network, allow_run_instructionsDenies remote contexts, host-network builds, and Dockerfiles containing RUN.
buildkitcontrol.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_bytesMediates 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_updateallow_privileged, allow_all_devices, allow_capabilities, allow_resource_updates, allow_restart_policy, require_memory_limit, require_cpu_limit, require_cpu_limit_hard, require_pids_limitDenies 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.
container_archiveallowed_paths, allow_setid, allow_device_nodes, allow_escaping_linksDenies unsafe target paths, tar traversal, setuid/setgid entries, device nodes, and escaping links.
image_loadallow_all_registries, allow_official, allowed_registries, allow_untaggedAllows Docker Hub official image tags by default (allow_official: true) and denies untagged archives unless opted in.
volumeallow_custom_drivers, allow_driver_optsDenies non-local drivers and driver options.
networkallow_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_ipv4Denies 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 / configallow_custom_drivers, allow_template_driversDenies custom and template drivers.
serviceallow_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_trustDenies host-network services and non-allowlisted bind mounts; 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.
swarmallow_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_unlockDenies unsafe init/join/update controls and swarm unlock. POST /swarm/join needs allowed_join_remote_addrs before broad join rules pass blind-write validation.
nodeallow_name_change, allow_role_change, allow_availability_change, allow_label_mutation, allowed_label_keysDenies name, role, availability, and arbitrary label mutations while allowing the configured owner-label key for controlled claims.
pluginallow_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_prefixesDenies 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_createallow_host_network, allow_shared_pid_namespace, allowed_infra_image_registriesGoverns POST /libpod/pods/create only (Podman's libpod-native pod creation; no Docker-compat equivalent). Denies a pod-level host network namespace (netns: {nsmode: "host"}) 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).
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).
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 from the query string, so this inspector never reads the request body. 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".

FieldTypeMatches
common_names[]stringExact CN on the verified leaf certificate
dns_names[]stringDNS SAN entries on the verified leaf
ip_addresses[]stringIP SAN entries on the verified leaf (not the TCP source IP)
uri_sans[]stringURI SAN entries on the verified leaf (for example spiffe://...)
public_key_sha256_pins[]stringLowercase 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:
      - 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b

Malformed 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/*/restart

If you are migrating from wollomatic, set clients.container_labels.label_prefix: socket-proxy.allow. to reuse existing labels. 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 profile match.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 with clients.unix_peer_profiles and uids/gidsSO_PEERCRED cannot 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_prefix permission 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 has clients.container_labels.enabled set.

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: deny
  • clients.source_ip_profiles matches the caller's remote IP against CIDRs in config order.

  • clients.client_certificate_profiles matches the verified mTLS leaf certificate in config order. Each assignment can match common_names, dns_names, ip_addresses, uri_sans, spiffe_ids, and public_key_sha256_pins; different selector fields on the same assignment are ANDed, while entries inside one field are ORed.

  • clients.unix_peer_profiles matches unix-socket callers by peer uids, gids, and pids. Different selector fields on the same assignment are ANDed, while entries inside one field are ORed.

  • clients.default_profile is the fallback when no specific assignment matches.

  • Each profile has its own rules and request_body policy, so one proxy can safely host a read-only dashboard, a container updater, and an admin UI at the same time.

  • clients.client_certificate_profiles requires listen.tls mutual TLS.

  • clients.unix_peer_profiles requires listen.socket.

  • response.visible_resource_labels and clients.profiles[].response.visible_resource_labels enforce read-side visibility on labeled list/events/inspect paths plus selected service/task log paths. Selectors use Docker label syntax (key or key=value), are ANDed together, and profile selectors are additive with the root response selectors.

  • response.name_patterns and response.image_patterns add 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_patterns is matched against Names[0] with the leading / stripped for containers, and against each RepoTags short name (the part after the last /) for images. image_patterns is matched against the container's Image field and against each full RepoTags reference 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 via clients.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 a 502 rather than buffered unbounded. Example — expose only traefik containers and images from a private registry to a specific client profile:

    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, and GET /events, while hidden inspect/log-style targets such as GET /services/*, GET /services/*/logs, GET /tasks/*, GET /tasks/*/logs, GET /secrets/*, GET /configs/*, GET /nodes/*, and GET /swarm return 404 instead 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_second when explicitly set. Set to 0 to 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). Since burst >= 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: allow

Guidance on starting values:

Consumer typeSuggested tokens_per_secondSuggested burstRationale
Interactive operator816Human clicks; a burst allows rapid multi-request workflows
Monitoring / metrics scraper48Periodic polls; low sustained rate is sufficient
CI agent100200Build pipelines issue large batches at startup
Read-only dashboard2040Page 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 >= 1 and <= 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: allow

When 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: deny

Combining 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: 32

Priority / 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: 20

Each priority tier has a hardcoded share of the global cap:

PriorityShareFloor at max_inflight: 100
low50%denied above 50 in-flight
normal (default)80%denied above 80 in-flight
high100%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.

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_second must be > 0
  • limits.rate.burst must be >= tokens_per_second or 0 (default)
  • limits.rate.endpoint_costs[].path must be non-empty and compile as a valid glob
  • limits.rate.endpoint_costs[].cost must be >= 1 and <= effective burst
  • limits.rate.endpoint_costs[].methods[] must not contain empty strings
  • limits.concurrency.max_inflight must be > 0
  • clients.global_concurrency.max_inflight must be > 0 when set
  • limits.priority must be one of low, normal, or high

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_code is one of rate_limit_exceeded, concurrency_cap, or priority_floor.
  • sockguard_inflight_requests{profile} — gauge tracking the current in-flight count for profiles that have max_inflight configured.

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.owner to POST /containers/create, /networks/create, /volumes/create, /services/create, /services/*/update, /secrets/create, /configs/create, /nodes/*/update, and /swarm/update
  • Stamp service writes at both Labels and TaskTemplate.ContainerSpec.Labels so downstream tasks inherit the same owner identity
  • Add the same label to POST /build via the labels query parameter so build-produced images can carry the owner identity too
  • 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, and DELETE paths 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 on POST /containers/create (HostConfig.NetworkMode/PidMode/IpcMode/UsernsMode) and deny the request if that container belongs to a different owner
  • Authorize resources embedded in workload payloads before forwarding them: container Image, named HostConfig.Binds/volume mounts, custom NetworkMode, and every NetworkingConfig.EndpointsConfig network; plus service ContainerSpec.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.

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. 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 body at all, so the label is stamped onto its label query parameter instead, same as Docker-compat secret create. 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.

FieldTypeNotes
idstringRequired, unique, ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$. Identifies the rule in logs and audit records.
modestringenforce (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[]stringOne or more of container_create, service_create, service_update. No duplicates.
inject_labels.labelsmap[string]stringUnconditionally 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.matchstringexact (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 / .tostring≤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:

ModeBehavior
enforceDefault. Denied requests return 403; throttled requests return 429.
warnRequests that would be denied are allowed upstream. The structured audit record carries decision=would_deny. Deny and throttle counters fire with a mode label.
auditSame 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.

Pre-auth admission gates — clients.allowed_cidrs CIDR checks and identity-lookup failures — stay enforce regardless of the profile's mode. Those are unsafe to relax through rollout mode because they fire before a profile is even resolved.

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: deny

Once 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]))

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 backends

When 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.debounce into a single reload.
  • SIGHUP — sending SIGHUP triggers a reload. Without reload.enabled, SIGHUP terminates the process (Go's default); with reload on, SIGHUP never terminates. On Synology / DSM and other btrfs bind-mount backends, SIGHUP is 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 (typical 5s15s) 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 key 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 path
  • upstream.socket — upstream Docker socket path
  • upstream.endpoints and upstream.failover — remote endpoint list and health-probe loop parameters (bound to the long-lived Resolver at startup)
  • log.* — log level, format, and output sink
  • health.* — health endpoint path, watchdog, and readiness probe config
  • metrics.* — metrics endpoint and path
  • admin.* — admin config (including admin.listen.*)
  • policy_bundle trust material — enabled, allowed_signing_keys, allowed_keyless, require_rekor_inclusion, verify_timeout

policy_bundle.signature_path is reload-mutable so an operator can re-sign the same YAML without a restart.

Everything else — rules, clients.*, response.*, request_body.*, ownership.*, insecure_allow_*, and upstream.request_timeout (of the upstream block, only request_timeout is mutable; socket, endpoints, and failover 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 labels ok, reject_load, reject_validation, reject_immutable, reject_signature.
  • 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 perms

Or 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.pem

When 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.address with no clients.allowed_cidrs would 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 setting clients.allowed_cidrs, configuring admin.listen.tls (mutual TLS), or — only on a private, trusted network — explicitly acknowledging the exposure with admin.listen.insecure_allow_wide_open: true.

Signed Policy Bundles

Sockguard can refuse to start (or reload) unless the on-disk YAML config is covered by a valid cosign sigstore bundle. This is Layer 0 of the security model: even a valid YAML that passes all structural validators is rejected if it was not signed by a trusted key.

policy_bundle:
  enabled: true
  signature_path: /etc/sockguard/sockguard.yaml.bundle   # cosign bundle file
  allowed_signing_keys:                                   # keyed: PEM public keys
    - pem: |
        -----BEGIN PUBLIC KEY-----
        MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
        -----END PUBLIC KEY-----
  allowed_keyless:                                        # keyless: Fulcio + Rekor
    - 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       # require Rekor transparency log entry (keyless)
  verify_timeout: 10s                 # per-verification network timeout

Sign the config with cosign:

cosign sign-blob \
  --bundle /etc/sockguard/sockguard.yaml.bundle \
  /etc/sockguard/sockguard.yaml

Verification runs at startup before any rule compiles. A missing, malformed, or wrong-key bundle aborts the process with a wrapped policy bundle: error. On every hot reload, a signature failure rejects the reload with reason reject_signature in sockguard_config_reload_total and never touches the running policy.

The policy_bundle trust material (enabled, allowed_signing_keys, allowed_keyless, require_rekor_inclusion, verify_timeout) is reload-immutable so a SIGHUP cannot silently widen the set of accepted signers. Only signature_path is reload-mutable, so an operator can re-sign without a restart.

Two verification paths are supported and can coexist:

  • Keyed — PEM-encoded ECDSA, RSA, or ed25519 public keys listed under allowed_signing_keys. Verification uses the key's algorithm directly; no network round-trip required.
  • Keyless (Fulcio + Rekor)allowed_keyless entries specify the exact OIDC issuer URL and a subject SAN regex. The signing cert's chain is verified against Fulcio; when require_rekor_inclusion: true a Rekor transparency-log entry is also required. The verify_timeout caps each network round-trip.

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.

policy_bundle.enabled: false is the default. The feature adds github.com/sigstore/sigstore-go as a dependency; it reuses the same stack already used for container image trust (request_body.container_create.image_trust).

Logging And Audit

Sockguard has two operator-facing logging streams:

  • log.access_log (default true) controls the structured request log written through the normal logger output.
  • log.audit.enabled (default false) 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 only json is accepted.
  • log.audit.output: same sink options as the main logger (stderr, stdout, or file:/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

PatternMatchesDoes 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
/**EverythingNothing

Before rule evaluation, Sockguard canonicalizes the request path: it strips any /vN.NN/ Docker API version prefix, 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.

Methods

  • Exact: GET, POST, PUT, DELETE, HEAD
  • Wildcard: * matches any method

Environment Variables

Every YAML field can be set via env var by prefixing with SOCKGUARD_ and replacing dots with underscores (metrics.enabledSOCKGUARD_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. See the YAML config section above for the full schema; every nested field has an env-var equivalent even when not enumerated here.

Listener and upstream

VariableYAML fieldDefaultDescription
SOCKGUARD_LISTEN_ADDRESSlisten.address127.0.0.1:2375TCP listener address. Loopback is allowed plaintext; non-loopback requires listen.tls or the two-flag legacy opt-in.
SOCKGUARD_LISTEN_INSECURE_ALLOW_PLAIN_TCPlisten.insecure_allow_plain_tcpfalseFirst of two acknowledgments for plaintext non-loopback TCP (unencrypted transport). Must be paired with insecure_allow_unauthenticated_clients.
SOCKGUARD_LISTEN_INSECURE_ALLOW_UNAUTHENTICATED_CLIENTSlisten.insecure_allow_unauthenticated_clientsfalseSecond 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_FILElisten.tls.cert_file(unset)Path to the listener certificate. Required to enable mTLS.
SOCKGUARD_LISTEN_TLS_KEY_FILElisten.tls.key_file(unset)Path to the listener private key. Required to enable mTLS.
SOCKGUARD_LISTEN_TLS_CLIENT_CA_FILElisten.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_SOCKETlisten.socket(unset)Switches to a unix socket listener. Sockguard hardens the socket to mode 0600 and rejects broader modes.
SOCKGUARD_UPSTREAM_SOCKETupstream.socket/var/run/docker.sockPath to the real Docker daemon socket Sockguard proxies to.
SOCKGUARD_UPSTREAM_REQUEST_TIMEOUTupstream.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_FAILOVER_HEALTH_INTERVALupstream.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_TIMEOUTupstream.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

VariableYAML fieldDefaultDescription
SOCKGUARD_LOG_LEVELlog.levelinfoOperational log level: debug, info, warn, error.
SOCKGUARD_LOG_FORMATlog.formatjsonOperational log format: json or text.
SOCKGUARD_LOG_ACCESS_LOGlog.access_logtrueEmit one structured access-log line per request with method, path, decision, latency, and trace fields.
SOCKGUARD_LOG_AUDIT_ENABLEDlog.audit.enabledfalseEmit dedicated audit events with stable schema, separate from the operational log.
SOCKGUARD_LOG_AUDIT_FORMATlog.audit.formatjsonAudit event format. JSON is required for SIEM ingestion; text exists for local debug only.
SOCKGUARD_LOG_AUDIT_OUTPUTlog.audit.outputstderrAudit sink: stderr, stdout, or a file path. File paths must already exist with writable permissions.

Health and observability

VariableYAML fieldDefaultDescription
SOCKGUARD_HEALTH_ENABLEDhealth.enabledtrueServe the /health liveness endpoint.
SOCKGUARD_HEALTH_PATHhealth.path/healthHealth endpoint path. Must start with / and differ from metrics.path / admin.path when those are set.
SOCKGUARD_HEALTH_WATCHDOG_ENABLEDhealth.watchdog.enabledfalseStart an active upstream socket monitor. Logs reachable/unreachable transitions and feeds /health.
SOCKGUARD_HEALTH_WATCHDOG_INTERVALhealth.watchdog.interval5sWatchdog probe interval. Must be a positive Go duration; 1s–30s is typical.
SOCKGUARD_HEALTH_READINESS_ENABLEDhealth.readiness.enabledfalseServe the opt-in /ready probe that issues a real GET /containers/json against the Docker API.
SOCKGUARD_HEALTH_READINESS_PATHhealth.readiness.path/readyReadiness endpoint path. Must start with / and differ from health.path / metrics.path / admin.path.
SOCKGUARD_HEALTH_READINESS_INTERVALhealth.readiness.interval10sReadiness probe interval. Must be a positive Go duration.
SOCKGUARD_HEALTH_READINESS_TIMEOUThealth.readiness.timeout5sPer-probe deadline for the readiness API call. Must be a positive Go duration.
SOCKGUARD_METRICS_ENABLEDmetrics.enabledfalseServe Prometheus text metrics on the proxy listener. See the Observability page for the full metric reference.
SOCKGUARD_METRICS_PATHmetrics.path/metricsScrape path. Must start with / and differ from health.path when both endpoints are enabled.
SOCKGUARD_ADMIN_ENABLEDadmin.enabledfalseServe the in-band POST <admin.path> candidate-config validation endpoint on the main listener.
SOCKGUARD_ADMIN_PATHadmin.path/admin/validateAdmin endpoint path. Must start with / and must differ from health.path and metrics.path when enabled.
SOCKGUARD_ADMIN_POLICY_VERSION_PATHadmin.policy_version_path/admin/policy/versionPath of the read-only policy-version endpoint. Must start with / and not collide with other endpoint paths.
SOCKGUARD_ADMIN_MAX_REQUEST_BYTESadmin.max_request_bytes524288Hard cap on candidate-YAML body size. Bodies above this return 413.
SOCKGUARD_ADMIN_LISTEN_SOCKETadmin.listen.socket(unset)Serve admin endpoints on a dedicated unix socket instead of the main listener.
SOCKGUARD_ADMIN_LISTEN_ADDRESSadmin.listen.address(unset)Serve admin endpoints on a dedicated TCP listener (mTLS via admin.listen.tls.*).
SOCKGUARD_ADMIN_LISTEN_INSECURE_ALLOW_WIDE_OPENadmin.listen.insecure_allow_wide_openfalseAcknowledge 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_ENABLEDreload.enabledfalseWatch the config file via fsnotify and reload on SIGHUP. Enabling this changes SIGHUP from "terminate" to "reload".
SOCKGUARD_RELOAD_DEBOUNCEreload.debounce"250ms"Coalesce a burst of fsnotify events into a single reload. Must be a valid Go duration string >= 0.
SOCKGUARD_RELOAD_POLL_INTERVALreload.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

VariableYAML fieldDefaultDescription
SOCKGUARD_RESPONSE_DENY_VERBOSITYresponse.deny_verbosityminimalminimal returns only the generic deny message. verbose echoes method, path, and reason — dev only.
SOCKGUARD_RESPONSE_REDACT_CONTAINER_ENVresponse.redact_container_envtrueReplace workload env arrays with empty arrays on container/service/task/plugin reads.
SOCKGUARD_RESPONSE_REDACT_MOUNT_PATHSresponse.redact_mount_pathstrueRedact mount and host-device source paths on container/volume/task/service/plugin//system/df reads.
SOCKGUARD_RESPONSE_REDACT_NETWORK_TOPOLOGYresponse.redact_network_topologytrueRedact network IDs, attached addresses, remote managers, and node reachability on relevant reads.
SOCKGUARD_RESPONSE_REDACT_SENSITIVE_DATAresponse.redact_sensitive_datatrueRedact config payloads, swarm join/unlock and CA material, and node/swarm TLS metadata.
SOCKGUARD_RESPONSE_REDACT_HOST_TOPOLOGYresponse.redact_host_topologyfalseRedact GET /info container-runtime plumbing fields (Containerd, FirewallBackend, DiscoveredDevices, NRI). Opt-in.
SOCKGUARD_RESPONSE_ALLOW_ATTESTATION_STATEMENTSresponse.allow_attestation_statementsfalseAllow GET /images/{name}/attestations?statement=true to return full attestation content instead of being denied.

Insecure opt-ins

These flags loosen Sockguard's defaults. Do not enable them without an explicit operational reason and a plan to revisit.

VariableYAML fieldDefaultDescription
SOCKGUARD_INSECURE_ALLOW_BODY_BLIND_WRITESinsecure_allow_body_blind_writesfalseAllow POST endpoints whose bodies Sockguard cannot inspect (currently arbitrary exec without an allowlist and plugin set without allowed prefixes).
SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATIONinsecure_allow_read_exfiltrationfalseAllow rules that match raw archive/export, log/attach, image-tarball-get, or image/plugin registry-push endpoints. 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_TUNNELSinsecure_accept_opaque_buildkit_tunnelsfalseDeprecated — 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.

VariableYAML fieldDefaultDescription
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_PRIVILEGEDrequest_body.container_create.allow_privilegedfalseAllow HostConfig.Privileged=true.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_NETWORKrequest_body.container_create.allow_host_networkfalseAllow HostConfig.NetworkMode=host.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_PIDrequest_body.container_create.allow_host_pidfalseAllow HostConfig.PidMode=host.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_IPCrequest_body.container_create.allow_host_ipcfalseAllow HostConfig.IpcMode=host.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_BIND_MOUNTSrequest_body.container_create.allowed_bind_mountsemptyComma-separated host-path prefixes allowed as bind sources. Named volumes always allowed.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_ALL_DEVICESrequest_body.container_create.allow_all_devicesfalseAllow any HostConfig.Devices host path. Prefer the allowlist below instead.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_DEVICESrequest_body.container_create.allowed_devicesemptyComma-separated host device paths allowed for HostConfig.Devices.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_DEVICE_REQUESTSrequest_body.container_create.allow_device_requestsfalseEscape hatch: allow all HostConfig.DeviceRequests without inspection. Prefer allowed_device_requests for least-privilege access.
(YAML-only)request_body.container_create.allowed_device_requestsemptyStructured 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_RULESrequest_body.container_create.allow_device_cgroup_rulesfalseAllow all HostConfig.DeviceCgroupRules without inspection. Prefer allowed_device_cgroup_rules for least-privilege access.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_DEVICE_CGROUP_RULESrequest_body.container_create.allowed_device_cgroup_rulesemptyComma-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_PRIVILEGESrequest_body.container_create.require_no_new_privilegesfalseRequire HostConfig.SecurityOpt to include no-new-privileges:true.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_NON_ROOT_USERrequest_body.container_create.require_non_root_userfalseRequire Config.User to be a non-zero UID or non-root username.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_READONLY_ROOTFSrequest_body.container_create.require_readonly_rootfsfalseRequire HostConfig.ReadonlyRootfs=true.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_DROP_ALL_CAPABILITIESrequest_body.container_create.require_drop_all_capabilitiesfalseRequire HostConfig.CapDrop to contain "ALL".
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_ALL_CAPABILITIESrequest_body.container_create.allow_all_capabilitiesfalseSkip the HostConfig.CapAdd allowlist. With this off, only entries in allowed_capabilities are permitted.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_CAPABILITIESrequest_body.container_create.allowed_capabilitiesemptyHostConfig.CapAdd allowlist (case-insensitive, optional CAP_ prefix).
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_MEMORY_LIMITrequest_body.container_create.require_memory_limitfalseRequire HostConfig.Memory > 0.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_CPU_LIMITrequest_body.container_create.require_cpu_limitfalseRequire one of NanoCpus, CpuQuota, CpuPeriod, CpuShares > 0.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_CPU_LIMIT_HARDrequest_body.container_create.require_cpu_limit_hardfalseRequire NanoCpus or CpuQuota specifically; CpuShares (relative priority) does not satisfy this stricter, independent check.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_PIDS_LIMITrequest_body.container_create.require_pids_limitfalseRequire HostConfig.PidsLimit > 0.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_SECCOMP_PROFILESrequest_body.container_create.allowed_seccomp_profilesemptyIf 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_SECCOMPrequest_body.container_create.deny_unconfined_seccompfalseWhen no allowlist is set, deny seccomp=unconfined specifically.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_APPARMOR_PROFILESrequest_body.container_create.allowed_apparmor_profilesemptyIf 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_APPARMORrequest_body.container_create.deny_unconfined_apparmorfalseWhen no allowlist is set, deny apparmor=unconfined specifically.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_DENY_SELINUX_DISABLErequest_body.container_create.deny_selinux_disablefalseDeny label=disable / label:disable SecurityOpt (turns off SELinux confinement).
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_DENY_SELINUX_LABEL_OVERRIDErequest_body.container_create.deny_selinux_label_overridefalseDeny label=user:/role:/type:/level: SELinux context overrides.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_DENY_UNCONFINED_SYSTEM_PATHSrequest_body.container_create.deny_unconfined_system_pathsfalseDeny systempaths=unconfined and explicit empty MaskedPaths/ReadonlyPaths arrays (the CLI-translated direct-API form).
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_USERNSrequest_body.container_create.allow_host_usernsfalseAllow HostConfig.UsernsMode=host.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_RESTRICT_NAMESPACE_SHARINGrequest_body.container_create.restrict_namespace_sharingfalseGate container:<ref> joins on NetworkMode/PidMode/IpcMode/UsernsMode against allowed_namespace_sharing_containers.
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_NAMESPACE_SHARING_CONTAINERSrequest_body.container_create.allowed_namespace_sharing_containersemptyComma-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_MODErequest_body.container_create.deny_namespace_path_modefalseDeny NetworkMode values with an ns: prefix (raw host namespace-file attachment).
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_SYSCTLSrequest_body.container_create.allow_sysctlsfalseAllow a non-empty HostConfig.Sysctls map (kernel parameter tuning).
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_RUNTIMESrequest_body.container_create.allowed_runtimesemptyComma-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_LABELSrequest_body.container_create.required_labelsemptyConfig.Labels keys that must be present with a non-empty value.
(YAML-only)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_keysemptyList 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_keylessemptyList of Fulcio keyless identities; each entry has issuer (exact OIDC URL) and subject_pattern (regex matched against the cert SAN).
(YAML-only)request_body.container_create.image_trust.require_rekor_inclusiontrueRequire a Rekor transparency log entry for keyless bundles.
(YAML-only)request_body.container_create.image_trust.verify_timeout10sPer-verification network timeout. 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.

VariableYAML fieldDefaultDescription
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_PRIVILEGEDrequest_body.libpod_container_create.allow_privilegedfalseAllow top-level privileged=true.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_HOST_NETWORKrequest_body.libpod_container_create.allow_host_networkfalseAllow netns.nsmode=host.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_HOST_PIDrequest_body.libpod_container_create.allow_host_pidfalseAllow pidns.nsmode=host.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_HOST_IPCrequest_body.libpod_container_create.allow_host_ipcfalseAllow ipcns.nsmode=host.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_HOST_USERNSrequest_body.libpod_container_create.allow_host_usernsfalseAllow userns.nsmode=host.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_BIND_MOUNTSrequest_body.libpod_container_create.allowed_bind_mountsemptyComma-separated host-path prefixes allowed as mounts[] bind sources. Named volumes[] entries always allowed.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_ALL_DEVICESrequest_body.libpod_container_create.allow_all_devicesfalseAllow any devices[] host path. Prefer the allowlist below instead.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_DEVICESrequest_body.libpod_container_create.allowed_devicesemptyComma-separated host device paths allowed for devices[].
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_RESTRICT_NAMESPACE_SHARINGrequest_body.libpod_container_create.restrict_namespace_sharingfalseGate {"nsmode":"container","value":"<ref>"} joins on netns/pidns/ipcns/userns against allowed_namespace_sharing_containers.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_NAMESPACE_SHARING_CONTAINERSrequest_body.libpod_container_create.allowed_namespace_sharing_containersemptyComma-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_CAPABILITIESrequest_body.libpod_container_create.allow_all_capabilitiesfalseSkip the cap_add allowlist. With this off, only entries in allowed_capabilities are permitted.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_CAPABILITIESrequest_body.libpod_container_create.allowed_capabilitiesemptycap_add allowlist (case-insensitive, optional CAP_ prefix).
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_SECCOMP_PROFILESrequest_body.libpod_container_create.allowed_seccomp_profilesemptyIf 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_SECCOMPrequest_body.libpod_container_create.deny_unconfined_seccompfalseWhen no allowlist is set, deny seccomp_profile_path=unconfined specifically.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOWED_APPARMOR_PROFILESrequest_body.libpod_container_create.allowed_apparmor_profilesemptyIf non-empty, apparmor_profile must be in the list.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_DENY_UNCONFINED_APPARMORrequest_body.libpod_container_create.deny_unconfined_apparmorfalseWhen no allowlist is set, deny apparmor_profile=unconfined specifically.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_DENY_SELINUX_DISABLErequest_body.libpod_container_create.deny_selinux_disablefalseDeny selinux_opts containing disable (turns off SELinux confinement).
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_NON_ROOT_USERrequest_body.libpod_container_create.require_non_root_userfalseRequire the user field to be a non-zero UID or non-root username.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_READONLY_ROOTFSrequest_body.libpod_container_create.require_readonly_rootfsfalseRequire read_only_filesystem=true.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_MEMORY_LIMITrequest_body.libpod_container_create.require_memory_limitfalseRequire resource_limits.memory.limit > 0.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_CPU_LIMITrequest_body.libpod_container_create.require_cpu_limitfalseRequire any of resource_limits.cpu.{quota,period,shares} > 0.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_CPU_LIMIT_HARDrequest_body.libpod_container_create.require_cpu_limit_hardfalseRequire resource_limits.cpu.quota specifically; cpu.shares (relative priority) does not satisfy this stricter, independent check.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_REQUIRE_PIDS_LIMITrequest_body.libpod_container_create.require_pids_limitfalseRequire resource_limits.pids.limit > 0.
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_SYSCTLSrequest_body.libpod_container_create.allow_sysctlsfalseAllow a non-empty sysctl map (kernel parameter tuning).
SOCKGUARD_REQUEST_BODY_LIBPOD_CONTAINER_CREATE_ALLOW_SYSTEMD_MODErequest_body.libpod_container_create.allow_systemd_modefalseAllow 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_MAPPINGSrequest_body.libpod_container_create.allow_custom_id_mappingsfalseAllow a non-default idmappings.uidMap/gidMap or --userns=auto. Independent of allow_host_userns, which only covers userns.nsmode=host.
(YAML-only)request_body.libpod_container_create.image_trust.*see container_create.image_trust aboveCosign signature verification on the image field. Identical semantics and sub-fields to container_create.image_trust.

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.

VariableYAML fieldDefaultDescription
SOCKGUARD_REQUEST_BODY_EXEC_ALLOW_PRIVILEGEDrequest_body.exec.allow_privilegedfalseAllow privileged exec sessions.
SOCKGUARD_REQUEST_BODY_EXEC_ALLOW_ROOT_USERrequest_body.exec.allow_root_userfalseAllow exec sessions running as root.
SOCKGUARD_REQUEST_BODY_EXEC_ALLOWED_ENV_VARSrequest_body.exec.allowed_env_varsemptyComma-separated allowlist of exec Env variable names (name-only match; empty = no restriction).
SOCKGUARD_REQUEST_BODY_EXEC_DENIED_ENV_VARSrequest_body.exec.denied_env_varsemptyComma-separated denylist of exec Env variable names; wins over the allowlist (name-only match; empty = nothing blocked).
SOCKGUARD_REQUEST_BODY_EXEC_ALLOWED_ENV_VALUESrequest_body.exec.allowed_env_valuesemptyComma-separated exact NAME=VALUE entries for selected exec variables; values are compared but never logged.
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOW_IMPORTSrequest_body.image_pull.allow_importsfalseAllow fromSrc image imports.
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOW_ALL_REGISTRIESrequest_body.image_pull.allow_all_registriesfalseAllow image pulls from any registry. Prefer ALLOWED_REGISTRIES instead.
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOW_OFFICIALrequest_body.image_pull.allow_officialtrueAllow Docker Hub official images (single-segment names).
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOWED_REGISTRIESrequest_body.image_pull.allowed_registriesemptyComma-separated allowed pull registries.
SOCKGUARD_REQUEST_BODY_BUILD_ALLOW_REMOTE_CONTEXTrequest_body.build.allow_remote_contextfalseAllow POST /build with a remote context URL.
SOCKGUARD_REQUEST_BODY_BUILD_ALLOW_HOST_NETWORKrequest_body.build.allow_host_networkfalseAllow POST /build with networkmode=host.
SOCKGUARD_REQUEST_BODY_BUILD_ALLOW_RUN_INSTRUCTIONSrequest_body.build.allow_run_instructionsfalseAllow Dockerfiles containing RUN instructions.
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_ALLOW_INFOrequest_body.buildkit.control.allow_infofalseAllow the passthrough Control/Info RPC (worker/version metadata; no policy-relevant fields).
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_ALLOW_LIST_WORKERSrequest_body.buildkit.control.allow_list_workersfalseAllow the passthrough Control/ListWorkers RPC (worker capability metadata; no policy-relevant fields).
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_ALLOW_STATUSrequest_body.buildkit.control.allow_statusfalseAllow the mediated Control/Status RPC — admitted only for a ref this same client/profile actually Solved.
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOWrequest_body.buildkit.control.solve.allowfalseAllow the mediated Control/Solve RPC — the actual build request.
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOWED_CACHE_IMPORT_TYPESrequest_body.buildkit.control.solve.allowed_cache_import_typesemptyComma-separated Cache.Imports[].Type allowlist (e.g. registry,local,gha). Empty = deny all.
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOWED_CACHE_EXPORT_TYPESrequest_body.buildkit.control.solve.allowed_cache_export_typesemptyComma-separated Cache.Exports[].Type allowlist. Empty = deny all.
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOWED_CACHE_REGISTRIESrequest_body.buildkit.control.solve.allowed_cache_registriesemptyComma-separated registry-host allowlist for a registry-typed cache import/export entry's ref attribute.
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOWED_EXPORTERSrequest_body.buildkit.control.solve.allowed_exportersemptyComma-separated Exporters[].Type allowlist (e.g. image,oci,local). Empty = deny all.
SOCKGUARD_REQUEST_BODY_BUILDKIT_CONTROL_SOLVE_ALLOWED_EXPORTER_REGISTRIESrequest_body.buildkit.control.solve.allowed_exporter_registriesemptyComma-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_HEALTHrequest_body.buildkit.session.healthfalseAllow the passthrough grpc.health.v1.Health/{Check,Watch} RPCs.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_AUTH_ALLOWrequest_body.buildkit.session.auth.allowfalseAllow moby.filesync.v1.Auth's Credentials/FetchToken/GetTokenAuthority/VerifyTokenAuthority RPCs, subject to the three allowlists below.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_AUTH_ALLOWED_REGISTRIESrequest_body.buildkit.session.auth.allowed_registriesemptyComma-separated exact registry-host allowlist every Auth RPC's Host must match.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_AUTH_ALLOWED_REALMSrequest_body.buildkit.session.auth.allowed_realmsemptyComma-separated exact FetchToken Realm allowlist.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_AUTH_ALLOWED_SCOPESrequest_body.buildkit.session.auth.allowed_scopesemptyComma-separated exact FetchToken scope allowlist; every requested scope must be a member.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_SECRETS_ALLOWrequest_body.buildkit.session.secrets.allowfalseAllow moby.buildkit.secrets.v1.Secrets/GetSecret, subject to allowed_ids.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_SECRETS_ALLOWED_IDSrequest_body.buildkit.session.secrets.allowed_idsemptyComma-separated exact secret-ID allowlist.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_SSH_ALLOWrequest_body.buildkit.session.ssh.allowfalseAllow moby.sshforward.v1.SSH's CheckAgent/ForwardAgent RPCs, subject to allowed_ids.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_SSH_ALLOWED_IDSrequest_body.buildkit.session.ssh.allowed_idsemptyComma-separated exact SSH agent-ID allowlist.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_FILE_SYNC_ALLOWrequest_body.buildkit.session.file_sync.allowfalseAllow moby.filesync.v1.FileSync/DiffCopy — required to sync the Dockerfile/build context to buildkitd at all.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_FILE_SYNC_MAX_FILESrequest_body.buildkit.session.file_sync.max_files0Cap 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_BYTESrequest_body.buildkit.session.file_sync.max_total_bytes0Cap cumulative bytes relayed across a FileSync/DiffCopy stream. 0 uses buildkitproxy.Limits.MaxFileSyncTotalBytes.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_FILE_SYNC_MAX_PATH_LENGTHrequest_body.buildkit.session.file_sync.max_path_length0Cap 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_BYTESrequest_body.buildkit.session.file_sync.max_file_bytes0Cap 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_ALLOWrequest_body.buildkit.session.file_send.allowfalseAllow 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_BYTESrequest_body.buildkit.session.file_send.max_bytes0Cap cumulative bytes relayed for a FileSend/DiffCopy stream. 0 uses buildkitproxy.Limits.MaxFileSendBytes.
SOCKGUARD_REQUEST_BODY_BUILDKIT_SESSION_UPLOAD_ALLOWrequest_body.buildkit.session.upload.allowfalseAllow 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_BYTESrequest_body.buildkit.session.upload.max_bytes0Cap cumulative bytes relayed for an Upload/Pull stream. 0 uses buildkitproxy.Limits.MaxUploadBytes.
SOCKGUARD_REQUEST_BODY_CONTAINER_UPDATE_ALLOW_RESTART_POLICYrequest_body.container_update.allow_restart_policyfalseAllow POST /containers/*/update to change the restart policy.
SOCKGUARD_REQUEST_BODY_CONTAINER_UPDATE_REQUIRE_MEMORY_LIMITrequest_body.container_update.require_memory_limitfalseRequire 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_LIMITrequest_body.container_update.require_cpu_limitfalseRequire 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_HARDrequest_body.container_update.require_cpu_limit_hardfalseRequire 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_LIMITrequest_body.container_update.require_pids_limitfalseRequire 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_ARCHIVE_ALLOWED_PATHSrequest_body.container_archive.allowed_pathsemptyComma-separated container paths allowed for PUT /containers/*/archive.
SOCKGUARD_REQUEST_BODY_IMAGE_LOAD_ALLOW_UNTAGGEDrequest_body.image_load.allow_untaggedfalseAllow POST /images/load for tarballs containing untagged images.
SOCKGUARD_REQUEST_BODY_VOLUME_ALLOW_DRIVER_OPTSrequest_body.volume.allow_driver_optsfalseAllow POST /volumes/create with custom DriverOpts.
SOCKGUARD_REQUEST_BODY_NETWORK_ALLOW_ENDPOINT_CONFIGrequest_body.network.allow_endpoint_configfalseAllow 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_ADDRESSINGrequest_body.network.endpoint_config.allow_static_addressingfalseNarrows 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_IPSrequest_body.network.endpoint_config.allow_link_local_ipsfalseAllow IPAMConfig.LinkLocalIPs, independent of allow_static_addressing. Only consulted when allow_endpoint_config is false.
SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_ALLOW_MAC_PINNINGrequest_body.network.endpoint_config.allow_mac_pinningfalseAllow 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_PRIORITYrequest_body.network.endpoint_config.allow_gw_priorityfalseAllow GwPriority (Engine API 1.55+). Only consulted when allow_endpoint_config is false.
SOCKGUARD_REQUEST_BODY_NETWORK_ENDPOINT_CONFIG_ALLOW_ALIASESrequest_body.network.endpoint_config.allow_aliasestrueAllow 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_IPV4request_body.network.allow_disable_ipv4falseAllow 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_OPTIONSrequest_body.container_create.allow_tmpfs_privileged_optionsfalseAllow HostConfig.Mounts[].TmpfsOptions.Options entries that re-enable exec, dev, or suid on a tmpfs mount.
SOCKGUARD_REQUEST_BODY_SECRET_ALLOW_TEMPLATE_DRIVERSrequest_body.secret.allow_template_driversfalseAllow POST /secrets/create with a template driver.
SOCKGUARD_REQUEST_BODY_CONFIG_ALLOW_TEMPLATE_DRIVERSrequest_body.config.allow_template_driversfalseAllow POST /configs/create with a template driver.
SOCKGUARD_REQUEST_BODY_SERVICE_ALLOWED_BIND_MOUNTSrequest_body.service.allowed_bind_mountsemptyComma-separated host-path prefixes allowed as bind sources for POST /services/create.
SOCKGUARD_REQUEST_BODY_SERVICE_REQUIRE_NON_ROOT_USERrequest_body.service.require_non_root_userfalseRequire a non-root ContainerSpec.User on swarm service create/update.
SOCKGUARD_REQUEST_BODY_SERVICE_REQUIRE_NO_NEW_PRIVILEGESrequest_body.service.require_no_new_privilegesfalseRequire ContainerSpec.Privileges.NoNewPrivileges: true on service writes.
SOCKGUARD_REQUEST_BODY_SERVICE_REQUIRE_READONLY_ROOTFSrequest_body.service.require_readonly_rootfsfalseRequire ContainerSpec.ReadOnly: true on service writes.
SOCKGUARD_REQUEST_BODY_SERVICE_REQUIRE_DROP_ALL_CAPABILITIESrequest_body.service.require_drop_all_capabilitiesfalseRequire ContainerSpec.CapabilityDrop to include ALL on service writes.
SOCKGUARD_REQUEST_BODY_SERVICE_DENY_UNCONFINED_SECCOMPrequest_body.service.deny_unconfined_seccompfalseDeny ContainerSpec.Privileges.Seccomp.Mode: "unconfined" on service writes.
SOCKGUARD_REQUEST_BODY_SERVICE_DENY_CUSTOM_SECCOMP_PROFILESrequest_body.service.deny_custom_seccomp_profilesfalseDeny Seccomp.Mode: "custom", or a Profile blob with no Mode (fail-closed).
SOCKGUARD_REQUEST_BODY_SERVICE_DENY_UNCONFINED_APPARMORrequest_body.service.deny_unconfined_apparmorfalseDeny ContainerSpec.Privileges.AppArmor.Mode: "disabled" on service writes.
SOCKGUARD_REQUEST_BODY_SERVICE_DENY_SELINUX_DISABLErequest_body.service.deny_selinux_disablefalseDeny ContainerSpec.Privileges.SELinuxContext.Disable: true on service writes.
SOCKGUARD_REQUEST_BODY_SERVICE_DENY_SELINUX_LABEL_OVERRIDErequest_body.service.deny_selinux_label_overridefalseDeny SELinuxContext.{User,Role,Type,Level} overrides on service writes.
SOCKGUARD_REQUEST_BODY_SERVICE_REQUIRE_CPU_LIMITrequest_body.service.require_cpu_limitfalseRequire 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_HARDrequest_body.service.require_cpu_limit_hardfalseSame 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_ADDRSrequest_body.swarm.allowed_join_remote_addrsemptyComma-separated host:port join targets allowed for POST /swarm/join.
SOCKGUARD_REQUEST_BODY_NODE_ALLOWED_LABEL_KEYSrequest_body.node.allowed_label_keysemptyComma-separated label keys allowed in POST /nodes/*/update.
SOCKGUARD_REQUEST_BODY_PLUGIN_ALLOWED_SET_ENV_PREFIXESrequest_body.plugin.allowed_set_env_prefixesemptyComma-separated KEY= prefixes allowed for POST /plugins/*/set.
SOCKGUARD_REQUEST_BODY_LIBPOD_POD_CREATE_ALLOW_HOST_NETWORKrequest_body.libpod_pod_create.allow_host_networkfalseAllow POST /libpod/pods/create with a pod-level host network namespace (netns: {nsmode: "host"}).
SOCKGUARD_REQUEST_BODY_LIBPOD_POD_CREATE_ALLOW_SHARED_PID_NAMESPACErequest_body.libpod_pod_create.allow_shared_pid_namespacefalseAllow "pid" in a pod's shared_namespaces.
SOCKGUARD_REQUEST_BODY_LIBPOD_POD_CREATE_ALLOWED_INFRA_IMAGE_REGISTRIESrequest_body.libpod_pod_create.allowed_infra_image_registriesemptyComma-separated allowlist of registries an explicit infra_image may come from.
SOCKGUARD_REQUEST_BODY_LIBPOD_VOLUME_ALLOW_CUSTOM_DRIVERSrequest_body.libpod_volume.allow_custom_driversfalseAllow 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_OPTSrequest_body.libpod_volume.allow_driver_optsfalseAllow POST /libpod/volumes/create with a non-empty Options map (libpod's driver-opts field; the wire key is Options, not DriverOpts).
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ALLOW_CUSTOM_DRIVERSrequest_body.libpod_network.allow_custom_driversfalseAllow 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_OPTIONSrequest_body.libpod_network.allow_driver_optionsfalseAllow POST /libpod/networks/create with a non-empty options map.
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ALLOW_CUSTOM_IPAM_CONFIGrequest_body.libpod_network.allow_custom_ipam_configfalseAllow POST /libpod/networks/create with custom static subnets.
SOCKGUARD_REQUEST_BODY_LIBPOD_NETWORK_ALLOW_IPAM_OPTIONSrequest_body.libpod_network.allow_ipam_optionsfalseAllow 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_endpoint_config/allow_disconnect_force/allow_disable_ipv4 fields have no libpod analog and are not consulted by this inspector (libpod predates and is independent of Docker's swarm mode).
SOCKGUARD_REQUEST_BODY_LIBPOD_SECRET_ALLOW_CUSTOM_DRIVERSrequest_body.libpod_secret.allow_custom_driversfalseAllow POST /libpod/secrets/create with a non-empty driver query parameter — libpod reads driver 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

VariableYAML fieldDefaultDescription
SOCKGUARD_CLIENTS_ALLOWED_CIDRSclients.allowed_cidrsemptyComma-separated CIDR allowlist applied to all client transports including /metrics.
SOCKGUARD_CLIENTS_CONTAINER_LABELS_ENABLEDclients.container_labels.enabledfalseResolve client containers' labels via the upstream Docker socket for label-driven rules.
SOCKGUARD_CLIENTS_CONTAINER_LABELS_LABEL_PREFIXclients.container_labels.label_prefixcom.sockguard.allow.Label-key prefix Sockguard considers when matching container-label rules.
SOCKGUARD_OWNERSHIP_OWNERownership.owneremptyOwner identifier stamped onto every audit event and used for ownership-scoped rules.
SOCKGUARD_OWNERSHIP_LABEL_KEYownership.label_keycom.sockguard.ownerLabel key Sockguard reads from container/image metadata to determine ownership.
SOCKGUARD_OWNERSHIP_ALLOW_UNOWNED_IMAGESownership.allow_unowned_imagestrueAllow operations on images that lack the ownership label.
SOCKGUARD_OWNERSHIP_ALLOW_CROSS_OWNER_NAMESPACE_SHARINGownership.allow_cross_owner_namespace_sharingfalseAllow 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 generic message. Never echoes the request method, path, or matched rule reason.
  • verbose: returns message, method, path (with /secrets/* and /swarm/unlockkey paths redacted), and reason. 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 (default true): replaces workload env arrays with empty arrays on container, service, task, and plugin reads.
  • redact_mount_paths (default true): redacts mount and host-device source paths on container, volume, task, service, plugin, and /system/df reads.
  • redact_network_topology (default true): redacts container, network, task, service, node, swarm, /info, and /system/df topology details such as network IDs, attached addresses, remote managers, and node reachability addresses.
  • redact_sensitive_data (default true): redacts config payload material, service secret/config references, swarm join/unlock and CA material, and node/swarm TLS metadata.
  • redact_host_topology (default false, opt-in): redacts GET /info container-runtime plumbing fields — Containerd, FirewallBackend, DiscoveredDevices, and NRI — 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?statement=true is denied outright (rather than redacted), since the field being gated is the entire attestation statement payload, not a specific key within it. It 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=warning

Compat env vars only generate rules when no explicit rules: are configured. If rules: is present in YAML, those rules take precedence even when they are identical to Sockguard's built-in defaults.

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 raw archive/export or log/attach streaming parity. Safer YAML configs should allow only the list/inspect endpoints a client needs.

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)

Sockguard also accepts the legacy singular ALLOW_RESTART=1 alias, but ALLOW_RESTARTS is the upstream Tecnativa/LinuxServer name.

Precedence

CLI flags > environment variables > config file > defaults