0pt's own "P007 pattern" release chain (added 2026-09-14) genuinely failed
the ADR-W16-003 promotion bar an audit (2026-09-15) checked it against:
syft/cosign installed via curl + a freshly-refetched checksums file (not a
pinned, out-of-band-verified digest), no vulnerability scan of the built
digest at all, cosign signed only the bare digest (no in-toto/SLSA
statement, no source-SHA/build-identity/environment/SBOM-hash binding, no
scoped key identity), and evidence committed straight back into this same
mutable repo (siax/production-evidence/*.json, siax/source-bundles/*.json
-- nine such commits already existed in this repo's history, deleted in
this change).
Delegates every bar item (1-7: pinned-tool install, SBOM, blocking vuln
scan, in-toto attestation, scoped-key signing, pubkey verification,
AUD0/ST0RE evidence storage) to sax3l/siax-templates' real, reviewed G17
reference implementation (PR #51, merged, pinned at
384e35060b91ea78ec045e14af114abf5d5eb23b), checked out at CI runtime via
the SIAX_CI_TOKEN org secret this repo already had wired for a different
cross-repo write -- picked over a vendored copy (the mechanism st0re/cl0ud/
api0 used) because 0pt has no prior vendoring convention anywhere in this
repo. Matches aud0/h0st's own migrations for the same reason. See
.siax/hooks/README.md for the full composition-mechanism rationale.
.siax/hooks/G17.sh is the adapter: owns only 0pt-specific values (image
repo, key identity, and a license-allowlist policy measured against 0pt's
own real SBOM -- 280 components, one genuine new finding this pass caught
that the old gate never checked at all: gcompat's NCSA license, triaged
and allowed as the real OS-substrate license it is).
cd.yml's own real signed-artifact wait/verify gate (predates this
migration, genuinely 0pt-specific -- polls the registry for release.yml's
artifact, verifies before triggering Coolify) is KEPT, updated from
`cosign verify` (bare digest) to `cosign verify-attestation --type ...`
to match the new attestation format `cosign attest` produces.
A fresh cosign keypair was generated for this migration -- the previously
committed cosign.pub had no corresponding private-key secret ever
provisioned on this repo (the old release.yml's own comment confirms:
"NOT YET SET on sax3l/0pt as of this writing"), so there was no real
existing key material to carry forward.
G20 (live-digest-equality) deliberately NOT wired in this pass: 0pt has no
Coolify application provisioned and no /version endpoint for it to poll
against -- see .siax/hooks/README.md "G20 -- deliberately not wired yet"
for what would need to exist first.
Verified for real during this migration (see PR description for the full
evidence): a real image built from this repo's own Dockerfile, pushed to a
local registry, SBOM'd (syft), scanned (trivy, 0 CRITICAL/HIGH findings),
DSSE-attested (cosign attest) and verified (cosign verify-attestation)
against the new cosign.pub, submitted to a mock AUD0 server enforcing the
real eventInputSchema contract, plus three real tamper-rejection negative
controls (wrong key, wrong predicate type, never-attested digest) against
this exact artifact, plus the reference implementation's own 29/29-assertion
selftest (including a real live check against production api.aud0.siax.io).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- X-Internal-Token skickades av edgen men verifierades ALDRIG — gate som
raw-ASGI-middleware: INTERNAL_TOKEN satt => alla routes utom /health kräver
exakt token, 401 (constant-time) annars. Ospecificerad token = inert (dev).
- Dockerfile: ARG GIT_SHA + OCI-label + /health.reporting revision
- compose var redan skriven för INTERNAL_TOKEN — motorn hade bara aldrig
deployats med den
- tester: tests/test_internal_token_gate.py (3: off/public/401-pass) — hela
services/api-sviten 62 passed, 2 skipped (dokumenterade known-gaps), 0 fail
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>
tests/p0/source-registry-postgres.realinfra.test.ts only applied
sql/002_source_registry.sql before running assertions. sql/007_ingestion_plane.sql
later added opt_sources.owner_id, and src/repositories/postgres-source-repository.ts's
INSERT has named that column ever since -- but nothing caught the drift because
SOURCE_REGISTRY_TEST_DATABASE_URL is never set in CI (.gitea/workflows/ci.yml has no
job that sets it), so this suite has silently skipped in every CI run.
Verified against a real disposable Postgres 16 container: before this fix, all 7
tests failed with `column "owner_id" of relation "opt_sources" does not exist`;
after replaying every sql/*.sql file in order (same pattern already used by
tests/analytics/analytics-domain-postgres.realinfra.test.ts), all 7 pass.
Found while auditing 0pt's Analytics/BI layer (MetricFlow/Cube/ClickHouse/
Trino/DuckDB) and standing up real Postgres/ClickHouse containers to verify
those claims for real; this bug is adjacent (source registry, not analytics)
but was directly encountered and is a small, safe, test-only fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Recorded decision (Simon 2026-08-31): personal data may be stored under
legitimate interest ONLY as business contacts bound to a company/org entity.
Gate enforces it: legitimate_interest requires purpose=business_contact and
entity_linked_only=true; recordStored() requires entity_id (orgnr/rgs/LEI,
hashed) and refuses person-only rows. +1 test; suite 167 pass | 1 skip;
typecheck clean.
PiiGateService makes any future personal-data source (ratsit.se and other
directory domains) technically GDPR-OK before the first byte is stored:
- recorded legal basis (art. 6) with basis_ref evidence, no policy => fail-closed
- data minimisation via field allowlist (art. 5(1)(c))
- storage limitation + enforceRetention purge (art. 5(1)(e))
- sha256-only identity keys + log redaction (art. 25/32)
- accessStatement / erase subject-rights surfaces (art. 15/17)
- robots/rate confirmation required for web+api sources; no stealth/bypass
- audit trail via AUD0 at risk L3 (0pt.pii_*)
- gate enabled by default in SourceQueryService
10 new tests; full suite 166 pass | 1 skip; typecheck (blocking gate) clean.
0pt had no SBOM/sign/digest chain at all (re-confirmed live before this
change: no cosign/syft/sbom hit anywhere under .gitea/workflows/, only doc
mentions in CHANGELOG.md/docs/architecture/ARCHITECTURE.md). Applies the
same two-part pattern already proven on sax3l/cl0ud and sax3l/st0re
(fix/wire-signed-digest-to-deploy, 2026-09-14):
1. .gitea/workflows/release.yml (new): builds the image, pushes it to
git.cloud.siax.io/sax3l/0pt:sha-<12>, SBOMs it (syft), signs it (cosign,
key-based), verifies the signature in the same run, and commits
source-bundle/production-evidence records back to the repo.
2. .gitea/workflows/cd.yml: the `deploy` job now polls the registry for
this exact commit's release-chain image and cosign-verifies it (against
the committed cosign.pub) before calling Coolify's deploy endpoint --
fails closed (10 min timeout) if release.yml never produced a verifiable
artifact for this sha. Added AFTER the existing guard step, which still
fails closed first on 0pt's real, pre-existing blocker: no Coolify
Application is provisioned for 0pt yet (COOLIFY_APP_ID is a literal
placeholder). This gate is wired ahead of that so it's already in place
the day the app is provisioned, instead of a second follow-up change.
Honest limitation carried over from cl0ud/st0re/h0st: once provisioned,
0pt's Coolify Application is expected to use the `dockerfile` build pack
(per siax.config.json), so Coolify would still perform its own independent
docker build rather than pulling the signed image, and /api/v1/deploy has
no digest parameter to pin against -- true digest-pinning needs a
production Coolify resource-type decision, out of CI-YAML scope. This gate
proves a signed, verified build of the exact commit exists before
authorizing any deploy call; it does not prove byte-identical output from
Coolify's own rebuild.
cosign.pub is a fresh, 0pt-specific keypair generated offline for this
change. NOT YET SET on sax3l/0pt (release.yml/cd.yml's gate will fail
closed until present): REGISTRY_USER, REGISTRY_TOKEN, COSIGN_PRIVATE_KEY,
COSIGN_PASSWORD, SIAX_CI_TOKEN repo secrets.
Validated without live Coolify/registry access: YAML-parsed all 2 files,
bash -n syntax-checked all 19 embedded run: blocks, and dry-ran the actual
verification logic -- the RepoDigests filter and bounded poll-retry loop
against mocked scenarios, plus a real cosign sign+verify round-trip
(offline signing-config, local throwaway registry container) proving the
gate's cosign verify command genuinely passes on the correct key/digest and
fails closed on a wrong key and on an unsigned image.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>