Multiple fixes: several secrets per module (069), ask a module's tool (049), copy an upstream image (046), declare a vendor image (064) #38
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 + ")"
|
||||
}
|
||||
|
||||
@@ -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>]"
|
||||
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+12
@@ -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);
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user