Skip to content

encrypt-container-secrets: state the verbatim key mapping; make A-3 hash-based - #31

Merged
phoenix-server merged 3 commits into
mainfrom
fix/encrypt-container-secrets-key-mapping-and-a3
Sep 17, 2026
Merged

phoenix-server merged 3 commits into
mainfrom
fix/encrypt-container-secrets-key-mapping-and-a3

Conversation

@phoenix-server

Copy link
Copy Markdown
Collaborator

Closes #26.

Problem

The verbatim key-name mapping was undocumented. sops exec-env injects each store key verbatim — the key name is the variable name — and modules/env-secrets.md never said so. An implementer migrating a value out of a compose environment: mapping (AUTH_PASSWORD: ${MOUSEHOLE_PASS}) keeps the store key MOUSEHOLE_PASS, the container starts, and the application crash-loops with "authentication is not configured" because it reads AUTH_PASSWORD. Nothing detected it either: the Phase 7 environment check counted matching environment lines, so a wrongly-named variable was invisible to it.

A-3 could not pass as written. docker exec <service> sh -c 'grep -rl AGE-SECRET-KEY-1 / 2>/dev/null' matches the dedicated key file that is supposed to be mounted, so its expected result was ambiguous rather than pass/fail — and it walks /proc, where it does not terminate inside a container (observed: still running after 20 s, with the intended key file already matched).

Change

  • modules/env-secrets.md: the verbatim rule with a concrete before/after (compose mapping → store key), the symptom, and the remedy in Failure Behavior; the Outputs and Idempotency notes now say names, not counts.
  • SCHEMATIC.md: the rule as a Preservation List constraint (the pitfalls list) and as a dotenv-store contract point. Phase 7 step 2 compares names — the store's keys (decrypted host-side, sops_ metadata filtered), the application process's environment names (found by the SOPS_AGE_KEY_FILE marker the wrapper exports, not a guessed PID), and the names the application reads; MISSING <name> is the misnamed-key signature, now first in the Phase 7 failure modes. Q-2's default names that discovery method.
  • A-3 is now content-hash based: the mounted key is identified by sha256 against the host's dedicated key (must match) and the master key (must not), and the master key's bytes are searched for by digest over the container's own filesystems — /proc, /sys, /dev pruned, -size filtered before anything is hashed, so it terminates in under a second.
  • The same unbounded pattern was swept: Phase 7 step 4 and A-4 are scoped to the writable locations, with docker diff alongside so a write outside them is still visible.
  • version: 0.2.1 → 0.2.2 (content change to a published spec), updated: 2026-09-17, plus a Decisions entry recording both fixes.

Verification

$ bash scripts/validate-catalog.sh
catalog ok: 19 entries, featured=['update-images-on-push', 'run-a-book-library', 'improve-docker-security', 'run-a-movies-and-series-library', 'run-a-music-library'], 18 specs, 21 pins verified

Both command blocks were run verbatim against real containers (alpine:3, busybox find/sha256sum/grep), one started with the wrapper's marker variable and a mounted key file:

Check Result
A-3, no copy of the master key present 0
A-3, a same-size file with different content present 0 (hash, not size or substring, decides)
A-3, a copy of the master key planted 1
A-3, mounted-key identity mounted key == dedicated key, mounted key != master key
Phase 7 step 2 (a)/(b), correct deployment empty / empty
Phase 7 step 2 (b), defective deployment (store key LEGACY_PASS, application reads AUTH_PASSWORD) MISSING AUTH_PASSWORD
Old A-3 form, same image still walking /proc after 20 s, intended key file already matched

Each A-3 scan finished in under a second; the whole run (container create → both scans → identity checks → cleanup) took 1.4 s. Containers were created for the test and removed afterwards.

