Skip to content

Commit 51cf24a

Browse files
committed
docs(container): documentation & CI audit guards (ADR-T-009 Phase 9)
Closes out ADR-T-009 by landing the documentation and CI-audit work that the earlier phases deferred. No runtime behaviour changes; the diff is operator-facing docs plus three new container-infra lint jobs. Highlights: - CHANGELOG: consolidate the Phase 1–9 story under a single ADR-T-009 entry covering the lean release / debug split, the three extracted helper crates (health-check, auth-keypair, config-probe), the Compose split, and the vendored su-exec audit. Also document the three breaking changes operators need to know about (mandatory database.connect_url and tracker.token, mutually-exclusive AUTH__*_PEM / AUTH__*_PATH pairs, and the demotion of TORRUST_INDEX_DATABASE_DRIVER to a first-boot TOML selector). - README: replace the bare `docker run` / `podman run` one-liners with the now-required override pair, and add a Compose-sandbox section pointing at `make up-dev` / `make up-prod`. Cross-link ADR-T-009 from the docs index. - docs/containers.md: document the test-stage build gate (no --skip-tests escape hatch, by design), the new IMPORTER_API_PORT and TZ env vars, and the canonical entry-script env-var manifest contract. - share/container/entry_script_sh: add the `ENTRY_ENV_VARS` / `END_ENTRY_ENV_VARS` manifest block as the single source of truth for every env var the script consults (including dynamically constructed AUTH__*_{PEM,PATH} names a naive grep would miss). - contrib/dev-tools/su-exec/AUDIT.md: new file recording provenance, choice rationale (vs gosu / setpriv / su), re-audit triggers (file-change + CVE, deliberately not calendar-based), and an append-only audit log anchored by SHA-256. - .github/workflows/container.yaml: new `lints` job that the existing `test` job now depends on, with three guards — (1) compose.yaml stays free of mailcatcher / SMTP wiring (comments stripped before grepping so the explanatory header doesn't trip the audit), (2) su-exec.c SHA-256 matches the most recent AUDIT.md entry, and (3) every env var in the entry-script manifest is documented in docs/containers.md. - ADR + implementation plan: flip status from Proposed / Phase 9 "Not started" to Implemented. - AGENTS.md: extend the helper-crates list with index-cli-common and index-entry-script.
1 parent af823d5 commit 51cf24a

22 files changed

Lines changed: 396 additions & 88 deletions

.github/workflows/container.yaml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,90 @@ env:
1515
CARGO_TERM_COLOR: always
1616

1717
jobs:
18+
lints:
19+
name: Lints (Container infra)
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- id: checkout
24+
name: Checkout Repository
25+
uses: actions/checkout@v6
26+
27+
# Phase 9 §9.1.3 — guard against re-introducing the
28+
# `mailcatcher` dev sidecar (or any SMTP/mail config)
29+
# into the production-shaped baseline. The override
30+
# file is *expected* to mention `mailcatcher` and is
31+
# deliberately excluded from the audit. Comments are
32+
# stripped before grepping so the explanatory header
33+
# in `compose.yaml` (which legitimately references
34+
# `mailcatcher` in prose) does not trip the audit;
35+
# we are looking for live YAML config, not docs.
36+
- id: compose-baseline-no-mailcatcher
37+
name: compose.yaml has no mailcatcher / SMTP wiring
38+
run: |
39+
set -eu
40+
# awk strips `# ...` comments while preserving line
41+
# numbering 1:1 with the source file, so any error
42+
# output points the reader at the real line.
43+
if awk '{ sub(/#.*/, ""); print }' compose.yaml \
44+
| grep -nE 'mailcatcher|MAILER|SMTP|smtp_'; then
45+
echo "::error file=compose.yaml::dev mail sidecar / SMTP config present in production-shaped baseline (ADR-T-009 §8.1, §9.1.3)"
46+
exit 1
47+
fi
48+
echo "compose.yaml clean."
49+
50+
# Phase 9 §9.2 — vendored `su-exec.c` must not change
51+
# without a fresh audit entry recording the new SHA-256
52+
# in contrib/dev-tools/su-exec/AUDIT.md.
53+
- id: su-exec-audit
54+
name: su-exec audit log matches vendored source
55+
run: |
56+
set -eu
57+
audit=contrib/dev-tools/su-exec/AUDIT.md
58+
test -s "$audit"
59+
recorded=$(sed -n '/^## Audit Log/,$ { s/^SHA-256: \([0-9a-f]\{64\}\)$/\1/p; }' "$audit" | tail -1)
60+
actual=$(sha256sum contrib/dev-tools/su-exec/su-exec.c | cut -d' ' -f1)
61+
if [ -z "$recorded" ]; then
62+
echo "::error file=$audit::no SHA-256 entry found in '## Audit Log' section (ADR-T-009 §9.2)"
63+
exit 1
64+
fi
65+
if [ "$recorded" != "$actual" ]; then
66+
echo "::error file=$audit::recorded SHA-256 ($recorded) does not match contrib/dev-tools/su-exec/su-exec.c ($actual). Append a new dated audit entry per ADR-T-009 §9.2."
67+
exit 1
68+
fi
69+
echo "su-exec audit current ($actual)."
70+
71+
# Phase 9 §9 / Acceptance Criterion #7 — every env var
72+
# listed in the entry script's manifest block must be
73+
# documented in docs/containers.md.
74+
- id: entry-env-docs
75+
name: entry-script env vars documented
76+
run: |
77+
set -eu
78+
script=share/container/entry_script_sh
79+
vars=$(sed -n '/^# ENTRY_ENV_VARS:/,/^# END_ENTRY_ENV_VARS/p' "$script" \
80+
| grep -oE '[A-Z][A-Z0-9_]+' \
81+
| sort -u)
82+
if [ -z "$vars" ]; then
83+
echo "::error file=$script::ENTRY_ENV_VARS manifest block not found or empty (ADR-T-009 §9 Crit. #7)"
84+
exit 1
85+
fi
86+
missing=0
87+
for v in $vars; do
88+
grep -q "$v" docs/containers.md || {
89+
echo "::error file=docs/containers.md::env var '$v' is in the entry-script manifest but not documented"
90+
missing=1
91+
}
92+
done
93+
grep -q 'compose\.override\.yaml' docs/containers.md || {
94+
echo "::error file=docs/containers.md::two-file Compose split (compose.override.yaml) is not documented"
95+
missing=1
96+
}
97+
[ "$missing" -eq 0 ]
98+
1899
test:
19100
name: Test (Docker)
101+
needs: lints
20102
runs-on: ubuntu-latest
21103

22104
strategy:

AGENTS.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,11 @@ use their own `ADR-<PREFIX>-<NNN>` form without the `§` prefix.
6868
| `R-` | render-text-as-image | `packages/render-text-as-image/` |
6969

7070
Helper crates (`index-health-check`, `index-auth-keypair`,
71-
`index-config`, `index-config-probe`) are internal
72-
implementation details of the root crate and do not own
73-
separate ADRs or specification docs. They share the `T-`
74-
prefix for any cross-references that target them.
71+
`index-config`, `index-config-probe`, `index-cli-common`,
72+
`index-entry-script`) are internal implementation details of
73+
the root crate and do not own separate ADRs or specification
74+
docs. They share the `T-` prefix for any cross-references
75+
that target them.
7576

