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
+24 -2
View File
@@ -71,8 +71,9 @@ func tools(c *Client) []stdio.Tool {
},
},
{
Name: "docker_logs",
Description: "The last lines one container wrote, both streams merged in order, each with its timestamp (default 200, at most 2000 lines; a line is cut at 4 KiB).",
Name: "docker_logs",
Description: "The last lines one container wrote, both streams merged in order, each with its timestamp (default 200, at most 2000 lines; a line is cut at 4 KiB). " +
"A secret the container was given that it printed is shown as [redacted: <name>], and so is a password inside a URI.",
Input: map[string]any{
"container": containerArg,
"lines": map[string]any{"type": "integer", "description": "how many lines from the end (default 200, at most 2000)"},
@@ -90,6 +91,27 @@ func tools(c *Client) []stdio.Tool {
return c.Logs(ctx, ref, n, optional(args, "since"))
},
},
{
Name: "docker_secrets_in_logs",
Description: "Which containers printed a secret they were given into their own log — by container, module and the secret's name, never its value: " +
"each one's recent lines compared with the values of its environment named like a secret and the passwords in its URIs, and any URI carrying a password. " +
"A finding is a secret to rotate once the program stops printing it (novox/hq issue 268).",
Input: map[string]any{
"held": map[string]any{"type": "string", "enum": []string{"all", "mesh", "other"}, "description": "whose: the mesh's (default), every container, or the others"},
"lines": map[string]any{"type": "integer", "description": "how many lines from the end of each log (default 5000, at most 50000)"},
},
Run: func(args map[string]any) (any, error) {
n, err := bounded(args, "lines", 5000, 50000)
if err != nil {
return nil, err
}
held := optional(args, "held")
if held == "" {
held = "mesh"
}
return c.SecretsInLogs(ctx, held, n)
},
},
{
Name: "docker_stats",
Description: "What the running containers use now — CPU, memory, network and disk I/O, processes — the heaviest by memory first; or one container's.",