diff --git a/.github/audit/_preamble.md b/.github/audit/_preamble.md index 5652a6eea..0e0de1593 100644 --- a/.github/audit/_preamble.md +++ b/.github/audit/_preamble.md @@ -20,6 +20,22 @@ for a check you could not determine — a transient network error, or an area yo ran out of room to reach — and say which it was. It is never a substitute for a check you could have run. +A condition no audit run can read — a provisioning step, a setting in an +external service's console — is not a check, so the each-clause rule above +does not reach it. Record it as INFO, never as +`UNVERIFIABLE`: nothing a later run can read would settle it, so every later +run would be inconclusive too. + +- Written into a `FAIL IF`: verdict only that rule's readable condition. +- Stated beside a rule, or staged under `## Future`: there is no rule to + verdict. +- Promoted above the fold: audit it as the promoted rule is written — a + readable condition as a `FAIL IF`, an unreadable one by the bullets above. + +GitHub state `AUDIT_PAT` reaches — rulesets, environments, secret placement, +workflow permissions — is readable, so it is always a check, and +`UNVERIFIABLE` stays right for a call that fails. + Where `docs/specs/security.md` says a risk is accepted ("What is not defended") or a gap is known ("Known gaps"), do not re-report it as a finding — report only if the situation has changed or is worse than described. diff --git a/.github/audit/hosted.md b/.github/audit/hosted.md index 99931b808..219711275 100644 --- a/.github/audit/hosted.md +++ b/.github/audit/hosted.md @@ -38,8 +38,7 @@ code from pending production configuration; do not treat local provider simulations as live OAuth acceptance, and treat a checked-in placeholder as no evidence about an external control. Production activation is staged under the spec's `## Future`: while it sits below the fold there is no check here, so -report its state as INFO under `### Qualitative findings`. Once it is promoted -above the fold, audit it as a `FAIL IF` like any other. +report its state as INFO under `### Qualitative findings`. ## Qualitative pass diff --git a/docs/specs/security-audit.md b/docs/specs/security-audit.md index 996b04dc1..6ca52be14 100644 --- a/docs/specs/security-audit.md +++ b/docs/specs/security-audit.md @@ -79,6 +79,7 @@ Source of truth: `2. Wait without ending your turn`, `3. Merge`, and `4. The ver - **Partial has three shapes**, each named in the INCONCLUSIVE issue: `UNVERIFIABLE` for a check reached but not determined; `_Incomplete …_` above a fragment cut off mid-report; `_No report …_` for a domain that never wrote one. The merged `## Summary` may likewise read `INCONCLUSIVE`, and **gives no coverage count for a cut-off domain** (rationale). - **With no `audit-report.md` the reporting step publishes each fragment verbatim under its own heading**, unmerged (rationale). - **Must return `VERDICT: INCONCLUSIVE` from a domain with any undetermined check unless it found a failure.** Only all-determined passing checks permit `VERDICT: PASS`; a domain's inconclusive verdict prevents a merged pass. +- **Never write a `FAIL IF` condition no audit run can read**: audit the readable half; stage the rest under `## Future` only while it is unbuilt, and otherwise state it beside the rule. `AUDIT_PAT`-readable GitHub state stays audited (rationale). - **`STATUS` is assigned in exactly two places**: where the status file is parsed, and in the single escalation block, **which orders `FAIL` > `MISSING` > `PASS`** — a dissent can raise `MISSING` to `FAIL` and never the reverse, and a `FAIL` alongside missing or unreadable fragments still reports them. - **The report is truncated to 32,000 characters before posting**, head kept, by `scripts/clamp-issue-body.mjs` (self-tested by `scripts/clamp-issue-body-selftest.mjs`). The call is non-fatal; the `audit-transcript` artifact holds the report in full; `.github/workflows/workflow-audit.yaml` truncates its commit list the same way (rationale). - **Every run uploads the `audit-transcript` artifact, which is world-readable and not secret-masked** — 14-day retention, deep-linked from failure issues (rationale). diff --git a/docs/specs/security-audit.rationale.md b/docs/specs/security-audit.rationale.md index ce88d36f7..9ccbb0515 100644 --- a/docs/specs/security-audit.rationale.md +++ b/docs/specs/security-audit.rationale.md @@ -62,6 +62,8 @@ Run 35205193090's `## Summary` also inverted the placeholder it was reading: "tw Collapsing the inconclusive case into `FAIL`, as the step originally did, filed an identical issue for "the repo is insecure" and "the auditor stopped early". +A `FAIL IF` condition no audit run can read makes the verdict a coin flip, because no run can ever determine it. `AUDIT_PAT`-readable GitHub state is not in that class: a failed call there is a real `UNVERIFIABLE`. `## Future` holds such an obligation only while its subject is unbuilt, since a staged item must eventually be promoted; a standing obligation on existing infrastructure is present-tense fact and stays beside its rule. `security-hosted.md` carried two: the Cloudflare script-injection exclusion, which is a zone setting, and a closing activation sentence that stated its own answer. Run 35586089654 (2026-09-21) reached both as `UNVERIFIABLE` in its sub-auditors and its domain lead resolved both to PASS, on the ground that the audited condition was the in-repo half; run 35709640946 (2026-09-22) left both `UNVERIFIABLE`, so a pass with 375 PASS and 0 FAIL returned INCONCLUSIVE and held the release gate shut (issue #747). Nothing in the tree had changed between them. #757 staged both under `security-hosted.md`'s `## Future` and kept the in-repo half — the deploy's `preflight` gate — as a `FAIL IF`. + GitHub rejects an over-long issue body outright; that rejection lands on a `set -e` step *after* the verdict is decided, and the finding then reaches no issue and no comment — only a red run and an artifact that expires. Truncation keeps the head because that is where the verdict and the links are, and the clamp call is non-fatal so a failure of the helper cannot reopen the window it closes. Issue prose per combination of conditions cannot be kept correct by fixing combinations. Four consecutive review rounds found the same defect in different clothes — an arm whose text was true only of the states that could reach it, made false by the next gate that widened. A note claiming nothing about the other conditions cannot be invalidated by a new one. diff --git a/docs/specs/security-ci.md b/docs/specs/security-ci.md index 8a37c6a5d..1f73d2ab4 100644 --- a/docs/specs/security-ci.md +++ b/docs/specs/security-ci.md @@ -88,7 +88,7 @@ Source of truth: `packageRules` in `.github/renovate.json`; `WINDOW` and `is_ten **Must keep Hosted credentials in dedicated environments.** `hosted-production` and `hosted-release-tag` admit only `main`; `hosted-preview` admits only `main` and `refs/pull/*/merge`. All require Ned or Edgar's review with administrator bypass disabled; self-review is allowed. Preview approval authorizes the PR code to receive test-resource credentials only. - **FAIL IF** a Hosted environment lacks those branch restrictions, required reviewers, or disabled administrator bypass; inspect all three environments and their deployment policies. -- **FAIL IF** Hosted credentials appear at repository/org scope, production credentials appear in `hosted-preview`, or preview credentials can reach production/TTR/marketing resources. Inspect GitHub secret placement and Cloudflare/Neon token scope; names alone do not isolate resources. +- **FAIL IF** Hosted credentials appear at repository/org scope, or production credentials appear in `hosted-preview`; inspect GitHub secret placement. - **FAIL IF** `HOSTED_TAG_TOKEN` appears outside `hosted-release-tag`, or that environment is used by a job other than `tag` in `.github/workflows/hosted-production.yml`. Its admin identity's repository-scoped Contents-write PAT can write code and bypass tag protection; it must never enter a deployment job or PR execution. - **FAIL IF** a Hosted preview deploy accepts a fork or a failing verification, preview cleanup checks out a PR ref rather than `main`, or a Hosted production tag can run before live verification succeeds; inspect the workflow dependency/condition graph. diff --git a/hosted/README.md b/hosted/README.md index e54c40e4c..03730e3ec 100644 --- a/hosted/README.md +++ b/hosted/README.md @@ -72,11 +72,8 @@ revision before a production release. | Recovery | Neon backups/PITR enabled, encrypted pre-migration dumps retained as GitHub artifacts for 30 days, age identity also retained independently in a password manager | Cloudflare Workers Scripts and Hyperdrive permissions are account-scoped, so -previews need their own test account. Production deployment isolation likewise -requires a boundary marketing's existing credentials cannot reach; coordinate -the hostname/zone placement before choosing an account. Do not reuse TTR's Neon -project, mail token, or OAuth registrations. `docs/specs/security-ci.md` -> -"Hosted Deployments" owns the credential isolation the audit checks. +previews need their own test account. `docs/specs/security-ci.md` -> "Hosted +Deployments" owns the credential placement the audit checks. ## GitHub setup @@ -142,11 +139,11 @@ back from GitHub; retain independent copies in your password manager. ## Provision the production boundary Use dedicated Dormouse resources in the existing Cloudflare, Neon, and Postmark -accounts. Do not reuse TTR's database, mail server/token, or OAuth registrations. +accounts. -1. Create a dedicated Dormouse production Postgres database (Neon is the TTR - precedent) on PostgreSQL 17; the backup/restore tooling pins PostgreSQL - 17.11. Keep TTR, development, and previews separate. Enable backups and a +1. Create a dedicated Dormouse production Postgres database on Neon, on + PostgreSQL 17; the backup/restore tooling pins PostgreSQL + 17.11. Keep development and previews separate. Enable backups and a suitable PITR window, and verify a restore into a separate database before accepting real accounts. 2. Create a Cloudflare Hyperdrive configuration for that database with **query @@ -166,10 +163,7 @@ accounts. Do not reuse TTR's database, mail server/token, or OAuth registrations `signin@hosted.dormouse.sh` (or update `EMAIL_FROM`). Configure SPF/DKIM and DMARC. Register the sender with Apple Private Email Relay for relay-address delivery. -5. Use a deployment identity separate from marketing, with access limited to - the Hosted deployment resources. If a Cloudflare account token cannot express - that isolation, use a separate account/deployment boundary. -6. Configure `hosted.dormouse.sh` as the Worker's custom domain. Exclude this +5. Configure `hosted.dormouse.sh` as the Worker's custom domain. Exclude this hostname from Cloudflare Web Analytics, Zaraz, and other script injection or rewriting rules. Disable account API caching. Keep `workers_dev` and public preview URLs disabled. @@ -182,8 +176,7 @@ keychain. Authenticate in your own terminal; account/provider sign-in is operato ## Separate OAuth registrations -Create Dormouse registrations; do not reuse TTR credentials or replace TTR's -callbacks. Register these exact URLs with no trailing slash: +Create Dormouse registrations. Register these exact URLs with no trailing slash: | Provider | Registration | Callback | | --- | --- | --- | diff --git a/scripts/spec-word-budgets.json b/scripts/spec-word-budgets.json index 23735a2d8..9a385e2fe 100644 --- a/scripts/spec-word-budgets.json +++ b/scripts/spec-word-budgets.json @@ -18,7 +18,7 @@ "docs/specs/relay.md": 10150, "docs/specs/remote-api.md": 4700, "docs/specs/remote-security-model.md": 4750, - "docs/specs/security-audit.md": 1950, + "docs/specs/security-audit.md": 2000, "docs/specs/security-ci.md": 2750, "docs/specs/security-hosted.md": 600, "docs/specs/security-local.md": 3150,