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:
@@ -380,6 +380,44 @@ func (c *Client) Logs(ctx context.Context, ref string, tail int, since string) (
|
||||
args = append(args, "--since", since)
|
||||
}
|
||||
args = append(args, ref)
|
||||
all, err := c.logLines(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(all) > tail {
|
||||
all = all[len(all)-tail:]
|
||||
}
|
||||
// What the container printed of the secrets it was given is not shown (novox/hq issue 268): the
|
||||
// answer of this tool is read by agents and kept in their transcripts, which would make it a
|
||||
// second copy of the leak. Redacted before the lines are cut, so a cut never splits a value.
|
||||
answer := map[string]any{"container": ref}
|
||||
env, envErr := c.envOf(ctx, ref)
|
||||
known := secretsIn(env)
|
||||
redacted := 0
|
||||
for i, l := range all {
|
||||
var n int
|
||||
all[i], n = redact(l, known)
|
||||
redacted += n
|
||||
}
|
||||
if envErr != nil {
|
||||
answer["redaction"] = "only passwords inside URIs: the environment could not be read (" + envErr.Error() + ")"
|
||||
}
|
||||
if redacted > 0 {
|
||||
answer["redacted"] = redacted
|
||||
answer["leak"] = "this container printed secrets it was given; docker_secrets_in_logs names them (novox/hq issue 268)"
|
||||
}
|
||||
const most = 4096
|
||||
for i, l := range all {
|
||||
if len(l) > most {
|
||||
all[i] = l[:most] + "…"
|
||||
}
|
||||
}
|
||||
answer["lines"], answer["count"] = all, len(all)
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
// logLines runs `docker logs …` and answers both streams' lines merged in the order written.
|
||||
func (c *Client) logLines(ctx context.Context, args []string) ([]string, error) {
|
||||
r := c.Run(ctx, "docker", args...)
|
||||
program := "docker"
|
||||
if r.Status != 0 && r.Err == "" && c.UID != 0 && socketRefused.MatchString(r.Stderr) {
|
||||
@@ -392,16 +430,7 @@ func (c *Client) Logs(ctx context.Context, ref string, tail int, since string) (
|
||||
// Both streams carry the container's lines, each led by its timestamp, so they merge in order.
|
||||
all := append(lines(r.Stdout), lines(r.Stderr)...)
|
||||
sort.SliceStable(all, func(a, b int) bool { return all[a] < all[b] })
|
||||
if len(all) > tail {
|
||||
all = all[len(all)-tail:]
|
||||
}
|
||||
const most = 4096
|
||||
for i, l := range all {
|
||||
if len(l) > most {
|
||||
all[i] = l[:most] + "…"
|
||||
}
|
||||
}
|
||||
return map[string]any{"container": ref, "lines": all, "count": len(all)}, nil
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// Stat is one container's use of the machine now.
|
||||
|
||||
Reference in New Issue
Block a user