Giving an AI agent the Docker socket (/var/run/docker.sock) is giving it root on the host:
# With the raw socket, this is all it takes to own the machine:
docker run -v /:/host --privileged alpine chroot /host shDockGate puts a policy check in the middle. The agent talks to DockGate, never to the socket. Every operation is validated against a tiny YAML policy β and audited β before DockGate forwards approved requests to the real Docker Engine.
Because DockGate speaks the native Docker Engine API, every Docker client works unchanged β the docker CLI, the Python/Go SDKs, or raw HTTP. You only change the endpoint.
- β Default deny β nothing is allowed unless a rule says so
- π Create guardrails β block privileged mode, host networking, bind mounts, capabilities, and untrusted images
- π Full audit trail β every allow/deny logged as JSON
- πͺ Fail closed β malformed or unknown requests are denied, not forwarded
- πͺΆ Tiny & fast β a single static Go binary, one dependency
Pull the image, give it the socket + a policy, and point your client at it:
docker run -d --name dockgate \
-p 127.0.0.1:2375:2375 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$PWD/policy.yaml:/etc/dockgate/policy.yaml:ro" \
--group-add "$(getent group docker | cut -d: -f3)" \
amdadulbari/dockgate:latestexport DOCKER_HOST=tcp://127.0.0.1:2375
docker ps # β
allowed
docker run --rm nginx # β
allowed (nginx is on the allow-list)
docker run --privileged nginx # β denied: privileged containers are not permitted
docker run --rm ubuntu # β denied: ubuntu is not in allowed_images
docker network create test # β denied by defaultBuilding from source?
make runbuilds./bin/dockgateand listens on127.0.0.1:2375.
Deny everything, then allow only what your agent needs. This is a complete, read-only policy β the agent can look, but never touch:
default_action: deny
rules:
- name: "Read-only"
effect: allow
actions:
- system.ping
- container.list
- container.inspect
- container.logs
- image.listdocker ps, docker logs, and docker inspect succeed; docker run, docker stop, docker rm, and everything else return 403.
π Full policy reference β rules, matching, and container-create guardrails
A policy is a default action plus an ordered list of rules. The first rule whose actions match the request decides the outcome; if none match, default_action applies.
default_action: deny # deny | allow (what to do when no rule matches)
rules:
- name: "Restart existing containers"
effect: allow # allow | deny (default: allow)
actions: [container.start, container.stop, container.restart]
- name: "Create hardened containers from approved images"
effect: allow
actions: [container.create]
allowed_images: # only these image patterns may be created
- "nginx" # bare name β any tag of nginx
- "redis:*" # glob β any tag of redis
- "ghcr.io/acme/*" # any image from your registry org
deny_privileged: true # reject --privileged
deny_host_network: true # reject --network host
deny_host_pid: true # reject --pid host
deny_host_ipc: true # reject --ipc host
deny_bind_mounts: true # reject -v /host/path:...
denied_capabilities: [ALL]# reject any --cap-add
- name: "Never allow exec into containers"
effect: deny
actions: [container.exec, exec.start]Action patterns β each actions entry is an exact action (container.create), a category wildcard (container.*), or * (everything).
Allow + constraints β if a matching allow rule has constraints (the fields below), the request must satisfy all of them, or it is denied and the log records exactly which one failed. Constraints apply to container.create:
| Field | Effect |
|---|---|
allowed_images |
Glob allow-list. A bare name (nginx) matches any tag/digest; redis:* requires a tag; ghcr.io/acme/* matches an org. |
deny_privileged |
Reject privileged containers. |
deny_host_network / deny_host_pid / deny_host_ipc |
Reject sharing the host network / PID / IPC namespace. |
deny_bind_mounts |
Reject host-path bind mounts (named volumes still allowed). |
denied_capabilities |
Forbid --cap-add of the listed capabilities (e.g. SYS_ADMIN, or ALL). |
Ready-made policies live in examples/policies/: read-only.yaml and restart-only.yaml.
π§ Action reference β the canonical names you use in actions:
| Category | Actions |
|---|---|
system |
system.ping, system.version, system.info, system.df, system.events, system.auth |
container |
container.list, container.create, container.inspect, container.logs, container.top, container.stats, container.changes, container.export, container.archive.read, container.archive.write, container.start, container.stop, container.restart, container.kill, container.pause, container.unpause, container.rename, container.update, container.resize, container.wait, container.attach, container.exec, container.remove, container.prune |
exec |
exec.start, exec.resize, exec.inspect |
image |
image.list, image.pull, image.inspect, image.history, image.save, image.load, image.search, image.tag, image.push, image.remove, image.build, image.commit, image.prune |
network |
network.list, network.inspect, network.create, network.connect, network.disconnect, network.remove, network.prune |
volume |
volume.list, volume.inspect, volume.create, volume.remove, volume.prune |
swarm / service / secret / config |
swarm.init, swarm.join, swarm.leave, swarm.inspect, service.list, service.create, secret.list, secret.create, config.list, config.create |
Unlisted endpoints classify as unknown and hit default_action. container.exec + exec.start are how a client runs commands inside a container β deny them unless you have a specific reason not to.
βοΈ Configuration & signals
Every flag has a DOCKGATE_* environment fallback.
| Flag | Env | Default | Description |
|---|---|---|---|
--listen |
DOCKGATE_LISTEN |
127.0.0.1:2375 |
Where agents connect (host:port or unix:///path). Use 0.0.0.0:2375 in a container. |
--docker-socket |
DOCKGATE_DOCKER_SOCKET |
/var/run/docker.sock |
The real Docker Engine socket. |
--policy |
DOCKGATE_POLICY |
policy.yaml |
Path to the YAML policy. |
--audit-log |
DOCKGATE_AUDIT_LOG |
- (stdout) |
Audit destination: - for stdout, or a file path. |
Signals β SIGHUP reloads the policy live (a bad edit is rejected and the previous policy kept); SIGINT / SIGTERM shut down gracefully.
kill -HUP "$(pgrep dockgate)" # apply an edited policy.yaml without dropping connectionsπ Audit log β one JSON object per line
{"time":"2026-08-24T17:53:14Z","method":"GET","path":"/_ping","action":"system.ping","decision":"allow","rule":"Read-only","reason":"allowed by rule \"Read-only\""}
{"time":"2026-08-24T17:53:14Z","method":"POST","path":"/v1.43/containers/create","action":"container.create","decision":"deny","rule":"...","reason":"privileged containers are not permitted","image":"nginx","status":403}tail -f audit.log | jq 'select(.decision=="deny")' # watch only denialsDockGate is an authorization gateway: an agent can only perform the Docker operations your policy permits, and every attempt is recorded. For that promise to hold:
- Keep the socket off the agent. Mount
/var/run/docker.sockinto DockGate only, and put the agent on a network where DockGate is its sole route to Docker. - Protect the DockGate endpoint. Anyone who can reach it gets whatever the policy allows β bind it to loopback or a private network, and front it with auth/mTLS on untrusted networks.
allowdeliberately.image.build,container.exec, andcontainer.updatecan be used to escalate; the shipped policy denies them by default.
Found a vulnerability? See SECURITY.md.
make test # unit + end-to-end tests
make race # tests under the race detector
make build # build ./bin/dockgate
make docker # build the container imageSmall and dependency-light (only gopkg.in/yaml.v3):
cmd/dockgate entrypoint, server lifecycle, signals
internal/dockerapi request β canonical action; container-create parsing
internal/policy policy loading + evaluation (the security core)
internal/audit structured JSON-lines audit logging
internal/proxy reverse proxy to the Docker socket
internal/gateway the HTTP handler tying it together
Contributions welcome β see CONTRIBUTING.md.
If DockGate is useful to you, please β star the repo β it genuinely helps.