Repository navigation
fix(socket-proxy): CONTAINERS=1 allows Alloy's three paths, not archive and export - #1156
Merged
Merged
Conversation
…ve and export
tecnativa/docker-socket-proxy's template matches CONTAINERS=1 as a prefix on
/containers, so GET /containers/{id}/archive, /export, /top and /changes were
allowed. POST=0 does not stop them, and the daemon serves them as root
(Tecnativa/docker-socket-proxy#182). All four answered 200 on prometheus.
Every proxy now runs stacks/observability/docker-socket-proxy/haproxy.cfg.
It is upstream v0.5.0's template with that rule narrowed to
/containers/json, /containers/{id}/json and /containers/{id}/logs, which is
all the proxy's request log shows its clients sending. This covers the six
compose stacks, and deploy-agent.sh's alloy-socket-proxy through a volume.
reload-config.sh restarts the proxy before Alloy, so a cfg-only change lands.
The checks:
- scripts/check_socket_proxy.sh boots the pinned image on the file in CI and
validate. It asserts that the allowed paths answer and that the refused
ones, `..` and encoded-slash spellings included, are denied by the proxy
itself. It also requires upstream's template to be unchanged under ours.
- check_compose_health.py fails a proxy that does not mount the config or is
not started on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The checker’s hard-coded Docker API 1.56 probes fail on otherwise supported pre-29 daemons.
2 open findings
What changed in this PR
Hardens Alloy’s Docker socket proxies by restricting CONTAINERS=1 to required endpoints.
Changes:
- Adds and deploys a narrowed HAProxy configuration across all proxies.
- Restarts proxies during configuration reloads.
- Adds validation, CI coverage, and security documentation.
| File | Description |
|---|---|
stacks/soc/compose.yaml |
Mounts and runs the hardened config. |
stacks/sensor/compose.yaml |
Mounts and runs the hardened config. |
stacks/observability/docker-socket-proxy/haproxy.cfg |
Defines narrowed container ACLs. |
stacks/observability/compose.yaml |
Activates the hardened config. |
stacks/lab/compose.yaml |
Mounts and runs the hardened config. |
stacks/bloodhound/compose.yaml |
Mounts and runs the hardened config. |
stacks/analyst/compose.yaml |
Mounts and runs the hardened config. |
scripts/validate.sh |
Adds proxy validation. |
scripts/reload-config.sh |
Restarts the proxy before Alloy. |
scripts/README.md |
Documents the checker. |
scripts/deploy-agent.sh |
Deploys the config to agent proxies. |
scripts/check_socket_proxy.sh |
Tests allowed and refused API paths. |
scripts/check_compose_health.py |
Enforces proxy mount and command settings. |
Makefile |
Adds the proxy-check target. |
docs/security.md |
Documents the exposure and mitigation. |
docs/changelog.md |
Records the security correction. |
.github/workflows/ci.yml |
Runs the checker in CI. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… v1.56
CI's runner daemon is older than prometheus's, so the versioned requests in
check_socket_proxy.sh got a 400 from the daemon there ("client version too
new"). That is not a proxy decision. The check now reads ApiVersion from
/version through the scratch proxy and builds the /v1.NN/ spellings from it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oad-config.sh does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


What was open
Every Alloy reaches the Docker API through
tecnativa/docker-socket-proxywithCONTAINERS=1,POST=0. In the image's template,CONTAINERS=1is a prefix match on/containers. So it also allows four GETs thatPOST=0does not stop:/containers/{id}/archive/containers/{id}/export/containers/{id}/top/containers/{id}/changesThe daemon serves them as root (Tecnativa/docker-socket-proxy#182; the fix, Tecnativa/docker-socket-proxy#183, is unmerged). That undoes what #188's
cap_dropbuys: a client trusted only to list containers can read through the proxy what it cannot read through its own mounts.Measured on prometheus's live proxy, with no file content transferred (HEAD on
archivereturns only a stat header, and every other body went to/dev/null): all four answered 200.Found while triaging #1005.
What the clients actually use
From the proxy's own HAProxy request log, since its 2026-10-07 start:
/containers/json,/containers/{id}/jsonand/containers/{id}/logs/networks,/version,/info,/_pingand/eventsNothing else.
The fix
stacks/observability/docker-socket-proxy/haproxy.cfg. It is upstream v0.5.0's template byte for byte, except:/containerspaths, anchored, matched on the url-decoded path.All the env flags keep their meaning, and the image pin is unchanged.
All six compose proxies (observability, lab, sensor, soc, bloodhound, analyst) mount that directory read-only and run
haproxy -fon it. A directory mount, so a git checkout can't leave a stale inode behind (A deploy that changes a mounted config file but not compose.yaml leaves the container on the old one #355).deploy-agent.shstages the file, checksums it, and mounts it intoalloy-socket-proxythrough a volume, for oracle and trinity.reload-config.shrestarts the proxy before Alloy, so a later change tohaproxy.cfgalone still lands. A restart, not SIGUSR2: a rejected reload keeps the old workers and says so only in the log.How it's checked
scripts/check_socket_proxy.shis new, and runs in CI, invalidate.shand asmake check-socket-proxy. It boots the pinned image on the committed config, with observability's env and the real socket, read-only. Then:<NOSRV>. They are:archive,export,top,changes,statsandattach;json/../archive,logs/../archive,%2Farchive,%252Farchiveandjson%2F..%2Farchive;/containers;POST /containers/create.Run against upstream's own template, the same check fails: every refused path reaches the daemon, and
exportstreams 43 MB.%2Farchiveandjson/../archiveget through too, because the daemon decodes%2Fand 301-redirects... That's why the rule matches on the decoded path, and the id class excludes/,%and?.check_compose_health.pynow fails a socket proxy that doesn't mount the config read-only, or isn't started on it (5 new self-tests). Either alone silently falls back to the image's template.Deploying
make up STACK=<stack>by hand, from the Mac (VLAN 30).scripts/deploy-agent.sh.After each deploy, check that the Alloy on that host still lists containers and ships container logs.
🤖 Generated with Claude Code