A module's own secret rotates when its definition says the module reads it at start (hq 180)
`secret rotate <node> <module> <name>` makes the secret anew the way the first mint did, seals it
to the machine and the operator, and sends the machine, so the module starts again on the new value
— said in the log with who asked and when, never the value. Only for a secret whose definition says
`"taken": "at-start"`: an own secret is now a path, or {path, taken}, and a definition that says
nothing of how a secret is taken is refused with the word to write, because a credential rotated
under software that never reads it again is worse than one left alone (issue 179). `applied` is
refused by name until the staged form ADR 0114 decided is built; a value given to the mesh is
refused as ADR 0113 says. `rotate` is a verb on the controller's seat with two shapes — a pair
credential by provision, an own secret by machine, module and name — so the console can ask.
Registered manifests keep their bytes: a path alone is written back as a path.
This commit is contained in:
@@ -505,7 +505,7 @@ func declaresOwn(m catalogue.Manifest) string {
|
||||
if len(m.OwnSecrets) == 0 {
|
||||
return "it declares no own secrets"
|
||||
}
|
||||
return "it declares: " + strings.Join(sortedNames(m.OwnSecrets), ", ")
|
||||
return "it declares: " + strings.Join(sortedNames(m.OwnSecrets.Paths()), ", ")
|
||||
}
|
||||
|
||||
func sortedNames(of map[string]string) []string {
|
||||
@@ -523,3 +523,88 @@ func orNone(names []string) string {
|
||||
}
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
// ErrNotRotatable says why the mesh will not rotate a module's own secret; the words are the caller's
|
||||
// to print, and the remedy is in them.
|
||||
type ErrNotRotatable struct{ Why string }
|
||||
|
||||
func (e ErrNotRotatable) Error() string { return e.Why }
|
||||
|
||||
// RotateModuleSecret makes a module's own secret anew, the way the first mint did (novox/hq
|
||||
// ADR 0114, issue 180). The caller sends the node, so the module is started again on the new value.
|
||||
//
|
||||
// **Only a secret the module reads when it starts.** A secret the module's code applies to a
|
||||
// backend that takes it once would, rotated this way, leave the backend on the old value and the
|
||||
// module reading the new one — the fault issue 179 was. That form is staged, which the mesh does
|
||||
// not build yet, and is refused by name. A secret whose manifest says neither is refused with the
|
||||
// word to write; a secret given to the mesh rather than made by it is refused as 0113 says: the
|
||||
// mesh will not replace what it cannot read.
|
||||
func (i *Inventory) RotateModuleSecret(ctx context.Context, node, module, name string) error {
|
||||
m, err := i.declared(ctx, module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
own, declared := m.OwnSecrets[name]
|
||||
if !declared {
|
||||
return fmt.Errorf("%s does not declare %q as an own secret; %s", module, name, declaresOwn(m))
|
||||
}
|
||||
switch own.Taken {
|
||||
case catalogue.TakenAtStart:
|
||||
case catalogue.TakenApplied:
|
||||
return ErrNotRotatable{Why: fmt.Sprintf(
|
||||
"%s applies %q to a backend that takes it once, so a rotation must be staged beside the "+
|
||||
"current value until the module confirms it — the mesh does not do that yet (ADR 0114). "+
|
||||
"Changing it is a person's work: change it in %s, then `secret accept %s %s %s`",
|
||||
module, name, module, node, module, name)}
|
||||
default:
|
||||
return ErrNotRotatable{Why: fmt.Sprintf(
|
||||
"%s does not say how it takes %q, so the mesh will not rotate it: a secret rotated under "+
|
||||
"software that never reads it again is worse than one left alone. Its definition says "+
|
||||
"\"own-secrets\": {%q: {\"path\": …, \"taken\": \"at-start\"}} when the module reads it as it "+
|
||||
"starts, or \"applied\" when its own code applies it",
|
||||
module, name, name)}
|
||||
}
|
||||
record, err := i.NodeByName(ctx, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := i.SealingKeyOf(ctx, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key == "" {
|
||||
return fmt.Errorf("%s has no sealing key, so nothing can be sealed to it", node)
|
||||
}
|
||||
var origin string
|
||||
err = i.store.Pool().QueryRow(ctx,
|
||||
`select origin from module_secret where node = $1 and module = $2 and name = $3`,
|
||||
record.ID, module, name).Scan(&origin)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return fmt.Errorf("%s on %s holds no %q yet; the first push makes it", module, node, name)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if origin == OriginAccepted {
|
||||
return ErrNotRotatable{Why: fmt.Sprintf(
|
||||
"%s on %s holds %q as a value given to the mesh, not made by it, and the mesh will not "+
|
||||
"replace what it cannot read (ADR 0113). Change it where it lives, then `secret accept "+
|
||||
"%s %s %s` with the new value",
|
||||
module, node, name, node, module, name)}
|
||||
}
|
||||
operator, err := i.OperatorKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
made, blob, err := secrets.MakeWithOperator(key, key, operator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
forOperator, operatorKey := operatorColumns(operator, blob)
|
||||
_, err = i.store.Pool().Exec(ctx,
|
||||
`update module_secret set sealed = $4, node_key = $5, origin = 'made', made_at = now(),
|
||||
operator_sealed = $6, operator_key = $7
|
||||
where node = $1 and module = $2 and name = $3`,
|
||||
record.ID, module, name, made.ForConsumer, key, forOperator, operatorKey)
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user