Multiple fixes: several secrets per module (069), ask a module's tool (049), copy an upstream image (046), declare a vendor image (064) #38

Merged
jschoubben merged 9 commits from multiple-fixes into main 2026-09-21 19:05:11 +00:00
13 changed files with 567 additions and 142 deletions
Showing only changes of commit 6ae4ae1dba - Show all commits
+2 -2
View File
@@ -122,7 +122,7 @@ func planFor(ctx context.Context, open *stores, nodeName string) (catalogue.Reso
}
continue
}
secret, err := inv.SecretFor(ctx, n.Name, nodeName, n.For, n.From)
secret, err := inv.SecretFor(ctx, n.Name, nodeName, n.For, n.From, n.Local)
if err != nil {
// Said rather than skipped. A machine that resolves cleanly and receives no
// credential is one that will fail to authenticate at some later, less obvious
@@ -693,7 +693,7 @@ func grantsFor(ctx context.Context, open *stores, node string) ([]catalogue.Gran
}
out = append(out, catalogue.Grant{
Provision: s.Name, Consumer: s.Consumer, At: onNetwork[s.Consumer],
From: from, Values: values, Slug: slug, Sealed: s.ForProvider})
From: from, Values: values, Slug: slug, Sealed: s.ForProvider, Local: s.Local})
}
return out, nil
}
+10 -2
View File
@@ -83,11 +83,11 @@ func rotateCommand(ctx context.Context, args []string) error {
for _, h := range holders {
// The module, because a machine may hold several credentials for one provision and
// rotating "anchor's database password" now means rotating three of them.
fmt.Printf(" %s on %s, from %s\n", h.ConsumerModule, h.Consumer, h.Provider)
fmt.Printf(" %s on %s, from %s%s\n", h.ConsumerModule, h.Consumer, h.Provider, asLocal(h.Local))
}
for _, h := range holders {
if err := inv.RotateSecret(ctx, h.Provision, h.Consumer, h.ConsumerModule, h.Provider); err != nil {
if err := inv.RotateSecret(ctx, h.Provision, h.Consumer, h.ConsumerModule, h.Provider, h.Local); err != nil {
// Partly rotated, and said so plainly. What is gone is remade on the next push, so
// the remedy is to run this again rather than to repair anything — but a machine
// whose secret was discarded and not resent is holding a credential the provider is
@@ -115,3 +115,11 @@ func rotateCommand(ctx context.Context, args []string) error {
"changed cannot authenticate — `status` says who is still behind\n", len(machines))
return nil
}
// asLocal names the credential inside the consumer where it holds several (ADR 0094).
func asLocal(local string) string {
if local == "" {
return ""
}
return " (as " + local + ")"
}
+6 -3
View File
@@ -52,6 +52,9 @@ func secretCommand(ctx context.Context, args []string) error {
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)")
local := set.String("local", "",
"with --provider: the name the credential goes by inside <module>, where its manifest keeps "+
"several for <name> (ADR 0094)")
if err := set.Parse(flags); err != nil {
return err
}
@@ -79,10 +82,10 @@ func secretCommand(ctx context.Context, args []string) error {
// 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 {
if err := open.inventory.AcceptSecretForPair(ctx, name, node, module, *provider, *local, 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("%s on %s now holds %q from %s%s, sealed to both machines.\n", module, node, name, *provider, asLocal(*local))
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
@@ -98,7 +101,7 @@ func secretCommand(ctx context.Context, args []string) error {
return nil
}
const secretUsage = "secret accept <node> <module> <name> [--from <file>] [--provider <node>]\n" +
const secretUsage = "secret accept <node> <module> <name> [--from <file>] [--provider <node> [--local <name>]]\n" +
"secret recover <node> <module> <name> --key <operator-key> [--out <file>] [--from-export <file>] [--provider <node>]\n" +
"secret export [--out <file>]"
+57 -39
View File
@@ -60,6 +60,9 @@ type Grant struct {
// Values are what that module contributed — the name it wants, and anything else the
// provision's own vocabulary defines.
Values map[string]any
// Local is the name the credential goes by inside the consumer where it keeps several for one
// provision (ADR 0094); empty for the ordinary one. The provider sees it as a holder of its own.
Local string
// Slug is the consumer module's identity slug, if it declared one — carried on the grant so the
// provider side derives the same login the consumer does, even across nodes where the consumer's
// manifest is not in view (novox/hq ADR 0049). Empty means "use the module name".
@@ -285,45 +288,48 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
"id": AccessID(a.Path), "type": "access", "path": a.Path, "mode": a.At(),
})
}
for _, to := range sortedKeys(m.Secrets) {
var found *Needed
for i, n := range r.Needs {
// **This module's need, not the provision's** (novox/hq 04-ISSUES/022). Matching
// on the name alone, every consumer of a provision took whichever credential
// happened to be last in the list — so on a node with two of them, one module
// would be given the other's password and fail to authenticate with a valid
// credential belonging to somebody else.
if n.Name == to && n.For == m.Module {
found = &r.Needs[i]
for _, to := range m.SecretRequirements() {
for _, file := range m.SecretFiles(to) {
var found *Needed
for i, n := range r.Needs {
// **This module's need, not the provision's** (novox/hq 04-ISSUES/022). Matching
// on the name alone, every consumer of a provision took whichever credential
// happened to be last in the list — so on a node with two of them, one module
// would be given the other's password and fail to authenticate with a valid
// credential belonging to somebody else. And this file's local name, where the
// module keeps several (ADR 0094).
if n.Name == to && n.For == m.Module && n.Local == file.Local {
found = &r.Needs[i]
}
}
if found != nil && found.ByRecord && found.Sealed == "" && !found.Manager {
// Answered by a record whose key has not been supplied since this consumer was
// put on it. **Refused, not skipped.** The mesh discarded the plaintext when the
// key was accepted and cannot seal another, so a machine that resolved cleanly
// would receive no file at all and fail at whatever tried to read it — which is
// the outcome ADR 0024 exists to avoid, arrived at politely.
//
// The manager holder is the one exception (novox/hq ADR 0050): an empty refresh token
// is a licence whose manager has not adopted one yet, a real waiting state rather than
// a lost key. It falls through to the skip below — its bound facts (carrying the
// manager's public key) are still delivered, which is what adoption needs to seal the
// first refresh token.
return nil, fmt.Errorf(
"%s on this machine uses the licence %q and no key has been sealed to it. "+
"The mesh cannot make one; supply it again with `licence key %s`",
m.Module, found.From, found.From)
}
if found == nil || found.Sealed == "" {
// Answered on this machine, or answered by a node the mesh could not seal to.
// Nothing to write either way, and writing an empty credential file would be
// worse than none: something would read it and fail authenticating.
continue
}
first = append(first, ownedBy(m.SecretsOwner, map[string]any{
"id": SecretID(SecretLocal(to, file.Local)), "type": "file", "path": file.Path,
"sealed": found.Sealed,
}))
}
if found != nil && found.ByRecord && found.Sealed == "" && !found.Manager {
// Answered by a record whose key has not been supplied since this consumer was
// put on it. **Refused, not skipped.** The mesh discarded the plaintext when the
// key was accepted and cannot seal another, so a machine that resolved cleanly
// would receive no file at all and fail at whatever tried to read it — which is
// the outcome ADR 0024 exists to avoid, arrived at politely.
//
// The manager holder is the one exception (novox/hq ADR 0050): an empty refresh token
// is a licence whose manager has not adopted one yet, a real waiting state rather than
// a lost key. It falls through to the skip below — its bound facts (carrying the
// manager's public key) are still delivered, which is what adoption needs to seal the
// first refresh token.
return nil, fmt.Errorf(
"%s on this machine uses the licence %q and no key has been sealed to it. "+
"The mesh cannot make one; supply it again with `licence key %s`",
m.Module, found.From, found.From)
}
if found == nil || found.Sealed == "" {
// Answered on this machine, or answered by a node the mesh could not seal to.
// Nothing to write either way, and writing an empty credential file would be
// worse than none: something would read it and fail authenticating.
continue
}
first = append(first, ownedBy(m.SecretsOwner, map[string]any{
"id": SecretID(to), "type": "file", "path": m.Secrets[to],
"sealed": found.Sealed,
}))
}
for _, to := range sortedKeys(m.Grants) {
for _, g := range with.Grants {
@@ -613,6 +619,15 @@ func grantPath(directory, consumer, module string) string {
return strings.TrimRight(directory, "/") + "/" + consumer + "." + module + ".secret"
}
// holderAs is a consumer's name at the provider with a local name after it, where it keeps several
// credentials for one provision (ADR 0094); the name alone otherwise.
func holderAs(as, local string) string {
if local == "" {
return as
}
return as + "_" + local
}
// contributions collects what every module in this set contributes, by requirement.
//
// Ordered by contributing module, because the result becomes a file on a machine and a file whose
@@ -649,8 +664,11 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
}
out[g.Provision] = append(out[g.Provision], Contribution{
From: g.From, Node: g.Consumer, At: g.At, Values: g.Values,
As: ConsumerIdentity(g.Consumer, IdentitySource(g.Slug, g.From)),
Secret: grantPath(directories[g.Provision], g.Consumer, g.From),
// One holder per local name: the identity the consumer is known by, and the local name
// after it where the module keeps several (ADR 0094). Not a login any backend checks —
// a secret is not a login — so the identity limit does not apply to the suffix.
As: holderAs(ConsumerIdentity(g.Consumer, IdentitySource(g.Slug, g.From)), g.Local),
Secret: grantPath(directories[g.Provision], g.Consumer, holderAs(g.From, g.Local)),
})
if granted[g.Provision] == nil {
granted[g.Provision] = map[string]bool{}
+181 -7
View File
@@ -278,6 +278,14 @@ type Manifest struct {
// makes `restart-on` precise.
Secrets map[string]string `json:"secrets,omitempty"`
// SecretsMany is the same key, `secrets`, where a requirement maps to SEVERAL files under local
// names — `"secret": {"admin": "/…/admin", "token": "/…/token"}` — because a module may need
// more than one value from a provider that gives one per pair (novox/hq 04-ISSUES/069, ADR
// 0094). Each local name is a pair credential of its own, keyed on that name, delivered as its
// own file, served to the provider as its own holder, and rotated with the others. Filled from
// the manifest's `secrets` object by UnmarshalJSON; never written by hand.
SecretsMany map[string]map[string]string `json:"-"`
// OwnSecrets are secrets this module needs in order to be itself, and where to put them.
//
// **Named for whose they are, not how secret they are.** `secrets` above is a credential for
@@ -619,6 +627,134 @@ func ReceivedID(requirement string) string { return "received-" + requirement }
//
// Every problem is reported rather than the first, because somebody writing a manifest fixes
// them in one pass or in four.
// manifestFields is Manifest without its methods, so the JSON methods below can use the ordinary
// field decoding for everything but `secrets`.
type manifestFields Manifest
// UnmarshalJSON reads `secrets` in both of its shapes — a path, or an object of local names to
// paths (ADR 0094) — and everything else exactly as the fields declare, unknown keys refused.
func (m *Manifest) UnmarshalJSON(raw []byte) error {
var keys map[string]json.RawMessage
if err := json.Unmarshal(raw, &keys); err != nil {
return err
}
plain := map[string]string{}
many := map[string]map[string]string{}
if secrets, ok := keys["secrets"]; ok && string(secrets) != "null" {
var byName map[string]json.RawMessage
if err := json.Unmarshal(secrets, &byName); err != nil {
return fmt.Errorf("secrets: an object of requirement to path, or to {local name: path}: %w", err)
}
for to, v := range byName {
switch {
case len(v) > 0 && v[0] == '"':
var path string
if err := json.Unmarshal(v, &path); err != nil {
return err
}
plain[to] = path
case len(v) > 0 && v[0] == '{':
var paths map[string]string
if err := json.Unmarshal(v, &paths); err != nil {
return fmt.Errorf("secrets.%s: an object of local name to path: %w", to, err)
}
many[to] = paths
default:
return fmt.Errorf("secrets.%s: a path, or an object of local name to path, not %s", to, v)
}
}
delete(keys, "secrets")
}
rest, err := json.Marshal(keys)
if err != nil {
return err
}
decoder := json.NewDecoder(bytes.NewReader(rest))
decoder.DisallowUnknownFields()
var fields manifestFields
if err := decoder.Decode(&fields); err != nil {
return err
}
*m = Manifest(fields)
if len(plain) > 0 {
m.Secrets = plain
}
if len(many) > 0 {
m.SecretsMany = many
}
return nil
}
// MarshalJSON writes `secrets` back in the shape it was read: paths, and objects of local names.
func (m Manifest) MarshalJSON() ([]byte, error) {
raw, err := json.Marshal(manifestFields(m))
if err != nil {
return nil, err
}
if len(m.SecretsMany) == 0 {
return raw, nil
}
var keys map[string]json.RawMessage
if err := json.Unmarshal(raw, &keys); err != nil {
return nil, err
}
merged := map[string]any{}
for to, path := range m.Secrets {
merged[to] = path
}
for to, paths := range m.SecretsMany {
merged[to] = paths
}
secrets, err := json.Marshal(merged)
if err != nil {
return nil, err
}
keys["secrets"] = secrets
return json.Marshal(keys)
}
// SecretFile is one file a module is given a credential in: the local name it goes by inside
// the module (empty for the ordinary one-file case, where the requirement's name serves) and where.
type SecretFile struct {
Local string
Path string
}
// SecretFiles is every file a module wants the credential for one requirement in, in a stable
// order: the plain path as one entry with no local name, or one entry per local name.
func (m Manifest) SecretFiles(to string) []SecretFile {
if path, ok := m.Secrets[to]; ok {
return []SecretFile{{Path: path}}
}
paths := m.SecretsMany[to]
out := make([]SecretFile, 0, len(paths))
for _, local := range sortedKeys(paths) {
out = append(out, SecretFile{Local: local, Path: paths[local]})
}
return out
}
// SecretRequirements is every requirement this module wants a credential file for, sorted.
func (m Manifest) SecretRequirements() []string {
seen := map[string]bool{}
for to := range m.Secrets {
seen[to] = true
}
for to := range m.SecretsMany {
seen[to] = true
}
return sortedKeys(seen)
}
// SecretLocal is the name a credential goes by inside the module: the local name where the
// requirement maps to several, else the requirement itself. It is what `${secret:<name>}` says.
func SecretLocal(to, local string) string {
if local == "" {
return to
}
return local
}
func ParseManifest(raw []byte) (Manifest, error) {
var m Manifest
// Strictly. **An unknown key is refused**, which is the discipline the host's declaration
@@ -908,11 +1044,47 @@ func ParseManifest(raw []byte) (Manifest, error) {
problems = append(problems, m.Module+" needs a secret with no name")
}
}
for to, where := range m.Secrets {
if !strings.HasPrefix(where, "/") {
problems = append(problems, fmt.Sprintf(
"%s keeps the credential for %q at %q, which is not an absolute path",
m.Module, to, where))
for _, to := range m.SecretRequirements() {
if _, plain := m.Secrets[to]; plain {
if _, also := m.SecretsMany[to]; also {
problems = append(problems, fmt.Sprintf(
"%s keeps the credential for %q both as one file and as several", m.Module, to))
}
}
for _, f := range m.SecretFiles(to) {
if !strings.HasPrefix(f.Path, "/") {
problems = append(problems, fmt.Sprintf(
"%s keeps the credential for %q at %q, which is not an absolute path",
m.Module, SecretLocal(to, f.Local), f.Path))
}
if f.Local != "" && !name.MatchString(f.Local) {
problems = append(problems, fmt.Sprintf(
"%s keeps a credential for %q under %q, which is not a usable name",
m.Module, to, f.Local))
}
// A local name is what `${secret:<name>}` says, so it may not be another requirement's
// name or one of the module's own secrets — the file would hold the wrong credential
// while every check passed.
if f.Local != "" {
if _, own := m.OwnSecrets[f.Local]; own {
problems = append(problems, fmt.Sprintf(
"%s keeps a credential for %q under %q, which is also one of its own secrets",
m.Module, to, f.Local))
}
for _, w := range m.Wants() {
if w == f.Local {
problems = append(problems, fmt.Sprintf(
"%s keeps a credential for %q under %q, which is also something it requires",
m.Module, to, f.Local))
}
}
}
}
if len(m.SecretsMany[to]) == 0 && m.Secrets[to] == "" {
if _, many := m.SecretsMany[to]; many {
problems = append(problems, fmt.Sprintf(
"%s keeps the credential for %q as several files and names none", m.Module, to))
}
}
var wanted bool
for _, w := range m.Wants() {
@@ -1119,8 +1291,10 @@ func (m Manifest) undeclaredMounts() []string {
for _, where := range m.OwnSecrets {
claim(where)
}
for _, where := range m.Secrets {
claim(where)
for _, to := range m.SecretRequirements() {
for _, f := range m.SecretFiles(to) {
claim(f.Path)
}
}
for _, where := range m.Receives {
claim(where)
+23 -3
View File
@@ -157,6 +157,10 @@ type Needed struct {
Sealed string
// For is the module that wanted it.
For string
// Local is the name this credential goes by inside that module, where the module wants several
// for one requirement (ADR 0094); empty for the ordinary one. Part of what identifies the pair
// credential, so two secrets from one provider to one module are two secrets.
Local string
// Manager is set when this holder is a refreshable-grant licence's MANAGER, delivered the refresh
// token rather than an access token (novox/hq ADR 0050). It changes one thing downstream: an empty
// Sealed is tolerated — the manager has not adopted a refresh token yet, which is a real waiting
@@ -298,7 +302,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
if at == "" {
at = "127.0.0.1"
}
needs = append(needs, Needed{
needs = eachLocal(needs, catalogue, Needed{
Name: want, From: node.Name, At: at,
Serves: servedHere(catalogue, chosen, want), For: because[want]})
} else if served := servedHere(catalogue, chosen, want); len(served) > 0 {
@@ -319,7 +323,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
if at == "" {
at = "127.0.0.1"
}
needs = append(needs, Needed{
needs = eachLocal(needs, catalogue, Needed{
Name: want, From: node.Name, At: at, Serves: served, For: because[want]})
}
continue
@@ -347,7 +351,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
node.Name, want, p.Node, meshNetwork))
return
}
needs = append(needs, Needed{Name: want, From: p.Node, At: p.At,
needs = eachLocal(needs, catalogue, Needed{Name: want, From: p.Node, At: p.At,
Serves: p.Serves, For: because[want]})
}
switch {
@@ -854,3 +858,19 @@ func perConsumer(needs []Needed, order []string, catalogue map[string]Manifest)
}
return out
}
// eachLocal appends the need once per file the wanting module keeps the credential in: once, with
// no local name, in the ordinary case; once per local name where the module wants several values
// from one provider (ADR 0094). Each is its own pair credential downstream.
func eachLocal(needs []Needed, catalogue map[string]Manifest, n Needed) []Needed {
files := catalogue[n.For].SecretFiles(n.Name)
if len(files) <= 1 {
return append(needs, n)
}
for _, f := range files {
one := n
one.Local = f.Local
needs = append(needs, one)
}
return needs
}
+20 -17
View File
@@ -61,23 +61,26 @@ func sealedFor(m Manifest, needs []Needed, with Rendering) (map[string]string, e
sealed[name] = value
}
}
for _, to := range sortedKeys(m.Secrets) {
if _, taken := sealed[to]; taken {
// A module whose own secret and whose requirement share a name. Refused rather than
// settled by precedence: whichever won, the manifest would read as though the other
// had, and the file would hold the credential for the wrong thing while every check
// passed.
return nil, fmt.Errorf(
"%s has a secret of its own called %q and also requires %q, so a file saying "+
"${secret:%s} could mean either — rename one of them", m.Module, to, to, to)
}
for i := range needs {
// `For == m.Module`, not name alone: on a node with two modules requiring the same
// provision, both appear in `needs`, and matching by name would fill ${secret:X} with
// whichever came last — the other module's credential (novox/hq 04-ISSUES/022). The
// `secrets:`-map path already guards this way; the ${secret:…} placeholder path did not.
if needs[i].Name == to && needs[i].For == m.Module && needs[i].Sealed != "" {
sealed[to] = needs[i].Sealed
for _, to := range m.SecretRequirements() {
for _, file := range m.SecretFiles(to) {
key := SecretLocal(to, file.Local)
if _, taken := sealed[key]; taken {
// A module whose own secret and whose requirement share a name. Refused rather than
// settled by precedence: whichever won, the manifest would read as though the other
// had, and the file would hold the credential for the wrong thing while every check
// passed.
return nil, fmt.Errorf(
"%s has a secret of its own called %q and also requires %q, so a file saying "+
"${secret:%s} could mean either — rename one of them", m.Module, key, key, key)
}
for i := range needs {
// `For == m.Module`, not name alone: on a node with two modules requiring the same
// provision, both appear in `needs`, and matching by name would fill ${secret:X} with
// whichever came last — the other module's credential (novox/hq 04-ISSUES/022). And
// the local name, where the module keeps several (ADR 0094).
if needs[i].Name == to && needs[i].For == m.Module && needs[i].Local == file.Local && needs[i].Sealed != "" {
sealed[key] = needs[i].Sealed
}
}
}
}
+125
View File
@@ -0,0 +1,125 @@
package catalogue
import (
"encoding/json"
"strings"
"testing"
)
// A module may need several values from one provider that gives one per pair (novox/hq
// 04-ISSUES/069, ADR 0094): `secrets` maps a requirement to several files under local names, and
// each local name is a pair credential of its own — its own need, its own file, its own holder.
const twoSecrets = `{"module":"ca","version":"1","requires":["secret"],
"secrets":{"secret":{"root-key":"/var/lib/ca/root.key","root-pass":"/var/lib/ca/root.pass"}},
"resources":[{"id":"state","type":"directory","path":"/var/lib/ca","mode":"0700"}]}`
func TestSecretsReadBothShapesAndWriteThemBack(t *testing.T) {
m, err := ParseManifest([]byte(twoSecrets))
if err != nil {
t.Fatal(err)
}
files := m.SecretFiles("secret")
if len(files) != 2 || files[0].Local != "root-key" || files[1].Path != "/var/lib/ca/root.pass" {
t.Fatalf("two files under local names, in order: %+v", files)
}
plain, err := ParseManifest([]byte(`{"module":"app","version":"1","requires":["secret"],"secrets":{"secret":"/var/lib/app/secret"}}`))
if err != nil {
t.Fatal(err)
}
if got := plain.SecretFiles("secret"); len(got) != 1 || got[0].Local != "" || got[0].Path != "/var/lib/app/secret" {
t.Fatalf("the plain shape is one file with no local name: %+v", got)
}
// Written back in the shape it was read, so a built manifest keeps its local names.
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
again, err := ParseManifest(raw)
if err != nil {
t.Fatalf("what was written does not read: %v\n%s", err, raw)
}
if len(again.SecretFiles("secret")) != 2 {
t.Fatalf("the local names did not survive a round trip:\n%s", raw)
}
}
func TestALocalNameMayNotCollideWithWhatTheModuleAlreadyCallsSomething(t *testing.T) {
for _, bad := range []string{
// One of the module's own secrets.
`{"module":"ca","version":"1","requires":["secret"],"own-secrets":{"root-key":"/var/lib/ca/own"},
"secrets":{"secret":{"root-key":"/var/lib/ca/root.key"}}}`,
// Something it requires.
`{"module":"ca","version":"1","requires":["secret","postgres-database"],
"secrets":{"secret":{"postgres-database":"/var/lib/ca/x"}}}`,
// Not a usable name.
`{"module":"ca","version":"1","requires":["secret"],"secrets":{"secret":{"Root Key":"/var/lib/ca/x"}}}`,
// A relative path.
`{"module":"ca","version":"1","requires":["secret"],"secrets":{"secret":{"root-key":"root.key"}}}`,
} {
if _, err := ParseManifest([]byte(bad)); err == nil {
t.Errorf("accepted:\n%s", bad)
}
}
}
func vaultAndCA() map[string]Manifest {
ca, _ := ParseManifest([]byte(twoSecrets))
vault := Manifest{Module: "mesh-vault", Version: "1", Provides: FromAnywhere("secret"),
Grants: map[string]string{"secret": "/var/lib/vault/grants"},
Receives: map[string]string{"secret": "/var/lib/vault/grants/mesh.json"}}
return shelf(vault, ca)
}
func TestEachLocalNameIsANeedAFileAndAHolderOfItsOwn(t *testing.T) {
got, err := Resolve(vaultAndCA(), []string{"mesh-vault", "ca"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
var locals []string
for _, n := range got.Needs {
if n.Name == "secret" && n.For == "ca" {
locals = append(locals, n.Local)
}
}
if strings.Join(locals, ",") != "root-key,root-pass" {
t.Fatalf("two secrets from one provider are two needs: %v", got.Needs)
}
for i := range got.Needs {
got.Needs[i].Sealed = "sealed-" + got.Needs[i].Local
}
out, err := got.Declaration(Rendering{})
if err != nil {
t.Fatal(err)
}
seen := map[string]string{}
for _, r := range out {
if r["type"] == "file" && strings.HasPrefix(r["path"].(string), "/var/lib/ca/root.") {
seen[r["id"].(string)] = r["sealed"].(string)
}
}
if seen["ca."+SecretID("root-key")] != "sealed-root-key" || seen["ca."+SecretID("root-pass")] != "sealed-root-pass" {
t.Fatalf("each local name is its own file with its own credential: %v", seen)
}
}
func TestAProviderSeesEachLocalNameAsAHolderOfItsOwn(t *testing.T) {
r := Resolution{Modules: []Manifest{vaultAndCA()["mesh-vault"]}}
got, err := r.contributions(SettingsBy{}, []Grant{
{Provision: "secret", Consumer: "workstation", From: "ca", Local: "root-key", Sealed: "x"},
{Provision: "secret", Consumer: "workstation", From: "ca", Local: "root-pass", Sealed: "y"},
}, map[string]string{"secret": "/var/lib/vault/grants"})
if err != nil {
t.Fatal(err)
}
given := got["secret"]
if len(given) != 2 {
t.Fatalf("two holders: %+v", given)
}
if given[0].As != "mesh_workstation_ca_root_key" && given[0].As != "mesh_workstation_ca_root-key" {
t.Fatalf("the holder is the consumer's identity with the local name after it: %q", given[0].As)
}
if given[0].Secret == given[1].Secret {
t.Fatalf("two holders share one file on the provider: %q", given[0].Secret)
}
}
@@ -0,0 +1,12 @@
-- A module may need several values from one provider that gives one per pair
-- (novox/hq 04-ISSUES/069, ADR 0094).
--
-- A pair credential was keyed on (provision, consumer node, consumer module, provider): one value
-- per module per provider. Seven catalogue modules hold two to four independent secrets of their
-- own -- a root certificate, its key and that key's password -- and the vault could serve each
-- module one. The pair now carries the LOCAL name the credential goes by inside the module; empty
-- for the ordinary one, so every existing row is the credential it was.
alter table secret add column local text not null default '';
alter table secret drop constraint secret_pkey;
alter table secret add primary key (name, local, consumer, consumer_module, provider);
+2 -2
View File
@@ -164,7 +164,7 @@ func TestAPairCredentialIsSealedToTheOperatorToo(t *testing.T) {
if _, err := inv.SetOperatorKey(ctx, pub); err != nil {
t.Fatal(err)
}
made, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider")
made, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
@@ -191,7 +191,7 @@ func TestAPairCredentialIsSealedToTheOperatorToo(t *testing.T) {
// 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 {
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") {
+48 -35
View File
@@ -24,11 +24,14 @@ type Secret struct {
// **Part of the key, not a label** (novox/hq 04-ISSUES/022). Two modules on one node wanting
// the same provision are two consumers, and were one credential until this.
ConsumerModule string
Provider string
ForConsumer string
ForProvider string
ConsumerKey string
ProviderKey string
// Local is the name the credential goes by inside the consumer where it keeps several for one
// provision (novox/hq ADR 0094); empty for the ordinary one. Part of the key.
Local string
Provider string
ForConsumer string
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
@@ -51,7 +54,7 @@ const (
// can no longer open what was sealed to the old one, so keeping the blob would deliver something
// unreadable for ever. The new secret reaches both ends in the same push, which is the only
// moment they can be changed together.
func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModule, provider string) (
func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModule, provider, local string) (
Secret, error) {
consumerKey, err := i.SealingKeyOf(ctx, consumer)
if err != nil {
@@ -74,12 +77,12 @@ 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, origin from secret
where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4`,
name, consumerNode.ID, consumerModule, providerNode.ID).
where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4 and local = $5`,
name, consumerNode.ID, consumerModule, providerNode.ID, local).
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
held.ConsumerModule, held.Local = consumerModule, local
return held, nil
}
if err == nil && held.Origin == OriginAccepted {
@@ -90,8 +93,8 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
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)
"again with `secret accept %s %s %s --provider %s%s`",
consumerModule, name, provider, consumer, consumerModule, name, provider, localFlag(local))
}
// And to the operator, when the mesh has one (novox/hq ADR 0085, amended): the third copy that
@@ -107,19 +110,19 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
forOperator, operatorKey := operatorColumns(operator, blob)
_, 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)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
on conflict (name, consumer, consumer_module, provider) do update set
consumer_key, provider_key, operator_sealed, operator_key, local)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
on conflict (name, local, 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(),
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key`,
name, consumerNode.ID, consumerModule, providerNode.ID,
made.ForConsumer, made.ForProvider, made.ConsumerKey, made.ProviderKey, forOperator, operatorKey)
made.ForConsumer, made.ForProvider, made.ConsumerKey, made.ProviderKey, forOperator, operatorKey, local)
if err != nil {
return Secret{}, err
}
return Secret{Name: name, Consumer: consumer, ConsumerModule: consumerModule,
return Secret{Name: name, Consumer: consumer, ConsumerModule: consumerModule, Local: local,
Provider: provider,
ForConsumer: made.ForConsumer, ForProvider: made.ForProvider,
ConsumerKey: made.ConsumerKey, ProviderKey: made.ProviderKey, Origin: OriginMade}, nil
@@ -133,7 +136,7 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
// 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 {
func (i *Inventory) AcceptSecretForPair(ctx context.Context, name, consumer, consumerModule, provider, local, value string) error {
consumerKey, err := i.SealingKeyOf(ctx, consumer)
if err != nil {
return err
@@ -165,16 +168,16 @@ func (i *Inventory) AcceptSecretForPair(ctx context.Context, name, consumer, con
}
_, 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
consumer_key, provider_key, operator_sealed, operator_key, origin, local)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
on conflict (name, local, 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)
forOperator, operatorKey, OriginAccepted, local)
return err
}
@@ -190,7 +193,7 @@ func (i *Inventory) AcceptSecretForPair(ctx context.Context, name, consumer, con
// **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 {
func (i *Inventory) RotateSecret(ctx context.Context, name, consumer, consumerModule, provider, local string) error {
consumerNode, err := i.NodeByName(ctx, consumer)
if err != nil {
return err
@@ -202,19 +205,19 @@ func (i *Inventory) RotateSecret(ctx context.Context, name, consumer, consumerMo
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)
and provider = $4 and local = $5`,
name, consumerNode.ID, consumerModule, providerNode.ID, local).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)
"--provider %s%s --from <file>`",
consumerModule, name, provider, consumer, consumerModule, name, provider, localFlag(local))
}
_, err = i.store.Pool().Exec(ctx,
`delete from secret where name = $1 and consumer = $2 and consumer_module = $3
and provider = $4`,
name, consumerNode.ID, consumerModule, providerNode.ID)
and provider = $4 and local = $5`,
name, consumerNode.ID, consumerModule, providerNode.ID, local)
return err
}
@@ -225,9 +228,9 @@ func (i *Inventory) SecretsFrom(ctx context.Context, provider string) ([]Secret,
return nil, err
}
rows, err := i.store.Pool().Query(ctx,
`select s.name, c.name, s.consumer_module, s.for_provider from secret s
`select s.name, c.name, s.consumer_module, s.local, s.for_provider from secret s
join node c on c.id = s.consumer
where s.provider = $1 order by s.name, c.name, s.consumer_module`, providerNode.ID)
where s.provider = $1 order by s.name, c.name, s.consumer_module, s.local`, providerNode.ID)
if err != nil {
return nil, err
}
@@ -236,7 +239,7 @@ func (i *Inventory) SecretsFrom(ctx context.Context, provider string) ([]Secret,
var out []Secret
for rows.Next() {
s := Secret{Provider: provider}
if err := rows.Scan(&s.Name, &s.Consumer, &s.ConsumerModule, &s.ForProvider); err != nil {
if err := rows.Scan(&s.Name, &s.Consumer, &s.ConsumerModule, &s.Local, &s.ForProvider); err != nil {
return nil, err
}
out = append(out, s)
@@ -401,7 +404,9 @@ type Holder struct {
// ConsumerModule is which module on that machine holds it. Part of what identifies a
// credential (novox/hq 04-ISSUES/022), so rotating one consumer's does not touch another's.
ConsumerModule string
Provider string
// Local is the credential's name inside the consumer where it holds several (ADR 0094).
Local string
Provider string
}
// HoldersOf is every pair sharing a credential for one provision.
@@ -415,11 +420,11 @@ type Holder struct {
// Empty consumer means all of them.
func (i *Inventory) HoldersOf(ctx context.Context, provision, consumer string) ([]Holder, error) {
rows, err := i.store.Pool().Query(ctx,
`select s.name, c.name, s.consumer_module, p.name from secret s
`select s.name, c.name, s.consumer_module, s.local, p.name from secret s
join node c on c.id = s.consumer
join node p on p.id = s.provider
where s.name = $1 and ($2 = '' or c.name = $2)
order by c.name, s.consumer_module, p.name`, provision, consumer)
order by c.name, s.consumer_module, s.local, p.name`, provision, consumer)
if err != nil {
return nil, err
}
@@ -428,10 +433,18 @@ func (i *Inventory) HoldersOf(ctx context.Context, provision, consumer string) (
var out []Holder
for rows.Next() {
var h Holder
if err := rows.Scan(&h.Provision, &h.Consumer, &h.ConsumerModule, &h.Provider); err != nil {
if err := rows.Scan(&h.Provision, &h.Consumer, &h.ConsumerModule, &h.Local, &h.Provider); err != nil {
return nil, err
}
out = append(out, h)
}
return out, rows.Err()
}
// localFlag is the `--local` a remedy has to name where a credential has a local name.
func localFlag(local string) string {
if local == "" {
return ""
}
return " --local " + local
}
+80 -31
View File
@@ -63,11 +63,11 @@ func TestASecretIsMadeOnceAndKept(t *testing.T) {
// Regenerating on every declaration would restart both ends on every push, and — worse — the
// password a provider was told to create would never be the one its consumer was given.
inv, ctx := twoNodesWithKeys(t)
first, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider")
first, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
second, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider")
second, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
@@ -81,7 +81,7 @@ func TestTheStoredSecretIsNotTheSecret(t *testing.T) {
// what an encrypted column does not achieve, because whoever runs the control plane can read
// through it.
inv, ctx := twoNodesWithKeys(t)
got, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider")
got, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
@@ -114,7 +114,7 @@ func TestANewSealingKeyMeansANewSecret(t *testing.T) {
// A node that rejoined generated a new key and can no longer open what was sealed to the old
// one. Keeping the blob would deliver something unreadable for ever, reported as configured.
inv, ctx := twoNodesWithKeys(t)
before, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider")
before, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
@@ -126,7 +126,7 @@ func TestANewSealingKeyMeansANewSecret(t *testing.T) {
if err := inv.RecordSealingKey(ctx, node.ID, fresh); err != nil {
t.Fatal(err)
}
after, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider")
after, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
@@ -142,14 +142,14 @@ func TestANewSealingKeyMeansANewSecret(t *testing.T) {
func TestRotatingReachesBothEnds(t *testing.T) {
inv, ctx := twoNodesWithKeys(t)
before, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider")
before, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
if err := inv.RotateSecret(ctx, "postgres-database", "consumer", "gitea", "provider"); err != nil {
if err := inv.RotateSecret(ctx, "postgres-database", "consumer", "gitea", "provider", ""); err != nil {
t.Fatal(err)
}
after, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider")
after, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
@@ -184,7 +184,7 @@ func TestAProviderIsToldEveryCredentialItMustCreate(t *testing.T) {
t.Fatal(err)
}
for _, who := range []string{"consumer", "second-consumer"} {
if _, err := inv.SecretFor(ctx, "postgres-database", who, "gitea", "provider"); err != nil {
if _, err := inv.SecretFor(ctx, "postgres-database", who, "gitea", "provider", ""); err != nil {
t.Fatal(err)
}
}
@@ -215,7 +215,7 @@ func TestANodeWithNoSealingKeyCannotBeGivenASecret(t *testing.T) {
t.Fatal(err)
}
}
_, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider")
_, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", "")
if err == nil {
t.Fatal("a credential was made for nodes that cannot open one")
}
@@ -226,7 +226,7 @@ func TestANodeWithNoSealingKeyCannotBeGivenASecret(t *testing.T) {
func TestSecretsGoWhenANodeLeaves(t *testing.T) {
inv, ctx := twoNodesWithKeys(t)
if _, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider"); err != nil {
if _, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", ""); err != nil {
t.Fatal(err)
}
if _, err := inv.store.Pool().Exec(ctx, `delete from node where name = 'consumer'`); err != nil {
@@ -349,7 +349,7 @@ func TestACredentialGoesWhenTheConsumerStopsAskingForIt(t *testing.T) {
if err := inv.Assign(ctx, "consumer", "meshboard"); err != nil {
t.Fatal(err)
}
if _, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider"); err != nil {
if _, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", ""); err != nil {
t.Fatal(err)
}
@@ -379,7 +379,7 @@ func TestACredentialGoesWhenEitherMachineDoes(t *testing.T) {
// The case that must not leave a live login behind: a machine removed from the mesh. Its
// credentials go with it, and the provider stops being told to keep them.
inv, ctx := twoNodesWithKeys(t)
if _, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider"); err != nil {
if _, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", ""); err != nil {
t.Fatal(err)
}
if _, err := inv.store.Pool().Exec(ctx, `delete from node where name = 'consumer'`); err != nil {
@@ -473,12 +473,12 @@ func TestEveryHolderOfACredentialCanBeNamed(t *testing.T) {
t.Fatal(err)
}
for _, consumer := range []string{"consumer", "third"} {
if _, err := inv.SecretFor(ctx, "postgres-database", consumer, "gitea", "provider"); err != nil {
if _, err := inv.SecretFor(ctx, "postgres-database", consumer, "gitea", "provider", ""); err != nil {
t.Fatal(err)
}
}
// And one for a different provision, which must not be swept up.
if _, err := inv.SecretFor(ctx, "cache", "consumer", "gitea", "provider"); err != nil {
if _, err := inv.SecretFor(ctx, "cache", "consumer", "gitea", "provider", ""); err != nil {
t.Fatal(err)
}
@@ -509,14 +509,14 @@ func TestEveryHolderOfACredentialCanBeNamed(t *testing.T) {
// And rotating gives both ends a new credential, together — the same one.
func TestRotatingGivesBothEndsTheSameNewCredential(t *testing.T) {
inv, ctx := twoNodesWithKeys(t)
before, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider")
before, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
if err := inv.RotateSecret(ctx, "postgres-database", "consumer", "gitea", "provider"); err != nil {
if err := inv.RotateSecret(ctx, "postgres-database", "consumer", "gitea", "provider", ""); err != nil {
t.Fatal(err)
}
after, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider")
after, err := inv.SecretFor(ctx, "postgres-database", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
@@ -543,14 +543,14 @@ func TestRotatingGivesBothEndsTheSameNewCredential(t *testing.T) {
if err := inv.RecordSealingKey(ctx, third.ID, key); err != nil {
t.Fatal(err)
}
untouched, err := inv.SecretFor(ctx, "postgres-database", "third", "gitea", "provider")
untouched, err := inv.SecretFor(ctx, "postgres-database", "third", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
if err := inv.RotateSecret(ctx, "postgres-database", "consumer", "gitea", "provider"); err != nil {
if err := inv.RotateSecret(ctx, "postgres-database", "consumer", "gitea", "provider", ""); err != nil {
t.Fatal(err)
}
again, err := inv.SecretFor(ctx, "postgres-database", "third", "gitea", "provider")
again, err := inv.SecretFor(ctx, "postgres-database", "third", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
@@ -565,17 +565,17 @@ func TestRotatingGivesBothEndsTheSameNewCredential(t *testing.T) {
// 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 {
if err := inv.AcceptSecretForPair(ctx, "secret", "consumer", "gitea", "provider", "", "hunter2"); err != nil {
t.Fatal(err)
}
got, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider")
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")
again, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider", "")
if err != nil {
t.Fatal(err)
}
@@ -584,15 +584,15 @@ func TestAnAcceptedPairCredentialIsKeptAndNeverRemade(t *testing.T) {
}
// Rotation is refused, and says what to do instead.
err = inv.RotateSecret(ctx, "secret", "consumer", "gitea", "provider")
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 {
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 {
if err := inv.RotateSecret(ctx, "postgres-database", "consumer", "gitea", "provider", ""); err != nil {
t.Fatalf("a made credential no longer rotates: %v", err)
}
}
@@ -601,7 +601,7 @@ func TestAnAcceptedPairCredentialIsKeptAndNeverRemade(t *testing.T) {
// 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 {
if err := inv.AcceptSecretForPair(ctx, "secret", "consumer", "gitea", "provider", "", "hunter2"); err != nil {
t.Fatal(err)
}
node, err := inv.NodeByName(ctx, "consumer")
@@ -612,15 +612,64 @@ func TestAnAcceptedPairCredentialIsNotRemadeWhenAKeyChanges(t *testing.T) {
if err := inv.RecordSealingKey(ctx, node.ID, fresh); err != nil {
t.Fatal(err)
}
_, err = inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider")
_, 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 {
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 {
if _, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider", ""); err != nil {
t.Fatal(err)
}
}
// Two secrets from one provider to one module are two credentials (novox/hq 04-ISSUES/069, ADR
// 0094): keyed on the local name, made and rotated apart, and listed apart for the provider.
func TestTwoLocalNamesAreTwoCredentials(t *testing.T) {
inv, ctx := twoNodesWithKeys(t)
key, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider", "root-key")
if err != nil {
t.Fatal(err)
}
pass, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider", "root-pass")
if err != nil {
t.Fatal(err)
}
if key.ForConsumer == pass.ForConsumer {
t.Fatal("two local names were given one credential")
}
if err := inv.RotateSecret(ctx, "secret", "consumer", "gitea", "provider", "root-key"); err != nil {
t.Fatal(err)
}
keyAgain, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider", "root-key")
if err != nil {
t.Fatal(err)
}
passAgain, err := inv.SecretFor(ctx, "secret", "consumer", "gitea", "provider", "root-pass")
if err != nil {
t.Fatal(err)
}
if keyAgain.ForConsumer == key.ForConsumer || passAgain.ForConsumer != pass.ForConsumer {
t.Fatal("rotating one local name touched the other, or neither")
}
holders, err := inv.HoldersOf(ctx, "secret", "")
if err != nil {
t.Fatal(err)
}
var locals []string
for _, h := range holders {
locals = append(locals, h.Local)
}
if strings.Join(locals, ",") != "root-key,root-pass" {
t.Fatalf("the holders are listed apart, by local name: %v", holders)
}
from, err := inv.SecretsFrom(ctx, "provider")
if err != nil {
t.Fatal(err)
}
if len(from) != 2 || from[0].Local == from[1].Local {
t.Fatalf("the provider is told two credentials to create: %+v", from)
}
}
+1 -1
View File
@@ -85,7 +85,7 @@ func TestWhatANodeSaysWhenItJoinsIsWhatThisMeshReads(t *testing.T) {
t.Fatal(err)
}
secret, err := inv.SecretFor(ctx, "postgres-database", request.Node, "gitea", "the-other-end")
secret, err := inv.SecretFor(ctx, "postgres-database", request.Node, "gitea", "the-other-end", "")
if err != nil {
t.Fatalf("nothing could be sealed to a key that arrived from a real node: %v", err)
}