diff --git a/02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md b/02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md new file mode 100644 index 0000000..5bf8e90 --- /dev/null +++ b/02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md @@ -0,0 +1,75 @@ +--- +topic: building it +status: accepted +date: 2026-09-21 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0085-a-secret-is-a-provision.md +--- + +# 86. A secret reaches a process as a file, and an exception is declared + +## Context + +The mesh seals a secret to the machine that uses it and discards the plaintext; the host unseals it +into a file at 0600 ([ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md)). Then +the module hands it to its container through an env-file, and the runtime puts it where anything +on the machine that can talk to the runtime can read it: `docker inspect` prints it, and +`/proc//environ` holds it for the life of the process +([issue 041](../04-ISSUES/041-a-sealed-credential-ends-up-in-the-process-environment/00-report.md)). + +Two facts made this a decision rather than a fix. **It is a supported shape**: placing +`${secret:…}` in a file a container reads as its environment is what playbook 06 shows, and 36 +containers in 25 catalogue modules do it, the controller's own among them. And **nothing says which +secrets are exposed**: a reader cannot tell from a manifest whether a credential is protected from +`inspect` or not, because the manifest looks the same either way. The vault +([ADR 0085](0085-a-secret-is-a-provision.md)) rests on the seal this undoes. + +## Considered Options + +1. **Leave it: the environment is where configuration goes.** Rejected — it gives the sealed value + back to a routine operation, and the care taken to seal it is then theatre. +2. **Refuse every secret in an environment.** Rejected — some software reads its configuration + from the environment and nothing else, and a rule the catalogue cannot obey is a rule that gets + switched off. +3. **A secret reaches a process as a file; an environment exception is declared, with a reason, + and refused otherwise.** Adopted. + +## Decision + +**A secret reaches a process as a file.** A module mounts the file the host wrote and points the +program at it; the mesh's own programs accept a `_FILE` twin for every variable that carries a +credential, the way the controller's store connections already did. + +**A container that reads a secret from its environment says so.** The manifest key +`secrets-in-environment` on the container carries the reason. It is catalogue-level — the host +never sees it — and it exists so a reader can tell from the manifest which secrets are exposed +that way and why. + +**Everything else is refused.** A `${secret:…}` placeholder inside a container's `env` is never +filled and is refused outright. A file carrying a secret that a container names in `env-file` is +refused unless the container declares the exception. + +## Consequences + +The property *a sealed credential is readable only where it is used* becomes a manifest-level +fact: true where no exception is declared, stated where one is. The controller reads all six of +its credentials from files; the 35 other containers are marked with their reason and convert one +by one where their software accepts a path, which is per-module work under +[issue 041](../04-ISSUES/041-a-sealed-credential-ends-up-in-the-process-environment/00-report.md). + +What got harder: a module author meets one more refusal, and the reason they write is only as +honest as they are. What is not decided: the reach of the exposure on a node — which identities can +talk to the runtime — which decides whether a declared exception is a hardening item or something +sharper. + +## How it is checked + +The catalogue engine refuses at composition, before anything reaches a machine, and its tests +refuse both shapes and prove the reason is stripped from what is sent. + +## References + +- [issue 041](../04-ISSUES/041-a-sealed-credential-ends-up-in-the-process-environment/00-report.md) +- [ADR 0048](0048-a-provider-creates-the-credential-the-mesh-minted.md), [ADR 0085](0085-a-secret-is-a-provision.md) +- [`03-DESIGN/01-to-be/13-credentials-and-their-rotation.md`](../03-DESIGN/01-to-be/13-credentials-and-their-rotation.md) diff --git a/03-DESIGN/01-to-be/13-credentials-and-their-rotation.md b/03-DESIGN/01-to-be/13-credentials-and-their-rotation.md index 47d1f33..b5db27e 100644 --- a/03-DESIGN/01-to-be/13-credentials-and-their-rotation.md +++ b/03-DESIGN/01-to-be/13-credentials-and-their-rotation.md @@ -5,11 +5,12 @@ code: - mesh-controller internal/inventory/secrets.go - mesh-controller cmd/mesh-controller/rotate.go - mesh-controller examples/postgres-provisioner -updated: 2026-09-20 +updated: 2026-09-21 decisions: - 02-DECISIONS/0001-mesh-brokers-nodes-host-agents-think.md - 02-DECISIONS/0009-modules-and-the-graph.md - 02-DECISIONS/0085-a-secret-is-a-provision.md + - 02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md --- # 13 — Credentials, and moving them @@ -88,6 +89,19 @@ reads exactly like a credential that was never delivered. That has now happened environment needs a person in the middle of the one path that exists so there is not one, and puts a superuser password where `docker inspect` prints it. +## A secret reaches a process as a file + +The care above ends at the container's door if the module then hands the value to the runtime as +an environment variable: `docker inspect` prints it, and the process's `/proc` entry holds it for +anything on the machine that can talk to the runtime. So a secret reaches a process **as a file** +([ADR 0086](../../02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md)): the module mounts +the file the host wrote and points the program at it, and the mesh's own programs take a `_FILE` +twin for every variable that carries a credential. Software that reads only its environment is +not forbidden; it is **declared**, on the container, with a reason, so the manifest says which +secrets are exposed that way. **Checked** at composition: the catalogue engine refuses a +`${secret:…}` in a container's `env` outright, and a secret-carrying file named in `env-file` +unless the container carries the declaration. + ## How it is checked Not by comparing two files. **Two ends holding a matching string proves they agree, not that either diff --git a/04-ISSUES/041-a-sealed-credential-ends-up-in-the-process-environment/00-report.md b/04-ISSUES/041-a-sealed-credential-ends-up-in-the-process-environment/00-report.md index d7e79d2..58fa584 100644 --- a/04-ISSUES/041-a-sealed-credential-ends-up-in-the-process-environment/00-report.md +++ b/04-ISSUES/041-a-sealed-credential-ends-up-in-the-process-environment/00-report.md @@ -1,9 +1,9 @@ --- -status: open +status: resolved opened: 2026-09-10 -located-in: [] -fixed-by: -amended-design: +located-in: [mesh-controller internal/catalogue, mesh-catalog modules/mesh-controller] +fixed-by: ADR 0086; mesh-controller feat/secret-not-in-environment (envfile twins for the broker settings, catalogue refusal of secrets in env / undeclared env-file); mesh-catalog (the controller reads its six credentials from files; 35 containers declare their exception); mesh-host (the installer delivers any MESH_…_FILE own secret) +amended-design: 03-DESIGN/01-to-be/13-credentials-and-their-rotation.md --- # 041 — A credential the mesh took care to seal ends up in the process environment @@ -65,3 +65,11 @@ environment; 28 catalogue manifests use env-file for a secret, and nothing in th engine refuses a `${secret:…}` placeholder in one. The vault work of [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md) rests on the seal this weakens. +## Resolved 2026-09-21 + +By [ADR 0086](../../02-DECISIONS/0086-a-secret-reaches-a-process-as-a-file.md): a secret reaches +a process as a file, an environment exception is declared with a reason, and the catalogue engine +refuses the undeclared shape. The controller, the instance this was opened on, reads all six of +its credentials from files. The 35 other containers carry a declared reason; converting each where +its software accepts a path remains per-module work and is not this issue's. +