Repository navigation
harden(alloy): reach the Docker API through a read-only proxy (#193) - #358
Merged
Merged
Conversation
The socket is no longer mounted into Alloy. `:ro` on a socket mount is close to decorative — it applies to the socket FILE, not the API behind it, and anything that can talk to that API can POST /containers/create with `/` bound read-write, which is root on this host and the age key with it. #188 took Alloy's capabilities and closed the direct read off /rootfs; this closes the larger half. PROVEN, NOT ASSUMED. Through the proxy: POST /containers/create -> 403 Forbidden GET /containers/json -> works THE ALLOWLIST WAS WRONG ON THE FIRST ATTEMPT, AND ONLY MEASUREMENT FOUND IT. discovery.docker calls /networks to compute the network labels it puts on every target, so with NETWORKS: 0 the component failed with Unable to refresh target groups ... error while computing network labels: 403 Forbidden — Request forbidden by administrative rules container log collection stopped entirely, and the agent stayed healthy while cAdvisor carried on unaffected — loki_source_docker_target_entries_total sat at 0 and nothing else looked wrong. That is the #62/#63 shape #193 predicted for a too-narrow allowlist, and the reason it insisted on before/after numbers rather than a reading of the docs. With NETWORKS: 1 the flow resumed: 864 entries streamed, 450 lines in two minutes. Before and after on this host: container_last_seen 10 -> 11 (the proxy is itself a container) cadvisor_version_info 1 -> 1 named containers unchanged apart from the proxy arriving and two of my own throwaway test containers leaving ONE CONTAINER IS DELIBERATELY NOT read_only, the exception #186 asked to have recorded rather than skipped. The image generates haproxy.cfg from its environment at every start: can't create /usr/local/etc/haproxy/haproxy.cfg: Read-only file system and a tmpfs over that directory shadows the template the image ships: sed: /usr/local/etc/haproxy/haproxy.cfg.template: No such file or directory Both observed by running it. It still drops every capability, takes no-new-privileges, holds no secret and publishes nothing. SECURITY.md and docs/security.md say plainly that this MOVES the trust boundary rather than removing it: the proxy holds the socket now, and what it buys is that Alloy — network listener, rootfs mount, largest surface in the stack — has no path to POST. oracle still mounts the socket directly. docker.alloy reads DOCKER_API and falls back to the socket when unset, so one config file still deploys to every host and oracle keeps working until it gets a proxy of its own. Service count claims move six/seven -> eight in README.md, the stack README and four compose comments; architecture.md gains the proxy in its host and ports tables. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…proxy-193 # Conflicts: # docs/roadmap.md
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.
Closes #193.
The socket is no longer mounted into Alloy.
:roon a socket mount is close todecorative — it applies to the socket file, not the API behind it, and anything
that can talk to that API can
POST /containers/createwith/boundread-write, which is root on this host and the age key with it. #188 took Alloy's
capabilities and closed the direct read off
/rootfs; this closes the largerhalf.
Proven, not assumed
The allowlist was wrong on the first attempt, and only measurement found it
discovery.dockercalls/networksto compute the network labels it puts onevery target. With
NETWORKS: 0:Container log collection stopped entirely, and the agent stayed healthy
while cAdvisor carried on unaffected —
loki_source_docker_target_entries_totalsat at 0 and nothing else looked wrong. That is exactly the #62/#63 shape #193
predicted for a too-narrow allowlist, and precisely why it insisted on
before/after numbers rather than a reading of the docs.
With
NETWORKS: 1the flow resumed: 864 entries streamed, 450 lines in twominutes.
Before and after, on this host
container_last_seencadvisor_version_infoOne container is deliberately not
read_onlyThe exception #186 asked to have recorded rather than skipped. The image
generates
haproxy.cfgfrom its environment at every start:and a tmpfs over that directory shadows the template the image ships:
Both observed by running it, not predicted. It still drops every capability,
takes
no-new-privileges, holds no secret and publishes nothing.What this buys, stated as #193 asked
SECURITY.mdanddocs/security.mdboth say plainly that this moves thetrust boundary rather than removing it. The proxy holds the socket now. What it
buys is that Alloy — network listener, read-only rootfs mount, largest attack
surface in the stack — has no path to
POST. It is not "the socket is now safe".oracleis not coveredIts agent still mounts the socket directly.
docker.alloyreadsDOCKER_APIandfalls back to the socket when unset, so one config file still deploys to every
host and
oraclekeeps working unchanged until it gets a proxy of its own —rather than breaking collection on a host this change does not reach.
Notes
make pin-digestscanre-resolve it and Dependabot can bump it.
README.md, the stackREADME and four compose comments;
architecture.mdgains the proxy in itshost and ports tables.
directly, then restored it to clean — so the running stack is temporarily
ahead of
mainuntil this merges and converge reconciles them.make validatepasses.🤖 Generated with Claude Code