docker: find and hide secrets a container printed into its log (hq issue 268)

letta printed two passwords into its log for weeks and nothing noticed,
and docker_logs handed them to whoever asked. docker_secrets_in_logs
compares each container's recent lines with the secret-named values of
its environment, the passwords in its URIs, and any URI carrying a
password, and names what it found by container, module and variable -
never the value. docker_logs redacts the same values before answering.
This commit is contained in:
jochen
2026-10-06 02:13:42 +02:00
parent f9f27d4878
commit bbb67e41a0
7 changed files with 541 additions and 13 deletions
+28 -1
View File
@@ -127,7 +127,8 @@ A failure is an error naming how it failed, never an empty answer.
|---|---|---|
| `docker_list` | r | every container: image, state, health, restarts, ports, mounts, compose project, `mesh_held`; filter by owner, state or name |
| `docker_inspect` | r | one container whole, **environment values left out** (names kept) |
| `docker_logs` | r | the last lines of both streams, merged in order, with timestamps (default 200, at most 2000) |
| `docker_logs` | r | the last lines of both streams, merged in order, with timestamps (default 200, at most 2000); **a secret the container printed is shown as `[redacted: <name>]`** |
| `docker_secrets_in_logs` | r | which containers printed a secret they were given, **by name, never by value** (below) |
| `docker_stats` | r | CPU, memory, I/O and process count per running container, heaviest first |
| `docker_start` / `docker_stop` / `docker_restart` | a | one container. On a mesh-held one, the answer says the host restores its declared state at its next apply |
| `docker_top` | r | the processes inside one container |
@@ -142,6 +143,31 @@ A failure is an error naming how it failed, never an empty answer.
| `docker_problems` | r | unhealthy, restarting, dead, killed for memory, failed, or restarted five times or more |
| `docker_ports` | r | every published port, and the containers on the host's network |
## Secrets in a container's own log (hq issue 268)
Software prints what it is given: a server announcing its password as it starts, a startup script
echoing the database URI it connects with. The container's log is then a copy of the secret, held by
whoever reads it — this bundle's `docker_logs` among them. `docker_secrets_in_logs` reads the last
lines of each container's log (the mesh's by default, 5000 lines each, at most 50000) and compares
them with:
- the values of the container's environment whose names say they are secrets (`PASSWORD`, `SECRET`,
`TOKEN`, `API_KEY`, …; not a path, a URL, a number or a switch), as given and URL-encoded;
- the password inside any URI its environment holds;
- the shape `scheme://user:password@`, anywhere in a line, whatever the source — a password a program
already masked (`***`) is not one.
A finding names the container, the module, the assignment and the secret's variable, with how many
lines carry it and the first and last time. **It never carries the value or the line.** A secret
delivered only as a mounted file, never in the environment, is not known here — the bundle runs as the
operator account, which cannot read the host's 0600 files — and is caught only inside a URI.
`docker_logs` redacts the same values before it answers, because what it answers is read by agents
and kept in their transcripts. Its answer says how many it redacted, and points here.
A finding is a secret to rotate once the program stops printing it; recreating the container drops
its old log (the runtime's file goes with the container).
## Tests
```
@@ -158,6 +184,7 @@ The tests run against a fake runner and cover:
- the restore note on a mesh-held act;
- prune being a dry run by default and never reaching a volume, a mesh container or `--volumes`;
- the log merge;
- a printed secret found by name and never answered by value, in the scan and in `docker_logs`;
- size parsing;
- what the daemon has not yet taken;
- event filtering;