7677
### General Rules
7778

CHANGELOG.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- ADR-T-009: Container infrastructure hardening (Phases 1, 2 & 3).
12+
- ADR-T-009: Container infrastructure refactor (Phases 1–9). Beyond
13+
the tactical hardening already noted under Phase 3, the refactor:
14+
- Splits the runtime image into a lean `release` (distroless
15+
`cc-debian13`) and `debug` (`cc-debian13:debug`) target. The
16+
`release` image keeps `/bin/busybox`, `/bin/su-exec`, and
17+
`/usr/bin/jq` root-only (mode `0700`/`0500 root:root`); the
18+
unprivileged `torrust` user gets `EACCES` on the entire toolset
19+
after privilege drop. The `debug` target retains the upstream
20+
`/busybox/` tree on `PATH` for interactive debugging.
21+
- Extracts three helper binaries into their own workspace crates
22+
with no transitive HTTP/TLS/async-runtime dependencies:
23+
`torrust-index-health-check` (renamed from `health_check`),
24+
`torrust-index-auth-keypair` (renamed from
25+
`torrust-generate-auth-keypair`), and the new
26+
`torrust-index-config-probe` (the same loader the application
27+
uses, exposing the resolved schema/database/auth state as JSON).
28+
- Splits Compose into a production-shaped
29+
[`compose.yaml`](./compose.yaml) baseline (no `mailcatcher`, no
30+
`tty`, dev ports bound to `127.0.0.1`, credentials referenced
31+
as bare `${VAR}`) and an auto-loaded
32+
[`compose.override.yaml`](./compose.override.yaml) supplying the
33+
dev sandbox. Two `Makefile` targets (`make up-dev`, `make up-prod`)
34+
wrap the documented invocation paths and validate required env
35+
vars before any container starts.
36+
- Adds `contrib/dev-tools/su-exec/AUDIT.md` recording provenance,
37+
rationale, and a SHA-256-anchored append-only audit log for the
38+
vendored `su-exec.c`. CI fails the build when the file changes
39+
without a matching audit entry.
40+
1341
- `torrust-index-config` workspace crate (`packages/index-config/`)
1442
containing the parsing surface of the configuration system: schema
1543
modules, validator, `load_settings`, `Info`, `Error`, the
@@ -91,6 +119,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
91119

