`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.
221 lines
7.5 KiB
Go
221 lines
7.5 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/secrets"
|
|
)
|
|
|
|
// With an operator key, a module's own secret is sealed to the operator as well — and the operator
|
|
// opens exactly the value the node was given.
|
|
func TestAnOwnSecretIsSealedToTheOperatorToo(t *testing.T) {
|
|
inv, ctx := twoNodesWithKeys(t)
|
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "postgres", Version: "1",
|
|
OwnSecrets: catalogue.OwnSecrets{"superuser": {Path: "/run/superuser"}, "replication": {Path: "/run/replication"}}}, Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Before there is a key: minted, and honestly unrecoverable.
|
|
if _, err := inv.SecretForModule(ctx, "consumer", "postgres", "superuser"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.KeptSecret(ctx, "consumer", "postgres", "superuser", "", ""); err == nil {
|
|
t.Fatal("a secret made before the operator key was reported recoverable")
|
|
}
|
|
kept, _, unrecoverable, err := inv.KeptForOperator(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(kept) != 0 || len(unrecoverable) != 1 {
|
|
t.Fatalf("before a key: %d kept, %d unrecoverable", len(kept), len(unrecoverable))
|
|
}
|
|
|
|
pub, priv, err := secrets.Keypair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if orphaned, err := inv.SetOperatorKey(ctx, pub); err != nil || orphaned != 0 {
|
|
t.Fatalf("set: orphaned %d, %v", orphaned, err)
|
|
}
|
|
|
|
// A new secret is sealed to both; an accepted one too.
|
|
if _, err := inv.SecretForModule(ctx, "provider", "postgres", "superuser"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.AcceptSecretForModule(ctx, "provider", "postgres", "replication", "given-by-a-person"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
kept, _, unrecoverable, err = inv.KeptForOperator(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(kept) != 2 || len(unrecoverable) != 1 {
|
|
t.Fatalf("after a key: %d kept, %d unrecoverable", len(kept), len(unrecoverable))
|
|
}
|
|
got, err := inv.KeptSecret(ctx, "provider", "postgres", "replication", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
value, err := secrets.Open(priv, got.Sealed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(value) != "given-by-a-person" {
|
|
t.Fatalf("recovered %q", value)
|
|
}
|
|
if got.Origin != "accepted" || got.Key != pub {
|
|
t.Fatalf("kept as %+v", got)
|
|
}
|
|
minted, err := inv.KeptSecret(ctx, "provider", "postgres", "superuser", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v, err := secrets.Open(priv, minted.Sealed); err != nil || len(v) != 40 {
|
|
t.Fatalf("the minted secret did not open to a 40-character value: %v", err)
|
|
}
|
|
|
|
// The old secret is kept, not resealed: asking again is a read, the plaintext is gone, and it
|
|
// stays honestly unrecoverable.
|
|
if _, err := inv.SecretForModule(ctx, "consumer", "postgres", "superuser"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.KeptSecret(ctx, "consumer", "postgres", "superuser", "", ""); err == nil {
|
|
t.Fatal("asking again did not remake, yet it became recoverable")
|
|
}
|
|
// Until the node rejoins with a new sealing key: then the secret is remade, and the remake is
|
|
// sealed to the operator — the one scenario the vault exists for.
|
|
rejoined, err := inv.NodeByName(ctx, "consumer")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
newKey, _ := aSealingKey(t)
|
|
if err := inv.RecordSealingKey(ctx, rejoined.ID, newKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.SecretForModule(ctx, "consumer", "postgres", "superuser"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
remade, err := inv.KeptSecret(ctx, "consumer", "postgres", "superuser", "", "")
|
|
if err != nil {
|
|
t.Fatalf("the remade secret is not recoverable: %v", err)
|
|
}
|
|
if _, err := secrets.Open(priv, remade.Sealed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Replacing the key says how many secrets stay sealed to the old one — and those move out of
|
|
// the recoverable list, whatever the export is labelled with.
|
|
pub2, _, _ := secrets.Keypair()
|
|
orphaned, err := inv.SetOperatorKey(ctx, pub2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if orphaned != 3 {
|
|
t.Fatalf("replacing the key orphaned %d, and three were sealed to it", orphaned)
|
|
}
|
|
if now, _ := inv.OperatorKey(ctx); now != pub2 {
|
|
t.Fatal("the new key is not the mesh's key")
|
|
}
|
|
kept, earlier, _, err := inv.KeptForOperator(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(kept) != 0 || len(earlier) != 3 {
|
|
t.Fatalf("after replacing the key: %d recoverable with it, %d sealed to the earlier key", len(kept), len(earlier))
|
|
}
|
|
doc, err := inv.OperatorExport(ctx)
|
|
if err != nil || doc == nil || len(doc.EarlierKey) != 3 || len(doc.Kept) != 0 {
|
|
t.Fatalf("the export does not say what the current key cannot open: %+v %v", doc, err)
|
|
}
|
|
}
|
|
|
|
// A pair credential — what the vault provides a module — is sealed to the operator too, and the
|
|
// operator's copy is the very value the consumer's node unseals.
|
|
func TestAPairCredentialIsSealedToTheOperatorToo(t *testing.T) {
|
|
inv := fresh(t)
|
|
ctx := context.Background()
|
|
// Two nodes with keys, keeping the consumer's opener: the test made the key, so it can play the
|
|
// consumer's host for one assertion.
|
|
var openAsConsumer func(string) ([]byte, bool)
|
|
for _, n := range []string{"consumer", "provider"} {
|
|
node, err := inv.AddNode(ctx, n)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, open := aSealingKey(t)
|
|
if n == "consumer" {
|
|
openAsConsumer = open
|
|
}
|
|
if err := inv.RecordSealingKey(ctx, node.ID, key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for _, m := range []string{"gitea", "mesh-vault"} {
|
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1"}, Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
pub, priv, err := secrets.Keypair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.SetOperatorKey(ctx, pub); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
made, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
kept, err := inv.KeptSecret(ctx, "consumer", "gitea", "secret", "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if kept.Kind != "pair" || kept.Provider != "provider" {
|
|
t.Fatalf("kept as %+v", kept)
|
|
}
|
|
all, _, _, err := inv.KeptForOperator(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var pairs int
|
|
for _, k := range all {
|
|
if k.Kind == "pair" {
|
|
pairs++
|
|
}
|
|
}
|
|
if pairs != 1 {
|
|
t.Fatalf("%d pair credential(s) recoverable, expected 1", pairs)
|
|
}
|
|
|
|
// A second provider of the same provision: two rows, refused rather than the first one taken,
|
|
// unless the provider is named. And replacing the key counts pair credentials as orphaned.
|
|
if _, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "consumer", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.KeptSecret(ctx, "consumer", "gitea", "secret", "", ""); err == nil || !strings.Contains(err.Error(), "--provider") {
|
|
t.Fatalf("two providers were not refused: %v", err)
|
|
}
|
|
if byName, err := inv.KeptSecret(ctx, "consumer", "gitea", "secret", "provider", ""); err != nil || byName.Provider != "provider" {
|
|
t.Fatalf("naming the provider did not select it: %+v %v", byName, err)
|
|
}
|
|
pub2, _, _ := secrets.Keypair()
|
|
if orphaned, err := inv.SetOperatorKey(ctx, pub2); err != nil || orphaned != 2 {
|
|
t.Fatalf("replacing the key orphaned %d pair credential(s), and two were sealed to it (%v)", orphaned, err)
|
|
}
|
|
fromOperator, err := secrets.Open(priv, kept.Sealed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The consumer's blob is sealed to the consumer node. Same value, two recipients.
|
|
fromNode, ok := openAsConsumer(made.ForConsumer)
|
|
if !ok {
|
|
t.Fatal("the consumer cannot open its own blob")
|
|
}
|
|
if string(fromOperator) != string(fromNode) {
|
|
t.Fatal("the operator's copy of the pair credential differs from the consumer's")
|
|
}
|
|
}
|