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.
YAML Config (recommended)
listen:
address: 127.0.0.1:2375
insecure_allow_plain_tcp: false
insecure_allow_unauthenticated_clients: false
tls:
cert_file: /run/secrets/sockguard/server-cert.pem
key_file: /run/secrets/sockguard/server-key.pem
client_ca_file: /run/secrets/sockguard/client-ca.pem
dns_names:
- portainer.internal
uri_sans:
- spiffe://sockguard.test/workload/portainer
insecure_allow_body_blind_writes: false
insecure_allow_read_exfiltration: false
upstream:
socket: /var/run/docker.sock
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
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)
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_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
allow_host_userns: false # deny HostConfig.UsernsMode=host (default)
allow_sysctls: false # deny a non-empty HostConfig.Sysctls map (default)
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: false # additionally require a Rekor transparency log entry
verify_timeout: 10s # per-verification network timeout (default: 10s)
exec:
allow_privileged: false
allow_root_user: false
allowed_commands:
- ["/usr/local/bin/pre-update", "--check"]
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
service:
allow_host_network: false
allowed_bind_mounts:
- /srv/services
allow_official: true
allowed_registries:
- ghcr.io
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
health:
enabled: true
path: /health
watchdog:
enabled: false
interval: 5s
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: GET, path: "/version" }
action: allow
- match: { method: GET, path: "/events" }
action: allow
- match: { method: GET, path: "/containers/json" }
action: allow
- match: { method: GET, path: "/containers/*/json" }
action: allow
- match: { method: "*", path: "/**" }
action: deny
reason: "no matching allow rule"The default listener is loopback TCP 127.0.0.1:2375, which keeps the Docker API proxy off the network unless you explicitly choose otherwise.
- For the safest deployment, use
listen.socketand share a unix socket. - Sockguard hardens the unix socket to
0600owner-only permissions.listen.socket_modemust stay0600; broader modes are rejected at startup. - For remote or container-network TCP, configure
listen.tlsso Sockguard requires mutual TLS. Sockguard's mTLS server minimum is TLS 1.3, so callers must support TLS 1.3. listen.tls.client_ca_filestill defines the issuing trust root. To avoid trusting every cert that CA can mint, optionally narrow the accepted verified client leaf with the selectors described under mTLS Client Selectors below. Different selector fields are ANDed, while entries inside one field are ORed.- Plaintext non-loopback TCP is rejected unless you set both
listen.insecure_allow_plain_tcp: trueandlisten.insecure_allow_unauthenticated_clients: true. Both acknowledgments are required — one without the other is rejected — so a single fat-fingered flag cannot expose the listener. That mode is only for legacy compatibility on a private, trusted network. health.watchdog.enabledstarts an active upstream socket monitor that checks Docker everyhealth.watchdog.interval, logs reachable/unreachable state transitions, and lets/healthanswer from the latest watchdog state instead of waiting for a scrape or probe to discover an outage.metrics.enabledis opt-in and serves Prometheus text metrics atmetrics.pathon the same listener. The endpoint is local to Sockguard, is never forwarded to Docker, bypasses Docker API allow rules like/health, and remains behind listener security plusclients.allowed_cidrs. Every scrape also exports asockguard_build_info{version,commit,build_date,go_version}gauge and asockguard_start_time_secondsgauge for version panels and uptime alerts. When the active watchdog is enabled, metrics also includesockguard_upstream_socket_upandsockguard_upstream_watchdog_checks_total.admin.enabledis opt-in and exposes a singlePOST <admin.path>endpoint (default/admin/validate) that runs the same parse + validate + compile pipeline as the offlinesockguard validatecommand against a YAML body in the request payload. Useful as a CI gate before promoting a candidate config to production. Running policy is never mutated. The endpoint rides the main listener, so the listener's CIDR allowlist, mTLS posture, and per-profile rate-limit / concurrency caps all apply. Bodies are hard-capped atadmin.max_request_bytes(default 512 KiB) viahttp.MaxBytesReaderand return413on overflow. Non-POST methods return405withAllow: POST. The response body is a structured JSON report:{"ok": bool, "rules": int, "profiles": int, "compat_active": bool, "errors": [...]?}. A failing candidate returns422with the validator's per-issue error list; a passing candidate returns200.admin.pathmust start with/and must not collide withhealth.pathormetrics.pathwhen those endpoints are also enabled.reload.enabledis opt-in and turns on hot reload of policy at runtime. When on, sockguard watches the loaded config file viafsnotify(Linux inotify / macOS kqueue) and also reloads onSIGHUP. A burst of editor events (vim's chmod + write + rename + create save dance, for example) is debounced into a single reload byreload.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.*, andpolicy_bundletrust 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 withchanged_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 asockguard_config_reload_last_success_timestamp_secondsgauge (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); withreload.enabled: trueit triggers a reload and never terminates the process. Default isreload.enabled: falsefor 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
traceparenttrace IDs and sampled flags, replaces the parent span with a proxy-local span ID for the forwarded request, and generates fresh local context when the caller does not send valid trace context. POST /containers/createbodies are inspected by default. Sockguard blocksHostConfig.Privileged=true,HostConfig.NetworkMode=host,HostConfig.PidMode=host,HostConfig.IpcMode=host,HostConfig.UsernsMode=host, a non-emptyHostConfig.Sysctlsmap (unlessallow_sysctls: true), bind mount sources outsiderequest_body.container_create.allowed_bind_mounts,HostConfig.Deviceshost paths outsiderequest_body.container_create.allowed_devices,HostConfig.DeviceRequests(unless explicitly allowed viaallow_device_requestsorallowed_device_requests),HostConfig.DeviceCgroupRules(unless explicitly allowed viaallow_device_cgroup_rulesorallowed_device_cgroup_rules), and anyHostConfig.CapAddentry that isn't covered byallow_all_capabilitiesorallowed_capabilities. Named volumes still work without allowlist entries because they are not host bind mounts.allowed_device_requestsis the structured opt-in for GPU passthrough and similar device request policy — each entry must specify adriver(exact match, case-insensitive), anallowed_capabilitieslist of capability sets (each request capability set must be a subset of at least one allowlisted set), and an optionalmax_countbound (-1means all devices; requestCount: -1is only allowed whenmax_countis also-1); setallow_device_requests: trueonly when you need unrestricted device request access.allowed_device_cgroup_rulesis the structured opt-in for cgroup device policy — it accepts Docker cgroup rule strings (<type> <major>:<minor> <perms>) with*wildcards for major or minor, and denies request wildcards unless the matching allowlist entry also uses a wildcard at that position; setallow_device_cgroup_rules: trueonly when you need unrestricted cgroup device access. Optional opt-in rails enforceno-new-privileges, non-rootConfig.User,HostConfig.ReadonlyRootfs=true,HostConfig.CapDrop=["ALL"], memory / CPU / PIDs limits, allowlisted seccomp and AppArmor profiles, and requiredConfig.Labelskeys — all default to off, so a configuration that does not set them keeps prior behavior except for the CapAdd allowlist and host-userns default-deny noted above.image_trustadds cosign-backed signature verification: setmode: enforceto deny containers whose image lacks a valid signature from one of yourallowed_signing_keys(PEM public keys) orallowed_keylessidentities (Fulcio cert chain matched by issuer URL and SAN regex); setmode: warnto log failures and allow the request through instead.require_rekor_inclusion: trueadditionally requires a Rekor transparency log entry for keyless bundles.verify_timeoutcontrols the per-verification network timeout (default 10s); set to a low value in air-gapped environments to fail fast. Either mode is a no-op when the image reference is empty — Docker refuses to create a container without an image anyway.POST /containers/createalso denies fiveHostConfigfields unconditionally — norequest_bodysetting opts back in:VolumesFrom,UTSMode=host, a non-emptyCgroupParent,GroupAdd, andExtraHosts. Each one opens a namespace-escape or privilege-escalation path, so it is blocked regardless of policy.POST /containers/*/execandPOST /exec/*/startare inspected whenrequest_body.exec.allowed_commandsis non-empty. Sockguard denies argv vectors that match no allowlist entry — each entry is an argv template whose tokens are sockguard globs (*matches a run of non-slash characters,**matches any sequence), and a command matches when its token count equals an entry's and every token matches the glob at that position, so an exec carrying a variable argument can be allowlisted without enumerating every literal form. It also denies privileged exec unlessallow_privileged: true, denies root-user exec unlessallow_root_user: true, and re-checksPOST /exec/*/startagainst Docker's stored exec metadata before execution. Docker exposes exec inspect and exec start as separate API calls, so this start-time check has an unavoidable time-of-check/time-of-use window; keep exec allowlists and client profile assignments narrow.POST /images/createis inspected by default. Sockguard blocksfromSrcimports unlessrequest_body.image_pull.allow_imports: trueand only allows Docker Hub official images unless you setallow_all_registries: trueor list explicitallowed_registries.POST /buildis inspected by default. Sockguard blocks remote contexts,networkmode=host, and Dockerfiles that containRUNinstructions unless you explicitly allow those behaviors underrequest_body.build.*.POST /volumes/createis inspected by default. Sockguard blocks non-local volume drivers and driver options unless you explicitly allow them underrequest_body.volume.*.POST /secrets/createandPOST /configs/createare inspected by default. Sockguard blocks custom and template drivers unless you explicitly allow them underrequest_body.secret.*andrequest_body.config.*.POST /services/createandPOST /services/*/updateare inspected by default. Sockguard blocks services that attach thehostnetwork, blocks bind mounts outsiderequest_body.service.allowed_bind_mounts, and constrains service images to Docker Hub official images unless you setrequest_body.service.allow_all_registries: trueor list explicitrequest_body.service.allowed_registries.POST /swarm/init,POST /swarm/join, andPOST /swarm/updateare inspected by default. Sockguard blocksForceNewCluster, external CA configuration, non-allowlisted join targets, token rotations, manager unlock-key rotations, manager autolock, and signing-CA updates unless you explicitly allow them underrequest_body.swarm.*.POST /networks/create,POST /networks/*/connect, andPOST /networks/*/disconnectare inspected by default. Sockguard blocks custom drivers, swarm/ingress/attachable/config-only controls, custom IPAM, driver options, endpoint static IP/MAC/alias/driver options, and forced disconnects unless explicitly allowed underrequest_body.network.*.POST /containers/*/updateis inspected by default. Sockguard blocks restart-policy changes, resource controls, privileged mode, device changes, and capability/security-profile fields unless explicitly allowed underrequest_body.container_update.*.PUT /containers/*/archiveis inspected by default. Sockguard blocks unsafe target paths, tar traversal, setuid/setgid entries, device nodes, and escaping symlinks/hardlinks unless explicitly allowed underrequest_body.container_archive.*.POST /images/loadis inspected by default. Sockguard denies image archive imports unless theirmanifest.jsonrepo tags satisfyrequest_body.image_load.*registry policy, or untagged imports are explicitly allowed.POST /swarm/unlockandPOST /nodes/*/updateare inspected by default. Swarm unlock is denied unlessrequest_body.swarm.allow_unlock: true; node updates block role, availability, name, and unapproved label mutations unless allowed underrequest_body.node.*.POST /plugins/pull,POST /plugins/*/upgrade,POST /plugins/*/set, andPOST /plugins/createare inspected by default. Sockguard constrains plugin registries, privileges, assignment prefixes, local tarconfig.json, host mounts, device exposure, and capabilities unless you explicitly allow them underrequest_body.plugin.*.POST /plugins/createis inspected whether the upload arrives as a raw tar body or amultipart/form-dataenvelope — the multipart stream is parsed and the embeddedconfig.jsonis extracted before policy evaluation.- Oversized bodies on bounded JSON/tar inspectors are rejected with
413 Payload Too Largebefore the inspector decodes them, so a misbehaving or hostile client cannot tie up the filter or the Docker daemon with oversized payloads. insecure_allow_body_blind_writesis now reserved for the body-bearing writes Sockguard still cannot safely constrain, chiefly arbitrary exec without anallowed_commandsallowlist,POST /swarm/joinwithoutrequest_body.swarm.allowed_join_remote_addrs, and plugin setting writes without allowed assignment prefixes.insecure_allow_read_exfiltrationstaysfalseby default and must be set explicitly before broad read rules can expose raw archive/export or stream-style endpoints such asGET /containers/*/archive,GET /containers/*/export,GET /containers/*/logs,GET /containers/*/attach/ws,POST /containers/*/attach,GET /services/*/logs,GET /tasks/*/logs,GET /images/get, orGET /images/*/get.response.redact_container_env,response.redact_mount_paths,response.redact_network_topology, andresponse.redact_sensitive_datadefault totrue. Sockguard redacts workload env arrays across container/service/task/plugin reads, redacts host-path-bearing mount and device metadata across container/volume/task/service/plugin/system-usage reads, strips container/network/swarm/node topology from container/network/service/task/node/swarm/info/system-usage responses, and redacts higher-risk payload material such as configSpec.Data, service secret/config references, swarm join/unlock material, and swarm/node TLS metadata. Disable them only for trusted admin clients that truly need Docker's raw metadata.
Request Body Policy Reference
All request-body policy fields default to the safest value unless noted. List fields default to empty lists.
| Group | Fields | Default behavior |
|---|---|---|
container_create | allow_privileged, allow_host_network, allow_host_pid, allow_host_ipc, allowed_bind_mounts, allow_all_devices, allowed_devices, allow_device_requests, allowed_device_requests, allow_device_cgroup_rules, allowed_device_cgroup_rules, 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_pids_limit, allowed_seccomp_profiles, deny_unconfined_seccomp, allowed_apparmor_profiles, deny_unconfined_apparmor, allow_host_userns, allow_sysctls, required_labels, image_trust | Denies privileged containers, host network/PID/IPC/user namespaces, kernel sysctls, non-allowlisted bind sources, non-allowlisted device mappings, device requests, device cgroup rules, and non-allowlisted CapAdd entries. 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. image_trust adds cosign signature verification (keyed PEM keys or keyless Fulcio+Rekor) with warn (log + allow) or enforce (deny on failure) modes. VolumesFrom, host UTSMode, a custom CgroupParent, GroupAdd, and ExtraHosts are denied unconditionally with no opt-out field. |
exec | allow_privileged, allow_root_user, allowed_commands | Denies privileged/root exec and requires an argv allowlist before broad exec rules pass blind-write validation. |
image_pull | allow_imports, allow_all_registries, allow_official, allowed_registries | Denies fromSrc imports and allows Docker Hub official images by default (allow_official: true). |
build | allow_remote_context, allow_host_network, allow_run_instructions | Denies remote contexts, host-network builds, and Dockerfiles containing RUN. |
container_update | allow_privileged, allow_all_devices, allow_capabilities, allow_resource_updates, allow_restart_policy | Denies privileged/device/capability-like edits, resource-control changes, and restart-policy changes. |
container_archive | allowed_paths, allow_setid, allow_device_nodes, allow_escaping_links | Denies unsafe target paths, tar traversal, setuid/setgid entries, device nodes, and escaping links. |
image_load | allow_all_registries, allow_official, allowed_registries, allow_untagged | Allows Docker Hub official image tags by default (allow_official: true) and denies untagged archives unless opted in. |
volume | allow_custom_drivers, allow_driver_opts | Denies non-local drivers and driver options. |
network | allow_custom_drivers, allow_swarm_scope, allow_ingress, allow_attachable, allow_config_only, allow_config_from, allow_custom_ipam_drivers, allow_custom_ipam_config, allow_ipam_options, allow_driver_options, allow_endpoint_config, allow_disconnect_force | Denies custom drivers, swarm/ingress/attachable/config-only controls, custom IPAM, driver options, endpoint static config, and forced disconnects. |
secret / config | allow_custom_drivers, allow_template_drivers | Denies custom and template drivers. |
service | allow_host_network, allowed_bind_mounts, allow_all_registries, allow_official, allowed_registries | Denies host-network services and non-allowlisted bind mounts; allows Docker Hub official images by default (allow_official: true). |
swarm | allow_force_new_cluster, allow_external_ca, allowed_join_remote_addrs, allow_token_rotation, allow_manager_unlock_key_rotation, allow_auto_lock_managers, allow_signing_ca_update, allow_unlock | Denies unsafe init/join/update controls and swarm unlock. POST /swarm/join needs allowed_join_remote_addrs before broad join rules pass blind-write validation. |
node | allow_name_change, allow_role_change, allow_availability_change, allow_label_mutation, allowed_label_keys | Denies name, role, availability, and arbitrary label mutations while allowing the configured owner-label key for controlled claims. |
plugin | allow_host_network, allow_host_ipc, allow_host_pid, allow_all_devices, allowed_bind_mounts, allowed_devices, allow_all_capabilities, allowed_capabilities, allow_all_registries, allow_official, allowed_registries, allowed_set_env_prefixes | Denies host namespaces, non-allowlisted mounts/devices/capabilities, and non-official registries by default (allow_official: true). POST /plugins/*/set needs allowed_set_env_prefixes before broad set rules pass blind-write validation. |
mTLS Client Selectors
listen.tls.client_ca_file is the issuing trust root. To avoid trusting every leaf that CA can mint, narrow admission with one or more of the selector fields below. Different fields are ANDed; entries inside one field are ORed. An empty selector means "any verified client certificate issued by the configured CA is accepted".
| Field | Type | Matches |
|---|---|---|
common_names | []string | Exact CN on the verified leaf certificate |
dns_names | []string | DNS SAN entries on the verified leaf |
ip_addresses | []string | IP SAN entries on the verified leaf (not the TCP source IP) |
uri_sans | []string | URI SAN entries on the verified leaf (for example spiffe://...) |
public_key_sha256_pins | []string | Lowercase hex SHA-256 of the leaf SubjectPublicKeyInfo, optionally prefixed with sha256: |
listen:
tls:
client_ca_file: /run/secrets/sockguard/client-ca.pem
common_names: ["portainer"]
dns_names: ["portainer.internal"]
ip_addresses: ["10.0.5.12"]
uri_sans: ["spiffe://sockguard.test/workload/portainer"]
public_key_sha256_pins:
- 3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1bMalformed selectors (invalid IPs, malformed URIs, non-hex SPKI pins, pins that are not 32 bytes) are rejected at startup. The TLS handshake fails closed with no upstream connection when a CA-issued client certificate does not match the configured allowlist.
Per-Client ACLs And Profiles
clients.allowed_cidrs is a coarse TCP-client gate evaluated before the global rule set. Requests whose source IP is outside every configured CIDR are denied with 403 and never reach the health handler or the rule evaluator.
When clients.container_labels.enabled is true, Sockguard resolves bridge-network callers by source IP through the Docker API and looks for per-client allow labels on the calling container. Each clients.container_labels.label_prefix + <method> label is interpreted as a comma-separated Sockguard glob allowlist for that HTTP method:
com.sockguard.allow.get=/containers/json,/containers/*/json,/events
com.sockguard.allow.post=/containers/*/restartIf 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 profilematch.source_cidrs) is adequate against configuration drift but not hard isolation against an attacker who can influence container-to-IP mapping on a shared bridge. For caller identity in the security boundary, prefer a unix socket withclients.unix_peer_profilesanduids/gids—SO_PEERCREDcannot be spoofed. See the Known Limitations section in the Security guide for the full caveat.
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_profilesmatches the caller's remote IP against CIDRs in config order. -
clients.client_certificate_profilesmatches the verified mTLS leaf certificate in config order. Each assignment can matchcommon_names,dns_names,ip_addresses,uri_sans,spiffe_ids, andpublic_key_sha256_pins; different selector fields on the same assignment are ANDed, while entries inside one field are ORed. -
clients.unix_peer_profilesmatches unix-socket callers by peeruids,gids, andpids. Different selector fields on the same assignment are ANDed, while entries inside one field are ORed. -
clients.default_profileis the fallback when no specific assignment matches. -
Each profile has its own
rulesandrequest_bodypolicy, so one proxy can safely host a read-only dashboard, a container updater, and an admin UI at the same time. -
clients.client_certificate_profilesrequireslisten.tlsmutual TLS. -
clients.unix_peer_profilesrequireslisten.socket. -
response.visible_resource_labelsandclients.profiles[].response.visible_resource_labelsenforce read-side visibility on labeled list/events/inspect paths plus selected service/task log paths. Selectors use Docker label syntax (keyorkey=value), are ANDed together, and profile selectors are additive with the root response selectors. -
response.name_patternsandresponse.image_patternsadd glob-based selector axes that operate alongside label selectors (AND semantics across all axes). Within each axis, at least one pattern must match (OR semantics).name_patternsis matched againstNames[0]with the leading/stripped for containers, and against eachRepoTagsshort name (the part after the last/) for images.image_patternsis matched against the container'sImagefield and against each fullRepoTagsreference for images. Patterns use the same glob dialect as rule path patterns (*= no slash,**= any depth). Both knobs are supported at the per-profile level viaclients.profiles[*].response.name_patterns/image_patterns. Pattern filtering buffers the upstream list response in memory under an 8 MiB cap; a larger response is rejected with a502rather than buffered unbounded. Example — expose only traefik containers and images from a private registry to a specific client profile:response: name_patterns: - "traefik" clients: profiles: - name: registry-reader response: image_patterns: - "ghcr.io/myorg/**" -
Hidden resources disappear from
GET /containers/json,/images/json,/networks,/volumes,/services,/tasks,/secrets,/configs,/nodes, andGET /events, while hidden inspect/log-style targets such asGET /services/*,GET /services/*/logs,GET /tasks/*,GET /tasks/*/logs,GET /secrets/*,GET /configs/*,GET /nodes/*, andGET /swarmreturn404instead of exposing a policy-specific deny body.
Rate Limiting and Concurrency Caps
Each named profile can carry a limits block that enforces two independent
mechanisms. Both are per-profile and disabled by default — omitting limits
entirely (or omitting either sub-block) leaves that profile unthrottled.
Token-bucket rate limiting
limits.rate enforces a maximum sustained request rate using a token bucket.
The bucket refills continuously at tokens_per_second; its capacity is burst
(peak burst size). When the bucket is empty a request is denied immediately with
429 Too Many Requests, a JSON body containing retry_after_seconds, and a
Retry-After header.
tokens_per_second(required): refill rate; must be> 0.burst(optional, default= tokens_per_second): bucket capacity. Must be>= tokens_per_secondwhen explicitly set. Set to0to accept the default (no burst allowance beyond one token per interval).
clients:
profiles:
- name: interactive-operator
limits:
rate:
tokens_per_second: 8
burst: 16 # allow short bursts; sustain at 8 req/s
rules:
- match: { method: GET, path: "/containers/**" }
action: allow
- match: { method: "*", path: "/**" }
action: deny
- name: ci-agent
limits:
rate:
tokens_per_second: 100
burst: 200 # CI workflows issue large list calls at startup
rules:
- match: { method: "*", path: "/**" }
action: allowGuidance on starting values:
| Consumer type | Suggested tokens_per_second | Suggested burst | Rationale |
|---|---|---|---|
| Interactive operator | 8 | 16 | Human clicks; a burst allows rapid multi-request workflows |
| Monitoring / metrics scraper | 4 | 8 | Periodic polls; low sustained rate is sufficient |
| CI agent | 100 | 200 | Build pipelines issue large batches at startup |
| Read-only dashboard | 20 | 40 | Page loads trigger a handful of list calls together |
Endpoint cost weighting
By default every request withdraws one token from the bucket. limits.rate.endpoint_costs
lets you assign a higher per-request cost to expensive Docker operations
(build, image pull, exec) so they consume the budget proportionally to the
work they actually do. The base rate stays comfortable for normal traffic
while abusive POST /build floods are bounded.
Each entry has:
path(required): glob matched against the normalized request path (Docker API version prefix is stripped). Same dialect as filter rules.methods(optional): list of HTTP methods to restrict the rule to. Case-insensitive. Empty matches all methods.cost(required): number of tokens withdrawn on match. Must be>= 1and<= effective burst(a cost greater than burst can never be satisfied; startup fails closed if you misconfigure it).
Rules are evaluated in declaration order; first match wins. Unmatched
requests fall back to the default cost of 1.
clients:
profiles:
- name: ci-agent
limits:
rate:
tokens_per_second: 100
burst: 200
endpoint_costs:
# Image pull is the single most expensive operation Docker exposes;
# registry round-trips can pin upstream for minutes.
- path: /images/create
methods: [POST]
cost: 20
# Build context uploads + multi-stage assembly are similarly heavy.
- path: /build
methods: [POST]
cost: 10
# Exec creation is cheap but exec start spawns a process — tax it.
- path: /containers/*/exec
methods: [POST]
cost: 5
rules:
- match: { method: "*", path: "/**" }
action: allowWhen a request is throttled because of its weighted cost, the structured
audit record (sampled once per (client, reason) per second) carries the
cost attribute alongside the existing rate-limit fields so operators can
distinguish expensive-endpoint denials from base-rate denials.
Concurrency caps
limits.concurrency.max_inflight caps the number of simultaneously in-flight
requests per client. Once the cap is reached, additional requests are denied
immediately with 429 Too Many Requests and a {"reason":"concurrency_cap"}
body (no Retry-After, since release timing depends on concurrent traffic).
The cap is decremented when the response completes or the connection is
hijacked/closed — a request that is denied as rate-limited or by policy never
counts against the cap.
clients:
profiles:
- name: streaming-client
limits:
concurrency:
max_inflight: 16 # allow up to 16 simultaneous streaming connections
rules:
- match: { method: GET, path: "/events" }
action: allow
- match: { method: "*", path: "/**" }
action: denyCombining both mechanisms
Rate limiting and concurrency caps can coexist on the same profile. The rate check runs first; a request that passes the rate check but hits the concurrency cap is denied with a concurrency reason (not a rate reason). A request denied by either mechanism is never counted as in-flight.
limits:
rate:
tokens_per_second: 50
burst: 100
concurrency:
max_inflight: 32Priority / fairness controls
A noisy low-priority client can saturate the per-profile concurrency cap on its own profile, but it should not be able to starve unrelated higher-priority profiles. The system-wide priority-aware fairness gate solves this.
Enable it by setting a global cap and tagging each profile with a priority tier:
clients:
global_concurrency:
max_inflight: 100 # system-wide ceiling, shared across all profiles
profiles:
- name: admin
limits:
priority: high # admits up to 100% of the global cap
concurrency:
max_inflight: 50
- name: ci
limits:
priority: normal # admits up to 80% of the global cap (default)
concurrency:
max_inflight: 30
- name: scraper
limits:
priority: low # admits up to 50% of the global cap
concurrency:
max_inflight: 20Each priority tier has a hardcoded share of the global cap:
| Priority | Share | Floor at max_inflight: 100 |
|---|---|---|
low | 50% | denied above 50 in-flight |
normal (default) | 80% | denied above 80 in-flight |
high | 100% | denied above 100 in-flight |
When total in-flight crosses the floor for a request's priority, the request
is denied with 429 Too Many Requests and {"reason":"priority_floor"}.
Per-profile concurrency caps still apply on top — a low profile with
max_inflight: 20 is bounded by both its own cap and the 50% global floor.
The gate is checked before the per-profile concurrency cap, so a low-priority
request that hits the global floor never occupies a per-profile slot. Profiles
configured without limits.priority or without any limits block default to
normal, so a single client cannot evade the gate by skipping per-profile
configuration. priority is only honored when clients.global_concurrency is
set; otherwise it has no effect.
Validation
Sockguard fails at startup with a clear error if any of these invariants are violated (fail-closed, never silent-disable):
limits.rate.tokens_per_secondmust be> 0limits.rate.burstmust be>= tokens_per_secondor0(default)limits.rate.endpoint_costs[].pathmust be non-empty and compile as a valid globlimits.rate.endpoint_costs[].costmust be>= 1and<= effective burstlimits.rate.endpoint_costs[].methods[]must not contain empty stringslimits.concurrency.max_inflightmust be> 0clients.global_concurrency.max_inflightmust be> 0when setlimits.prioritymust be one oflow,normal, orhigh
Observability
When Prometheus metrics are enabled (metrics.enabled: true), two additional
series appear:
sockguard_throttle_requests_total{profile, reason_code, mode}— counter incremented on every denial by this subsystem.reason_codeis one ofrate_limit_exceeded,concurrency_cap, orpriority_floor.sockguard_inflight_requests{profile}— gauge tracking the current in-flight count for profiles that havemax_inflightconfigured.
Audit log events for throttle denials are sampled to the first occurrence of
each (client, reason) pair per second to avoid log-volume blowout under load.
The Prometheus counters are not sampled. priority_floor audit records
additionally carry priority, current_global_inflight, priority_threshold,
and global_max_inflight so operators can attribute denials to specific tiers.
Owner Label Isolation
Setting ownership.owner turns on per-proxy resource ownership isolation. Sockguard will:
- Add
ownership.label_key=ownership.ownertoPOST /containers/create,/networks/create,/volumes/create,/services/create,/services/*/update,/secrets/create,/configs/create,/nodes/*/update, and/swarm/update - Stamp service writes at both
LabelsandTaskTemplate.ContainerSpec.Labelsso downstream tasks inherit the same owner identity - Add the same label to
POST /buildvia thelabelsquery parameter so build-produced images can carry the owner identity too - Inject
label=<owner>filters into list, prune, and events requests, including/services,/tasks,/secrets,/configs, and/nodes(node.label=<owner>there), so responses only reveal resources owned by this proxy instance - Inspect target resources on individual
GET,POST,PUT, andDELETEpaths and deny cross-owner access to owned containers, images, networks, volumes, services, tasks, secrets, configs, nodes, and swarm state
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 can still be pulled and inspected without relabeling. Set it to false in tighter multi-tenant setups.
Rollout Modes
Each named profile can set a mode field that controls how Sockguard applies
denials from that profile's rules, request-body inspectors, ownership
isolation, visibility checks, client-ACL label policy, and rate-limit /
concurrency throttle gates:
| Mode | Behavior |
|---|---|
enforce | Default. Denied requests return 403; throttled requests return 429. |
warn | Requests that would be denied are allowed upstream. The structured audit record carries decision=would_deny. Deny and throttle counters fire with a mode label. |
audit | Same as warn — the request is served — but the log record is tagged decision=would_deny rather than producing a warning-level event. |
warn and audit exist for staged rollouts: add a tighter rule to a profile
in warn or audit mode, observe would_deny in dashboards and logs until
you are confident the deny rate is correct, then flip to enforce.
Pre-auth admission gates —
clients.allowed_cidrsCIDR checks and identity-lookup failures — stayenforceregardless 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: denyOnce the would-deny rate in dashboards matches expectations, set mode: enforce (or remove the field — enforce is the default).
The mode label on deny/throttle counters lets you compare blocked vs.
would-have-been-blocked volume side by side:
sum by (mode) (rate(sockguard_http_denied_requests_total[5m]))Hot Reload
Sockguard can reload policy at runtime without dropping connections.
reload:
enabled: true # default false; opt-in
debounce: 250ms # collapse burst of fs events into one reload (default "250ms")
poll_interval: "" # "" = off; opt-in stat fallback for inotify-unreliable backendsWhen reload.enabled: true:
- fsnotify file watch — Sockguard watches the loaded config file via Linux
inotify / macOS kqueue. A burst of editor-save events (vim's chmod + write +
rename + create sequence, for example) is debounced by
reload.debounceinto a single reload. - SIGHUP — sending
SIGHUPtriggers a reload. Withoutreload.enabled, SIGHUP terminates the process (Go's default); with reload on, SIGHUP never terminates. On Synology / DSM and other btrfs bind-mount backends,SIGHUPis the canonical reload trigger — inotify events on the host filesystem don't always propagate into the container, so the fsnotify watch may miss otherwise-valid edits. - Stat-based poll fallback — set
reload.poll_interval(typical5s–15s) to have Sockguard periodically re-stat the config file and fire a reload when its size, modification time, or inode have moved. Use this on filesystems where fsnotify drops events (Synology btrfs bind-mounts, some FUSE backends, NFS). The poll is off by default because regular Linux inotify and macOS kqueue cover the common cases reliably. - Atomic swap — on success, the running handler chain is replaced atomically. In-flight requests at the swap moment complete on the previous chain; new requests immediately route through the new one. No connections are dropped.
- Rollback on failure — if the new file is unreadable, the YAML is
malformed, the validator rejects the config, or a signature check fails, the
running policy is preserved untouched. The failure is logged with a
structured
result=reject_load|reject_validation|reject_immutable|reject_signaturekey 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 pathupstream.socket— upstream Docker socket pathlog.*— log level, format, and output sinkhealth.*— health endpoint path and watchdog configmetrics.*— metrics endpoint and pathadmin.*— admin config (includingadmin.listen.*)policy_bundletrust 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_* — is rebuilt and atomically applied on every
successful reload.
Reload outcomes
When metrics.enabled: true, two series track reload outcomes:
sockguard_config_reload_total{result}— counter with labelsok,reject_load,reject_validation,reject_immutable,reject_signature.sockguard_config_reload_last_success_timestamp_seconds— gauge; omitted from scrape output until the first successful reload.
Admin Listener
The admin endpoints (POST /admin/validate and GET /admin/policy/version)
can run on a separate listener, keeping admin traffic entirely off the main
Docker-API data plane.
admin:
enabled: true
listen:
socket: /var/run/sockguard-admin/admin.sock # operator-only permsOr loopback TCP:
admin:
enabled: true
listen:
address: 127.0.0.1:2376
tls:
cert_file: /run/secrets/sockguard/admin-cert.pem
key_file: /run/secrets/sockguard/admin-key.pem
client_ca_file: /run/secrets/sockguard/admin-ca.pemWhen admin.listen is unset, admin endpoints are served on the main listener
and inherit its CIDR allowlist, mTLS posture, and per-profile rate-limit /
concurrency caps. When set, Docker-API traffic and admin traffic are fully
isolated. See the Admin API page for the full endpoint reference and
recommended posture.
The admin.* block including admin.listen.* is immutable across hot reload
— restart is required to change the listener binding.
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 timeoutSign the config with cosign:
cosign sign-blob \
--bundle /etc/sockguard/sockguard.yaml.bundle \
/etc/sockguard/sockguard.yamlVerification 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_keylessentries specify the exact OIDC issuer URL and a subject SAN regex. The signing cert's chain is verified against Fulcio; whenrequire_rekor_inclusion: truea Rekor transparency-log entry is also required. Theverify_timeoutcaps 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(defaulttrue) controls the structured request log written through the normal logger output.log.audit.enabled(defaultfalse) enables a dedicated JSON audit stream with a stable schema: request ID, client request ID, trace ID, trace parent/span IDs, sampled flag, raw and normalized path, decision,reason_code, reason, matched rule, selected profile, flattened actor and transport identity fields, ownership context, and final HTTP status.
Access logs intentionally carry both path and normalized_path. path is the raw client URL path as received, including any Docker API version prefix, percent-encoded separators, or other client-controlled shape; keep it for forensic replay. normalized_path is the canonical path after Sockguard's policy normalization and is the field to use for SIEM grouping, alerting, rule dashboards, and allow/deny analysis. Audit events use the same split as raw_path and normalized_path.
Every request also carries trace correlation fields. If a caller sends a valid W3C traceparent, Sockguard preserves trace_id and trace_sampled, records the incoming parent as trace_parent_id, forwards a new proxy-local trace_span_id, and emits the same fields in access logs, audit events, and upstream reverse-proxy error logs. Invalid or absent trace context starts a fresh local trace and drops stale tracestate.
Audit logs use a separate sink from the access logger:
log.audit.format: currently onlyjsonis accepted.log.audit.output: same sink options as the main logger (stderr,stdout, orfile:/absolute/path).
Every audit event includes an ownership object with enabled, owner, and label_key. When ownership.owner is set, that owner identifier appears in every audit event, including requests that do not touch owned resources. Treat it as an operator-visible tenant or workload identifier, not a secret.
Audit events intentionally preserve both the proxy-generated canonical request_id and any caller-supplied client_request_id. Upstream proxy failures rewrite the audit reason_code to bounded terminal values such as upstream_socket_unreachable or upstream_response_rejected_by_policy so the final outcome is explicit even after a request was policy-allowed.
Rule Matching
Rules are evaluated in order. First match wins. If no rule matches, the request is denied.
Path Patterns
| Pattern | Matches | Does Not Match |
|---|---|---|
/containers/json | /containers/json, /v1.45/containers/json | /containers/abc |
/containers/* | /containers/json, /containers/abc123 | /containers/abc/start |
/containers/** | /containers/json, /containers/abc/start, /containers/abc/logs/stream | /images/json |
/** | Everything | Nothing |
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.enabled → SOCKGUARD_METRICS_ENABLED).
List-typed values accept comma-separated entries. Precedence is CLI flags >
env vars > YAML > built-in defaults.
The table below covers the most commonly used variables. 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
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_LISTEN_ADDRESS | listen.address | 127.0.0.1:2375 | TCP listener address. Loopback is allowed plaintext; non-loopback requires listen.tls or the two-flag legacy opt-in. |
SOCKGUARD_LISTEN_INSECURE_ALLOW_PLAIN_TCP | listen.insecure_allow_plain_tcp | false | First of two acknowledgments for plaintext non-loopback TCP (unencrypted transport). Must be paired with insecure_allow_unauthenticated_clients. |
SOCKGUARD_LISTEN_INSECURE_ALLOW_UNAUTHENTICATED_CLIENTS | listen.insecure_allow_unauthenticated_clients | false | Second acknowledgment for plaintext non-loopback TCP (any host that can reach the port can impersonate a client). Use only on private trusted networks; never expose to the host or Internet. |
SOCKGUARD_LISTEN_TLS_CERT_FILE | listen.tls.cert_file | (unset) | Path to the listener certificate. Required to enable mTLS. |
SOCKGUARD_LISTEN_TLS_KEY_FILE | listen.tls.key_file | (unset) | Path to the listener private key. Required to enable mTLS. |
SOCKGUARD_LISTEN_TLS_CLIENT_CA_FILE | listen.tls.client_ca_file | (unset) | CA bundle that verifies client certificates. Pair with selectors under listen.tls (common_names, dns_names, ip_addresses, uri_sans, public_key_sha256_pins) to narrow trust. |
SOCKGUARD_LISTEN_SOCKET | listen.socket | (unset) | Switches to a unix socket listener. Sockguard hardens the socket to mode 0600 and rejects broader modes. |
SOCKGUARD_UPSTREAM_SOCKET | upstream.socket | /var/run/docker.sock | Path to the real Docker daemon socket Sockguard proxies to. |
Logging
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_LOG_LEVEL | log.level | info | Operational log level: debug, info, warn, error. |
SOCKGUARD_LOG_FORMAT | log.format | json | Operational log format: json or text. |
SOCKGUARD_LOG_ACCESS_LOG | log.access_log | true | Emit one structured access-log line per request with method, path, decision, latency, and trace fields. |
SOCKGUARD_LOG_AUDIT_ENABLED | log.audit.enabled | false | Emit dedicated audit events with stable schema, separate from the operational log. |
SOCKGUARD_LOG_AUDIT_FORMAT | log.audit.format | json | Audit event format. JSON is required for SIEM ingestion; text exists for local debug only. |
SOCKGUARD_LOG_AUDIT_OUTPUT | log.audit.output | stderr | Audit sink: stderr, stdout, or a file path. File paths must already exist with writable permissions. |
Health and observability
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_HEALTH_ENABLED | health.enabled | true | Serve the /health liveness endpoint. |
SOCKGUARD_HEALTH_PATH | health.path | /health | Health endpoint path. Must start with / and differ from metrics.path / admin.path when those are set. |
SOCKGUARD_HEALTH_WATCHDOG_ENABLED | health.watchdog.enabled | false | Start an active upstream socket monitor. Logs reachable/unreachable transitions and feeds /health. |
SOCKGUARD_HEALTH_WATCHDOG_INTERVAL | health.watchdog.interval | 5s | Watchdog probe interval. Must be a positive Go duration; 1s–30s is typical. |
SOCKGUARD_METRICS_ENABLED | metrics.enabled | false | Serve Prometheus text metrics on the proxy listener. See the Observability page for the full metric reference. |
SOCKGUARD_METRICS_PATH | metrics.path | /metrics | Scrape path. Must start with / and differ from health.path when both endpoints are enabled. |
SOCKGUARD_ADMIN_ENABLED | admin.enabled | false | Serve the in-band POST <admin.path> candidate-config validation endpoint on the main listener. |
SOCKGUARD_ADMIN_PATH | admin.path | /admin/validate | Admin endpoint path. Must start with / and must differ from health.path and metrics.path when enabled. |
SOCKGUARD_ADMIN_POLICY_VERSION_PATH | admin.policy_version_path | /admin/policy/version | Path of the read-only policy-version endpoint. Must start with / and not collide with other endpoint paths. |
SOCKGUARD_ADMIN_MAX_REQUEST_BYTES | admin.max_request_bytes | 524288 | Hard cap on candidate-YAML body size. Bodies above this return 413. |
SOCKGUARD_ADMIN_LISTEN_SOCKET | admin.listen.socket | (unset) | Serve admin endpoints on a dedicated unix socket instead of the main listener. |
SOCKGUARD_ADMIN_LISTEN_ADDRESS | admin.listen.address | (unset) | Serve admin endpoints on a dedicated TCP listener (mTLS via admin.listen.tls.*). |
SOCKGUARD_RELOAD_ENABLED | reload.enabled | false | Watch the config file via fsnotify and reload on SIGHUP. Enabling this changes SIGHUP from "terminate" to "reload". |
SOCKGUARD_RELOAD_DEBOUNCE | reload.debounce | "250ms" | Coalesce a burst of fsnotify events into a single reload. Must be a valid Go duration string >= 0. |
SOCKGUARD_RELOAD_POLL_INTERVAL | reload.poll_interval | "" | Opt-in stat-based fallback for inotify-unreliable filesystems (Synology btrfs bind-mounts, some FUSE / NFS backends). Periodically re-stats the config file and fires a reload when size, mtime, or inode changes. Empty string disables polling. Must be a valid Go duration string >= 0. |
Response controls
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_RESPONSE_DENY_VERBOSITY | response.deny_verbosity | minimal | minimal returns only the generic deny message. verbose echoes method, path, and reason — dev only. |
SOCKGUARD_RESPONSE_REDACT_CONTAINER_ENV | response.redact_container_env | true | Replace workload env arrays with empty arrays on container/service/task/plugin reads. |
SOCKGUARD_RESPONSE_REDACT_MOUNT_PATHS | response.redact_mount_paths | true | Redact mount and host-device source paths on container/volume/task/service/plugin//system/df reads. |
SOCKGUARD_RESPONSE_REDACT_NETWORK_TOPOLOGY | response.redact_network_topology | true | Redact network IDs, attached addresses, remote managers, and node reachability on relevant reads. |
SOCKGUARD_RESPONSE_REDACT_SENSITIVE_DATA | response.redact_sensitive_data | true | Redact config payloads, swarm join/unlock and CA material, and node/swarm TLS metadata. |
Insecure opt-ins
These flags loosen Sockguard's defaults. Do not enable them without an explicit operational reason and a plan to revisit.
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_INSECURE_ALLOW_BODY_BLIND_WRITES | insecure_allow_body_blind_writes | false | Allow POST endpoints whose bodies Sockguard cannot inspect (currently arbitrary exec without an allowlist and plugin set without allowed prefixes). |
SOCKGUARD_INSECURE_ALLOW_READ_EXFILTRATION | insecure_allow_read_exfiltration | false | Allow rules that match raw archive/export, log/attach, and image-tarball-get endpoints. Tighten the rules instead whenever possible. |
Container-create body inspection
POST /containers/create is inspected by default; bodies that violate any of
the gates below are denied before the request reaches Docker.
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_PRIVILEGED | request_body.container_create.allow_privileged | false | Allow HostConfig.Privileged=true. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_NETWORK | request_body.container_create.allow_host_network | false | Allow HostConfig.NetworkMode=host. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_PID | request_body.container_create.allow_host_pid | false | Allow HostConfig.PidMode=host. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_IPC | request_body.container_create.allow_host_ipc | false | Allow HostConfig.IpcMode=host. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_BIND_MOUNTS | request_body.container_create.allowed_bind_mounts | empty | Comma-separated host-path prefixes allowed as bind sources. Named volumes always allowed. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_ALL_DEVICES | request_body.container_create.allow_all_devices | false | Allow any HostConfig.Devices host path. Prefer the allowlist below instead. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_DEVICES | request_body.container_create.allowed_devices | empty | Comma-separated host device paths allowed for HostConfig.Devices. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_DEVICE_REQUESTS | request_body.container_create.allow_device_requests | false | Escape hatch: allow all HostConfig.DeviceRequests without inspection. Prefer allowed_device_requests for least-privilege access. |
| (YAML-only) | request_body.container_create.allowed_device_requests | empty | Structured HostConfig.DeviceRequests allowlist. Each entry has driver (required), allowed_capabilities (list of capability sets; request sets must each be a subset of at least one), and optional max_count (-1 = all devices). Empty = deny all (default). Not settable via env var due to nested structure; configure in YAML. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_DEVICE_CGROUP_RULES | request_body.container_create.allow_device_cgroup_rules | false | Allow all HostConfig.DeviceCgroupRules without inspection. Prefer allowed_device_cgroup_rules for least-privilege access. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_DEVICE_CGROUP_RULES | request_body.container_create.allowed_device_cgroup_rules | empty | Comma-separated Docker cgroup rule strings (<type> <major>:<minor> <perms>) that HostConfig.DeviceCgroupRules entries must match. Wildcards (*) in major/minor are allowed in allowlist entries and match any value; request wildcards are only permitted when the allowlist entry also uses a wildcard at that position. Empty list = deny all (default). Example: c 1:3 rwm,c 226:* rwm. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_NO_NEW_PRIVILEGES | request_body.container_create.require_no_new_privileges | false | Require HostConfig.SecurityOpt to include no-new-privileges:true. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_NON_ROOT_USER | request_body.container_create.require_non_root_user | false | Require Config.User to be a non-zero UID or non-root username. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_READONLY_ROOTFS | request_body.container_create.require_readonly_rootfs | false | Require HostConfig.ReadonlyRootfs=true. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_DROP_ALL_CAPABILITIES | request_body.container_create.require_drop_all_capabilities | false | Require HostConfig.CapDrop to contain "ALL". |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_ALL_CAPABILITIES | request_body.container_create.allow_all_capabilities | false | Skip the HostConfig.CapAdd allowlist. With this off, only entries in allowed_capabilities are permitted. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_CAPABILITIES | request_body.container_create.allowed_capabilities | empty | HostConfig.CapAdd allowlist (case-insensitive, optional CAP_ prefix). |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_MEMORY_LIMIT | request_body.container_create.require_memory_limit | false | Require HostConfig.Memory > 0. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_CPU_LIMIT | request_body.container_create.require_cpu_limit | false | Require one of NanoCpus, CpuQuota, CpuPeriod, CpuShares > 0. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRE_PIDS_LIMIT | request_body.container_create.require_pids_limit | false | Require HostConfig.PidsLimit > 0. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_SECCOMP_PROFILES | request_body.container_create.allowed_seccomp_profiles | empty | If non-empty, HostConfig.SecurityOpt seccomp=<profile> must be in the list. Include default to allow the implicit Docker default. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_DENY_UNCONFINED_SECCOMP | request_body.container_create.deny_unconfined_seccomp | false | When no allowlist is set, deny seccomp=unconfined specifically. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOWED_APPARMOR_PROFILES | request_body.container_create.allowed_apparmor_profiles | empty | If non-empty, HostConfig.SecurityOpt apparmor=<profile> must be in the list. Include docker-default to accept the implicit default. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_DENY_UNCONFINED_APPARMOR | request_body.container_create.deny_unconfined_apparmor | false | When no allowlist is set, deny apparmor=unconfined specifically. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_HOST_USERNS | request_body.container_create.allow_host_userns | false | Allow HostConfig.UsernsMode=host. |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_ALLOW_SYSCTLS | request_body.container_create.allow_sysctls | false | Allow a non-empty HostConfig.Sysctls map (kernel parameter tuning). |
SOCKGUARD_REQUEST_BODY_CONTAINER_CREATE_REQUIRED_LABELS | request_body.container_create.required_labels | empty | Config.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_keys | empty | List of PEM public keys (ECDSA/RSA/ed25519) trusted to sign images. At least one key or keyless entry required when mode is not off. |
| (YAML-only) | request_body.container_create.image_trust.allowed_keyless | empty | List of Fulcio keyless identities; each entry has issuer (exact OIDC URL) and subject_pattern (regex matched against the cert SAN). |
| (YAML-only) | request_body.container_create.image_trust.require_rekor_inclusion | false | Require a Rekor transparency log entry for keyless bundles. |
| (YAML-only) | request_body.container_create.image_trust.verify_timeout | 10s | Per-verification network timeout. Must be a positive Go duration string (e.g. 5s, 30s). |
Other request-body inspectors
Comma-separated list values (ALLOWED_*, ALLOWED_BIND_MOUNTS, etc.) accept
multiple entries via the env var; e.g.
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOWED_REGISTRIES=ghcr.io,quay.io.
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_REQUEST_BODY_EXEC_ALLOW_PRIVILEGED | request_body.exec.allow_privileged | false | Allow privileged exec sessions. |
SOCKGUARD_REQUEST_BODY_EXEC_ALLOW_ROOT_USER | request_body.exec.allow_root_user | false | Allow exec sessions running as root. |
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOW_IMPORTS | request_body.image_pull.allow_imports | false | Allow fromSrc image imports. |
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOW_ALL_REGISTRIES | request_body.image_pull.allow_all_registries | false | Allow image pulls from any registry. Prefer ALLOWED_REGISTRIES instead. |
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOW_OFFICIAL | request_body.image_pull.allow_official | true | Allow Docker Hub official images (single-segment names). |
SOCKGUARD_REQUEST_BODY_IMAGE_PULL_ALLOWED_REGISTRIES | request_body.image_pull.allowed_registries | empty | Comma-separated allowed pull registries. |
SOCKGUARD_REQUEST_BODY_BUILD_ALLOW_REMOTE_CONTEXT | request_body.build.allow_remote_context | false | Allow POST /build with a remote context URL. |
SOCKGUARD_REQUEST_BODY_BUILD_ALLOW_HOST_NETWORK | request_body.build.allow_host_network | false | Allow POST /build with networkmode=host. |
SOCKGUARD_REQUEST_BODY_BUILD_ALLOW_RUN_INSTRUCTIONS | request_body.build.allow_run_instructions | false | Allow Dockerfiles containing RUN instructions. |
SOCKGUARD_REQUEST_BODY_CONTAINER_UPDATE_ALLOW_RESTART_POLICY | request_body.container_update.allow_restart_policy | false | Allow POST /containers/*/update to change the restart policy. |
SOCKGUARD_REQUEST_BODY_CONTAINER_ARCHIVE_ALLOWED_PATHS | request_body.container_archive.allowed_paths | empty | Comma-separated container paths allowed for PUT /containers/*/archive. |
SOCKGUARD_REQUEST_BODY_IMAGE_LOAD_ALLOW_UNTAGGED | request_body.image_load.allow_untagged | false | Allow POST /images/load for tarballs containing untagged images. |
SOCKGUARD_REQUEST_BODY_VOLUME_ALLOW_DRIVER_OPTS | request_body.volume.allow_driver_opts | false | Allow POST /volumes/create with custom DriverOpts. |
SOCKGUARD_REQUEST_BODY_NETWORK_ALLOW_ENDPOINT_CONFIG | request_body.network.allow_endpoint_config | false | Allow POST /networks/*/connect with non-empty endpoint config. |
SOCKGUARD_REQUEST_BODY_SECRET_ALLOW_TEMPLATE_DRIVERS | request_body.secret.allow_template_drivers | false | Allow POST /secrets/create with a template driver. |
SOCKGUARD_REQUEST_BODY_CONFIG_ALLOW_TEMPLATE_DRIVERS | request_body.config.allow_template_drivers | false | Allow POST /configs/create with a template driver. |
SOCKGUARD_REQUEST_BODY_SERVICE_ALLOWED_BIND_MOUNTS | request_body.service.allowed_bind_mounts | empty | Comma-separated host-path prefixes allowed as bind sources for POST /services/create. |
SOCKGUARD_REQUEST_BODY_SWARM_ALLOWED_JOIN_REMOTE_ADDRS | request_body.swarm.allowed_join_remote_addrs | empty | Comma-separated host:port join targets allowed for POST /swarm/join. |
SOCKGUARD_REQUEST_BODY_NODE_ALLOWED_LABEL_KEYS | request_body.node.allowed_label_keys | empty | Comma-separated label keys allowed in POST /nodes/*/update. |
SOCKGUARD_REQUEST_BODY_PLUGIN_ALLOWED_SET_ENV_PREFIXES | request_body.plugin.allowed_set_env_prefixes | empty | Comma-separated KEY= prefixes allowed for POST /plugins/*/set. |
Clients and ownership
| Variable | YAML field | Default | Description |
|---|---|---|---|
SOCKGUARD_CLIENTS_ALLOWED_CIDRS | clients.allowed_cidrs | empty | Comma-separated CIDR allowlist applied to all client transports including /metrics. |
SOCKGUARD_CLIENTS_CONTAINER_LABELS_ENABLED | clients.container_labels.enabled | false | Resolve client containers' labels via the upstream Docker socket for label-driven rules. |
SOCKGUARD_CLIENTS_CONTAINER_LABELS_LABEL_PREFIX | clients.container_labels.label_prefix | com.sockguard.allow. | Label-key prefix Sockguard considers when matching container-label rules. |
SOCKGUARD_OWNERSHIP_OWNER | ownership.owner | empty | Owner identifier stamped onto every audit event and used for ownership-scoped rules. |
SOCKGUARD_OWNERSHIP_LABEL_KEY | ownership.label_key | com.sockguard.owner | Label key Sockguard reads from container/image metadata to determine ownership. |
SOCKGUARD_OWNERSHIP_ALLOW_UNOWNED_IMAGES | ownership.allow_unowned_images | true | Allow operations on images that lack the ownership label. |
response.deny_verbosity controls how much metadata Sockguard includes in its own 403 JSON deny responses:
minimal(default): returns only the genericmessage. Never echoes the request method, path, or matched rule reason.verbose: returnsmessage,method,path(with/secrets/*and/swarm/unlockkeypaths redacted), andreason. Intended for rule authoring and dev work only — never a production default because it can leak request details to denied callers.
response.redact_container_env, response.redact_mount_paths, response.redact_network_topology, and response.redact_sensitive_data control response redaction for known protected Docker JSON response shapes. The redaction layer runs on successful body-bearing 2xx responses across request methods, not only GET 200, and fails closed with a generic 502 if a protected successful response cannot be parsed or sanitized safely. Non-success responses, HEAD responses, no-body statuses, non-protected paths, and streaming endpoints (logs, attach, events) pass through untouched. Streaming-style endpoints are gated by request-side rules and the read-side exfiltration guardrail instead. The toggles:
redact_container_env(defaulttrue): replaces workload env arrays with empty arrays on container, service, task, and plugin reads.redact_mount_paths(defaulttrue): redacts mount and host-device source paths on container, volume, task, service, plugin, and/system/dfreads.redact_network_topology(defaulttrue): redacts container, network, task, service, node, swarm,/info, and/system/dftopology details such as network IDs, attached addresses, remote managers, and node reachability addresses.redact_sensitive_data(defaulttrue): redacts config payload material, service secret/config references, swarm join/unlock and CA material, and node/swarm TLS metadata.
Security note — the redaction toggles do not cover stream bodies. These toggles operate only on structured JSON Docker responses. Hijacked / streaming endpoints (
GET /containers/*/logs,POST /containers/*/attach,GET /services/*/logs,GET /events, exec attach, image-build progress) are forwarded byte-for-byte; secrets a workload writes to its own stdout will reach an allowed caller. Gate those paths by rule, not by redaction. See the Known Limitations section in the Security guide for the full caveat.
Tecnativa Compatibility
For drop-in migration, sockguard accepts Tecnativa-style env vars:
CONTAINERS=1 # Allow /containers/** (GET/HEAD when POST=0)
IMAGES=1 # Allow /images/** (GET/HEAD when POST=0)
SERVICES=1 # Allow /services/** (GET/HEAD when POST=0)
SECRETS=0 # Deny /secrets/**
EVENTS=1 # Allow /events (default)
PING=1 # Allow /_ping (default)
VERSION=1 # Allow /version (default)
POST=0 # Read-only mode for section vars (default)
SOCKET_PATH=/var/run/docker.sock
LOG_LEVEL=warningCompat env vars only generate rules 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
Getting Started
Install sockguard with Docker Compose, Docker Run, or a release binary, and point your apps at the proxy socket.
Presets
Ready-made sockguard configs for drydock, Traefik, Portainer, Watchtower, Homepage, Homarr, Diun, Autoheal, GitHub Actions and GitLab runners, the CIS Docker Benchmark, and read-only dashboards.