Replace a value given by hand like one the mesh made (hq ADR 0228)

A given own secret the module reads at start is held by nobody but that
module, so the mesh need not read it to replace it: secret rotate now
works on it, and a value given through secret accept is replaced on its
own after the module's first good start under the mesh. Only a value an
outside party issues (own-secrets "issued-by": "outside") or one the
module applies stays as given, refused with the reason.
This commit is contained in:
jochen
2026-10-06 12:13:48 +02:00
parent 722682f1c4
commit e51c6a2cb9
20 changed files with 761 additions and 60 deletions
+42 -10
View File
@@ -558,7 +558,10 @@ type Manifest struct {
// that takes it only once, so a rotation must be staged beside the current value — the form the
// mesh does not build yet, and refuses by name. A secret that says neither is not rotated by
// the mesh: the one fault worse than an unrotated credential is a rotated one the software
// never saw.
// never saw. `"issued-by": "outside"` says a party outside the mesh issues the value — an API
// key, a bot token, a licence — so the mesh never puts one of its own in its place (novox/hq
// ADR 0228); any other at-start secret given by hand lives only until the module's first good
// start, and is then replaced.
OwnSecrets OwnSecrets `json:"own-secrets,omitempty"`
// SecretsOwner is who the files holding this module's secrets belong to on the machine —
@@ -1801,6 +1804,13 @@ func ParseManifest(raw []byte) (Manifest, error) {
"or %q (applied by the module's own code to a backend that takes it once)",
m.Module, name, own.Taken, TakenAtStart, TakenApplied))
}
if own.IssuedBy != "" && own.IssuedBy != IssuedOutside {
problems = append(problems, fmt.Sprintf(
"%s says its secret %q is issued by %q; a secret says %q when a party outside the mesh "+
"issues it — a vendor's key, a bot's token, a licence — and says nothing when the mesh "+
"may make it (novox/hq ADR 0228)",
m.Module, name, own.IssuedBy, IssuedOutside))
}
}
localOf := map[string]string{}
for _, to := range m.SecretRequirements() {
@@ -2263,10 +2273,24 @@ const (
TakenApplied = "applied"
)
// OwnSecret is where one of a module's own secrets lands, and how the module takes it.
// IssuedOutside says a value was issued by a party outside the mesh — a vendor's API key, a bot's
// token, a licence — so no value the mesh makes would work in its place (novox/hq ADR 0228).
const IssuedOutside = "outside"
// OwnSecret is where one of a module's own secrets lands, how the module takes it, and — for a
// value only an outside party can issue — that it is one.
type OwnSecret struct {
Path string
Taken string
Path string
Taken string
IssuedBy string
}
// MeshMayMake says the mesh may put a value it makes in place of the one the secret holds: the
// module reads it as it starts, and nobody outside the mesh issued it (novox/hq ADR 0228). A value
// given to the mesh for such a secret lives only until the module's first good start under the
// mesh; anything else given stays as given.
func (s OwnSecret) MeshMayMake() bool {
return s.Taken == TakenAtStart && s.IssuedBy != IssuedOutside
}
// OwnSecrets is a module's own secrets by name. On the wire each is a path, or an object naming
@@ -2287,15 +2311,16 @@ func (o *OwnSecrets) UnmarshalJSON(raw []byte) error {
continue
}
var long struct {
Path string `json:"path"`
Taken string `json:"taken,omitempty"`
Path string `json:"path"`
Taken string `json:"taken,omitempty"`
IssuedBy string `json:"issued-by,omitempty"`
}
dec := json.NewDecoder(bytes.NewReader(body))
dec.DisallowUnknownFields()
if err := dec.Decode(&long); err != nil {
return fmt.Errorf("own-secrets.%s: a path, or {\"path\", \"taken\"}: %w", name, err)
return fmt.Errorf("own-secrets.%s: a path, or {\"path\", \"taken\", \"issued-by\"}: %w", name, err)
}
out[name] = OwnSecret{Path: long.Path, Taken: long.Taken}
out[name] = OwnSecret{Path: long.Path, Taken: long.Taken, IssuedBy: long.IssuedBy}
}
*o = out
return nil
@@ -2304,11 +2329,18 @@ func (o *OwnSecrets) UnmarshalJSON(raw []byte) error {
func (o OwnSecrets) MarshalJSON() ([]byte, error) {
entries := make(map[string]any, len(o))
for name, s := range o {
if s.Taken == "" {
if s.Taken == "" && s.IssuedBy == "" {
entries[name] = s.Path
continue
}
entries[name] = map[string]string{"path": s.Path, "taken": s.Taken}
long := map[string]string{"path": s.Path}
if s.Taken != "" {
long["taken"] = s.Taken
}
if s.IssuedBy != "" {
long["issued-by"] = s.IssuedBy
}
entries[name] = long
}
return json.Marshal(entries)
}