feat(security): add response security headers + make CI security gates real #19

Closed
admin wants to merge 2 commits from ci/security-headers-and-scanning into main
Owner

Summary

  • BACKLOG P0-13: next.config.mjs now sets HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and a same-origin CSP on every route. Previously the only header came from Vercel's platform default (HSTS) — nothing was set by the app itself, so it would have silently disappeared on cutover to Coolify/Traefik. Verified at runtime (curl -I against a real production build), not just that the build compiles.
  • BACKLOG P0-16: Trivy in security.yml/nightly.yml now actually fails the build on HIGH/CRITICAL findings (--exit-code 1) instead of the previous --exit-code 0 (purely advisory, never blocked a merge).
    • ⚠️ This PR's own security.yml / trivy job is currently RED. Verified locally against this repo's real dependency tree: 19 HIGH findings exist today in pnpm-lock.yaml — several in Next.js itself relating to middleware/auth bypass, information disclosure, and SSRF (plus nanoid, postcss, sharp). This is the fix working as intended (surfacing what --exit-code 0 was hiding), not a regression this PR caused — but it needs a human decision on timing, especially given the sibling auth-gate PR #11's reliance on middleware.ts. Recommend triaging the Next.js bump as a fast, separate follow-up.
    • services/scrapers/ stays explicitly excluded from the scan (--skip-dirs) — that tree is a dated, attributed, account-owner keep-decision under active review in a separate thread (BACKLOG P3-3) and is out of scope here; not touched, not commented on beyond preserving the existing note.
  • BACKLOG P0-19: dedicated gitleaks job added, full git history (--log-opts=--all), pinned + checksum-verified binary. Verified locally against this repo's actual history first (81 commits, no leaks) so this blocking gate doesn't open red on day one — confirmed green on the real Gitea run too.
  • BACKLOG P1-14: SBOM (CycloneDX) + license inventory (pnpm licenses list) as CI artifacts (informational, continue-on-error). All actions/* pinned to commit SHA (were floating @v4 tags) across every workflow file; Trivy/gitleaks pinned to specific released versions. cosign image-signing explicitly not added — there's no container-registry publish step in this repo's CI to attach a signature to yet (Coolify builds directly from git); flagged as a real follow-up once that pipeline exists.
  • BACKLOG P1-15: explicit permissions: contents: read + concurrency added everywhere it was missing. Pinned pnpm-store cache added to ci.yml's quality job. Root CODEOWNERS added. The "doctor step" part of P1-15 is deliberately left for the follow-up repo-hygiene PR, alongside the siax:doctor script it invokes (avoids referencing a script this PR doesn't contain).

A real compatibility bug found and fixed mid-PR

actions/upload-artifact@v4 (pinned SHA) failed on this Gitea Actions runner — verified by pushing and inspecting the real run. continue-on-error kept it from breaking the build, but artifacts silently weren't being saved. Swapped to v3.2.2, re-pushed, re-verified: now succeeds. Left in as a code comment for the next person who hits this.

Test plan

  • pnpm run typecheck / pnpm run test / pnpm run build — all pass.
  • YAML-validated every touched workflow file.
  • Pushed and inspected the real Gitea Actions run via the API for every job in this PR: quality ✅ (including the new pnpm-store cache step), gitleaks ✅, license-inventory ✅ (including artifact upload after the v3 fix), trivy ❌ (expected — see above), security (ci.yml's advisory copy) ✅.
  • Locally installed and ran Trivy + gitleaks against this repo before touching CI config, so the expected red/green outcome above isn't a guess.

Not merged

Per instructions. The red trivy job is expected and documented above — please read that note before assuming this PR is broken.

🤖 Generated with Claude Code

## Summary - **BACKLOG P0-13**: `next.config.mjs` now sets HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and a same-origin CSP on every route. Previously the only header came from Vercel's platform default (HSTS) — nothing was set by the app itself, so it would have silently disappeared on cutover to Coolify/Traefik. **Verified at runtime** (`curl -I` against a real production build), not just that the build compiles. - **BACKLOG P0-16**: Trivy in `security.yml`/`nightly.yml` now actually fails the build on HIGH/CRITICAL findings (`--exit-code 1`) instead of the previous `--exit-code 0` (purely advisory, never blocked a merge). - ⚠️ **This PR's own `security.yml` / `trivy` job is currently RED.** Verified locally against this repo's real dependency tree: **19 HIGH findings exist today** in `pnpm-lock.yaml` — several in **Next.js itself** relating to middleware/auth bypass, information disclosure, and SSRF (plus `nanoid`, `postcss`, `sharp`). This is the fix working as intended (surfacing what `--exit-code 0` was hiding), not a regression this PR caused — but it needs a human decision on timing, especially given the sibling auth-gate PR #11's reliance on `middleware.ts`. Recommend triaging the Next.js bump as a fast, separate follow-up. - `services/scrapers/` stays explicitly excluded from the scan (`--skip-dirs`) — that tree is a dated, attributed, account-owner keep-decision under active review in a separate thread (BACKLOG P3-3) and is out of scope here; not touched, not commented on beyond preserving the existing note. - **BACKLOG P0-19**: dedicated `gitleaks` job added, full git history (`--log-opts=--all`), pinned + checksum-verified binary. Verified locally against this repo's actual history first (81 commits, **no leaks**) so this blocking gate doesn't open red on day one — confirmed green on the real Gitea run too. - **BACKLOG P1-14**: SBOM (CycloneDX) + license inventory (`pnpm licenses list`) as CI artifacts (informational, `continue-on-error`). All `actions/*` pinned to commit SHA (were floating `@v4` tags) across every workflow file; Trivy/gitleaks pinned to specific released versions. `cosign` image-signing explicitly **not** added — there's no container-registry publish step in this repo's CI to attach a signature to yet (Coolify builds directly from git); flagged as a real follow-up once that pipeline exists. - **BACKLOG P1-15**: explicit `permissions: contents: read` + `concurrency` added everywhere it was missing. Pinned pnpm-store cache added to `ci.yml`'s quality job. Root `CODEOWNERS` added. The "doctor step" part of P1-15 is deliberately left for the follow-up repo-hygiene PR, alongside the `siax:doctor` script it invokes (avoids referencing a script this PR doesn't contain). ## A real compatibility bug found and fixed mid-PR `actions/upload-artifact@v4` (pinned SHA) **failed** on this Gitea Actions runner — verified by pushing and inspecting the real run. `continue-on-error` kept it from breaking the build, but artifacts silently weren't being saved. Swapped to `v3.2.2`, re-pushed, re-verified: now succeeds. Left in as a code comment for the next person who hits this. ## Test plan - [x] `pnpm run typecheck` / `pnpm run test` / `pnpm run build` — all pass. - [x] YAML-validated every touched workflow file. - [x] Pushed and inspected the **real Gitea Actions run** via the API for every job in this PR: `quality` ✅ (including the new pnpm-store cache step), `gitleaks` ✅, `license-inventory` ✅ (including artifact upload after the v3 fix), `trivy` ❌ (expected — see above), `security` (ci.yml's advisory copy) ✅. - [x] Locally installed and ran Trivy + gitleaks against this repo before touching CI config, so the expected red/green outcome above isn't a guess. ## Not merged Per instructions. The red `trivy` job is expected and documented above — please read that note before assuming this PR is broken. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
admin added 2 commits 2026-08-11 12:18:51 +00:00
BACKLOG P0-13: next.config.mjs now sets HSTS, X-Frame-Options,
X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and a
same-origin CSP on every route. Previously the only security header came
from Vercel's platform default (HSTS) -- nothing was set by the app itself,
so it would have silently disappeared on cutover to Coolify/Traefik.
Verified at runtime (curl -I against a production build), not just that the
build compiles.

BACKLOG P0-16: Trivy in security.yml/nightly.yml now actually fails the
build on HIGH/CRITICAL findings (`--exit-code 1`) instead of the previous
`--exit-code 0` (purely advisory, never blocked a merge). Verified locally
against this repo's real dependency tree -- 19 HIGH findings currently exist
in pnpm-lock.yaml (next.js, postcss, sharp, nanoid), several in next.js
itself relating to middleware/auth bypass. See PR description: this is
expected to turn the security.yml `trivy` job red until those are bumped in
a follow-up -- that's the fix working as intended, not a regression, but it
needs a human decision on timing given the sibling auth-gate PR (#11).

`services/scrapers/` stays explicitly excluded from the scan
(--skip-dirs) -- that tree is a dated, deliberate keep-decision under
active review in a separate thread (BACKLOG P3-3) and is out of scope here.

BACKLOG P0-19: dedicated gitleaks job added to security.yml, full git
history (--log-opts=--all), pinned + checksum-verified binary. Verified
locally against this repo's actual history first (81 commits, no leaks) so
this blocking gate doesn't open red on day one.

BACKLOG P1-14: SBOM (CycloneDX) + license inventory (`pnpm licenses list`)
generated as informational, non-blocking build artifacts. actions/checkout,
actions/setup-node, actions/cache, actions/upload-artifact all pinned to
commit SHA (was floating @v4 tags) across every workflow file, Trivy/gitleaks
pinned to specific released versions instead of floating install scripts.
cosign image-signing explicitly NOT added -- there is no container-registry
publish step in this repo's CI to attach a signature to yet (Coolify builds
directly from git); flagging as a real follow-up once that pipeline exists.

BACKLOG P1-15: explicit `permissions: contents: read` + `concurrency` added
to every workflow that lacked them (only .github/workflows/ci.yml already
had concurrency). Pinned pnpm-store cache added to ci.yml's quality job.
Root CODEOWNERS added (`* @sax3l`) so Gitea has someone to assign once
required-approvals is turned on (separate repo-settings change, not a file
change). The "doctor step" part of P1-15 is added in the follow-up repo-
hygiene PR, alongside the `siax:doctor` script it invokes.
fix(ci): use actions/upload-artifact@v3, not v4, on this Gitea runner
Security Scan (SIAX Cloud) / gitleaks (push) Successful in 9s
Security Scan (SIAX Cloud) / license-inventory (push) Successful in 27s
CI (SIAX Cloud) / security (push) Successful in 16s
Security Scan (SIAX Cloud) / trivy (push) Failing after 36s
CI (SIAX Cloud) / quality (push) Successful in 2m8s
CI (SIAX Cloud) / security (pull_request) Successful in 21s
Security Scan (SIAX Cloud) / gitleaks (pull_request) Successful in 8s
Security Scan (SIAX Cloud) / trivy (pull_request) Failing after 18s
Security Scan (SIAX Cloud) / license-inventory (pull_request) Successful in 23s
CI (SIAX Cloud) / quality (pull_request) Successful in 2m21s
d6adad7caf
Verified empirically by pushing and inspecting the real run
(git.siax.io/sax3l/0pt/actions/runs/4788): v4 (pinned SHA) failed with
conclusion=failure on both upload steps. continue-on-error already kept
this from breaking the build, but the artifacts silently weren't being
saved. v3.2.2 is the known-compatible version for self-hosted Gitea's
artifact backend; re-verify after push.
Author
Owner

Status: BLOCKED by branch protection. The red CI was genuine — and PR #26 has now cleared it.
Re-verified against main = 364b7643.

1. The hard blocker (unchanged)

Five of the eight changed files are under .gitea/workflows/ (cd.yml, ci.yml,
health-check.yml, nightly.yml, security.yml), which matches
protected_file_patterns = ".gitea/workflows/**;.gitignore" on main. That closes both the merge
API and direct push.

Worth noting for a possible split: three of the eight files are not protected —
next.config.mjs, CODEOWNERS, and .github/workflows/ci.yml (the pattern names .gitea/, not
.github/). A PR carrying only those three would be mergeable today. That is Simon's call, not
mine; I have not created one.

2. The red check was real, and is now fixed by something else

Security Scan (SIAX Cloud) / trivy failed on head d6adad7c (jobs 11796 / 11825):

pnpm-lock.yaml (pnpm)
Total: 19 (HIGH: 19, CRITICAL: 0)
  ❌  Failure - Main Trivy fs scan (vuln + secrets -- BLOCKING)
exitcode '1': failure

This PR failed on its own new gate — it is the change that flips trivy from --exit-code 0 to
--exit-code 1. So the failure was honest, not infrastructure: no ENOSPC, no sibling-container
DNS problem, no workflow_dispatch status gap. The findings were real next / nanoid /
postcss / sharp CVEs.

Measured today with the same tool and flags:

trivy fs --scanners vuln --severity HIGH,CRITICAL   on main @ 83209b07 (before PR #26) -> 18 HIGH
trivy fs --scanners vuln --severity HIGH,CRITICAL   on main @ 364b7643 (after  PR #26) ->  0
trivy fs --scanners vuln,secret --severity HIGH,CRITICAL
        on the merge of this PR into today's main               ->  0 vulns, 0 secrets

So once this branch is brought up to today's main, its own blocking trivy gate passes. The
failure status still shown here is from 2026-08-11 and is stale.

Also note: trivy is not in status_check_contexts, so that red never gated anything anyway.
Neither does the whole Security Scan (SIAX Cloud) workflow, nor CI (SIAX Cloud) / integration.
Only quality (push) and security (push) gate — and both are green here.

3. Not superseded — three independent content proofs against today's main

git show origin/main:next.config.mjs        -> 11 lines, no headers() and no security header at all
git ls-tree origin/main --name-only | grep -i codeowners  -> (empty; CODEOWNERS absent)
git show origin/main:.gitea/workflows/security.yml | grep -n exit-code
   4:# fleet convention (--exit-code 0 so findings never block merges; they are
  30:  trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 0 .

git merge-tree --write-tree origin/main pr/19 merges cleanly (exit 0) despite being 32 commits
behind; landing set = 8 files, 221 insertions / 42 deletions, no reverts. mergeable: false in
the API is the same stale artifact as on PR #22 (pull_request.status = 1, conflicted_files = NULL).

4. main's CHANGELOG already claims this work is done. It is not.

CHANGELOG.md on main, [Unreleased]:

fix(security): security headers (CSP/HSTS/X-Frame-Options/etc.) + CI security gates actually enforce (Trivy --exit-code 1, gitleaks) instead of advisory-only ...

and, two bullets down, CODEOWNERS in the repo-hygiene list. Neither exists on main. The only
place any of Strict-Transport-Security / X-Frame-Options / Content-Security-Policy appears in
the entire tree is that CHANGELOG line:

git grep -l -iE 'Strict-Transport-Security|X-Frame-Options|Content-Security-Policy' origin/main
  origin/main:CHANGELOG.md

It was written by 328f8a4 docs+chore: repo hygiene ..., a pure documentation commit. Anyone
reading the CHANGELOG would close this PR as already-landed. It is not. (node scripts/siax-doctor.mjs on main agrees: [FAIL] P1-15 CODEOWNERS exists -- missing: CODEOWNERS.)

What Simon has to do: decide whether to relax protected_file_patterns on main for one
merge, or to land the three unprotected files first and keep the workflow half waiting. Either
way, this branch should be brought up to today's main so CI re-runs and the stale red clears.

Generated with Claude Code

**Status: BLOCKED by branch protection. The red CI was genuine — and PR #26 has now cleared it.** Re-verified against `main` = `364b7643`. ### 1. The hard blocker (unchanged) Five of the eight changed files are under `.gitea/workflows/` (`cd.yml`, `ci.yml`, `health-check.yml`, `nightly.yml`, `security.yml`), which matches `protected_file_patterns = ".gitea/workflows/**;.gitignore"` on `main`. That closes both the merge API and direct push. Worth noting for a possible split: **three of the eight files are *not* protected** — `next.config.mjs`, `CODEOWNERS`, and `.github/workflows/ci.yml` (the pattern names `.gitea/`, not `.github/`). A PR carrying only those three would be mergeable today. That is Simon's call, not mine; I have not created one. ### 2. The red check was real, and is now fixed by something else `Security Scan (SIAX Cloud) / trivy` failed on head `d6adad7c` (jobs 11796 / 11825): ``` pnpm-lock.yaml (pnpm) Total: 19 (HIGH: 19, CRITICAL: 0) ❌ Failure - Main Trivy fs scan (vuln + secrets -- BLOCKING) exitcode '1': failure ``` This PR failed on **its own new gate** — it is the change that flips trivy from `--exit-code 0` to `--exit-code 1`. So the failure was honest, not infrastructure: no ENOSPC, no sibling-container DNS problem, no `workflow_dispatch` status gap. The findings were real `next` / `nanoid` / `postcss` / `sharp` CVEs. Measured today with the same tool and flags: ``` trivy fs --scanners vuln --severity HIGH,CRITICAL on main @ 83209b07 (before PR #26) -> 18 HIGH trivy fs --scanners vuln --severity HIGH,CRITICAL on main @ 364b7643 (after PR #26) -> 0 trivy fs --scanners vuln,secret --severity HIGH,CRITICAL on the merge of this PR into today's main -> 0 vulns, 0 secrets ``` So once this branch is brought up to today's `main`, its own blocking trivy gate passes. The `failure` status still shown here is from 2026-08-11 and is stale. Also note: `trivy` is **not** in `status_check_contexts`, so that red never gated anything anyway. Neither does the whole `Security Scan (SIAX Cloud)` workflow, nor `CI (SIAX Cloud) / integration`. Only `quality (push)` and `security (push)` gate — and both are green here. ### 3. Not superseded — three independent content proofs against today's `main` ``` git show origin/main:next.config.mjs -> 11 lines, no headers() and no security header at all git ls-tree origin/main --name-only | grep -i codeowners -> (empty; CODEOWNERS absent) git show origin/main:.gitea/workflows/security.yml | grep -n exit-code 4:# fleet convention (--exit-code 0 so findings never block merges; they are 30: trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 0 . ``` `git merge-tree --write-tree origin/main pr/19` merges cleanly (exit 0) despite being 32 commits behind; landing set = 8 files, 221 insertions / 42 deletions, no reverts. `mergeable: false` in the API is the same stale artifact as on PR #22 (`pull_request.status = 1`, `conflicted_files = NULL`). ### 4. `main`'s CHANGELOG already claims this work is done. It is not. `CHANGELOG.md` on `main`, `[Unreleased]`: > `fix(security): security headers (CSP/HSTS/X-Frame-Options/etc.) + CI security gates actually > enforce (Trivy --exit-code 1, gitleaks) instead of advisory-only ...` and, two bullets down, `CODEOWNERS` in the repo-hygiene list. Neither exists on `main`. The only place any of `Strict-Transport-Security` / `X-Frame-Options` / `Content-Security-Policy` appears in the entire tree is that CHANGELOG line: ``` git grep -l -iE 'Strict-Transport-Security|X-Frame-Options|Content-Security-Policy' origin/main origin/main:CHANGELOG.md ``` It was written by `328f8a4 docs+chore: repo hygiene ...`, a pure documentation commit. Anyone reading the CHANGELOG would close this PR as already-landed. It is not. (`node scripts/siax-doctor.mjs` on `main` agrees: `[FAIL] P1-15 CODEOWNERS exists -- missing: CODEOWNERS`.) **What Simon has to do:** decide whether to relax `protected_file_patterns` on `main` for one merge, or to land the three unprotected files first and keep the workflow half waiting. Either way, this branch should be brought up to today's `main` so CI re-runs and the stale red clears. Generated with [Claude Code](https://claude.com/claude-code)
admin closed this pull request 2026-09-08 20:49:46 +00:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/0pt#19