An operator delivers a pair credential, and the mesh never replaces it
secret accept grows --provider: the value is sealed to the consumer's node, the provider's node and the operator's key, and the pair records origin 'accepted'. An accepted pair is not remade when a key changes (the mesh does not hold the value; the read is refused naming the remedy) and rotate refuses it (accepting a new value is the rotation). The vault's third species has its entry (novox/hq 04-ISSUES/070, ADR 0092).
This commit is contained in:
@@ -49,6 +49,9 @@ func secretCommand(ctx context.Context, args []string) error {
|
||||
set := flag.NewFlagSet("secret accept", flag.ContinueOnError)
|
||||
from := set.String("from", "",
|
||||
"read the value from this file instead of asking (use - for standard input)")
|
||||
provider := set.String("provider", "",
|
||||
"the node providing <name>: the value becomes the PAIR credential between <module> on <node> "+
|
||||
"and that provider, sealed to both — the vault's operator-delivered secret (ADR 0092)")
|
||||
if err := set.Parse(flags); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -72,6 +75,18 @@ func secretCommand(ctx context.Context, args []string) error {
|
||||
}
|
||||
defer open.Close()
|
||||
|
||||
if *provider != "" {
|
||||
// Into the pair, not into the module's own secrets: what the provider is asked to create
|
||||
// and what the consumer reads are the same value, and neither end can be told a different
|
||||
// one later without the other (novox/hq 04-ISSUES/070).
|
||||
if err := open.inventory.AcceptSecretForPair(ctx, name, node, module, *provider, value); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s on %s now holds %q from %s, sealed to both machines.\n", module, node, name, *provider)
|
||||
fmt.Printf(" the mesh cannot read it back, will not replace it with one of its own, and will not rotate it\n")
|
||||
fmt.Printf(" run `push %s` and `push %s` to send it\n", *provider, node)
|
||||
return nil
|
||||
}
|
||||
if err := open.inventory.AcceptSecretForModule(ctx, node, module, name, value); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -83,7 +98,7 @@ func secretCommand(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const secretUsage = "secret accept <node> <module> <name> [--from <file>]\n" +
|
||||
const secretUsage = "secret accept <node> <module> <name> [--from <file>] [--provider <node>]\n" +
|
||||
"secret recover <node> <module> <name> --key <operator-key> [--out <file>] [--from-export <file>] [--provider <node>]\n" +
|
||||
"secret export [--out <file>]"
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- A pair credential records whether the mesh made it or a person supplied it
|
||||
-- (novox/hq 04-ISSUES/070, ADR 0092).
|
||||
--
|
||||
-- Every pair credential so far was made: generated, sealed to both ends, the plaintext discarded,
|
||||
-- remade whenever either end's key changed and replaced whole by `rotate`. A module's own secret
|
||||
-- has carried `origin` since the beginning so an accepted one is never replaced by a minted one;
|
||||
-- a pair could not be accepted at all, so the vault's third species -- a credential for something
|
||||
-- outside the mesh, which only a person can supply -- had no entry.
|
||||
--
|
||||
-- An accepted pair is not remade when a key changes (the mesh cannot: it does not hold the
|
||||
-- value) and is not rotated (there is nothing to rotate to); both are refused aloud, and the
|
||||
-- remedy is to accept it again.
|
||||
|
||||
alter table secret add column origin text not null default 'made';
|
||||
@@ -29,8 +29,17 @@ type Secret struct {
|
||||
ForProvider string
|
||||
ConsumerKey string
|
||||
ProviderKey string
|
||||
// Origin is `made` — the mesh generated it — or `accepted` — a person supplied it, for
|
||||
// something outside the mesh, and the mesh cannot make another (novox/hq 04-ISSUES/070).
|
||||
Origin string
|
||||
}
|
||||
|
||||
// Where a pair credential came from.
|
||||
const (
|
||||
OriginMade = "made"
|
||||
OriginAccepted = "accepted"
|
||||
)
|
||||
|
||||
// SecretFor is the credential one module uses for one provision, making it the first time.
|
||||
//
|
||||
// **Made once and kept**, rather than regenerated whenever it is asked for. A secret that changed
|
||||
@@ -64,15 +73,26 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
|
||||
|
||||
var held Secret
|
||||
err = i.store.Pool().QueryRow(ctx,
|
||||
`select for_consumer, for_provider, consumer_key, provider_key from secret
|
||||
`select for_consumer, for_provider, consumer_key, provider_key, origin from secret
|
||||
where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4`,
|
||||
name, consumerNode.ID, consumerModule, providerNode.ID).
|
||||
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey)
|
||||
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin)
|
||||
if err == nil && held.ConsumerKey == consumerKey && held.ProviderKey == providerKey {
|
||||
held.Name, held.Consumer, held.Provider = name, consumer, provider
|
||||
held.ConsumerModule = consumerModule
|
||||
return held, nil
|
||||
}
|
||||
if err == nil && held.Origin == OriginAccepted {
|
||||
// A person supplied this, and the mesh does not hold the value: it cannot seal it to the
|
||||
// new key. Refused aloud rather than replaced by something the mesh made up, which would
|
||||
// be delivered, reported as applied, and fail to authenticate somewhere else entirely
|
||||
// (novox/hq 04-ISSUES/070).
|
||||
return Secret{}, fmt.Errorf(
|
||||
"%s's %q credential from %s was accepted from a person, and a sealing key at one end "+
|
||||
"has changed since. The mesh cannot re-seal a value it does not hold: accept it "+
|
||||
"again with `secret accept %s %s %s --provider %s`",
|
||||
consumerModule, name, provider, consumer, consumerModule, name, provider)
|
||||
}
|
||||
|
||||
// And to the operator, when the mesh has one (novox/hq ADR 0085, amended): the third copy that
|
||||
// makes a vault-provided secret recoverable, and nothing the mesh can open.
|
||||
@@ -102,7 +122,60 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
|
||||
return Secret{Name: name, Consumer: consumer, ConsumerModule: consumerModule,
|
||||
Provider: provider,
|
||||
ForConsumer: made.ForConsumer, ForProvider: made.ForProvider,
|
||||
ConsumerKey: made.ConsumerKey, ProviderKey: made.ProviderKey}, nil
|
||||
ConsumerKey: made.ConsumerKey, ProviderKey: made.ProviderKey, Origin: OriginMade}, nil
|
||||
}
|
||||
|
||||
// AcceptSecretForPair takes a value a person supplied into a pair credential — sealed to the
|
||||
// consumer's node and to the provider's, and to the operator when the mesh has one — where the
|
||||
// mesh would otherwise have made one (novox/hq 04-ISSUES/070, ADR 0092).
|
||||
//
|
||||
// This is the vault's third species: a credential for something outside the mesh, which only a
|
||||
// person can supply. It is the counterpart to AcceptSecretForModule for a module's own secret;
|
||||
// what differs is that both ends of the pair are sealed to, and that the record says `accepted`
|
||||
// so a later read never replaces it with a minted one. The plaintext is discarded here.
|
||||
func (i *Inventory) AcceptSecretForPair(ctx context.Context, name, consumer, consumerModule, provider, value string) error {
|
||||
consumerKey, err := i.SealingKeyOf(ctx, consumer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
providerKey, err := i.SealingKeyOf(ctx, provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if consumerKey == "" || providerKey == "" {
|
||||
return fmt.Errorf(
|
||||
"both %s and %s need a sealing key before a credential can be sealed to them — a "+
|
||||
"node joins to get one", consumer, provider)
|
||||
}
|
||||
consumerNode, err := i.NodeByName(ctx, consumer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
providerNode, err := i.NodeByName(ctx, provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sealed, err := secrets.Accept(value, consumerKey, providerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
forOperator, operatorKey, err := i.operatorSeal(ctx, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = i.store.Pool().Exec(ctx,
|
||||
`insert into secret (name, consumer, consumer_module, provider, for_consumer, for_provider,
|
||||
consumer_key, provider_key, operator_sealed, operator_key, origin)
|
||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
on conflict (name, consumer, consumer_module, provider) do update set
|
||||
for_consumer = excluded.for_consumer, for_provider = excluded.for_provider,
|
||||
consumer_key = excluded.consumer_key, provider_key = excluded.provider_key,
|
||||
created_at = now(), origin = excluded.origin,
|
||||
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key`,
|
||||
name, consumerNode.ID, consumerModule, providerNode.ID,
|
||||
sealed.ForConsumer, sealed.ForProvider, sealed.ConsumerKey, sealed.ProviderKey,
|
||||
forOperator, operatorKey, OriginAccepted)
|
||||
return err
|
||||
}
|
||||
|
||||
// RotateSecret discards what was there, so the next declaration carries a new one.
|
||||
@@ -113,6 +186,10 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
|
||||
//
|
||||
// The new secret then reaches both ends on the same push, together, which is what makes rotation
|
||||
// a single event rather than a fanout with a window where half the mesh holds a dead credential.
|
||||
//
|
||||
// **An accepted credential is not rotated.** The mesh did not make it and cannot make its
|
||||
// replacement; deleting it would have the next read mint one, which is exactly the wrong value
|
||||
// delivered with the mesh insisting it was (novox/hq 04-ISSUES/070). Refused, and the remedy named.
|
||||
func (i *Inventory) RotateSecret(ctx context.Context, name, consumer, consumerModule, provider string) error {
|
||||
consumerNode, err := i.NodeByName(ctx, consumer)
|
||||
if err != nil {
|
||||
@@ -122,6 +199,18 @@ func (i *Inventory) RotateSecret(ctx context.Context, name, consumer, consumerMo
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var origin string
|
||||
err = i.store.Pool().QueryRow(ctx,
|
||||
`select origin from secret where name = $1 and consumer = $2 and consumer_module = $3
|
||||
and provider = $4`,
|
||||
name, consumerNode.ID, consumerModule, providerNode.ID).Scan(&origin)
|
||||
if err == nil && origin == OriginAccepted {
|
||||
return fmt.Errorf(
|
||||
"%s's %q credential from %s was accepted from a person, and the mesh cannot make "+
|
||||
"its replacement. Accept the new value instead: `secret accept %s %s %s "+
|
||||
"--provider %s --from <file>`",
|
||||
consumerModule, name, provider, consumer, consumerModule, name, provider)
|
||||
}
|
||||
_, err = i.store.Pool().Exec(ctx,
|
||||
`delete from secret where name = $1 and consumer = $2 and consumer_module = $3
|
||||
and provider = $4`,
|
||||
|
||||
@@ -558,3 +558,69 @@ func TestRotatingGivesBothEndsTheSameNewCredential(t *testing.T) {
|
||||
t.Fatal("rotating one machine's credential changed another machine's")
|
||||
}
|
||||
}
|
||||
|
||||
// **A person can deliver a pair credential** (novox/hq 04-ISSUES/070, ADR 0092): the vault's
|
||||
// third species, a value for something outside the mesh. It is sealed to both ends like a made
|
||||
// one; what differs is that the mesh will neither replace it with one of its own nor rotate it,
|
||||
// because it cannot make the replacement.
|
||||
func TestAnAcceptedPairCredentialIsKeptAndNeverRemade(t *testing.T) {
|
||||
inv, ctx := twoNodesWithKeys(t)
|
||||
if err := inv.AcceptSecretForPair(ctx, "secret", "consumer", "gitea", "provider", "hunter2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Origin != OriginAccepted {
|
||||
t.Fatalf("an accepted credential reads back as %q", got.Origin)
|
||||
}
|
||||
again, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if again.ForConsumer != got.ForConsumer || again.ForProvider != got.ForProvider {
|
||||
t.Fatal("reading an accepted credential twice produced two different values")
|
||||
}
|
||||
|
||||
// Rotation is refused, and says what to do instead.
|
||||
err = inv.RotateSecret(ctx, "secret", "consumer", "gitea", "provider")
|
||||
if err == nil || !strings.Contains(err.Error(), "secret accept") {
|
||||
t.Fatalf("rotating an accepted credential was not refused with the remedy: %v", err)
|
||||
}
|
||||
// And a made one still rotates.
|
||||
if _, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RotateSecret(ctx, "postgres-database", "consumer", "gitea", "provider"); err != nil {
|
||||
t.Fatalf("a made credential no longer rotates: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A key that changed at either end makes the accepted value unreadable there, and the mesh cannot
|
||||
// re-seal what it does not hold: refused aloud, never quietly replaced by a minted one.
|
||||
func TestAnAcceptedPairCredentialIsNotRemadeWhenAKeyChanges(t *testing.T) {
|
||||
inv, ctx := twoNodesWithKeys(t)
|
||||
if err := inv.AcceptSecretForPair(ctx, "secret", "consumer", "gitea", "provider", "hunter2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node, err := inv.NodeByName(ctx, "consumer")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fresh, _ := aSealingKey(t)
|
||||
if err := inv.RecordSealingKey(ctx, node.ID, fresh); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider")
|
||||
if err == nil || !strings.Contains(err.Error(), "accept it again") {
|
||||
t.Fatalf("an accepted credential was remade, or refused without the remedy: %v", err)
|
||||
}
|
||||
// Accepting it again is the remedy, and it works.
|
||||
if err := inv.AcceptSecretForPair(ctx, "secret", "consumer", "gitea", "provider", "hunter3"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user