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…
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:
@@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user