No other schematic is touched, and no pin changes: nothing in the catalog pins this schematic, and every pin is read at its own commit, so this commit cannot invalidate one (the validator's 21 pins verify on this branch).

Not in this PR: the frontmatter/description parity work (#23) and the ten-principles list (#24). marketplace.json still reads "SOPS and age" where this spec's frontmatter says "SOPS + age" — pre-existing, #23's scope, left alone.

…3 could not pass

Both defects in #26, found while implementing v0.2.1 for real.

## 1. The verbatim key-name mapping was undocumented

`modules/env-secrets.md` said the container "hands the values to the
application as environment variables" without ever saying that
`sops exec-env` injects each store key **verbatim** — the key name *is*
the variable name, with no mapping layer and no rename.

That is exactly the case a migration breaks. A value that compose used to
map (`environment: AUTH_PASSWORD: ${MOUSEHOLE_PASS}`) keeps its store key
`MOUSEHOLE_PASS`, the container starts, and the application crash-loops
with "authentication is not configured" because it reads `AUTH_PASSWORD`.
Nothing in the boot path errors, which is what makes it expensive.

Nothing caught it either: the Phase 7 environment check counted matching
environment lines, and a wrongly-named variable simply does not appear in
that count.

Now:

- `modules/env-secrets.md` opens with the rule and a concrete
  before/after (compose mapping → store key), the failure symptom, and the
  remedy under Failure Behavior.
- `SCHEMATIC.md` carries the rule as a Preservation List constraint
  (pitfalls) and as a dotenv-store contract point.
- Phase 7 step 2 compares **names** instead of counting lines: the store's
  keys (decrypted host-side, `sops_` metadata filtered), the application
  process's environment names (found by the `SOPS_AGE_KEY_FILE` marker the
  wrapper exports, not by a guessed PID), and the names the application
  reads. A `MISSING <name>` is the misnamed-key signature, and the Phase 7
  failure modes now list it first.
- Q-2's default names that marker-based discovery.

## 2. A-3 could not pass as written

```sh
docker exec <service> sh -c 'grep -rl AGE-SECRET-KEY-1 / 2>/dev/null'
```

It matches the dedicated key file that is *supposed* to be mounted, so the
expected result was ambiguous, and it walks `/proc`, where it does not
terminate inside a container.

A-3 is now a content-hash procedure: the mounted key is identified by
sha256 against the host's dedicated key (must match) and against the
master key (must not), and the master key's bytes are searched for by
digest over the container's own filesystems — `/proc`, `/sys` and `/dev`
pruned, `-size` filtered before anything is hashed.

The same unbounded pattern was swept out of Phase 7 step 4 and A-4: both
are now scoped to the writable locations, with `docker diff` alongside so
a write outside the scanned directories is still visible.

`version: 0.2.1 → 0.2.2`, `updated: 2026-09-17`, and a Decisions entry
recording both fixes.

Refs #26
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 17, 2026

Copy link
Copy Markdown

Deploying agentic-schematics with  Cloudflare Pages  Cloudflare Pages

Latest commit: e140882
Status: ✅  Deploy successful!
Preview URL: https://027a2ed7.agentic-schematics.pages.dev
Branch Preview URL: https://fix-encrypt-container-secret.agentic-schematics.pages.dev

View logs

…hat produced them

The Decisions entry narrated two defects and cited the issue that tracked them,
and the A-3 provenance paragraph told the story of the test it replaced. Both now
state what is true: the store key name is the variable name, and A-3 decides by
content hash rather than by a substring that also matches the key which is
supposed to be mounted.

Everything a builder needs is kept — the traps (a count of environment lines
cannot see a wrongly-named variable; a substring scan matches the mounted key and
descends into /proc, where it does not terminate) and the verification provenance
(verified in alpine:3: 0 with no copy present, 0 for a same-size file with
different content, 1 when a copy is planted, each under a second).

Prose only: no command, threshold, order, or check logic changed.
…se, as the state a reader migrates from rather than a 'used to'
@phoenix-server
phoenix-server merged commit da0ac23 into main Sep 17, 2026
2 checks passed
@phoenix-server
phoenix-server deleted the fix/encrypt-container-secrets-key-mapping-and-a3 branch September 17, 2026 23:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

encrypt-container-secrets: the verbatim key-name mapping is undocumented, and acceptance test A-3 cannot pass as written

2 participants