diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index 616908c..f06e91e 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -271,9 +271,9 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) { return nil, fmt.Errorf( "%s needs a secret called %q and none was made for it", m.Module, name) } - first = append(first, map[string]any{ + first = append(first, ownedBy(m.SecretsOwner, map[string]any{ "id": NeedID(name), "type": "file", "path": m.OwnSecrets[name], "sealed": sealed, - }) + })) } // Operator-owned paths this module is granted use of (novox/hq ADR 0051). Written before // the module's own resources, and so before the container that mounts them: the host must @@ -320,10 +320,10 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) { // worse than none: something would read it and fail authenticating. continue } - first = append(first, map[string]any{ + 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 { @@ -799,6 +799,14 @@ func keptFile(dir string, kept *KeptExport) (map[string]any, error) { }, nil } +// ownedBy gives a secret file the owner the module named, when it named one (Manifest.SecretsOwner). +func ownedBy(owner string, file map[string]any) map[string]any { + if owner != "" { + file["owner"] = owner + } + return file +} + // sortedKeys is map iteration made repeatable, which everything written to a machine needs. func sortedKeys[V any](m map[string]V) []string { out := make([]string, 0, len(m)) diff --git a/internal/catalogue/manifest.go b/internal/catalogue/manifest.go index 638fe86..d2096cb 100644 --- a/internal/catalogue/manifest.go +++ b/internal/catalogue/manifest.go @@ -297,6 +297,18 @@ type Manifest struct { // module, in a file anybody can read, for ever. OwnSecrets map[string]string `json:"own-secrets,omitempty"` + // SecretsOwner is who the files holding this module's secrets belong to on the machine — + // `uid:gid`, or a name — when its process is not root. + // + // **A secret reaches a process as a file** (novox/hq ADR 0086), and a file the host writes at + // 0600 as root is a file a container running as another account cannot read: the control + // plane, `USER 65534` in a scratch image, crash-looped on `permission denied` the first time + // its credentials were mounted instead of read from an env-file (which the daemon reads, as + // root, on the host side — which is exactly why that shape hid the problem). Absent means + // root, which is what a process that runs as root needs and what a process that does not + // cannot use. + SecretsOwner string `json:"secrets-owner,omitempty"` + // Keeps is where this module wants every operator-sealed secret in the mesh written — the // vault's field, and so far nobody else's (novox/hq ADR 0085, amended). // diff --git a/internal/catalogue/secrets_in_environment_test.go b/internal/catalogue/secrets_in_environment_test.go index 9bbde92..da911b4 100644 --- a/internal/catalogue/secrets_in_environment_test.go +++ b/internal/catalogue/secrets_in_environment_test.go @@ -70,3 +70,34 @@ func TestAnEnvFileWithoutASecretNeedsNothing(t *testing.T) { t.Fatal(err) } } + +// A module whose process is not root names who its secret files belong to, and every secret file +// the composer writes for it carries that owner — the mounted file is readable where it is used. +func TestSecretFilesCarryTheOwnerTheModuleNamed(t *testing.T) { + m := aModuleWithAnEnvFileSecret("said") + m.SecretsOwner = "65534:65534" + out, err := declare(t, m) + if err != nil { + t.Fatal(err) + } + var seen bool + for _, r := range out { + if r["path"] != "/var/lib/app/token.secret" { + continue + } + seen = true + if r["owner"] != "65534:65534" { + t.Errorf("the secret file is owned by %v", r["owner"]) + } + } + if !seen { + t.Fatal("no secret file in the declaration") + } + m.SecretsOwner = "" + out, _ = declare(t, m) + for _, r := range out { + if r["path"] == "/var/lib/app/token.secret" && r["owner"] != nil { + t.Errorf("an owner was invented: %v", r["owner"]) + } + } +}