diff --git a/REGISTER.md b/REGISTER.md index e1baeb8..f51948f 100644 --- a/REGISTER.md +++ b/REGISTER.md @@ -8,13 +8,13 @@ Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. La | ID | Finding | System | Severity | Disclosure | Escalation | Fix owner | Status | Cadence | Next check | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| [RISK-F-0009](findings/RISK-F-0009-openbao-deny-set-covers-a-third-of-high-risk-lanes.md) | agent-high-risk-boundary denies 6 of 17 high-risk lanes; the direct bao path is unprotected for the rest | railiance-platform | **high** | embargoed | none | railiance-platform | open | 1h (1) | 2026-08-20 11:02Z | +| [RISK-F-0009](findings/RISK-F-0009-openbao-deny-set-covers-a-third-of-high-risk-lanes.md) | agent-high-risk-boundary denies 6 of 17 high-risk lanes; the direct bao path is unprotected for the rest | railiance-platform | **high** | embargoed | none | railiance-platform | open | 1h (1) | **due** | | [RISK-F-0008](findings/RISK-F-0008-audit-retention-legal-basis-assumed.md) | The legal basis for retaining audit facts against an erasure request has been assumed, never established | audit-core | medium | public | **required** (t2, partially-answered) | risk-nexus | open | instant (0) | **due** | | [RISK-F-0007](findings/RISK-F-0007-unverified-tenant-boundary.md) | No consumer's tenant boundary is verified anywhere | estate | **high** | embargoed | **answered** (t4, assigned) | per-consumer, on request | accepted | instant (0) | **due** | -| [RISK-F-0006](findings/RISK-F-0006-apps-pg-no-backup-configured.md) | apps-pg has no backup configured at all: R0 means no recovery | railiance-platform | **high** | embargoed | **answered** (t3, approved) | railiance-platform | open | 1h (1) | 2026-08-20 11:02Z | -| [RISK-F-0005](findings/RISK-F-0005-audit-core-unfiltered-read-path.md) | audit-core read path applies no tenant filter; the bound is deployment, not code | audit-core | medium | embargoed | none | audit-core | open | 1h (1) | 2026-08-20 11:02Z | -| [RISK-F-0004](findings/RISK-F-0004-tenant-engine-unfiltered-event-read.md) | tenant-engine events() returns the entire event log unfiltered | tenant-engine | **high** | embargoed | none | tenant-engine | open | 1h (1) | 2026-08-20 11:02Z | -| [RISK-F-0003](findings/RISK-F-0003-ops-warden-read-boundary-ungraded-lanes.md) | ops-warden agent read-boundary does not fire on ungraded catalog lanes | ops-warden | medium | embargoed | none | ops-warden | mitigated | 1h (1) | 2026-08-20 11:02Z | +| [RISK-F-0006](findings/RISK-F-0006-apps-pg-no-backup-configured.md) | apps-pg has no backup configured at all: R0 means no recovery | railiance-platform | **high** | embargoed | **answered** (t3, approved) | railiance-platform | open | 1h (1) | **due** | +| [RISK-F-0005](findings/RISK-F-0005-audit-core-unfiltered-read-path.md) | audit-core read path applies no tenant filter; the bound is deployment, not code | audit-core | medium | embargoed | none | audit-core | open | 1h (1) | **due** | +| [RISK-F-0004](findings/RISK-F-0004-tenant-engine-unfiltered-event-read.md) | tenant-engine events() returns the entire event log unfiltered | tenant-engine | **high** | embargoed | none | tenant-engine | open | 1h (1) | **due** | +| [RISK-F-0003](findings/RISK-F-0003-ops-warden-read-boundary-ungraded-lanes.md) | ops-warden agent read-boundary does not fire on ungraded catalog lanes | ops-warden | medium | embargoed | none | ops-warden | mitigated | 1h (1) | **due** | | [RISK-F-0002](findings/RISK-F-0002-ops-warden-sign-ungated.md) | ops-warden signs SSH certificates with no authorization decision, and its unblock is now unsafe | ops-warden | medium | embargoed | **withdrawn** (t6, withdrawn-hazard-window-closed) | ops-warden | open | instant (0) | **due** | | [RISK-F-0001](findings/RISK-F-0001-flex-auth-unauthenticated-check.md) | flex-auth /v1/check authenticates no caller | flex-auth | **high** | public | **withdrawn** (t1, withdrawn-before-sending) | flex-auth | fixed | instant (0) | **due** | @@ -26,13 +26,29 @@ Hazards created by acting in the wrong order. Each binds another finding's remed | --- | --- | --- | --- | | RISK-F-0002 | RISK-F-0001 | **lifted** | LIFTED 2026-08-19 — flex-auth /v1/check now authenticates callers (RISK-F-0001 fixed). Enabling policy.enabled is now an availability question for ops-warden, no longer an attestation hazard. | +## Waiting on someone + +Every wait resolves on its default date whether or not anyone answers. +Silence never buys a softer grade — see `docs/method/dependencies.md`. + +| Finding | Who | What would change | Default if silent | On | +| --- | --- | --- | --- | --- | +| RISK-F-0009 | railiance-platform | embargo lifts on coverage; live verification would refine the grade but is not required for it | the eight uncovered paths stand as recorded and the finding is re-raised | 2026-09-03 | +| RISK-F-0008 | audit-core | a working keyed commitment narrows RISK-REG-0001 to retained-by-obligation categories only | encrypt-then-hash recorded as the only known route, and the retention period recorded as unstateable | 2026-11-17 | +| RISK-F-0008 | the-custodian | fixes when the estate stops running on an assumption | the assumption is recorded in the register as an assumption | 2026-11-17 | +| RISK-F-0007 | user-engine | likelihood falls for user-engine if a verification exists; a defect becomes its own finding if not | the on-request path is recorded as having produced no answer, which makes the acceptance itself unsupported and is escalated | 2026-09-03 | +| RISK-F-0006 | railiance-platform | embargo lifts on a demonstrated restore; the approved spend becomes a real figure | recorded as stalled with approval already granted, which is the worst kind of stall | 2026-09-18 | +| RISK-F-0005 | audit-core | likelihood rises to L3 if any other production credential carries may_read | graded on the sender alone, as stated; the wider question is recorded as unanswered | 2026-09-19 | +| RISK-F-0004 | tenant-engine | grade rises if the log carries tenant payload rather than metadata | grade stands as recorded; absent fix tracking is recorded as a stall | 2026-09-03 | +| RISK-F-0002 | ops-warden | if it admits no ingress, enabling the gate stops all signing — an availability blocker, not a risk one | the register records the ordering as unverified and re-raises it; the finding stands at medium | 2026-08-27 | + ## Embargoes Held from publication with a stated condition. A hold with no moving condition is a stall. | Finding | Since | Lifts when | Re-decided | | --- | --- | --- | --- | -| RISK-F-0009 | 2026-08-20 | the deny set covers every high-risk lane with a KV path, verified against live OpenBao | — | +| RISK-F-0009 | 2026-08-20 | railiance-platform reports the deny set covers every high-risk lane with a KV path (live verification refines the grade, it is not the condition) | — | | RISK-F-0007 | 2026-08-19 | a verification exists for at least one consumer boundary | 2026-09-18 | | RISK-F-0006 | 2026-08-19 | a backup exists and a restore has been demonstrated once | 2026-09-18 | | RISK-F-0005 | 2026-08-19 | AUDIT-WP-0008-T04 lands a tenant filter in the read path | 2026-11-17 | diff --git a/docs/method/dependencies.md b/docs/method/dependencies.md new file mode 100644 index 0000000..ca56560 --- /dev/null +++ b/docs/method/dependencies.md @@ -0,0 +1,107 @@ +--- +id: RISK-METHOD-DEPENDENCIES +type: method +title: "Waiting: how this register depends on other people without becoming a queue" +status: adopted +owner: risk-nexus +adopted: "2026-08-20" +workplan: RISK-WP-0001 +review_interval: 180d +--- + +# Waiting + +By 2026-08-20 the register had accumulated nine waits in four days, one of them +four hops deep: `RISK-F-0003`'s embargo waited on `RISK-F-0009`, which waited +on `railiance-platform` fixing a deny set, which waited on someone verifying it +against live OpenBao, which waited on a credential nobody has. + +Nothing in that chain is anyone's fault, and every link was individually +reasonable. That is exactly why it needs a rule: deep dependencies are not +built deliberately, they accrete one sensible step at a time. + +## The principle + +**The register never waits to decide. It decides, and revises when told.** + +A wait is a *refinement* pending, not a decision pending. If the register +cannot act until someone answers, the register has made that person's silence +into its own paralysis — and `INTENT.md` is explicit that a register nobody +acts on is worse than none. + +## The four rules + +### 1. Every wait is typed + +No record may say "waiting on X". A wait carries six things: + +```yaml +waiting_on: + - who: tenant-engine + what: "confirm or correct the unfiltered events() read; open fix tracking" + since: "2026-08-19" + would_change: "grade rises if the log carries payload rather than metadata" + default: "grade stands as recorded; absent fix tracking noted as a stall" + default_at: "2026-09-03" +``` + +`would_change` is the discipline. If nobody can say what the answer would +change, there is nothing being waited for, and the wait should be deleted +rather than carried. + +### 2. Depth one + +**A record may wait on at most one other record, and never on a record that is +itself waiting.** + +When the chain would go deeper, the far end is cut: the record takes its own +default and says which unresolved thing it declined to wait for. Two hops is +the point at which nobody can see the whole line any more, and a wait nobody +can see is indistinguishable from a thing that was dropped. + +Applied 2026-08-20: `RISK-F-0009`'s embargo condition was +"verified against live OpenBao", which no one in the estate can currently do. +It now lifts on **`railiance-platform` reporting the coverage**, with live +verification recorded as a refinement rather than a condition. That cut the +`RISK-F-0003` chain from four hops to two. + +### 3. Defaults are dates, and defaults are pessimistic + +Every wait resolves on a date whether or not anyone answers. The default is +**the reading the stated facts already support** — never a hold, never a +downgrade earned by silence. + +This is what removes the incentive to wait. Silence does not buy an owner a +softer grade or a quieter register; it costs them the grade the evidence +supports, which is usually the one they would want corrected. Answering is how +a grade improves, and that is the right shape for the incentive. + +The register says so in advance, to the owner, in writing. A default nobody was +warned about is an ambush, not a rule. + +### 4. A condition naming somebody else's action carries a date beside it + +"Embargo lifts when X ships" is a dependency with no end. "Lifts when X ships, +or is re-decided on 2026-09-20" terminates. + +Re-decided is not the same as lifted — the re-decision may extend the hold with +a fresh reason. What it may not do is extend by default, which is how holds +quietly become permanent. + +## What this does not solve + +Some dependencies are real and cannot be defaulted away. Nobody can verify an +OpenBao policy without a token, and no rule here conjures one. + +What the rules do is stop that from propagating: the register grades on what is +stated, records what it could not verify, and keeps its own position +independent of the blockage. `docs/method/verification.md` bounds what this repo +can establish itself, and every grade resting on a document rather than a probe +says so on its face. + +## Where the waits are visible + +`make check` reports every open wait with its age, its owner and its default +date, flags any default that has come due, and flags any wait that points at a +record which is itself waiting — a depth-two violation, caught by tooling +rather than by someone noticing. diff --git a/findings/RISK-F-0002-ops-warden-sign-ungated.md b/findings/RISK-F-0002-ops-warden-sign-ungated.md index 5da0f12..6fe406c 100644 --- a/findings/RISK-F-0002-ops-warden-sign-ungated.md +++ b/findings/RISK-F-0002-ops-warden-sign-ungated.md @@ -33,6 +33,13 @@ last_checked: "2026-08-20T10:02:41Z" next_check: "2026-08-20T10:02:41Z" cadence: instant clean_streak: 0 +waiting_on: + - who: ops-warden + what: "probe whether the flex-auth pin you call admits ingress, before enabling policy.enabled" + since: "2026-08-20" + would_change: "if it admits no ingress, enabling the gate stops all signing — an availability blocker, not a risk one" + default: "the register records the ordering as unverified and re-raises it; the finding stands at medium" + default_at: "2026-08-27" graded_by: risk-nexus ruling: RISK-RULING-2026-08-19 --- diff --git a/findings/RISK-F-0004-tenant-engine-unfiltered-event-read.md b/findings/RISK-F-0004-tenant-engine-unfiltered-event-read.md index e0840ec..cc37445 100644 --- a/findings/RISK-F-0004-tenant-engine-unfiltered-event-read.md +++ b/findings/RISK-F-0004-tenant-engine-unfiltered-event-read.md @@ -29,6 +29,13 @@ last_checked: "2026-08-20T10:02:42Z" next_check: "2026-08-20T11:02:42Z" cadence: 1h clean_streak: 1 +waiting_on: + - who: tenant-engine + what: "confirm or correct the unfiltered events() read; open fix tracking" + since: "2026-08-19" + would_change: "grade rises if the log carries tenant payload rather than metadata" + default: "grade stands as recorded; absent fix tracking is recorded as a stall" + default_at: "2026-09-03" graded_by: risk-nexus ruling: RISK-RULING-2026-08-19-B --- diff --git a/findings/RISK-F-0005-audit-core-unfiltered-read-path.md b/findings/RISK-F-0005-audit-core-unfiltered-read-path.md index 74f906d..579a7a9 100644 --- a/findings/RISK-F-0005-audit-core-unfiltered-read-path.md +++ b/findings/RISK-F-0005-audit-core-unfiltered-read-path.md @@ -29,6 +29,13 @@ last_checked: "2026-08-20T10:02:42Z" next_check: "2026-08-20T11:02:42Z" cadence: 1h clean_streak: 1 +waiting_on: + - who: audit-core + what: "is may_read false on every production credential, or only on the sender" + since: "2026-08-19" + would_change: "likelihood rises to L3 if any other production credential carries may_read" + default: "graded on the sender alone, as stated; the wider question is recorded as unanswered" + default_at: "2026-09-19" graded_by: risk-nexus ruling: RISK-RULING-2026-08-19-B --- diff --git a/findings/RISK-F-0006-apps-pg-no-backup-configured.md b/findings/RISK-F-0006-apps-pg-no-backup-configured.md index e354607..3b2b757 100644 --- a/findings/RISK-F-0006-apps-pg-no-backup-configured.md +++ b/findings/RISK-F-0006-apps-pg-no-backup-configured.md @@ -35,6 +35,13 @@ last_checked: "2026-08-20T10:02:42Z" next_check: "2026-08-20T11:02:42Z" cadence: 1h clean_streak: 1 +waiting_on: + - who: railiance-platform + what: "the backup target chosen, its monthly cost, and a demonstrated restore" + since: "2026-08-19" + would_change: "embargo lifts on a demonstrated restore; the approved spend becomes a real figure" + default: "recorded as stalled with approval already granted, which is the worst kind of stall" + default_at: "2026-09-18" graded_by: risk-nexus ruling: RISK-RULING-2026-08-19-B --- diff --git a/findings/RISK-F-0007-unverified-tenant-boundary.md b/findings/RISK-F-0007-unverified-tenant-boundary.md index f8d6bdf..680fc2c 100644 --- a/findings/RISK-F-0007-unverified-tenant-boundary.md +++ b/findings/RISK-F-0007-unverified-tenant-boundary.md @@ -38,6 +38,13 @@ last_checked: "2026-08-20T10:02:41Z" next_check: "2026-08-20T10:02:41Z" cadence: instant clean_streak: 0 +waiting_on: + - who: user-engine + what: "does anything verify that a caller for tenant A cannot reach tenant B (RISK-V-0002)" + since: "2026-08-20" + would_change: "likelihood falls for user-engine if a verification exists; a defect becomes its own finding if not" + default: "the on-request path is recorded as having produced no answer, which makes the acceptance itself unsupported and is escalated" + default_at: "2026-09-03" graded_by: risk-nexus ruling: RISK-RULING-2026-08-19-B --- diff --git a/findings/RISK-F-0008-audit-retention-legal-basis-assumed.md b/findings/RISK-F-0008-audit-retention-legal-basis-assumed.md index 0a8ef7a..7615865 100644 --- a/findings/RISK-F-0008-audit-retention-legal-basis-assumed.md +++ b/findings/RISK-F-0008-audit-retention-legal-basis-assumed.md @@ -36,6 +36,19 @@ last_checked: "2026-08-20T10:02:41Z" next_check: "2026-08-20T10:02:41Z" cadence: instant clean_streak: 0 +waiting_on: + - who: audit-core + what: "does a keyed commitment restore erasability without breaking chain verification; what is the platform-pg co-residency horizon" + since: "2026-08-20" + would_change: "a working keyed commitment narrows RISK-REG-0001 to retained-by-obligation categories only" + default: "encrypt-then-hash recorded as the only known route, and the retention period recorded as unstateable" + default_at: "2026-11-17" + - who: the-custodian + what: "rule the trigger list for buying an external determination" + since: "2026-08-19" + would_change: "fixes when the estate stops running on an assumption" + default: "the assumption is recorded in the register as an assumption" + default_at: "2026-11-17" graded_by: risk-nexus ruling: RISK-RULING-2026-08-19-C --- diff --git a/findings/RISK-F-0009-openbao-deny-set-covers-a-third-of-high-risk-lanes.md b/findings/RISK-F-0009-openbao-deny-set-covers-a-third-of-high-risk-lanes.md index f2c27c4..aee3667 100644 --- a/findings/RISK-F-0009-openbao-deny-set-covers-a-third-of-high-risk-lanes.md +++ b/findings/RISK-F-0009-openbao-deny-set-covers-a-third-of-high-risk-lanes.md @@ -22,13 +22,20 @@ likelihood: L2 fidelity_modifier: false production_rescore: false disclosure: embargoed -embargo_condition: "the deny set covers every high-risk lane with a KV path, verified against live OpenBao" +embargo_condition: "railiance-platform reports the deny set covers every high-risk lane with a KV path (live verification refines the grade, it is not the condition)" embargo_since: "2026-08-20" escalation: none last_checked: "2026-08-20T10:02:42Z" next_check: "2026-08-20T11:02:42Z" cadence: 1h clean_streak: 1 +waiting_on: + - who: railiance-platform + what: "report whether the deny set covers every high-risk lane with a KV path" + since: "2026-08-20" + would_change: "embargo lifts on coverage; live verification would refine the grade but is not required for it" + default: "the eight uncovered paths stand as recorded and the finding is re-raised" + default_at: "2026-09-03" graded_by: risk-nexus ruling: RISK-RULING-2026-08-20 --- diff --git a/tools/register_check.py b/tools/register_check.py index 0a62ab3..2d90db5 100644 --- a/tools/register_check.py +++ b/tools/register_check.py @@ -121,6 +121,29 @@ def main() -> int: ] section("Stalled — escalation trigger 5", bottom, "none") + # Waits: typed, dated, and defaulted (docs/method/dependencies.md). + waiting, due_defaults, deep = [], [], [] + waiting_ids = {f["id"] for f in fs if f.get("waiting_on")} + for f in fs: + for w in (f.get("waiting_on") or []): + since = lib.moment(w.get("since")) + age = f"{(NOW - since).days}d" if since else "?" + when = lib.moment(w.get("default_at")) + waiting.append(f"{f['id']} — {w.get('who')}: {str(w.get('what'))[:64]} ({age} old, defaults {w.get('default_at')})") + if when and NOW >= when: + due_defaults.append( + f"{f['id']} — {w.get('who')} did not answer by {w.get('default_at')}; apply: {w.get('default')}" + ) + # depth-two check: a record waiting on a record that is itself waiting + for ref in ([f.get("constraint_on")] if f.get("constraint_on") else []): + if ref in waiting_ids and f.get("waiting_on"): + deep.append(f"{f['id']} waits, and points at {ref} which also waits — depth two, cut one") + section("Waiting on someone", waiting, "nothing is waiting on anyone") + if due_defaults: + section("Defaults now due — apply them", due_defaults, "none") + if deep: + section("Dependency depth violations", deep, "none") + embargo = [ f"{f['id']} — lifts when: {f.get('embargo_condition')}" for f in fs diff --git a/tools/register_index.py b/tools/register_index.py index 6d9fdec..c149fe1 100644 --- a/tools/register_index.py +++ b/tools/register_index.py @@ -96,6 +96,18 @@ def render() -> str: id=f["id"], on=f.get("constraint_on", "—"), sev=f.get("constraint_severity", "—"), text=f["constraint"])) + waits = [(f, w) for f in fs for w in (f.get("waiting_on") or [])] + if waits: + out += ["", "## Waiting on someone", "", + "Every wait resolves on its default date whether or not anyone answers.", + "Silence never buys a softer grade — see `docs/method/dependencies.md`.", + "", "| Finding | Who | What would change | Default if silent | On |", + "| --- | --- | --- | --- | --- |"] + for f, w in waits: + out.append("| {id} | {who} | {chg} | {dflt} | {at} |".format( + id=f["id"], who=w.get("who", "—"), chg=w.get("would_change", "—"), + dflt=w.get("default", "—"), at=w.get("default_at", "—"))) + embargoed = [f for f in fs if f.get("disclosure") == "embargoed"] if embargoed: out += ["", "## Embargoes", "",