From e572937d97faa5edf7baaf4b93a34c6321bdc195 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 02:09:03 +0100 Subject: [PATCH] Vault: secret_own reads the module-own secrets it keeps, as fingerprints, so a generation can be told without the value Issue: 1945 --- modules/mesh-vault/cmd/mesh-vault/tools.go | 59 +++++++++++++++++++ .../mesh-vault/cmd/mesh-vault/tools_test.go | 51 ++++++++++++++++ modules/mesh-vault/module.json | 1 + 3 files changed, 111 insertions(+) diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go index a737cfdf..149d2337 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -12,6 +12,7 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-sdk/go/seal" "os" "path/filepath" "strings" @@ -340,6 +341,19 @@ func definitions(env *Env) []toolDef { } return env.Vault.Store.Handed(limitOf(a)) }}, + {Name: "secret_own", + Description: "The secrets the vault keeps for modules themselves (`own///`, e.g. a module's " + + "bus account `broker`): for each its name, generation, the fingerprint of the value it holds now " + + "(sha256:), when it was made and last changed, by whom, where the value came from, and whether it is a " + + "covered bus account. Narrow by node, module or name prefix. At most 1000 rows; says when cut. Never a value.", + Input: map[string]any{"node": str("only this machine's secrets"), "module": str("only this module's secrets"), + "name": str("only secrets whose name (the last part) starts with this, e.g. broker")}, + run: func(_ string, a map[string]any) (any, error) { + if env.Vault == nil { + return nil, errors.New("the vault is not ready") + } + return ownAnswer(env.Vault, arg(a, "node"), arg(a, "module"), arg(a, "name")) + }}, {Name: "secret_vault_export", Description: "Every secret the vault itself holds, as its copy sealed to the operator key, in the shape of " + "`secret export`: saved to a file, `nox secret recover vault mesh-vault --key " + @@ -661,3 +675,48 @@ func tools(env *Env) []stdio.Tool { } return out } + +// ownRowsMax bounds what secret_own answers. +const ownRowsMax = 1000 + +// ownAnswer lists the own secrets the store holds, narrowed by node, module and name prefix: fingerprints only. +func ownAnswer(v *Vault, node, module, prefix string) (map[string]any, error) { + records, err := v.Store.List() + if err != nil { + return nil, err + } + rows := []map[string]any{} + matched := 0 + for _, r := range records { + parts := strings.Split(r.Name, "/") + if len(parts) != 4 || parts[0] != "own" { + continue + } + if (node != "" && parts[1] != node) || (module != "" && parts[2] != module) || !strings.HasPrefix(parts[3], prefix) { + continue + } + matched++ + if len(rows) >= ownRowsMax { + continue + } + fp := "" + var note any + if value, err := seal.Open(r.AtRest, v.Own.PrivateKey); err != nil { + note = "the vault could not open its copy: " + err.Error() + } else { + fp = Fingerprint(value) + } + row := map[string]any{"name": r.Name, "node": parts[1], "module": parts[2], "secret": parts[3], + "generation": r.Generation, "fingerprint": fp, "made": r.MadeAt, "changed": r.ChangedAt, + "changedBy": r.ChangedBy, "origin": r.Origin, "busAccount": Covered(r.Name)} + if r.ChangedVia != "" { + row["changedVia"] = r.ChangedVia + } + if note != nil { + row["error"] = note + } + rows = append(rows, row) + } + return map[string]any{"secrets": rows, "count": len(rows), "matched": matched, "cut": matched > len(rows), + "note": "the store records no hash of a bus account's password; fingerprint is sha256 of the value the vault holds"}, nil +} diff --git a/modules/mesh-vault/cmd/mesh-vault/tools_test.go b/modules/mesh-vault/cmd/mesh-vault/tools_test.go index dee060cb..aeef5d91 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools_test.go @@ -150,3 +150,54 @@ func contains(list []string, s string) bool { } return false } + +func TestSecretOwnListsFingerprintsOfOwnSecretsOnly(t *testing.T) { + v := newVault(t, AllowList{}) + for name, value := range map[string]string{"own/ace/mesh-build-go/broker": "pw-one", "own/ace/mesh-build-go/token": "tok-secret", + "own/g14/dunst/broker": "pw-two", "pair/postgres/gitea": "pair-value"} { + if _, err := v.Put(name, value, "the test"); err != nil { + t.Fatal(err) + } + } + if _, err := v.Put("own/ace/mesh-build-go/broker", "pw-one-b", "the test"); err != nil { + t.Fatal(err) + } + own := toolNamed(t, definitions(&Env{Vault: v.Vault}), "secret_own") + for _, c := range []struct { + args map[string]any + names []string + }{ + {map[string]any{}, []string{"own/ace/mesh-build-go/broker", "own/ace/mesh-build-go/token", "own/g14/dunst/broker"}}, + {map[string]any{"node": "ace"}, []string{"own/ace/mesh-build-go/broker", "own/ace/mesh-build-go/token"}}, + {map[string]any{"module": "dunst"}, []string{"own/g14/dunst/broker"}}, + {map[string]any{"name": "bro"}, []string{"own/ace/mesh-build-go/broker", "own/g14/dunst/broker"}}, + {map[string]any{"node": "ace", "name": "broker"}, []string{"own/ace/mesh-build-go/broker"}}, + {map[string]any{"node": "nowhere"}, nil}, + } { + got, err := own.Run(c.args) + if err != nil { + t.Fatal(err) + } + raw, _ := json.Marshal(got) + for _, value := range []string{"pw-one", "pw-two", "tok-secret", "pair-value"} { + if strings.Contains(string(raw), value) { + t.Errorf("%v returned a value: %s", c.args, raw) + } + } + rows := got.(map[string]any)["secrets"].([]map[string]any) + if len(rows) != len(c.names) || got.(map[string]any)["cut"] != false { + t.Fatalf("%v said %s", c.args, raw) + } + for i, n := range c.names { + if rows[i]["name"] != n { + t.Errorf("%v row %d is %v, want %s", c.args, i, rows[i]["name"], n) + } + } + if len(rows) > 0 && rows[0]["name"] == "own/ace/mesh-build-go/broker" { + if rows[0]["generation"] != 2 || rows[0]["fingerprint"] != Fingerprint("pw-one-b") || rows[0]["busAccount"] != true || + rows[0]["changedBy"] != "the test" { + t.Errorf("%v: broker row said %v", c.args, rows[0]) + } + } + } +} diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index 44944c95..50384964 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -145,6 +145,7 @@ "secret_holders", "secret_verify", "secret_handed", + "secret_own", "secret_asks", "secret_vault_export", "provisioner_retirement",