From 6ab38066c9107977319157180bd8bb29e59976d9 Mon Sep 17 00:00:00 2001 From: mesh-admin Date: Wed, 14 Oct 2026 01:36:11 +0000 Subject: [PATCH] mesh-vault: bound the record of hand-outs and record only a well-formed ask (review 255) Co-Authored-By: Claude Opus 5.5 --- modules/mesh-vault/cmd/mesh-vault/main.go | 5 +- modules/mesh-vault/cmd/mesh-vault/store.go | 101 +++++++++++++++++---- modules/mesh-vault/cmd/mesh-vault/vault.go | 26 +++++- modules/mesh-vault/module.json | 3 +- 4 files changed, 112 insertions(+), 23 deletions(-) diff --git a/modules/mesh-vault/cmd/mesh-vault/main.go b/modules/mesh-vault/cmd/mesh-vault/main.go index 35a66b7b..25f5cd4d 100644 --- a/modules/mesh-vault/cmd/mesh-vault/main.go +++ b/modules/mesh-vault/cmd/mesh-vault/main.go @@ -30,7 +30,8 @@ func build() *Env { say("%v", err) } if env.Ledger != nil && env.Receives != "" { - env.Custody = &Custody{Ledger: env.Ledger, Receives: env.Receives, Emit: emit, Now: time.Now, Log: say} + env.Custody = &Custody{Ledger: env.Ledger, Receives: env.Receives, Resource: "secret", Node: os.Getenv("MESH_NODE"), + Emit: emit, Now: time.Now, Log: say} } store, err := OpenStore(os.Getenv("MESH_VAULT_STORE")) if err != nil { @@ -61,7 +62,9 @@ func adoptOperatorKey(env *Env) { if err == nil { _, err = env.Vault.SetOperatorKey(key, "the mesh's export") } + env.refuseKey("") if err != nil { + env.refuseKey(err.Error()) say("the operator key in the mesh's export is not taken, or not every copy is sealed to it yet: %v", err) } return diff --git a/modules/mesh-vault/cmd/mesh-vault/store.go b/modules/mesh-vault/cmd/mesh-vault/store.go index cde51126..192c60fe 100644 --- a/modules/mesh-vault/cmd/mesh-vault/store.go +++ b/modules/mesh-vault/cmd/mesh-vault/store.go @@ -18,6 +18,7 @@ import ( "regexp" "sort" "strings" + "sync" "time" "git.novox.be/novox/mesh-sdk/go/seal" @@ -52,8 +53,20 @@ type Store struct { dir string // failWrite, when a test sets it, fails the write of a record: how a failure partway is proven. failWrite func(Record) error + // HandedMax is the size at which the record of hand-outs is rolled (0: HandedMaxDefault). + HandedMax int64 + + handedMu sync.Mutex + rolled int } +// The record of hand-outs is bounded: rolled at HandedMaxDefault into handed.jsonl.1 … .HandedKept, the +// oldest dropped, so a flood of asks costs at most (HandedKept+1) × HandedMaxDefault on the vault's disk. +const ( + HandedMaxDefault = 1 << 20 + HandedKept = 3 +) + // OpenStore opens (and makes) the store at dir. func OpenStore(dir string) (*Store, error) { if dir == "" { @@ -132,17 +145,54 @@ type Handing struct { Why string `json:"why,omitempty"` } -// RecordHanding appends one handing to the store's record, synced before it returns. -func (s *Store) RecordHanding(h Handing) error { +// RecordHanding appends one handing to the store's record, synced before it returns. It says whether the +// record was rolled to make room. +func (s *Store) RecordHanding(h Handing) (rolled bool, err error) { raw, err := json.Marshal(h) if err != nil { + return false, err + } + s.handedMu.Lock() + defer s.handedMu.Unlock() + path := filepath.Join(s.dir, "handed.jsonl") + max := s.HandedBound() + if info, err := os.Stat(path); err == nil && info.Mode().IsRegular() && info.Size()+int64(len(raw))+1 > max { + if err := s.roll(path); err != nil { + return false, err + } + rolled = true + } + return rolled, appendSynced(path, append(raw, '\n')) +} + +// roll moves handed.jsonl to .1, .1 to .2 and so on, dropping what falls past HandedKept. +func (s *Store) roll(path string) error { + _ = os.Remove(fmt.Sprintf("%s.%d", path, HandedKept)) + for i := HandedKept - 1; i >= 1; i-- { + if err := os.Rename(fmt.Sprintf("%s.%d", path, i), fmt.Sprintf("%s.%d", path, i+1)); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + } + if err := os.Rename(path, path+".1"); err != nil { return err } - f, err := os.OpenFile(filepath.Join(s.dir, "handed.jsonl"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) + s.rolled++ + return syncDir(s.dir) +} + +// Rolled is how many times the record of hand-outs was rolled since the vault started. +func (s *Store) Rolled() int { + s.handedMu.Lock() + defer s.handedMu.Unlock() + return s.rolled +} + +func appendSynced(path string, raw []byte) error { + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) if err != nil { return err } - if _, err := f.Write(append(raw, '\n')); err != nil { + if _, err := f.Write(raw); err != nil { f.Close() return err } @@ -153,26 +203,43 @@ func (s *Store) RecordHanding(h Handing) error { return f.Close() } -// Handed is the latest handings, newest first, at most limit. +// Handed is the latest handings, newest first, at most limit: the current record, then the rolled ones. func (s *Store) Handed(limit int) ([]Handing, error) { - raw, err := os.ReadFile(filepath.Join(s.dir, "handed.jsonl")) - if errors.Is(err, os.ErrNotExist) { - return []Handing{}, nil - } - if err != nil { - return nil, err - } - lines := strings.Split(strings.TrimSpace(string(raw)), "\n") + s.handedMu.Lock() + defer s.handedMu.Unlock() + path := filepath.Join(s.dir, "handed.jsonl") out := []Handing{} - for i := len(lines) - 1; i >= 0 && len(out) < limit; i-- { - var h Handing - if json.Unmarshal([]byte(lines[i]), &h) == nil { - out = append(out, h) + for i := 0; i <= HandedKept && len(out) < limit; i++ { + file := path + if i > 0 { + file = fmt.Sprintf("%s.%d", path, i) + } + raw, err := os.ReadFile(file) + if errors.Is(err, os.ErrNotExist) { + continue + } + if err != nil { + return nil, err + } + lines := strings.Split(strings.TrimSpace(string(raw)), "\n") + for j := len(lines) - 1; j >= 0 && len(out) < limit; j-- { + var h Handing + if json.Unmarshal([]byte(lines[j]), &h) == nil { + out = append(out, h) + } } } return out, nil } +// HandedBound is the size at which the record of hand-outs is rolled. +func (s *Store) HandedBound() int64 { + if s.HandedMax > 0 { + return s.HandedMax + } + return HandedMaxDefault +} + // List is every record, by name. func (s *Store) List() ([]Record, error) { entries, err := os.ReadDir(filepath.Join(s.dir, "secrets")) diff --git a/modules/mesh-vault/cmd/mesh-vault/vault.go b/modules/mesh-vault/cmd/mesh-vault/vault.go index 5386cac9..2ba4ec53 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "errors" "fmt" + "regexp" "sync" "time" @@ -138,16 +139,26 @@ func (v *Vault) announce(c Changed) { } } -// recordHanding keeps who asked for what, and what they got, in the vault's own store. +// recordHanding keeps who asked for what, and what they got, in the vault's own store, and says loudly +// when the record had to be rolled to make room. func (v *Vault) recordHanding(h Handing) error { h.At = v.Now().UTC() - err := v.Store.RecordHanding(h) + rolled, err := v.Store.RecordHanding(h) if err != nil { v.log("the record of %s asking for %s could not be written: %v", h.To, h.Name, err) + return err } - return err + if rolled { + v.log("THE RECORD OF HAND-OUTS WAS ROLLED (%d times since the vault started): it reached its bound of %d bytes; "+ + "the oldest of %d kept files is dropped. Many asks in little time is worth a look: secret_handed and vault_status", + v.Store.Rolled(), v.Store.HandedBound(), HandedKept) + } + return nil } +// busUser is the shape of a caller the runtime proves (the SDK's): the one shape a refusal is recorded for. +var busUser = regexp.MustCompile(`^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$`) + // Get answers caller with the secret name sealed to publicKey, if caller may ask for it. The caller is the // bus user the runtime proved, never an argument; an unproven call is refused. func (v *Vault) Get(caller, name, publicKey string) (Answer, error) { @@ -158,9 +169,16 @@ func (v *Vault) Get(caller, name, publicKey string) (Answer, error) { if askers == nil { askers = NoAsker{} } + if !busUser.MatchString(caller) || len(caller) > 200 { + return Answer{}, errors.New("the vault answers only a caller the runtime proved") + } + // A name is judged before anything is recorded, so an arbitrary argument fills no record. + if err := validName(name); err != nil { + return Answer{}, err + } if ok, why := askers.May(caller, name); !ok { v.log("refused %s to %s: %s", name, caller, why) - v.recordHanding(Handing{To: caller, Name: name, Outcome: "refused", Why: why}) + _ = v.recordHanding(Handing{To: caller, Name: name, Outcome: "refused", Why: why}) return Answer{}, fmt.Errorf("%s may not ask for %s: %s", caller, name, why) } key, err := askerKey(publicKey) diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index abba5ba2..29c631f5 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -116,7 +116,8 @@ "provisioner_retirement", "provisioner_retire_approve", "provisioner_retire_reject", - "provisioner_delete" + "provisioner_delete", + "vault_status" ], "claims": [ {