92120
### Changed
93121

122+
- **BREAKING:** `database.connect_url` and `tracker.token` are now
123+
mandatory schema fields (no defaults in shipped TOMLs). Operators
124+
must supply both via env-var override
125+
(`TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL`,
126+
`TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN`) or by mounting a
127+
populated `index.toml`. Missing values fail at config-parse time
128+
with a precise serde `missing field` error rather than silently
129+
falling back to a hidden default (ADR-T-009 §D2).
130+
- **BREAKING:** `TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__*_PEM` and
131+
`TORRUST_INDEX_CONFIG_OVERRIDE_AUTH__*_PATH` are mutually exclusive
132+
within a single key, both keys must use the same delivery
133+
mechanism, and the pair must either both be configured or both be
134+
absent. Mixed/half-pair configurations are rejected by the entry
135+
script before the application starts (ADR-T-009 §D3).
136+
- **BREAKING:** `TORRUST_INDEX_DATABASE_DRIVER` is now a *first-boot
137+
TOML selector only* (read by the entry script to choose which
138+
shipped default `index.toml` to seed). It no longer dispatches the
139+
application's runtime database driver — that is derived from the
140+
URL scheme of `database.connect_url`. Operators who scripted around
141+
this env var to switch databases at runtime must instead supply
142+
`TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL`
143+
(ADR-T-009 §D2/§7.4).
94144
- **BREAKING:** TLS configuration renamed from `[net.tsl]` to `[net.tls]`
95145
in operator TOMLs and from `"tsl"` to `"tls"` in the settings JSON
96146
API response. The original spelling was a typo; corrected as a clean

