A module names who its secret files belong to (secrets-owner)
The control plane runs as 65534 and crash-looped on permission denied the first time its credentials were mounted as files the host wrote as root at 0600 — the env-file shape hid this because the daemon reads an env-file on the host side. The composer now gives a module's secret files the owner the manifest names.
This commit is contained in:
@@ -271,9 +271,9 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
|
|||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"%s needs a secret called %q and none was made for it", m.Module, name)
|
"%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,
|
"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
|
// 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
|
// 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.
|
// worse than none: something would read it and fail authenticating.
|
||||||
continue
|
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],
|
"id": SecretID(to), "type": "file", "path": m.Secrets[to],
|
||||||
"sealed": found.Sealed,
|
"sealed": found.Sealed,
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
for _, to := range sortedKeys(m.Grants) {
|
for _, to := range sortedKeys(m.Grants) {
|
||||||
for _, g := range with.Grants {
|
for _, g := range with.Grants {
|
||||||
@@ -799,6 +799,14 @@ func keptFile(dir string, kept *KeptExport) (map[string]any, error) {
|
|||||||
}, nil
|
}, 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.
|
// sortedKeys is map iteration made repeatable, which everything written to a machine needs.
|
||||||
func sortedKeys[V any](m map[string]V) []string {
|
func sortedKeys[V any](m map[string]V) []string {
|
||||||
out := make([]string, 0, len(m))
|
out := make([]string, 0, len(m))
|
||||||
|
|||||||
@@ -297,6 +297,18 @@ type Manifest struct {
|
|||||||
// module, in a file anybody can read, for ever.
|
// module, in a file anybody can read, for ever.
|
||||||
OwnSecrets map[string]string `json:"own-secrets,omitempty"`
|
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
|
// 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).
|
// vault's field, and so far nobody else's (novox/hq ADR 0085, amended).
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -70,3 +70,34 @@ func TestAnEnvFileWithoutASecretNeedsNothing(t *testing.T) {
|
|||||||
t.Fatal(err)
|
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"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user