Follow-up to #188 / #192, which took every capability off Alloy. That closed the
direct read of ~/.config/sops/age/keys.txt off the /rootfs mount. It did not
close the larger half.
Alloy still mounts the Docker socket:
- /var/run/docker.sock:/var/run/docker.sock:ro
:ro is close to decorative here. It applies to the socket file, not to the
API behind it. Anything that can talk to that API can:
POST /containers/create {"HostConfig":{"Binds":["/:/host"],"Privileged":true}}
which is root on the host, and the age key with it. So SECURITY.md's statement
that file permissions are all that protect the plaintext artefacts holds for
every process on this host except a compromised Alloy.
Why Alloy needs the socket at all
Three components, and none of them need write:
| component |
uses |
prometheus.exporter.cadvisor |
container names and labels — every rule in containers.rules.yaml filters on name!="" |
discovery.docker |
enumerates containers to tail |
loki.source.docker |
streams stdout/stderr |
So the shape of the fix is an allowlisting proxy in front of the socket,
permitting roughly GET /version, /info, /containers/json,
/containers/{id}/json, /containers/{id}/logs, /events, and nothing else.
Alloy then talks to the proxy over the compose network and never sees the socket.
What makes this its own change rather than part of #188
Note on what a proxy does and does not buy
It moves the trust boundary rather than removing it — the proxy container holds
the socket instead. What it buys is that Alloy, which is the component with a
network listener, a rootfs mount and the largest attack surface in the stack, no
longer has a path to POST. That is worth having, and it should be described
that way rather than as "the socket is now safe".
Done when
Follow-up to #188 / #192, which took every capability off Alloy. That closed the
direct read of
~/.config/sops/age/keys.txtoff the/rootfsmount. It did notclose the larger half.
Alloy still mounts the Docker socket:
- /var/run/docker.sock:/var/run/docker.sock:ro:rois close to decorative here. It applies to the socket file, not to theAPI behind it. Anything that can talk to that API can:
which is root on the host, and the age key with it. So
SECURITY.md's statementthat file permissions are all that protect the plaintext artefacts holds for
every process on this host except a compromised Alloy.
Why Alloy needs the socket at all
Three components, and none of them need write:
prometheus.exporter.cadvisorcontainers.rules.yamlfilters onname!=""discovery.dockerloki.source.dockerSo the shape of the fix is an allowlisting proxy in front of the socket,
permitting roughly
GET /version,/info,/containers/json,/containers/{id}/json,/containers/{id}/logs,/events, and nothing else.Alloy then talks to the proxy over the compose network and never sees the socket.
What makes this its own change rather than part of #188
compose.yaml, be digest-pinned, and be something Dependabot bumps andmake pin-digestscan re-digest.the allowlist is missing an endpoint, cAdvisor loses its
namelabels or thelog discovery returns nothing, and the agent stays healthy while producing
less. Screenshot capture, and three faults it uncovered #62 and ContainerHighMemory cannot fire — no service sets a memory limit #63 are both that shape.
else's PR. fix(observability): take every capability off Alloy (#188) #192 has a usable template: distinct container names, cAdvisor
series count,
loki_source_filebyte counters, and aloggermarker writtenand then found in Loki.
Note on what a proxy does and does not buy
It moves the trust boundary rather than removing it — the proxy container holds
the socket instead. What it buys is that Alloy, which is the component with a
network listener, a rootfs mount and the largest attack surface in the stack, no
longer has a path to
POST. That is worth having, and it should be describedthat way rather than as "the socket is now safe".
Done when
compose.yamlafter, not assumed
docs/security.mdandSECURITY.mdupdated — both currently say this istracked and not done