chore: remove the unused second stack definition

deploy/stack.yaml arrived in the first commit and was never deployed. Portainer
runs the repository's docker-compose.yml. Keeping both meant two definitions
drifting apart, with the documentation naming the one nobody used, which is how
the credential question came up at all.

The hardening it offered is narrower than it looks: Docker secrets keep values
out of docker inspect and the Portainer console, but local/secrets.py loads them
into the process environment regardless, and anyone able to read docker inspect
can already read the secret files. With a single Portainer user, the benefit that
remains does not outweigh maintaining a divergent copy.

local/secrets.py stays: inert against the deployed file, and it lets a stack
switch to Docker secrets later without touching code. The preflight and its tests
degrade cleanly when no such stack is present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-21 13:48:22 -03:00
parent e95a42dbed
commit 9926d3ab55
5 changed files with 25 additions and 344 deletions

View File

@@ -222,38 +222,30 @@ refactor: `'This runtime only supports APP_ENV=local'`,
- Replace marker matching with behavioural assertions (import the module, assert
the adapter classes in use).
### `[ ]` 2.12 — Production credentials are environment variables, and the docs name the wrong file
### `[x]` 2.12 — Two divergent stack definitions; the docs named the wrong one
Found 2026-09-21, by asking which file Portainer deploys.
Found 2026-09-21 by asking which file Portainer deploys. `PORTAINER.md` called
`deploy/stack.yaml` "the production stack"; the deployed file is the repository's
`docker-compose.yml`. `deploy/stack.yaml` came from the first commit and was never
deployed — it supplied credentials as Docker secrets where the deployed file uses
plain environment variables.
`PORTAINER.md` called `deploy/stack.yaml` "the production stack". The deployed
file is the repository's `docker-compose.yml`. They are not equivalent:
**Decision (2026-09-21): keep `docker-compose.yml`, delete `deploy/stack.yaml`.**
| File | Credentials | Deployed |
|---|---|---|
| `deploy/stack.yaml` | 11 Docker secret files | no |
| `docker-compose.yml` | 8 plain environment variables | yes |
The gain from Docker secrets here is narrower than it sounds. It keeps values out
of `docker inspect` and the Portainer UI, but `local/secrets.py` loads them into
the process environment anyway, and anyone who can read `docker inspect` is
already root or in the docker group and could read the secret files directly. The
operator is the only Portainer user, so the main benefit — limiting what a
lower-privileged console user can see — does not apply. Maintaining two
definitions that drift was the larger real cost.
So `DATABASE_PASSWORD`, `AWS_SECRET_ACCESS_KEY` and `OPERATOR_PASSWORD` sit in the
container environment, readable through `docker inspect`, `docker service inspect`,
the Portainer stack editor, and `/proc/<pid>/environ` for anything in that
container. The R2 secret key is the worst of them: it grants read and write over
every customer's artwork.
`local/secrets.py` stays. It is inert against the deployed file and costs nothing,
and it means a stack can switch to Docker secrets later without a code change.
The `*_FILE` loading from 2.3 makes the hardened file bootable, so the work is
done — what remains is a decision, because changing how production receives its
credentials is not a change to make quietly:
- **Migrate to `deploy/stack.yaml`:** create the 11 secrets in Portainer, point the
stack at that file. Best posture, most operational steps, and it also switches the
published ports (8080/8081 vs 18080/18081) so the reverse proxy needs updating.
- **Add Docker secrets to `docker-compose.yml`:** smaller change, keeps ports and
the current stack definition, still removes the values from the environment.
- **Accept it explicitly** and delete `deploy/stack.yaml` so two divergent
definitions stop drifting.
Rotate the R2 key and operator password whichever is chosen, since the current
values have been readable from the stack environment.
Still open: **rotate the R2 secret key.** Not because of Portainer, but because it
grants read and write over every customer's artwork and has been readable from the
stack environment for some time. The operator password is worth rotating with it.
### `[x]` 2.6 — Base images are not pinned `(F10)`