CI hardening: dependency bot token, apt signing key, CSP test #996

Merged
tiagoagueda merged 3 commits from sec2-e2-ci-hardening into main 2026-10-18 11:19:26 +00:00
Owner

CI hardening for three SEC2 findings.

#975 (Medium), the dependency workflow. dependencies.yml is now two jobs. upgrade holds no secret: it resolves, installs and tests the new lock, then uploads uv.lock and the pre-commit config as an artifact (upload-artifact@v3, like ci.yml). propose (needs: upgrade) starts from a fresh container and a clean checkout, runs no uv, pytest or anything from the new packages, requires the artifact to be exactly two regular files, lets the pre-commit file move at most one line, fails if git status --porcelain names anything but the two files, runs every git command with -c core.hooksPath=/dev/null, and is the only job that references DEPENDENCIES_TOKEN. tests/test_tooling.py reads the workflow and asserts all of that (it fails on the old file). zizmor 1.30.1 with .forgejo/zizmor.yml reports nothing.

#981, the PGDG apt key. Took the issue's first option: docker/apt.postgresql.org.asc is committed, COPYed into the runtime stage and installed by the CI job; nothing downloads a key. No new apt package: tests/test_image_build.py computes the OpenPGP v4 fingerprint of the file in Python and compares it with PostgreSQL's published B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 (also checked with gpg --show-keys), and fails if the Dockerfile or ci.yml fetch a key.

#984, the CSP test. The hand copy is gone. strict_csp now only asserts that the policy in force is base.SECURE_CSP, the one policy every settings module inherits, so the file holds no directive literals. I did not add the map page or a preview frame (optional in the issue): the map page loads tiles and I could not run the browser here.

Closes #975, Closes #981, Closes #984

Tested. Whole unit suite locally after messages.py compile (the only failures of a first run were 25 tests needing the compiled catalogues, which CI compiles; they pass once compiled), ruff, makemigrations --check, messages.py extract --check, zizmor. Only CI can prove: the e2e CSP tests (browser job, read it carefully: tests/e2e/test_csp.py changed), the PostgreSQL job and image build using the committed key, and the artifact hand-over between the two jobs on this Forgejo. The dependency workflow can only be exercised by a manual dispatch (Actions > Dependencies > Run workflow) by the owner; I did not run it.

🤖 Generated with Claude Code

CI hardening for three SEC2 findings. **#975 (Medium), the dependency workflow.** `dependencies.yml` is now two jobs. `upgrade` holds no secret: it resolves, installs and tests the new lock, then uploads `uv.lock` and the pre-commit config as an artifact (`upload-artifact@v3`, like `ci.yml`). `propose` (`needs: upgrade`) starts from a fresh container and a clean checkout, runs no `uv`, `pytest` or anything from the new packages, requires the artifact to be exactly two regular files, lets the pre-commit file move at most one line, fails if `git status --porcelain` names anything but the two files, runs every git command with `-c core.hooksPath=/dev/null`, and is the only job that references `DEPENDENCIES_TOKEN`. `tests/test_tooling.py` reads the workflow and asserts all of that (it fails on the old file). zizmor 1.30.1 with `.forgejo/zizmor.yml` reports nothing. **#981, the PGDG apt key.** Took the issue's first option: `docker/apt.postgresql.org.asc` is committed, `COPY`ed into the runtime stage and installed by the CI job; nothing downloads a key. No new apt package: `tests/test_image_build.py` computes the OpenPGP v4 fingerprint of the file in Python and compares it with PostgreSQL's published B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 (also checked with `gpg --show-keys`), and fails if the Dockerfile or `ci.yml` fetch a key. **#984, the CSP test.** The hand copy is gone. `strict_csp` now only asserts that the policy in force is `base.SECURE_CSP`, the one policy every settings module inherits, so the file holds no directive literals. I did not add the map page or a preview frame (optional in the issue): the map page loads tiles and I could not run the browser here. Closes #975, Closes #981, Closes #984 **Tested.** Whole unit suite locally after `messages.py compile` (the only failures of a first run were 25 tests needing the compiled catalogues, which CI compiles; they pass once compiled), ruff, `makemigrations --check`, `messages.py extract --check`, zizmor. **Only CI can prove:** the e2e CSP tests (browser job, read it carefully: `tests/e2e/test_csp.py` changed), the PostgreSQL job and image build using the committed key, and the artifact hand-over between the two jobs on this Forgejo. The dependency workflow can only be exercised by a manual dispatch (*Actions > Dependencies > Run workflow*) by the owner; I did not run it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The monthly workflow resolved, installed and tested freshly released packages as
root in the same container that later held DEPENDENCIES_TOKEN. A step boundary is
not a privilege boundary: a git hook or a binary left on the PATH by an earlier
step would have run with the token in its environment.

Now `upgrade` has no secret and hands uv.lock and the pre-commit config over as an
artifact; `propose` starts from a fresh container and a clean checkout, runs no uv
and no pytest, checks that the artifact holds two regular files, that the
pre-commit file moved at most one line and that git status names nothing else,
runs every git command with core.hooksPath switched off on the command line, and
alone references the token. tests/test_tooling.py reads the workflow and holds
that split in place. Only a real run can prove the artifact hand-over on this
Forgejo; a manual dispatch by the owner exercises it.

Closes #975

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The runtime image and the PostgreSQL CI job fetched the PGDG key over HTTPS and
made it the key apt trusts for that repository, with TLS the only check, where the
uv binary and installer are pinned by digest. The key is now docker/apt.postgresql.org.asc,
copied into the image and installed by CI. A test computes the OpenPGP v4
fingerprint of the file in Python (no gpg, so no new package) and compares it with
the one PostgreSQL publishes, and another fails if either file downloads a key.

Closes #981

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Run the CSP browser test under the policy production sends
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Failing after 32m21s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m55s
CI / Database tests on PostgreSQL (pull_request) Successful in 5m17s
CI / Security audit (pull_request) Successful in 1m26s
CI / Browser tests (Chromium) (pull_request) Failing after 48m32s
ed7278054c
The test overrode SECURE_CSP with a hand copy that had gone stale (base-uri,
frame-src, worker-src, the style nonce and report-uri were missing). The policy is
defined once in base.py and inherited by every settings module, so the fixture now
only asserts that the policy in force is that one and no longer holds a directive.

Closes #984

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tiagoagueda force-pushed sec2-e2-ci-hardening from ed7278054c
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Failing after 32m21s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m55s
CI / Database tests on PostgreSQL (pull_request) Successful in 5m17s
CI / Security audit (pull_request) Successful in 1m26s
CI / Browser tests (Chromium) (pull_request) Failing after 48m32s
to ab40b27a89
All checks were successful
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 13m30s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m20s
CI / Database tests on PostgreSQL (pull_request) Successful in 3m26s
CI / Security audit (pull_request) Successful in 58s
CI / Browser tests (Chromium) (pull_request) Successful in 29m31s
2026-10-18 10:17:37 +00:00
Compare
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo!996
No description provided.