Files
mesh-catalog/modules/docker/cmd/docker-tools/secrets_test.go
T
jochen bbb67e41a0 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.
2026-10-06 02:13:42 +02:00

157 lines
5.7 KiB
Go

package main
import (
"context"
"encoding/json"
"strings"
"testing"
)
// The shape of the leak in hq issue 268: a server password announced at start and a database URI
// echoed whole. The values are made up for the test.
const (
serverPassword = "Zq8-server-pass_word"
dbPassword = "Db_pa55-word-xyz"
)
var lettaEnv = []string{
"LETTA_PG_URI=postgresql://letta@db:5432/letta",
"LETTA_SERVER_PASSWORD=" + serverPassword,
"OTHER_URI=postgresql://other:" + dbPassword + "@db:5432/other",
"PGPASSFILE=/run/secrets/pgpass",
"SECURE=true",
"AUTH_URL=https://id.example/auth",
"TOKEN_TTL=3600",
"POSTGRES_PASSWORD=letta",
"TZ=Europe/Brussels",
}
func TestOnlyValuesNamedAsSecretsAndPasswordsInURIsAreKnown(t *testing.T) {
got := map[string]string{}
for _, s := range secretsIn(lettaEnv) {
got[s.Name] = s.Value
}
if got["LETTA_SERVER_PASSWORD"] != serverPassword || got["OTHER_URI (the password in its URI)"] != dbPassword {
t.Fatalf("missed a secret: %v", keys(got))
}
for _, not := range []string{"LETTA_PG_URI (the password in its URI)", "PGPASSFILE", "SECURE", "AUTH_URL", "TOKEN_TTL", "POSTGRES_PASSWORD", "TZ"} {
if _, ok := got[not]; ok {
t.Errorf("%s taken for a secret", not)
}
}
}
func scanMachine(logs string) *fake {
env, _ := json.Marshal(lettaEnv)
return (&fake{}).
on("docker ps --all --quiet --no-trunc", Ran{Stdout: "aaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbb\n"}).
on("docker container inspect aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbb", Ran{Stdout: "[" + held + "," + stray + "]"}).
on("docker container inspect --format {{json .Config.Env}}", Ran{Stdout: string(env) + "\n"}).
on("docker logs --timestamps --tail 5000 aaaaaaaaaaaa", Ran{Stdout: logs})
}
const leakyLog = "2026-10-05T19:16:40Z External Postgres configuration detected, using postgresql://letta@db:5432/letta\n" +
"2026-10-05T19:16:41Z Creating engine postgresql://other:" + dbPassword + "@db:5432/other\n" +
"2026-10-05T19:16:42Z ▶ Using secure mode with password: " + serverPassword + "\n" +
"2026-10-05T19:16:43Z connecting to mongodb://app:s3cr3t-elsewhere@mongo:27017\n" +
"2026-10-05T19:16:44Z Using database: postgresql://letta:***@db:5432/letta\n" +
"2026-10-05T19:20:42Z ▶ Using secure mode with password: " + serverPassword + "\n"
func TestAScanNamesEachPrintedSecretAndNeverItsValue(t *testing.T) {
got, err := client(scanMachine(leakyLog), 1000).SecretsInLogs(context.Background(), "mesh", 5000)
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(got)
for _, v := range []string{serverPassword, dbPassword, "s3cr3t-elsewhere"} {
if strings.Contains(string(raw), v) {
t.Fatalf("the answer carries a secret's value: %s", raw)
}
}
leaks := got["leaks"].([]Leak)
byName := map[string]Leak{}
for _, l := range leaks {
byName[l.Secret] = l
if l.Container != "mesh-web" || l.Module != "hello-web" || l.HeldBy != "hello-web.server" {
t.Errorf("finding not named by its container and module: %+v", l)
}
}
if l := byName["LETTA_SERVER_PASSWORD"]; l.Lines != 2 || l.First != "2026-10-05T19:16:42Z" || l.Last != "2026-10-05T19:20:42Z" {
t.Errorf("server password: %+v", l)
}
if l := byName["OTHER_URI (the password in its URI)"]; l.Lines != 1 {
t.Errorf("password in a URI from the environment: %+v", l)
}
if l := byName["a password inside a URI (not from its environment)"]; l.Lines != 1 {
t.Errorf("a URI's password by its shape (and not a masked one): %+v", l)
}
if len(leaks) != 3 || got["containers_scanned"] != 1 {
t.Errorf("leaks %d, scanned %v (only the mesh's)", len(leaks), got["containers_scanned"])
}
}
func TestACleanLogIsSaidToBeClean(t *testing.T) {
got, err := client(scanMachine("2026-10-05T19:16:40Z started\n"), 1000).SecretsInLogs(context.Background(), "mesh", 5000)
if err != nil {
t.Fatal(err)
}
if got["count"] != 0 || !strings.HasPrefix(got["verdict"].(string), "no container") {
t.Errorf("%v", got)
}
}
func TestAContainerWhoseLogCannotBeReadIsSaidSoRatherThanCalledClean(t *testing.T) {
f := scanMachine("")
f.rules = append([]rule{{"docker logs", Ran{Status: 1, Stderr: "Error response from daemon: configured logging driver does not support reading\n"}}}, f.rules...)
got, err := client(f, 1000).SecretsInLogs(context.Background(), "mesh", 5000)
if err != nil {
t.Fatal(err)
}
if len(got["unread"].([]map[string]string)) != 1 || got["containers_scanned"] != 0 {
t.Errorf("%v", got)
}
}
func TestLogsRedactWhatTheContainerPrintedOfItsSecrets(t *testing.T) {
env, _ := json.Marshal(lettaEnv)
f := (&fake{}).
on("docker logs", Ran{Stdout: leakyLog}).
on("docker container inspect --format {{json .Config.Env}} letta", Ran{Stdout: string(env)})
got, err := client(f, 1000).Logs(context.Background(), "letta", 200, "")
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(got)
for _, v := range []string{serverPassword, dbPassword, "s3cr3t-elsewhere"} {
if strings.Contains(string(raw), v) {
t.Fatalf("docker_logs answered a secret: %s", raw)
}
}
lines := got["lines"].([]string)
if !strings.Contains(lines[2], "[redacted: LETTA_SERVER_PASSWORD]") ||
!strings.Contains(lines[3], "mongodb://app:[redacted: a password in a URI]@mongo") ||
!strings.Contains(lines[4], "letta:***@db") || got["redacted"] != 4 {
t.Errorf("%v %v", lines, got["redacted"])
}
}
func TestLogsWithoutTheEnvironmentStillHideAURIsPasswordAndSaySo(t *testing.T) {
f := (&fake{}).on("docker logs", Ran{Stdout: leakyLog})
got, err := client(f, 1000).Logs(context.Background(), "letta", 200, "")
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(got)
if strings.Contains(string(raw), dbPassword) || got["redaction"] == nil {
t.Errorf("%s", raw)
}
}
func keys(m map[string]string) []string {
out := []string{}
for k := range m {
out = append(out, k)
}
return out
}