README.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,56 @@ If you are using `Version 1` of `torrust-tracker-backend`, please view our [upgr
3131

3232
### Container Version
3333

34-
The Torrust Index is [deployed to DockerHub][dockerhub], you can run a demo immediately with the following commands:
34+
The Torrust Index is [deployed to DockerHub][dockerhub], you can run a demo
35+
immediately with the following commands. Per
36+
[ADR-T-009 §D2](./adr/009-container-infrastructure-refactor.md), the image
37+
no longer ships a default tracker token or `database.connect_url`, so two
38+
overrides are mandatory at startup — a bare `docker run -it
39+
torrust/index:develop` now fails with a `missing field` error rather than
40+
booting against hidden defaults.
3541

3642
#### Docker
3743

3844
```sh
39-
docker run -it torrust/index:develop
45+
docker run -it \
46+
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MyAccessToken" \
47+
--env TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL="sqlite:///var/lib/torrust/index/database/sqlite3.db?mode=rwc" \
48+
torrust/index:develop
4049
```
4150

4251
> Please read our [container guide][containers.md] for more information.
4352
4453
#### Podman
4554

4655
```sh
47-
podman run -it torrust/index:develop
56+
podman run -it \
57+
--env TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MyAccessToken" \
58+
--env TORRUST_INDEX_CONFIG_OVERRIDE_DATABASE__CONNECT_URL="sqlite:///var/lib/torrust/index/database/sqlite3.db?mode=rwc" \
59+
torrust/index:develop
4860
```
4961

5062
> Please read our [container guide][containers.md] for more information.
5163
64+
#### Compose (development sandbox)
65+
66+
For a complete local stack (index + tracker + MySQL + mailcatcher) the
67+
repository ships a Compose split: a production-shaped
68+
[`compose.yaml`](./compose.yaml) baseline plus an auto-loaded
69+
[`compose.override.yaml`](./compose.override.yaml) that supplies dev
70+
defaults. Two `Makefile` wrappers cover the documented invocation
71+
paths:
72+
73+
```sh
74+
# Dev sandbox (auto-loads compose.override.yaml):
75+
make up-dev
76+
77+
# Production-shaped (validates required credentials first):
78+
make up-prod
79+
```
80+
81+
See [Compose Split](./docs/containers.md#compose-split) in the container
82+
guide for the required env vars and the validation contract.
83+
5284
### Development Version
5385

5486
- Please assure you have the ___[latest stable (or nightly) version of Rust][Rust]___.
@@ -144,6 +176,7 @@ The following services are provided by the default configuration:
144176
- [ADR-T-006: Refactor the Error System](adr/006-error-system-refactor.md) — Replace the 41-variant `ServiceError` god enum with domain-scoped error enums (`AuthError`, `UserError`, `TorrentError`, `CategoryTagError`) and a thin `ApiError` wrapper.
145177
- [ADR-T-007: Refactor the JWT System](adr/007-jwt-system-refactor.md) — Centralise JWT handling into `src/jwt.rs`, redesign claims to RFC 7519, move to RS256 asymmetric signing, and consolidate session validation into a single code path.
146178
- [ADR-T-008: Refactor the Roles and Permissions System](adr/008-roles-and-permissions-refactor.md) — Replace Casbin with a native Rust permission system (`PermissionMatrix` + `RequirePermission<A>` Axum extractors), migrate from `administrator: bool` to a `role` column, and add a `/me/permissions` discovery endpoint.
179+
- [ADR-T-009: Container Infrastructure Refactor](adr/009-container-infrastructure-refactor.md) — Split the runtime image into `release` (distroless, root-only toolset) and `debug` bases; extract three helper binaries (`torrust-index-health-check`, `torrust-index-auth-keypair`, `torrust-index-config-probe`) into their own workspace crates with no HTTP/TLS/async-runtime deps; strip credentials from shipped TOMLs and make `database.connect_url` / `tracker.token` mandatory schema fields; split Compose into a production-shaped `compose.yaml` baseline plus an auto-loaded `compose.override.yaml` dev sandbox; and add an internal audit record for vendored `su-exec`.
147180

148181
## Contributing
149182

adr/009-container-infrastructure-refactor.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-T-009: Container Infrastructure Refactor
22

3-
**Status:** Proposed
3+
**Status:** Implemented (Phases 1–9 complete)
44
**Date:** 2026-04-19
55
**Supersedes:** Earlier `ADR-T-009` draft ("Container
66
Infrastructure Hardening") whose tactical S-N items were

adr/009-implementation-plan.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# ADR-T-009 — Implementation Plan
22

33
**Companion to:** [adr/009-container-infrastructure-refactor.md](009-container-infrastructure-refactor.md)
4-
**Status:** Tracking
4+
**Status:** Implemented (Phases 1–9 complete)
55
**Date:** 2026-04-19
66

77
This document captures the *how* of ADR-T-009. The ADR records
@@ -22,7 +22,7 @@ re-litigated here — when in doubt, defer to the ADR.
2222
| 6 | Config probe | Done |
2323
| 7 | Entry-script contract | Done |
2424
| 8 | Compose split | Done |
25-
| 9 | Documentation & audit (D8, D9) | Not started |
25+
| 9 | Documentation & audit (D8, D9) | Done |
2626

2727
## Phase Dependency Graph
2828

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/bin/bash
22

3-
TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.public.e2e.container.mysql.toml) \
3+
TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.public.e2e.container.toml) \
44
TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.public.e2e.container.sqlite3.toml) \
55
docker compose down

contrib/dev-tools/container/e2e/mysql/e2e-env-up.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#!/bin/bash
22

3-
TORRUST_INDEX_CONFIG=$(cat ./share/default/config/index.public.e2e.container.mysql.toml) \
3+
TORRUST_INDEX_CONFIG=$(cat ./share/default/config/index.public.e2e.container.toml) \
44
docker compose build
55

66
USER_ID=${USER_ID:-1000} \
7-
TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.public.e2e.container.mysql.toml) \
7+
TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.public.e2e.container.toml) \
88
TORRUST_INDEX_DATABASE="torrust_index_e2e_testing" \
99
TORRUST_INDEX_DATABASE_DRIVER="mysql" \
1010
TORRUST_INDEX_CONFIG_OVERRIDE_TRACKER__TOKEN="MyAccessToken" \

contrib/dev-tools/container/e2e/mysql/run-e2e-tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ docker ps
3838

3939
# Run E2E tests with shared app instance
4040
TORRUST_INDEX_E2E_SHARED=true \
41-
TORRUST_INDEX_CONFIG_TOML_PATH="./share/default/config/index.public.e2e.container.mysql.toml" \
41+
TORRUST_INDEX_CONFIG_TOML_PATH="./share/default/config/index.public.e2e.container.toml" \
4242
TORRUST_INDEX_E2E_DB_CONNECT_URL="mysql://root:root_secret_password@127.0.0.1:3306/torrust_index_e2e_testing" \
4343
cargo test ||
4444
{
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/bin/bash
22

3-
TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.public.e2e.container.sqlite3.toml) \
3+
TORRUST_INDEX_CONFIG_TOML=$(cat ./share/default/config/index.public.e2e.container.toml) \
44
TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.public.e2e.container.sqlite3.toml) \
55
docker compose down

0 commit comments

Comments
 (0)