Vault: secret_own reads the module-own secrets it keeps, as fingerprints, so a generation can be told without the value
mesh/delivery closed: a newer head of the same pull request
mesh/check pass e572937d97fa on 567736f7795a | unit 2143 run, 2198 kept, 5 skipped | integration 2165 run, 2217 kept, 9 skipped | over bound: unit, in…

Issue: 1945
This commit is contained in:
2026-10-26 02:09:03 +01:00
parent 567736f779
commit e572937d97
3 changed files with 111 additions and 0 deletions
@@ -12,6 +12,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"git.novox.be/novox/mesh-sdk/go/seal"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -340,6 +341,19 @@ func definitions(env *Env) []toolDef {
} }
return env.Vault.Store.Handed(limitOf(a)) return env.Vault.Store.Handed(limitOf(a))
}}, }},
{Name: "secret_own",
Description: "The secrets the vault keeps for modules themselves (`own/<node>/<module>/<name>`, e.g. a module's " +
"bus account `broker`): for each its name, generation, the fingerprint of the value it holds now " +
"(sha256:<hex>), 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", {Name: "secret_vault_export",
Description: "Every secret the vault itself holds, as its copy sealed to the operator key, in the shape of " + 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 <name> --key <operator key file> " + "`secret export`: saved to a file, `nox secret recover vault mesh-vault <name> --key <operator key file> " +
@@ -661,3 +675,48 @@ func tools(env *Env) []stdio.Tool {
} }
return out 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
}
@@ -150,3 +150,54 @@ func contains(list []string, s string) bool {
} }
return false 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])
}
}
}
}
+1
View File
@@ -145,6 +145,7 @@
"secret_holders", "secret_holders",
"secret_verify", "secret_verify",
"secret_handed", "secret_handed",
"secret_own",
"secret_asks", "secret_asks",
"secret_vault_export", "secret_vault_export",
"provisioner_retirement", "provisioner_retirement",