DO NOT MERGE (deploys) — fix(security,repo-honesty): real security headers + a doctor that checks truth, not presence #44
Closed
admin
wants to merge 1 commits from
fix/security-headers-and-doctor-truth-20260907 into main
pull from: fix/security-headers-and-doctor-truth-20260907
merge into: :main
:main
:fix/prod-restore-2026-09-29
:fix/source-bundle-20261104
:fix/prod-evidence-2026-09-29
:docs/deploy-runbook
:fix/keycloak-portal-trust-20261102
:fix/smoke-main-sha
:chore/optimize-pipelines
:chore/remove-stale-package-lock-20261101
:fix/trivy-skip-sbom-evidence-20261101
:fix/doctor-fixture-v3-20261101
:fix/next-16.4.1-20261031
:wip/asus-20261005/Documents_0pt-9cad5f9
:feat/import-dispatch-policy
:opt-vehiq-work-salvage-20260927/0pt-p060-p068_0pt-uncommitted-wip
:fix-chore/normalize-sax3l-ci-refs-20260925
:salvage/20260925/siax-arkiv/siax-work/0pt-feature/0pt-remove-noncompliant-scrapers
:salvage/20260925/siax-arkiv/siax-platforms-work/0pt-feat/p014-data-os-hardening
:salvage/20260925/siax-arkiv/siax-work/0pt-oai/devos-band29-0pt
:salvage/20260925/siax-arkiv/siax-work/0pt-stash-stash0
:feat/golden-standard-scaffold
:docs/coolify-deploy-hook-is-live-opus5
:docs/bi-canonical-stack-correction-opus5
:feat/p060-p068-ingestion-plane
:chore/siax-repo-v3-manifest
:feat/p014-data-os-hardening
:feat/p010-data-os-completion
:feat/siax-platforms-p0-data-os
:fix/coolify-app-id
:docs/repo-hygiene-v2-standard
:fix/honest-health-check
:docs/compliance-blockers-2026-08-11
:security/auth-gate-anonymous-api-routes
:fix/remove-vercel-deploy-target
:unify/github-main-2026-07-28
:docs/1p-egress-measurement-point
:foundation/hardening
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
⛔ DO NOT MERGE — merging this deploys to production
.gitea/workflows/cd.ymltriggers onpush: branches: [main]and POSTs tohttps://cloud.siax.io/api/v1/deploy?uuid=${COOLIFY_APP_ID}. Its guard steponly aborts when
COOLIFY_APP_IDis still the literal placeholder orCOOLIFY_TOKENis unset. Neither is true any more — verified against theGitea API on 2026-09-07:
GET /repos/sax3l/0pt/actions/variablesCOOLIFY_APP_ID = shxd6lepxuxfsjsp0ath3z1g(a real UUID, not the placeholder)GET /repos/sax3l/0pt/actions/secretsCOOLIFY_TOKENpresent (created 2026-08-31)So the guard passes and the deploy fires. Merging this PR is a production
deploy of 0pt onto Coolify. That decision is Simon's, not CI's. Please merge
deliberately, not because the checks are green.
Two side notes for whoever merges:
cd.yml's own header comment still says "no Coolify application has beenprovisioned for 0pt yet … COOLIFY_APP_ID below is a literal placeholder".
That comment is stale — the repo variable overrides it.
.gitea/workflows/**is a protected path here, so this PR does not touch it.
0pt.app currently returns
HTTP 402 DEPLOYMENT_DISABLEDfrom Vercel, whoseplatform default was the only thing setting HSTS. On Coolify behind Traefik
there is no such default.
What this fixes
Two defects of the same kind: the repo asserted things about itself that were
not true, and nothing in it could tell.
A —
CHANGELOG.mdclaimed security headers that did not existCHANGELOG.mdhas claimedCSP/HSTS/X-Frame-Options/etc.since 2026-08.On
mainat4514d97:One file. Itself. No
headers(), no middleware injection, no proxy config inthis repo. Live confirmation (2026-09-07 18:24 UTC):
On the existing branch
ci/security-headers-and-scanning(8d2ff73): itsnext.config.mjswas the right idea, but it is 142 commits behindmain, and5 of its 8 changed files are under
.gitea/workflows/**, which is a protectedpath on this repo (
protected_file_patterns: .gitea/workflows/**;.gitignore).It could not be brought forward as-is. Its header work is carried forward here,
its workflow changes are not, and its carve-outs were re-derived against the
current tree rather than trusted:
form-action https://checkout.stripe.com https://api.stripe.com,justified by the Stripe Checkout hop. That hop is
window.location.href = body.urlinapp/bestall/forskott/page.tsx— atop-level navigation, which
form-actiondoes not govern. The allowance wasdecorative, so it is dropped and
form-action 'self'is used instead. Theonly
<form>in the tree isonSubmit-only.components/embed.tsx, which renders aNocoDB/Metabase view in an
<iframe>. A flatframe-src 'self'would blankthat iframe with nothing but a devtools warning the day someone wires it up —
exactly the failure
sax3l/siax-iorecorded as SIO-013 (its CSP silentlyblocked PostHog for months).
frame-srcis therefore derived fromNOCODB_BASE_URL/METABASE_SITE_URL, the same env vars the feature isconfigured with, and the build-time-not-runtime caveat is written down in the
file.
'unsafe-eval'is added only undernext dev, not unconditionally.What lands, on
/(.*), matchingsax3l/siax-io'snext.config.mjsidiom:plus
poweredByHeader: false.preloadis inert until0pt.appis submitted athstspreload.org — a one-way account-holder decision that shipping this header
does not make.
B —
scripts/siax-doctor.mjsvalidated presence, not truthIts manifest checks were all
requireJsonField, whose whole body is "does thiskey exist". Reproduced on this tree with the pre-change script:
That is why a descriptor asserting a running service survived while every one of
its hosts returned 402/404.
Design. Liveness is not knowable offline, and a gate that needs the public
internet fails for reasons unrelated to the change under review — people learn to
ignore those. So the split is:
functions of the repo's own bytes:
operationalState/lifecycle/categoryagainstthe standard's enums,
gitHostconst,nodeagainst the fleet pattern,every
fqdna bare well-formed hostname.repo's own deploy/contract config (
siax.config.json environments.*.domain,contracts/openapi.yaml servers:) and every configured domain must bedeclared back. A hostname nothing deploys is a hostname nobody owns.
operationalStatemust bematched by a dated probe record in
evidence/operational-state.jsonthatagrees with it.
"running"requires every prod probe to be 2xx;"down"requires none to be. The record'sokflags are re-derived fromhttpStatus, so a hand-writtenok: truenext to a 402 fails too. Aliveness claim older than 90 days stops counting as evidence (only for
states that assert the service is up — an honest
"down"does not decayinto a lie by sitting still).
--live, opt-in) — actually probes the declared hosts, comparesreality to the declared state, and checks the headers on the wire.
--write-evidencerefreshes the record. Deliberately not in the CI path.Proof it catches the exact historical defect, escalating through every way a
false claim could be pushed through:
operationalState: "running"insiax.repo.jsononlyGOV-06— evidence was recorded for"down"; exit 1"running"GOV-06— "operationalState "running" but the recorded probes say otherwise: 0pt.app=402, api.0pt.app=404"; exit 1ok: false→ok: trueby handGOV-06— "records ok=true next to httpStatus=402"; exit 1httpStatusforged to 200 (evidence fully fabricated)--livecatches it: "declared "running", live probe says: 0pt.app=402, api.0pt.app=404"; exit 1tests/repo/siax-doctor.test.tsasserts those negative cases against fixturetrees (via a new
SIAX_DOCTOR_ROOTenv override) on every run, so "the checkcannot fail" cannot come back. It also runs the doctor itself inside
pnpm run test, which puts it in the blockingCI (SIAX Cloud) / qualitycontext without needing a workflow change.C — the CHANGELOG now tells the truth
The header claim stands, because the headers landed. The four claims it was
bundled with were verified and are still false on
main, so they are listedas not-implemented rather than left asserted:
--exit-code 0(advisory) in bothsecurity.ymlandnightly.yml;--exit-code 1was claimed.actions/checkout@v4,actions/setup-node@v4).All four live in
.gitea/workflows/**and need an authorised workflow change.Regression gates added
tests/security/security-headers.test.tssiax-doctorgreen, and able to go redtests/repo/siax-doctor.test.tsBoth run under
pnpm run test, i.e. the blockingqualityjob. No.gitea/workflows/**change.Proof the header gate fails, using the exact command CI runs:
Verification
pnpm run typecheckpnpm run testpnpm run build.next/routes-manifest.jsoncarries all six headersnode scripts/siax-doctor.mjsnode scripts/siax-doctor.mjs --livedownmatches the real 402/404)npx eslint tests/security tests/repoProtected paths: nothing under
.gitea/workflows/**, no.gitignorechange —confirmed by
git status --porcelain.🤖 Generated with Claude Code
Two defects of the same kind: the repo asserted things about itself that were not true, and nothing in it could tell. A. CHANGELOG.md claimed CSP/HSTS/X-Frame-Options since 2026-08. `main` contained none of it -- a repo-wide grep for `content-security-policy|strict-transport-security|x-frame-options` matched exactly one file, CHANGELOG.md itself. The only header on 0pt.app was Vercel's own HSTS default, which does not survive the move to Coolify/Traefik that siax.config.json and .gitea/workflows/cd.yml are already pointed at. - next.config.mjs: `headers()` on `/(.*)` with CSP (frame-ancestors 'none', base-uri 'self', object-src 'none', form-action 'self'), HSTS, X-Frame-Options DENY, X-Content-Type-Options, Referrer-Policy, Permissions-Policy; plus poweredByHeader: false. Same idiom as sax3l/siax-io's next.config.mjs. - Carve-outs re-derived against THIS tree, not inherited: the stale `ci/security-headers-and-scanning` branch (8d2ff73, 142 commits behind) carried a `form-action https://checkout.stripe.com` allowance justified by a redirect that is actually `window.location.href`, which form-action does not govern -- dropped rather than kept as decorative permission. frame-src is derived from NOCODB_BASE_URL/METABASE_SITE_URL, the same env the components/embed.tsx iframe is configured from, so wiring that component up cannot be silently blanked by this header (the failure sax3l/siax-io recorded as SIO-013). 'unsafe-eval' is dev-only. - tests/security/security-headers.test.ts: blocking regression gate inside `pnpm run test`, i.e. the required "CI (SIAX Cloud) / quality" context. Asserts against expectations written out in the test, not imported from the config. Proven to fail: removing X-Frame-Options -> `pnpm run test` exits 1; weakening frame-ancestors to 'self' -> exits 1. B. scripts/siax-doctor.mjs validated that siax.repo.json fields were PRESENT, never what they said. Measured on this tree: operationalState "running" (false) and "down" (true) both gave 40/40, exit 0, byte-identical output. - Value checks (enums, git.siax.io const, fleet node pattern, bare-hostname form), cross-file consistency (declared prod hostnames must appear in this repo's own siax.config.json / contracts/openapi.yaml, and vice versa), and GOV-06 substantiation: operationalState must be backed by a dated probe record in evidence/operational-state.json that agrees with it. Liveness is not knowable offline; whether a liveness CLAIM is backed is. Offline, deterministic, no network -- CI can never go red for want of an internet connection. - `--live` (opt-in) probes the declared hosts for real and checks the headers on the wire; `--write-evidence` refreshes the record. Not wired into CI on purpose. - tests/repo/siax-doctor.test.ts runs the doctor inside `pnpm run test` and asserts the negative cases against fixture trees (SIAX_DOCTOR_ROOT), so the "cannot fail" defect cannot come back. C. CHANGELOG.md is now true. The header claim stands; the four claims it was bundled with (Trivy --exit-code 1, gitleaks, SHA-pinned actions, SBOM + license artifacts) are still false on main and are listed as not implemented instead of left asserted. All four live in .gitea/workflows/**, a protected path on this repo, so they need an authorised workflow change. Nothing under .gitea/workflows/** or .gitignore is touched. Verified: pnpm run typecheck exit 0; pnpm run test exit 0 (74 files / 965 tests passed, up from 72/945); pnpm run build exit 0 and the built .next/routes-manifest.json carries all six headers; node scripts/siax-doctor.mjs 55/55 exit 0; --live 57/57 exit 0. eslint on the new files exit 0 (repo lint is advisory and its 17 pre-existing problems are unchanged). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Wire-level proof: the built container really serves them
next.config.mjssets response headers; it does not prove they reach a client.So the repo's own
Dockerfile(the artifact Coolify builds —build_pack: dockerfile) was built and run, and the running container probed:X-Powered-By(poweredByHeader: falseworks),'unsafe-eval'— the dev-only carve-out did not leak into the productionimage,
/api/healthcarries the same set, so API responses are covered too, notjust HTML.
For contrast, the same probe against the live host right now:
CI
CI (SIAX Cloud) / qualityandCI (SIAX Cloud) / security— the two contextsthis branch's protection actually requires — are green on both the push and
the pull_request runs.
CI (SIAX Cloud) / integrationis red, and was already red onmain: run13589(the currentmainHEAD, "Merge pull request … AUD0 outbox …") fails atthe identical step, "Integration tests against LIVE OpenSearch + ClickHouse
(blocking)". That job runs only
pnpm vitest run tests/integration, which thisPR does not touch. Pre-existing, not introduced here — but also not fixed here,
so it is worth its own look.
🤖 Generated with Claude Code
OMSLUTEN av PR #56 (mergad): samma innehåll cherry-pickat (
8ebc90f) med konfliktlösningar mot main (next.config.mjs standalone + headers bevarade båda, CHANGELOG honesty-resolution). Branchenfix/security-headers-and-doctor-truth-20260907städas bort.Pull request closed