RISK-WP-0001 T01-T06,T08: the four instruments, the index, and the first grading
Severity (impact x likelihood, fidelity modifier for controls that lie, headline-vs-constraint, build-mode double grade, the floor), disclosure (publish/embargoed/restricted, and the build-mode deferral re-taken and narrowed with RISK-F-0001 in hand), escalation (the five INTENT triggers settled plus an ordering-hazard trigger the RISK-F-0002 case forced; proposed, awaiting the custodian), review (intervals, what a review is, what missing one produces, the production re-score). Then applied: RISK-F-0001 critical/embargoed/escalated, RISK-F-0002 medium with a high constraint on RISK-F-0001's remediation, filed as a peer and escalated only on the ordering, RISK-F-0003 high/embargoed/no escalation. No unset field remains. REGISTER.md is generated; make check reports overdue, stalled, ungraded and unanswered escalations without changing anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e268259f94
commit
d5a3953f2e
15 changed files with 1334 additions and 14 deletions
16
Makefile
Normal file
16
Makefile
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
PY := python3
|
||||
TOOLS := tools
|
||||
|
||||
.PHONY: help register check
|
||||
|
||||
help:
|
||||
@echo "make register - rebuild REGISTER.md from findings/"
|
||||
@echo "make check - verify the index is current, then report what is going quiet"
|
||||
|
||||
register:
|
||||
@$(PY) $(TOOLS)/register_index.py
|
||||
|
||||
check:
|
||||
@$(PY) $(TOOLS)/register_index.py --check
|
||||
@echo
|
||||
@$(PY) $(TOOLS)/register_check.py
|
||||
37
REGISTER.md
Normal file
37
REGISTER.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Register
|
||||
|
||||
Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. Last built 2026-08-19.
|
||||
|
||||
3 open of 3 findings; 0 notes below the floor.
|
||||
|
||||
## Findings
|
||||
|
||||
| ID | Finding | System | Severity | Disclosure | Escalation | Fix owner | Status | Review by |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| [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 | **high** | embargoed | none | ops-warden | open | 2026-09-18 |
|
||||
| [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 | **required** (t6, pending-operator) | ops-warden | open | 2026-11-17 |
|
||||
| [RISK-F-0001](findings/RISK-F-0001-flex-auth-unauthenticated-check.md) | flex-auth /v1/check authenticates no caller | flex-auth | **critical** | embargoed | **required** (t1, pending-operator) | flex-auth | open | 2026-08-26 |
|
||||
|
||||
## Constraints
|
||||
|
||||
Hazards created by acting in the wrong order. Each binds another finding's remediation.
|
||||
|
||||
| From | Binds | Severity | Constraint |
|
||||
| --- | --- | --- | --- |
|
||||
| RISK-F-0002 | RISK-F-0001 | **high** | policy.enabled must not be turned on while flex-auth /v1/check answers unauthenticated callers — the gate would sign a false attestation |
|
||||
|
||||
## 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-0003 | 2026-08-19 | the five exec_capable lanes graded under WARDEN-WP-0032-T05 | 2026-09-18 |
|
||||
| RISK-F-0002 | 2026-08-19 | FLEX-WP-0015-T02 shipped and ops-warden policy.enabled true in production | 2026-11-17 |
|
||||
| RISK-F-0001 | 2026-08-19 | FLEX-WP-0015-T02 ships to production | 2026-08-26 |
|
||||
|
||||
## How to read this
|
||||
|
||||
Severity is `docs/method/severity.md`; disclosure `docs/method/disclosure.md`;
|
||||
escalation `docs/method/escalation.md`; review dates `docs/method/review.md`.
|
||||
A constraint may be graded higher than the finding that carries it — read both.
|
||||
116
docs/method/disclosure.md
Normal file
116
docs/method/disclosure.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
id: RISK-METHOD-DISCLOSURE
|
||||
type: method
|
||||
title: "Disclosure: publish now, hold, or restrict"
|
||||
status: adopted
|
||||
owner: risk-nexus
|
||||
adopted: "2026-08-19"
|
||||
workplan: RISK-WP-0001-T02
|
||||
review_interval: 180d
|
||||
---
|
||||
|
||||
# Disclosure
|
||||
|
||||
Whether and when a finding is published. `policy-nexus` is the surface; this
|
||||
document decides what it is handed.
|
||||
|
||||
## What disclosure is not
|
||||
|
||||
**A finding file in this repo is not a publication.** This repo is a private
|
||||
checkout on a private forge. Holding a finding means not routing it to
|
||||
`policy-nexus`; it does not mean hiding it from the estate, from the owning
|
||||
repo, or from the operator. Every state below is fully visible internally.
|
||||
|
||||
## The states
|
||||
|
||||
| State | Meaning | Entry condition |
|
||||
| --- | --- | --- |
|
||||
| `public` | Published through `policy-nexus` now. | The finding is fixed, or reading it gives no one an advantage they do not already have. |
|
||||
| `embargoed` | Held, with a stated condition that lifts it. | Live, unfixed, and the text would help someone reach the defect faster than they could without it. |
|
||||
| `restricted` | Held with no expected lift. | Publication would remain harmful after the fix — third-party material, a named person, or a credential-shaped detail that survives remediation. |
|
||||
|
||||
There is no fourth state and no `unset` after grading. A finding whose
|
||||
disclosure has not been decided is an ungraded finding.
|
||||
|
||||
## Embargo is a record, not a silence
|
||||
|
||||
`INTENT.md` requires the record that a delay was deliberate rather than a
|
||||
document quietly going missing. An `embargoed` finding therefore carries:
|
||||
|
||||
```yaml
|
||||
disclosure: embargoed
|
||||
embargo_condition: "FLEX-WP-0015-T02 ships to production"
|
||||
embargo_since: "2026-08-19"
|
||||
embargo_review: "2026-08-26"
|
||||
```
|
||||
|
||||
`embargo_condition` must be an event someone can observe, not a mood. "Until
|
||||
it is safer" is not a condition. `embargo_review` follows the finding's
|
||||
severity interval from `docs/method/review.md`; when it passes, the embargo is
|
||||
re-decided, not extended by default.
|
||||
|
||||
An embargo that has outlived two consecutive reviews without its condition
|
||||
moving is itself a finding — the remediation has stalled, and the hold is now
|
||||
doing the work the fix was supposed to do.
|
||||
|
||||
## The build-mode deferral, re-taken
|
||||
|
||||
`INTENT.md` recorded controlled disclosure as deferred to production, reasoning
|
||||
that build mode has no users to expose. `RISK-F-0001` arrived and tested it: a
|
||||
live authorization bypass in the service every other service trusts, with
|
||||
publish-or-forget as the only available choice.
|
||||
|
||||
**The deferral is narrowed, not kept and not abandoned.**
|
||||
|
||||
What was right about it: build mode does have no consumers to protect, and
|
||||
building an embargo *mechanism* — timed release, staged notification,
|
||||
coordinated disclosure with third parties — before there is anyone to
|
||||
coordinate with would be machinery for its own sake.
|
||||
|
||||
What was wrong about it: it conflated the mechanism with the decision. The
|
||||
argument for publishing in build mode is that there are no users to expose.
|
||||
That argument says nothing about attackers, and `RISK-F-0001` is exactly the
|
||||
class where the two come apart — the finding names an unauthenticated
|
||||
decision surface and the service that carries it. Publishing that while it is
|
||||
live helps precisely one kind of reader.
|
||||
|
||||
So the ruling is:
|
||||
|
||||
1. **Build-mode default stays publish.** Architecture, method, fixed findings,
|
||||
and findings whose exposure is already bounded go out. The estate publishing
|
||||
what it knows is wrong remains the norm and does not need a case made for it
|
||||
each time.
|
||||
2. **Live-and-reachable is the exception, and it exists now.** A finding that
|
||||
is unfixed *and* whose text shortens the path to the defect is `embargoed`
|
||||
until the fix lands. That is the missing middle `INTENT.md` said did not
|
||||
exist. It costs one front-matter field and a line in `REGISTER.md`.
|
||||
3. **The mechanism stays deferred.** No timed release, no coordinated
|
||||
disclosure protocol, no notification tiers. Those wait for real users, as
|
||||
originally reasoned. What is not deferred is the *decision*, because
|
||||
`RISK-F-0001` demonstrated the decision is needed before the machinery is.
|
||||
|
||||
This is a decision of this repo, taken 2026-08-19 with `RISK-F-0001`,
|
||||
`RISK-F-0002` and `RISK-F-0003` in hand rather than hypothetically. It is
|
||||
revisable, and the production transition is the scheduled moment to revisit it.
|
||||
|
||||
## Test for "shortens the path"
|
||||
|
||||
Ask: does the finding tell a reader something that materially reduces the work
|
||||
of reaching the defect, beyond what reading the repo would give them?
|
||||
|
||||
- A file path and line number in a private repo — no, that is already there.
|
||||
- "This surface authenticates nobody, here is its cluster address" — yes.
|
||||
- "These five named lanes vend real secret values without the boundary firing"
|
||||
— yes.
|
||||
- "This system had no backups configured" — no, once backups exist; yes, while
|
||||
they do not, because it names when destruction is unrecoverable.
|
||||
|
||||
When the answer is genuinely unclear, embargo and re-decide at the review. The
|
||||
cost of a wrong embargo is a delayed publication; the cost of a wrong publish
|
||||
is not recoverable.
|
||||
|
||||
## Publication happens elsewhere
|
||||
|
||||
A `public` finding is handed to `policy-nexus` under its publication contract
|
||||
and gets a permanent address there. This repo never serves it and never
|
||||
edits it after handover; corrections go through the same route as the original.
|
||||
148
docs/method/escalation.md
Normal file
148
docs/method/escalation.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
---
|
||||
id: RISK-METHOD-ESCALATION
|
||||
type: method
|
||||
title: "Escalation: what reaches the operator personally"
|
||||
status: proposed
|
||||
owner: the-custodian
|
||||
drafted_by: risk-nexus
|
||||
drafted: "2026-08-19"
|
||||
workplan: RISK-WP-0001-T03
|
||||
review_interval: 90d
|
||||
---
|
||||
|
||||
# Escalation
|
||||
|
||||
`INTENT.md` gives `the-custodian` the duty of deciding what must reach the
|
||||
operator personally rather than sitting in a register, and says the rule is not
|
||||
yet written. This is the rule, drafted by `risk-nexus`.
|
||||
|
||||
**Status: `proposed`.** The custodian adopts it. It is not in force until the
|
||||
front-matter says `adopted`, and an unadopted rule is worse than an unwritten
|
||||
one because it looks like coverage.
|
||||
|
||||
Getting this wrong in either direction is a failure: escalating everything
|
||||
makes the operator the queue, escalating nothing makes the register a place
|
||||
where serious things go quiet.
|
||||
|
||||
## What escalation is
|
||||
|
||||
A message to the operator personally — inbox message to `the-custodian`, and
|
||||
said out loud in whatever session is running — containing exactly four things:
|
||||
|
||||
1. the finding id and one sentence of what is true;
|
||||
2. the decision being asked for;
|
||||
3. what happens if no answer comes;
|
||||
4. by when.
|
||||
|
||||
It is **not** a copy of the finding, not a status report, and not a request for
|
||||
the operator to do the work. If the operator's answer would not change
|
||||
anything, it was not an escalation.
|
||||
|
||||
The operator is expected to rule, or to defer explicitly with a date. Silence
|
||||
is not resolution: an unanswered escalation past its date goes back into the
|
||||
register as an open item on this repo, visible in `REGISTER.md`, and is raised
|
||||
again once. It is never quietly dropped, and it is never repeated weekly until
|
||||
someone answers to make it stop.
|
||||
|
||||
## The triggers
|
||||
|
||||
Each of the five candidates from `INTENT.md` is adopted, dropped, or bounded.
|
||||
A trigger without a threshold is not a rule.
|
||||
|
||||
### 1. Real tenant data — adopted, bounded
|
||||
|
||||
Escalate when a finding exposes real data belonging to a real counterparty, or
|
||||
**the decision that governs access to it**, while live and unfixed.
|
||||
|
||||
The bound matters: `INTENT.md` asks whether governing the access counts as
|
||||
exposing the data. It does. An authorization oracle that can be forged is not
|
||||
one step removed from the data; it is the step. Test data and empty tenants do
|
||||
not trigger it — the word is *real*.
|
||||
|
||||
### 2. Legal or regulatory obligation — adopted, unbounded
|
||||
|
||||
Escalate when a finding creates or reveals an obligation with an outside
|
||||
counterparty: a notification duty, a retention or erasure requirement, a
|
||||
contractual commitment, an in-scope determination under a regime.
|
||||
|
||||
No threshold. The estate cannot decide unilaterally that an obligation is
|
||||
small, and this repo is explicitly not the place where legal advice is given.
|
||||
|
||||
### 3. Spend — adopted, bounded
|
||||
|
||||
Escalate when remediation needs money that is not already committed:
|
||||
**recurring above €50/month, or one-off above €500.**
|
||||
|
||||
Below those, the owning repo decides and records it. The numbers are a
|
||||
starting proposal from this repo; the operator owns them and should overwrite
|
||||
them on adoption if they are wrong.
|
||||
|
||||
### 4. Ownership disagreement — adopted, bounded by one exchange
|
||||
|
||||
Escalate when two repos disagree about who owns a fix **and one routing
|
||||
exchange has already failed to settle it.**
|
||||
|
||||
Disagreement itself is normal and is the register's job to route. What needs
|
||||
the operator is a *stuck* disagreement, because only the operator can assign
|
||||
work across repos that will not take it. One exchange, then escalate — not
|
||||
three, and not zero.
|
||||
|
||||
### 5. Stalled remediation — adopted, bounded by interval
|
||||
|
||||
Escalate when a finding's fix has not moved for **twice its review interval**
|
||||
(`docs/method/review.md`): 14 days for `critical`, 60 for `high`, 180 for
|
||||
`medium`. `low` findings do not escalate on staleness; they age out through
|
||||
review.
|
||||
|
||||
"Not moved" means the fix owner's tracking record has not changed state and no
|
||||
new evidence has arrived. A deliberate, stated deferral by the owner is
|
||||
movement. Silence is not.
|
||||
|
||||
### 6. Ordering hazard across repos — added
|
||||
|
||||
Escalate once when remediating finding A in the wrong order relative to
|
||||
finding B would create a **false attestation** — a control that starts
|
||||
claiming a check happened when it did not.
|
||||
|
||||
This trigger is not in `INTENT.md`'s list. It is added because `RISK-F-0002`
|
||||
produced the case: enabling the `ops-warden` signing gate against a forgeable
|
||||
`flex-auth` would convert an absent control into a signed audit record
|
||||
asserting an authorization that was never made. Ordering hazards are cheap to
|
||||
avoid before the fact and expensive to detect afterwards, because the evidence
|
||||
is what lies.
|
||||
|
||||
The escalation is a single acknowledgement of the ordering, not a standing
|
||||
supervision request. Once the operator has the constraint, the register carries
|
||||
it.
|
||||
|
||||
## What does not escalate
|
||||
|
||||
Stated so the rule bites in both directions.
|
||||
|
||||
- **Known, owned, and moving.** However bad. A `critical` finding with an
|
||||
active fix and a responsive owner is register work.
|
||||
- **Severity alone.** There is deliberately no "escalate everything
|
||||
`critical`" trigger. Severity drives review cadence, not the operator's
|
||||
attention. `RISK-F-0002` argued its own case for not escalating and was
|
||||
right on triggers 1-5; it reaches the operator only through trigger 6, and
|
||||
only once.
|
||||
- **The reporter asking for it.** A repo can flag that it thinks something
|
||||
needs the operator. That is input, not a decision. This repo rules.
|
||||
- **Anything this repo can decide itself.** Severity, disclosure, filing
|
||||
shape, review dates. Asking the operator to confirm a judgement that is this
|
||||
repo's to make is how the operator becomes the queue.
|
||||
|
||||
## Testing the rule against the register
|
||||
|
||||
The draft was tested against all three open findings before it was proposed;
|
||||
the rulings are in `docs/rulings/2026-08-19-first-grading.md`.
|
||||
|
||||
Summary: `RISK-F-0001` escalates on trigger 1. `RISK-F-0002` does not escalate
|
||||
on triggers 1-5 — agreeing with the reporter's own reading — but the
|
||||
`RISK-F-0001`/`RISK-F-0002` ordering constraint escalates once on trigger 6,
|
||||
which is the disagreement the rule was worth writing to surface. `RISK-F-0003`
|
||||
does not escalate; it is known, owned, tracked, and moving.
|
||||
|
||||
One escalation, covering two findings, out of three findings. That ratio is
|
||||
the rule doing its job. If it ever reaches "most findings", the rule is wrong,
|
||||
not the register.
|
||||
109
docs/method/review.md
Normal file
109
docs/method/review.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
---
|
||||
id: RISK-METHOD-REVIEW
|
||||
type: method
|
||||
title: "Review and expiry: what happens when nobody looks"
|
||||
status: adopted
|
||||
owner: risk-nexus
|
||||
adopted: "2026-08-19"
|
||||
workplan: RISK-WP-0001-T04
|
||||
review_interval: 180d
|
||||
---
|
||||
|
||||
# Review and expiry
|
||||
|
||||
`INTENT.md`: a finding that has sat untouched past its review date is itself a
|
||||
finding. Silence is not resolution. This document makes that operable.
|
||||
|
||||
## Intervals
|
||||
|
||||
Set from the headline severity at grading time, and reset at each review.
|
||||
|
||||
| Severity | Interval | Stale (2×, escalation trigger 5) |
|
||||
| --- | --- | --- |
|
||||
| `critical` | 7 days | 14 days |
|
||||
| `high` | 30 days | 60 days |
|
||||
| `medium` | 90 days | 180 days |
|
||||
| `low` | 180 days | — (ages out through review) |
|
||||
|
||||
A finding graded `critical` is being looked at weekly or it is not being
|
||||
managed. If weekly is unbearable, the honest response is to re-grade it, not
|
||||
to let the date slide.
|
||||
|
||||
## What a review is
|
||||
|
||||
Four questions, answered in writing on the finding. It takes minutes; it is not
|
||||
an investigation.
|
||||
|
||||
1. **Is the grade still right?** Re-read impact and likelihood against what has
|
||||
changed. New facts move the grade in both directions.
|
||||
2. **Is the blocker still true?** This is the one `RISK-F-0002` bought with
|
||||
evidence: *a blocker is a claim about the world at a date*. Its own stated
|
||||
blocker — "`flex-auth` is not deployed yet" — was invalidated within a day
|
||||
by `RISK-F-0001`, and nothing would have re-checked it. Every review
|
||||
re-checks every stated blocker.
|
||||
3. **Has the fix moved?** Read the owner's tracking record, not our memory of
|
||||
it. Confirm the record still exists and still refers to this defect.
|
||||
4. **Is the disclosure state still right?** An embargo whose condition has
|
||||
been met is published; an embargo whose condition has not moved in two
|
||||
reviews is a stall.
|
||||
|
||||
The finding gets a dated line under `## Reviews`, `last_reviewed` is updated,
|
||||
and `review_by` is pushed by one interval. A review that changes nothing still
|
||||
writes the line — "checked, nothing moved" is the evidence that the silence was
|
||||
observed rather than accidental.
|
||||
|
||||
## When a review is missed
|
||||
|
||||
Overdue is not a status change on the finding. It is a fact about *this repo*,
|
||||
and it surfaces in three places:
|
||||
|
||||
- `make check` reports it (`tools/register_check.py`), always, unconditionally.
|
||||
- `REGISTER.md` shows the finding as overdue with the day count.
|
||||
- At **twice** the interval, escalation trigger 5 fires for `critical`,
|
||||
`high` and `medium`. That is the point at which the register's own silence
|
||||
reaches the operator.
|
||||
|
||||
The register does not auto-escalate severity for lateness, and it does not
|
||||
auto-close anything. Both would be the register lying about its own state to
|
||||
make a number look better.
|
||||
|
||||
## The production re-score
|
||||
|
||||
Every finding carries `severity_at_production` alongside `severity`
|
||||
(`docs/method/severity.md`). Where the two differ, the finding is flagged
|
||||
`production_rescore: true`.
|
||||
|
||||
On the day any part of the estate declares production readiness, every flagged
|
||||
finding is re-scored before that declaration completes. This is not a review
|
||||
date — it is an event, and it fires regardless of where the review dates
|
||||
happen to sit.
|
||||
|
||||
Until then, `make check` lists the flagged findings so the size of that
|
||||
obligation is visible rather than discovered on the day.
|
||||
|
||||
## Front-matter this adds
|
||||
|
||||
```yaml
|
||||
last_reviewed: "2026-08-19"
|
||||
review_by: "2026-08-26"
|
||||
production_rescore: true
|
||||
```
|
||||
|
||||
`review_by` is what the nag reads. It is a date, never a duration, so that
|
||||
nothing has to recompute an interval to know whether a finding is late.
|
||||
|
||||
## Closing a finding
|
||||
|
||||
A finding leaves `open` for exactly one of:
|
||||
|
||||
- `fixed` — the owner's record shows the defect gone, and this repo has read
|
||||
something concrete rather than been told. Publication follows if the
|
||||
disclosure state was `embargoed`.
|
||||
- `accepted` — the estate is deliberately carrying it. Requires who accepted
|
||||
it, why, and a re-review date. `accepted` is not `closed`; it keeps a review
|
||||
interval forever.
|
||||
- `withdrawn` — the finding was wrong, or the defect never existed. Say which.
|
||||
|
||||
There is no `stale`, no `wontfix` and no silent expiry. A finding that nobody
|
||||
will fix and nobody will accept stays `open` and keeps arriving in the nag,
|
||||
because that is the true state.
|
||||
169
docs/method/severity.md
Normal file
169
docs/method/severity.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
---
|
||||
id: RISK-METHOD-SEVERITY
|
||||
type: method
|
||||
title: "Severity: how bad, how likely, and what is not a risk at all"
|
||||
status: adopted
|
||||
owner: risk-nexus
|
||||
adopted: "2026-08-19"
|
||||
workplan: RISK-WP-0001-T01
|
||||
review_interval: 180d
|
||||
---
|
||||
|
||||
# Severity
|
||||
|
||||
This is `risk-nexus`'s judgement instrument. It is not canon, it is not a
|
||||
standard, and it binds nobody else. It exists so that two findings graded a
|
||||
month apart are graded the same way, and so that a grade can be argued with.
|
||||
|
||||
It was written against three real findings (`RISK-F-0001`, `RISK-F-0002`,
|
||||
`RISK-F-0003`) and it must keep grading those three sensibly or it is wrong.
|
||||
|
||||
## The two axes
|
||||
|
||||
### Impact — what happens if it goes wrong once
|
||||
|
||||
| Band | Name | Test |
|
||||
| --- | --- | --- |
|
||||
| `I1` | negligible | Confined to one component. No data leaves it, no record is falsified, no recovery is lost. |
|
||||
| `I2` | limited | One system's data or availability. Recoverable. Confined to a single tenant, actor or lane. |
|
||||
| `I3` | serious | Crosses a boundary — tenant, system, or trust — or removes recoverability for one system. |
|
||||
| `I4` | severe | Crosses the estate. What is compromised here propagates to everything that trusts it, or the data loss is unbounded. |
|
||||
|
||||
Impact is scored at **one occurrence**, not at the worst imaginable campaign.
|
||||
"An attacker who already owns the cluster could do this too" is not an impact
|
||||
argument.
|
||||
|
||||
### Likelihood — how far anyone has to reach
|
||||
|
||||
| Band | Name | Test |
|
||||
| --- | --- | --- |
|
||||
| `L1` | remote | Requires access nobody currently holds and no ordinary process grants. |
|
||||
| `L2` | possible | Requires a foothold the estate does grant somewhere — an in-cluster workload, an agent session, a scoped token. |
|
||||
| `L3` | likely | Reachable from inside the normal working set with no additional step. |
|
||||
| `L4` | present | No barrier at all, or it is already happening. |
|
||||
|
||||
Likelihood is about **reach**, not about intent or about whether anyone has
|
||||
bothered. `risk-nexus` does not model attackers; it models what the system
|
||||
permits.
|
||||
|
||||
Where the reporter has not established exposure, the finding says so and the
|
||||
grade uses the band the *stated* facts support — not the worst case, and not
|
||||
zero. `RISK-F-0001` explicitly declines to assume a default-deny NetworkPolicy
|
||||
exists; the grade must decline with it, and the unverified fact becomes a
|
||||
review item rather than a silent assumption in either direction.
|
||||
|
||||
## The grid
|
||||
|
||||
| | `L1` | `L2` | `L3` | `L4` |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `I4` | medium | high | **critical** | **critical** |
|
||||
| `I3` | low | medium | high | **critical** |
|
||||
| `I2` | low | low | medium | high |
|
||||
| `I1` | note | low | low | medium |
|
||||
|
||||
Four severities: `low`, `medium`, `high`, `critical`. `note` is not a
|
||||
severity; see the floor.
|
||||
|
||||
## The fidelity modifier
|
||||
|
||||
**A control that lies is one impact band worse than the same control absent.**
|
||||
|
||||
Apply `+1` impact band (capped at `I4`) when the failure mode produces a
|
||||
*false record* rather than *no record*: an attestation that a check passed
|
||||
when nothing checked, an audit line asserting an authorization that was never
|
||||
made, a green signal derived from an unreachable test.
|
||||
|
||||
The reasoning is `RISK-F-0002`'s and the register adopts it: an absent control
|
||||
is a gap you can find by looking; a lying control is a gap that survives
|
||||
looking, because the evidence you would look at is the thing that is wrong.
|
||||
Only one of the two states misleads the person investigating afterwards.
|
||||
|
||||
The modifier applies to the state being scored. A finding that describes both
|
||||
states — control absent today, control lying if switched on in the wrong order —
|
||||
gets **two scores and one of them is the register's headline**; see
|
||||
"Which state is scored".
|
||||
|
||||
## Which state is scored
|
||||
|
||||
The headline `severity` is the state of the world **today**. A hazard that
|
||||
would be created by a *future* action is not the headline, because a register
|
||||
that scores hypotheticals stops describing the estate.
|
||||
|
||||
The hazard is not lost. It is recorded on the finding as a named
|
||||
**constraint** with its own grade, and it attaches to whatever action would
|
||||
trigger it — usually another finding's remediation. `RISK-F-0002` is the
|
||||
worked example: the gate being off is today (headline), the gate being
|
||||
switched on while the oracle is forgeable is a constraint on `RISK-F-0001`'s
|
||||
fix, graded separately and higher.
|
||||
|
||||
If the constraint's grade is higher than the headline, the finding says so in
|
||||
its ruling. A reader must not be able to come away with the low number and
|
||||
miss the high one.
|
||||
|
||||
## Build mode
|
||||
|
||||
Every finding is graded twice:
|
||||
|
||||
- `severity` — today, in build mode, with today's likelihood.
|
||||
- `severity_at_production` — the same impact, with likelihood re-read for a
|
||||
system carrying real users and real tenant data.
|
||||
|
||||
Build mode is a legitimate likelihood input. "No consumer holds real data
|
||||
here yet" genuinely lowers reach. It is *not* an impact input: what the defect
|
||||
would do on the day it is exercised does not change because the calendar has
|
||||
not reached production.
|
||||
|
||||
Where the two grades differ, the production transition is a mandatory
|
||||
re-score. `docs/method/review.md` binds the review date to it, so the re-score
|
||||
is a scheduled event and not somebody's memory.
|
||||
|
||||
## The floor
|
||||
|
||||
`INTENT.md`: if a finding would not change anyone's decision, it is a note,
|
||||
not a risk. Concretely, a register entry requires **both**:
|
||||
|
||||
1. **An owner who could act.** Some repo, or the operator, can do something
|
||||
about it. No actor, no entry.
|
||||
2. **A decision that changes.** Recording it alters what someone does, when
|
||||
they do it, or what they must not do first.
|
||||
|
||||
Fails either test → it is a note in `notes/`, not a finding in `findings/`.
|
||||
Notes are not graded, not reviewed, and not published. They exist so that
|
||||
"we saw it" survives without inflating the register.
|
||||
|
||||
An `I1`/`L1` cell is `note` in the grid for the same reason: something that
|
||||
is both negligible and unreachable is a thing we know, not a risk we carry.
|
||||
|
||||
Two things the floor does **not** exclude:
|
||||
|
||||
- **Known and deliberate.** `RISK-F-0002` is a decision somebody made on
|
||||
purpose. It still passes the floor, because it changes what may be switched
|
||||
on and in what order. Deliberate is not the same as tracked.
|
||||
- **Omission-shaped.** `RISK-F-0003` is a default that silently produces
|
||||
ungoverned lanes. The individual lane is small; the default is not.
|
||||
|
||||
## Provenance is a grading input
|
||||
|
||||
All four defects known to this register were found by repos reading their own
|
||||
code against a ladder, within days of each other. None was found by monitoring.
|
||||
|
||||
Where a finding's provenance is "we happened to look", the register does not
|
||||
get to assume that similar defects would have been caught. That raises
|
||||
likelihood for the class, not for the instance, and it belongs in the ruling's
|
||||
reasoning rather than in a modifier — the register grades what is filed, and
|
||||
notes when the filing was luck.
|
||||
|
||||
## Recording a grade
|
||||
|
||||
The finding's front-matter carries:
|
||||
|
||||
```yaml
|
||||
severity: critical # headline, today
|
||||
severity_at_production: critical
|
||||
impact: I4 # band, before modifiers
|
||||
likelihood: L3
|
||||
fidelity_modifier: false # true if +1 applied, with the reason in the ruling
|
||||
```
|
||||
|
||||
and the ruling section states impact, likelihood, any modifier, and the one
|
||||
sentence that would have to become false for the grade to change.
|
||||
225
docs/rulings/2026-08-19-first-grading.md
Normal file
225
docs/rulings/2026-08-19-first-grading.md
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
---
|
||||
id: RISK-RULING-2026-08-19
|
||||
type: ruling
|
||||
title: "First grading: RISK-F-0001, RISK-F-0002, RISK-F-0003"
|
||||
status: recorded
|
||||
owner: risk-nexus
|
||||
date: "2026-08-19"
|
||||
workplan: RISK-WP-0001-T06
|
||||
---
|
||||
|
||||
# First grading — 2026-08-19
|
||||
|
||||
The first application of `docs/method/`. Three findings, graded together
|
||||
because the instruments were written for them and a scale that has never been
|
||||
used is a draft.
|
||||
|
||||
Where a grading changed what an owning repo should do, it went back as a
|
||||
message. No owning repo's files were edited.
|
||||
|
||||
## RISK-F-0001 — flex-auth /v1/check authenticates no caller
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Impact | `I4` |
|
||||
| Likelihood | `L3` |
|
||||
| Fidelity modifier | no |
|
||||
| **Severity** | **critical** |
|
||||
| At production | critical |
|
||||
| Disclosure | embargoed |
|
||||
| Escalation | yes — trigger 1 |
|
||||
| Review by | 2026-08-26 |
|
||||
|
||||
**Impact `I4`.** `flex-auth` is the estate's authorization oracle. A forged
|
||||
allow does not stay in `flex-auth`; it is the answer other services act on,
|
||||
and `tenant-engine` has separately stated that direct authority over its rows
|
||||
means privilege escalation across NetKingdom rather than tampering confined to
|
||||
one store. That is the definition of `I4` — what is compromised here
|
||||
propagates to everything that trusts it.
|
||||
|
||||
**Likelihood `L3`, not `L4`.** Any workload with cluster reach can assert any
|
||||
subject and any tenant. That is the normal working set with no additional
|
||||
step. It is not `L4` only because reach is inside the cluster rather than open.
|
||||
|
||||
The register follows the reporter in **not** assuming a default-deny
|
||||
NetworkPolicy exists, and equally in not assuming its absence proves wider
|
||||
exposure. `flex-auth` did not state it either way. This is the single fact
|
||||
that would most change the grade, so it is the first question at review — see
|
||||
"Asks" below.
|
||||
|
||||
**No fidelity modifier.** The endpoint answers honestly about a caller it
|
||||
never checked; it makes no attestation of its own. The false-record hazard
|
||||
belongs to `RISK-F-0002`'s constraint, where a *consumer* of this endpoint
|
||||
would start signing records that assert an authorization was made.
|
||||
|
||||
**Production.** Unchanged. Impact is already estate-wide and likelihood does
|
||||
not depend on there being users.
|
||||
|
||||
**Disclosure: embargoed** until `FLEX-WP-0015-T02` ships to production. This
|
||||
is the finding that forced the deferral to be re-taken
|
||||
(`docs/method/disclosure.md`): it names an unauthenticated decision surface
|
||||
and the service carrying it, which shortens the path for exactly one kind of
|
||||
reader. Publish on fix.
|
||||
|
||||
**Escalation: yes, trigger 1.** `INTENT.md` asked whether governing access to
|
||||
tenant data counts as exposing it. Ruled: it does. An authorization oracle
|
||||
that can be forged is not one step removed from the data, it is the step.
|
||||
|
||||
**Nothing further is asked of `flex-auth`.** They own it, classify it as their
|
||||
only urgent follow-up, and are shipping through staged promotion.
|
||||
|
||||
## RISK-F-0002 — ops-warden signs SSH certificates with no authorization decision
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Impact | `I3` |
|
||||
| Likelihood | `L2` |
|
||||
| Fidelity modifier | no (headline) |
|
||||
| **Severity** | **medium** |
|
||||
| Constraint severity | **high** |
|
||||
| At production | medium |
|
||||
| Disclosure | embargoed |
|
||||
| Escalation | via trigger 6 only, jointly with RISK-F-0001 |
|
||||
| Review by | 2026-11-17 |
|
||||
|
||||
`RISK-F-0002` asked this repo three questions. All three are answered here.
|
||||
|
||||
### 1. Severity — which state is scored
|
||||
|
||||
The headline scores **today**: the gate is off, and that is a missing control
|
||||
honestly represented. `I3` — SSH certificates into production hosts cross a
|
||||
trust boundary. `L2` — the caller must already hold a scoped `VAULT_TOKEN` and
|
||||
the actor must exist in `inventory.yaml`; TTLs are enforced and every issuance
|
||||
is logged. Possession of the signing token is the whole authorization model,
|
||||
which is thin, but it is not nothing and it is not `L3`.
|
||||
|
||||
**The reporter's argument is accepted in full**: the two states are not equally
|
||||
bad, and the second is the one worth a severity. It is recorded as a
|
||||
constraint, not as the headline, because the register describes the estate as
|
||||
it is and the second state does not exist yet.
|
||||
|
||||
> **Constraint (severity `high`).** Enabling `policy.enabled` while
|
||||
> `flex-auth`'s `/v1/check` answers unauthenticated callers converts an absent
|
||||
> control into a false attestation: an attacker with ClusterIP reach obtains a
|
||||
> genuine `allow`, and the signature log carries a `policy_decision_id`
|
||||
> asserting the issuance was authorized. `I3 + fidelity → I4`, `L2` → `high`.
|
||||
|
||||
The constraint attaches to `RISK-F-0001`'s remediation, and the ordering is
|
||||
recorded on both findings:
|
||||
|
||||
```
|
||||
flex-auth warn-only -> ops-warden gate presents its SA token -> logs clean
|
||||
-> flex-auth fail-closed -> ops-warden policy.enabled: true
|
||||
```
|
||||
|
||||
The headline being lower than the constraint is exactly the case
|
||||
`docs/method/severity.md` was written to handle. A reader must not come away
|
||||
with "medium" and miss the high one.
|
||||
|
||||
### 2. Peer or consequence — **peer**
|
||||
|
||||
Filed as its own finding, as `ops-warden` filed it. Two reasons, both from
|
||||
`INTENT.md`'s routing rule: the fix owner differs, and the "off" state has
|
||||
standing regardless of how `RISK-F-0001` resolves. If `RISK-F-0001` were
|
||||
withdrawn tomorrow, `ops-warden` would still be signing certificates with no
|
||||
per-request judgement.
|
||||
|
||||
What is *not* independent is the ordering, and that is carried as a constraint
|
||||
on both rather than by collapsing one finding into the other.
|
||||
|
||||
### 3. Escalation — the register disagrees with the reporter, narrowly
|
||||
|
||||
`ops-warden` judged this does not need the operator: known, owned, and its
|
||||
dangerous failure mode written down in both repos. On triggers 1-5 the
|
||||
register agrees — no real tenant data, no obligation, no spend, no ownership
|
||||
dispute, no stall.
|
||||
|
||||
But the reasoning "it is written down in both repos" is the argument the
|
||||
register cannot accept on its own, because `RISK-F-0002` is itself the
|
||||
evidence against it: its own blocker was written down, filed away, and
|
||||
invalidated in a day with nothing re-checking it. Written down is not the same
|
||||
as held.
|
||||
|
||||
So **trigger 6 fires once** — an ordering hazard whose wrong sequence creates
|
||||
a false attestation. One acknowledgement from the operator that the ordering
|
||||
exists, then the register carries it. Not a standing supervision request.
|
||||
|
||||
This is the disagreement the escalation rule was worth writing to surface, and
|
||||
it is the reason trigger 6 exists at all.
|
||||
|
||||
**Disclosure: embargoed** until the ordering completes — `FLEX-WP-0015-T02`
|
||||
shipped *and* `policy.enabled: true` in production. Publishing "production SSH
|
||||
signing performs no authorization decision" while true shortens the path.
|
||||
|
||||
## RISK-F-0003 — agent read-boundary does not fire on ungraded catalog lanes
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Impact | `I4` |
|
||||
| Likelihood | `L2` |
|
||||
| Fidelity modifier | no (see below) |
|
||||
| **Severity** | **high** |
|
||||
| At production | high |
|
||||
| Disclosure | embargoed |
|
||||
| Escalation | no |
|
||||
| Review by | 2026-09-18 |
|
||||
|
||||
**Impact `I4`, driven by one lane.** Four of the five `exec_capable` ungraded
|
||||
lanes are `I3`-shaped — a publish token, an OIDC login, an ingestion key, a
|
||||
hub write token, each bad within its own system. `openbao-api-key` is not:
|
||||
it vends access to the store the estate's other secrets live in, and a
|
||||
disclosure there propagates past every boundary the others respect. The
|
||||
finding is graded on its worst lane.
|
||||
|
||||
**Likelihood `L2`.** Requires an agent session with `WARDEN_AGENT_ID` set —
|
||||
a foothold the estate does grant. `ops-warden` states the exposed population
|
||||
is not established and should not be assumed zero or large, and the register
|
||||
holds that line: `L2` is what the stated facts support. The unverified OpenBao
|
||||
`agent-high-risk-boundary` coverage would lower it if confirmed, and is a
|
||||
review question rather than an assumption.
|
||||
|
||||
**No fidelity modifier, deliberately, and it was close.** `ADR-0004` reads as
|
||||
a categorical rule while the implementation is an opt-in list, so the estate's
|
||||
*documentation* asserts a boundary that does not exist for 14 lanes. That is
|
||||
documentation drift, and the modifier in `docs/method/severity.md` is scoped
|
||||
to attestations a system produces at runtime. If any compliance evidence
|
||||
anywhere cites `ADR-0004` as implemented, that is a false attestation and a
|
||||
separate finding — this register has not looked, and says so rather than
|
||||
assuming.
|
||||
|
||||
**Escalation: no.** Known, owned, tracked (`WARDEN-WP-0032-T05` for the five
|
||||
lanes, `WARDEN-WP-0032-T06` for the structural fix), and moving. No trigger
|
||||
fires. Severity alone deliberately does not escalate.
|
||||
|
||||
**The operator's maturity-context direction is noted, not re-decided.** The
|
||||
finding records that an absent grade should resolve from maturity context —
|
||||
tolerable in early or experimental contexts, `high` or `critical` in
|
||||
production ones — and that the durable form waits on the zone model
|
||||
(`ZONE-WP-0001-T03`). The register's only remark is the one the finding
|
||||
already makes and this repo confirms from its own registration work today:
|
||||
`.repo-classification.yaml` `category` cannot carry that signal.
|
||||
`railiance-platform` runs production OpenBao and owns three of the five lanes
|
||||
while classified `tooling`, and this repo has just classified itself with the
|
||||
same vocabulary. The maturity ladder needs its own field.
|
||||
|
||||
**Disclosure: embargoed** until the five lanes are graded under
|
||||
`WARDEN-WP-0032-T05`. It names five live lanes that vend real values without
|
||||
the boundary firing.
|
||||
|
||||
## What this grading asks of other repos
|
||||
|
||||
Sent as messages, not as edits.
|
||||
|
||||
| Repo | Ask |
|
||||
| --- | --- |
|
||||
| `flex-auth` | Confirm whether a default-deny NetworkPolicy fronts the `/v1/check` Service. It is the single fact that most changes a `critical` grade, and only they can answer it. |
|
||||
| `ops-warden` | Confirm the OpenBao `agent-high-risk-boundary` policy's coverage of the five `exec_capable` lanes. |
|
||||
| both | The ordering constraint is recorded and carried by this register. Neither repo needs to re-derive it. |
|
||||
|
||||
## What this grading proved about the instruments
|
||||
|
||||
The scale earned two of its features on first use: the fidelity modifier
|
||||
(needed by `RISK-F-0002`, and correctly *not* applied to `RISK-F-0003`), and
|
||||
the headline-versus-constraint split (needed the moment a finding described
|
||||
two states). The floor was not exercised here — all three findings clear it
|
||||
easily — and gets its real test in `RISK-WP-0001-T07`.
|
||||
46
findings/README.md
Normal file
46
findings/README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Filing a finding
|
||||
|
||||
One file per finding: `findings/RISK-F-NNNN-<slug>.md`, YAML front-matter,
|
||||
then prose. Next id is one past the highest here.
|
||||
|
||||
## What the reporter fills in
|
||||
|
||||
```yaml
|
||||
id: RISK-F-0004
|
||||
type: finding
|
||||
title: "one line, what is true — not what should be done"
|
||||
status: open # open | fixed | accepted | withdrawn
|
||||
reported_by: <repo> # who found it
|
||||
reported_via: <repo> # who routed it here, if different
|
||||
date_reported: "YYYY-MM-DD"
|
||||
system: <repo> # the system the defect is in
|
||||
environment: production # production | build | both
|
||||
fix_owner: <repo> # who owns the fix — never risk-nexus
|
||||
fix_tracking: <WP-ID or unset>
|
||||
related: [RISK-F-0001] # optional
|
||||
```
|
||||
|
||||
## What risk-nexus fills in — leave these out
|
||||
|
||||
`severity`, `severity_at_production`, `impact`, `likelihood`,
|
||||
`fidelity_modifier`, `production_rescore`, `disclosure`, `embargo_*`,
|
||||
`escalation*`, `constraint*`, `last_reviewed`, `review_by`, `graded_by`,
|
||||
`ruling`.
|
||||
|
||||
Setting them yourself is not an error to be corrected — it is a boundary this
|
||||
repo would rather keep. The reporter says what is true; this repo says how bad
|
||||
it is and who hears about it (`INTENT.md`). Leaving them out, or writing
|
||||
`unset`, both work; the nag reports either way until they are graded.
|
||||
|
||||
## What makes a good finding here
|
||||
|
||||
- **State exposure only as far as you can support it.** "Not established" is a
|
||||
complete answer and grades better than a guess. `RISK-F-0001` declining to
|
||||
assume a NetworkPolicy is the model.
|
||||
- **Say how it was found.** Provenance is a grading input.
|
||||
- **Suggest a direction if you have one, marked as a suggestion.** The fix is
|
||||
yours; the grade is ours.
|
||||
- **A note is fine.** If it would not change anyone's decision, it belongs in
|
||||
`notes/` — see the floor in `docs/method/severity.md`.
|
||||
|
||||
After filing: `make check`. Then this repo grades it.
|
||||
|
|
@ -11,12 +11,24 @@ system: flex-auth
|
|||
environment: production
|
||||
fix_owner: flex-auth
|
||||
fix_tracking: FLEX-WP-0015-T02
|
||||
# The three fields below are risk-nexus's, not the reporter's. Left unset
|
||||
# deliberately: the reporter says what is true, this repo says how bad it is
|
||||
# and who hears about it (INTENT, "What it does not own").
|
||||
severity: unset
|
||||
disclosure: unset
|
||||
escalation: unset
|
||||
# Graded by risk-nexus 2026-08-19 — docs/rulings/2026-08-19-first-grading.md
|
||||
severity: critical
|
||||
severity_at_production: critical
|
||||
impact: I4
|
||||
likelihood: L3
|
||||
fidelity_modifier: false
|
||||
production_rescore: false
|
||||
disclosure: embargoed
|
||||
embargo_condition: "FLEX-WP-0015-T02 ships to production"
|
||||
embargo_since: "2026-08-19"
|
||||
embargo_review: "2026-08-26"
|
||||
escalation: required
|
||||
escalation_trigger: 1
|
||||
escalation_status: pending-operator
|
||||
last_reviewed: "2026-08-19"
|
||||
review_by: "2026-08-26"
|
||||
graded_by: risk-nexus
|
||||
ruling: RISK-RULING-2026-08-19
|
||||
---
|
||||
|
||||
# RISK-F-0001 — flex-auth authenticates no caller on the decision surface
|
||||
|
|
@ -101,3 +113,43 @@ All four were found the same way, by repos reading their own code against a
|
|||
ladder, within a day of each other. That is a fact about the estate's
|
||||
observability worth carrying into triage: **four live defects, none found by
|
||||
monitoring.**
|
||||
|
||||
## Register ruling — 2026-08-19
|
||||
|
||||
`critical` (`I4` × `L3`, no fidelity modifier), embargoed until
|
||||
`FLEX-WP-0015-T02` ships, **escalated** to the operator on trigger 1.
|
||||
|
||||
The question this finding put — whether governing access to tenant data counts
|
||||
as exposing it — is answered **yes**. An authorization oracle that can be
|
||||
forged is not one step removed from the data; it is the step.
|
||||
|
||||
Impact is `I4` because a forged allow does not stay here: it is the answer
|
||||
other services act on, and `tenant-engine` has stated what direct authority
|
||||
over its rows would mean. Likelihood is `L3` — the normal working set, inside
|
||||
the cluster, no additional step — and the register follows the reporter in
|
||||
neither assuming a default-deny NetworkPolicy nor assuming its absence.
|
||||
|
||||
No fidelity modifier: this endpoint answers honestly about a caller it never
|
||||
checked. The false-record hazard lives in `RISK-F-0002`'s constraint, where a
|
||||
consumer of this endpoint would begin signing records asserting an
|
||||
authorization that was never made. **That constraint binds this finding's
|
||||
remediation**: `ops-warden`'s `policy.enabled` must not be turned on until
|
||||
`/v1/check` authenticates its callers, and the ordering is
|
||||
|
||||
```
|
||||
flex-auth warn-only -> ops-warden gate presents its SA token -> logs clean
|
||||
-> flex-auth fail-closed -> ops-warden policy.enabled: true
|
||||
```
|
||||
|
||||
Nothing further is asked of `flex-auth` beyond what they have committed to,
|
||||
except one fact only they can supply: **is there a default-deny NetworkPolicy
|
||||
in front of the Service?** It is the single fact that would most change this
|
||||
grade, and it is the first question at review.
|
||||
|
||||
Reasoning: `docs/rulings/2026-08-19-first-grading.md`.
|
||||
|
||||
## Reviews
|
||||
|
||||
- **2026-08-19** — graded. Next review 2026-08-26 (`critical` → 7 days).
|
||||
Open at review: the NetworkPolicy question; whether `FLEX-WP-0015-T02` has
|
||||
moved; whether the embargo still holds.
|
||||
|
|
|
|||
|
|
@ -12,10 +12,27 @@ environment: production
|
|||
fix_owner: ops-warden
|
||||
fix_tracking: WARDEN-WP-0007 (gate shipped, disabled) / FLEX-WP-0007 (runtime deploy)
|
||||
related: [RISK-F-0001]
|
||||
# risk-nexus's to set, not the reporter's (INTENT, "What it does not own").
|
||||
severity: unset
|
||||
disclosure: unset
|
||||
escalation: unset
|
||||
# Graded by risk-nexus 2026-08-19 — docs/rulings/2026-08-19-first-grading.md
|
||||
severity: medium
|
||||
severity_at_production: medium
|
||||
impact: I3
|
||||
likelihood: L2
|
||||
fidelity_modifier: false
|
||||
production_rescore: false
|
||||
constraint_on: RISK-F-0001
|
||||
constraint_severity: high
|
||||
constraint: "policy.enabled must not be turned on while flex-auth /v1/check answers unauthenticated callers — the gate would sign a false attestation"
|
||||
disclosure: embargoed
|
||||
embargo_condition: "FLEX-WP-0015-T02 shipped and ops-warden policy.enabled true in production"
|
||||
embargo_since: "2026-08-19"
|
||||
embargo_review: "2026-11-17"
|
||||
escalation: required
|
||||
escalation_trigger: 6
|
||||
escalation_status: pending-operator
|
||||
last_reviewed: "2026-08-19"
|
||||
review_by: "2026-11-17"
|
||||
graded_by: risk-nexus
|
||||
ruling: RISK-RULING-2026-08-19
|
||||
---
|
||||
|
||||
# RISK-F-0002 — the SSH signing gate is off, and turning it on is now the more dangerous move
|
||||
|
|
@ -120,3 +137,59 @@ the risk of a decision we had already made and filed away as merely blocked.
|
|||
The estate's habit of recording a blocker once and not revisiting it is the
|
||||
thing to watch. A blocker is a claim about the world at a date. `RISK-F-0001`
|
||||
invalidated this one in a day, and nothing would have re-checked it.
|
||||
|
||||
## Register ruling — 2026-08-19
|
||||
|
||||
All three questions this finding put are answered.
|
||||
|
||||
**1. Severity — `medium` today, with a `high` constraint.** The headline
|
||||
scores the state of the world now: the gate is off, a missing control honestly
|
||||
represented. `I3` (SSH certificates into production hosts cross a trust
|
||||
boundary) × `L2` (scoped `VAULT_TOKEN`, actor in `inventory.yaml`, TTLs, every
|
||||
issuance logged — thin, but not nothing).
|
||||
|
||||
The argument that the two states are not equally bad is **accepted in full**,
|
||||
and it is now written into the scale as the fidelity modifier: a control that
|
||||
lies is one impact band worse than the same control absent
|
||||
(`docs/method/severity.md`). It is recorded as a constraint rather than the
|
||||
headline because the register describes the estate as it is, and the dangerous
|
||||
state does not exist yet:
|
||||
|
||||
> **Constraint, severity `high`.** Enabling `policy.enabled` while `/v1/check`
|
||||
> answers unauthenticated callers converts an absent control into a false
|
||||
> attestation — a genuine `allow` obtained by anyone with ClusterIP reach, and
|
||||
> a `policy_decision_id` in the signature log asserting the issuance was
|
||||
> authorized. `I3 + fidelity → I4`, `L2` → `high`.
|
||||
|
||||
The constraint is attached to `RISK-F-0001`'s remediation and recorded on both
|
||||
findings. A reader must not take away `medium` and miss it.
|
||||
|
||||
**2. Peer, not consequence.** Filed as `ops-warden` filed it. The fix owner
|
||||
differs and the "off" state has standing regardless of how `RISK-F-0001`
|
||||
resolves — if that finding were withdrawn tomorrow, production signing would
|
||||
still carry no per-request judgement. What is not independent is the ordering,
|
||||
and that travels as a constraint rather than by collapsing the two records.
|
||||
|
||||
**3. Escalation — the register disagrees, narrowly.** On triggers 1-5 it
|
||||
agrees with `ops-warden`: no real tenant data, no obligation, no spend, no
|
||||
ownership dispute, no stall. But "it is written down in both repos" is the one
|
||||
argument the register cannot accept here, because this finding is itself the
|
||||
evidence against it: its own blocker was written down, filed, and invalidated
|
||||
in a day with nothing re-checking it.
|
||||
|
||||
So trigger 6 — ordering hazard producing a false attestation — fires **once**.
|
||||
One acknowledgement that the operator holds the ordering, then the register
|
||||
carries it. Not a standing supervision request. That trigger exists in
|
||||
`docs/method/escalation.md` because of this finding.
|
||||
|
||||
**The general point is adopted.** "A blocker is a claim about the world at a
|
||||
date" is now question 2 of every review in `docs/method/review.md`, and this
|
||||
finding is cited there as the case that bought it.
|
||||
|
||||
Reasoning: `docs/rulings/2026-08-19-first-grading.md`.
|
||||
|
||||
## Reviews
|
||||
|
||||
- **2026-08-19** — graded. Next review 2026-11-17 (`medium` → 90 days).
|
||||
Open at review: has `FLEX-WP-0007` or `WARDEN-WP-0007` moved; is the stated
|
||||
blocker still true; does the ordering constraint still hold.
|
||||
|
|
|
|||
|
|
@ -11,10 +11,22 @@ system: ops-warden
|
|||
environment: production
|
||||
fix_owner: ops-warden
|
||||
fix_tracking: WARDEN-WP-0032-T05
|
||||
# risk-nexus's fields, deliberately left unset by the reporter.
|
||||
severity: unset
|
||||
disclosure: unset
|
||||
escalation: unset
|
||||
# Graded by risk-nexus 2026-08-19 — docs/rulings/2026-08-19-first-grading.md
|
||||
severity: high
|
||||
severity_at_production: high
|
||||
impact: I4
|
||||
likelihood: L2
|
||||
fidelity_modifier: false
|
||||
production_rescore: false
|
||||
disclosure: embargoed
|
||||
embargo_condition: "the five exec_capable lanes graded under WARDEN-WP-0032-T05"
|
||||
embargo_since: "2026-08-19"
|
||||
embargo_review: "2026-09-18"
|
||||
escalation: none
|
||||
last_reviewed: "2026-08-19"
|
||||
review_by: "2026-09-18"
|
||||
graded_by: risk-nexus
|
||||
ruling: RISK-RULING-2026-08-19
|
||||
---
|
||||
|
||||
# RISK-F-0003 — the agent read-boundary has a fourteen-lane blind spot
|
||||
|
|
@ -122,3 +134,50 @@ OpenBao and owns three of the five lanes above, is `category: tooling`.
|
|||
- `wiki/playbooks/agent-read-boundary.md`
|
||||
- `RISK-F-0002` — ops-warden sign ungated (same system, different control)
|
||||
- `zone-engine` `ZONE-WP-0001-T02` — the work that surfaced this
|
||||
|
||||
## Register ruling — 2026-08-19
|
||||
|
||||
`high` (`I4` × `L2`), embargoed until the five `exec_capable` lanes are graded
|
||||
under `WARDEN-WP-0032-T05`, no escalation.
|
||||
|
||||
**Graded on its worst lane.** Four of the five are `I3`-shaped — a publish
|
||||
token, an OIDC login, an ingestion key, a hub write token, each bad inside its
|
||||
own system. `openbao-api-key` is not: it reaches the store the estate's other
|
||||
secrets live in, and a disclosure there propagates past every boundary the
|
||||
others respect.
|
||||
|
||||
**Likelihood `L2`** — an agent session with `WARDEN_AGENT_ID` is a foothold the
|
||||
estate does grant. The register holds the line this finding drew: the exposed
|
||||
population is not established, so it is neither assumed zero nor assumed large,
|
||||
and the unverified OpenBao `agent-high-risk-boundary` coverage is a review
|
||||
question rather than an assumption in either direction. Confirming that
|
||||
coverage for these five paths would lower the grade.
|
||||
|
||||
**No fidelity modifier, and it was close.** `ADR-0004` reads categorical while
|
||||
the implementation is opt-in, so the documentation asserts a boundary that does
|
||||
not exist for 14 lanes. The modifier is scoped to attestations a system
|
||||
produces at runtime, and documentation drift is not that. But if any compliance
|
||||
evidence anywhere cites `ADR-0004` as implemented, that *is* a false
|
||||
attestation and a separate finding. This register has not looked, and says so
|
||||
rather than assuming.
|
||||
|
||||
**No escalation.** Known, owned, tracked (`WARDEN-WP-0032-T05` for the five
|
||||
lanes, `WARDEN-WP-0032-T06` for the structural fix), and moving. Severity alone
|
||||
deliberately does not reach the operator.
|
||||
|
||||
**On the maturity-context direction**, which is the operator's and not
|
||||
re-decided here: the note that `.repo-classification.yaml` `category` cannot
|
||||
carry the signal is confirmed from this repo's own registration work today.
|
||||
`railiance-platform` runs production OpenBao and owns three of the five lanes
|
||||
while classified `tooling`; `risk-nexus` classified itself with the same
|
||||
vocabulary an hour ago. The maturity ladder needs its own field, and that is
|
||||
`ZONE-WP-0001-T03`'s to build.
|
||||
|
||||
Reasoning: `docs/rulings/2026-08-19-first-grading.md`.
|
||||
|
||||
## Reviews
|
||||
|
||||
- **2026-08-19** — graded. Next review 2026-09-18 (`high` → 30 days).
|
||||
Open at review: OpenBao policy coverage of the five paths; whether
|
||||
`WARDEN-WP-0032-T05` has landed; whether any compliance evidence cites
|
||||
`ADR-0004` as implemented.
|
||||
|
|
|
|||
BIN
tools/__pycache__/register_lib.cpython-312.pyc
Normal file
BIN
tools/__pycache__/register_lib.cpython-312.pyc
Normal file
Binary file not shown.
96
tools/register_check.py
Normal file
96
tools/register_check.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Report what the register is not saying out loud.
|
||||
|
||||
RISK-WP-0001-T08. Report only: it writes nothing, changes no field,
|
||||
escalates nothing on its own, and blocks nothing. A human or the custodian
|
||||
acts on the output. Exit code is 0 unless a finding cannot be read.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
|
||||
import register_lib as lib
|
||||
|
||||
TODAY = dt.date.today()
|
||||
|
||||
|
||||
def date(value):
|
||||
if isinstance(value, dt.date):
|
||||
return value
|
||||
if isinstance(value, str) and value not in ("", "unset"):
|
||||
return dt.date.fromisoformat(value)
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
fs = [f for f in lib.findings() if f.get("status") == "open"]
|
||||
lines: list[str] = []
|
||||
|
||||
def section(title: str, rows: list[str], quiet: str) -> None:
|
||||
lines.append(f"{title}:")
|
||||
if rows:
|
||||
lines.extend(f" {r}" for r in rows)
|
||||
else:
|
||||
lines.append(f" {quiet}")
|
||||
lines.append("")
|
||||
|
||||
ungraded = [
|
||||
f"{f['id']} — {', '.join(k for k in lib.GRADED_FIELDS if str(f.get(k, 'unset')) == 'unset')}"
|
||||
for f in fs
|
||||
if any(str(f.get(k, "unset")) == "unset" for k in lib.GRADED_FIELDS)
|
||||
]
|
||||
section("Ungraded", ungraded, "none — every open finding carries a grade")
|
||||
|
||||
overdue = []
|
||||
for f in fs:
|
||||
due = date(f.get("review_by"))
|
||||
if due and TODAY > due:
|
||||
overdue.append(f"{f['id']} — review was due {due} ({(TODAY - due).days}d ago)")
|
||||
section("Overdue review", overdue, "none")
|
||||
|
||||
stale = []
|
||||
for f in fs:
|
||||
seen = date(f.get("last_reviewed"))
|
||||
interval = lib.REVIEW_INTERVAL_DAYS.get(f.get("severity"))
|
||||
if not seen or not interval or f.get("severity") == "low":
|
||||
continue
|
||||
limit = seen + dt.timedelta(days=interval * lib.STALE_MULTIPLIER)
|
||||
if TODAY > limit:
|
||||
stale.append(
|
||||
f"{f['id']} ({f.get('severity')}) — untouched since {seen}; "
|
||||
f"escalation trigger 5 fires (limit was {limit})"
|
||||
)
|
||||
section("Stalled — escalation trigger 5", stale, "none")
|
||||
|
||||
embargo = []
|
||||
for f in fs:
|
||||
if f.get("disclosure") != "embargoed":
|
||||
continue
|
||||
due = date(f.get("embargo_review"))
|
||||
if due and TODAY > due:
|
||||
embargo.append(
|
||||
f"{f['id']} — embargo not re-decided since {due}; lifts when: {f.get('embargo_condition')}"
|
||||
)
|
||||
section("Embargo overdue for re-decision", embargo, "none")
|
||||
|
||||
esc = [
|
||||
f"{f['id']} — trigger {f.get('escalation_trigger')}, {f.get('escalation_status')}"
|
||||
for f in fs
|
||||
if f.get("escalation") == "required" and f.get("escalation_status") != "answered"
|
||||
]
|
||||
section("Escalations awaiting the operator", esc, "none")
|
||||
|
||||
rescore = [
|
||||
f"{f['id']} — {f.get('severity')} now, {f.get('severity_at_production')} at production"
|
||||
for f in fs
|
||||
if f.get("production_rescore")
|
||||
]
|
||||
section("Owed at the production transition", rescore, "none — no finding is graded lower for build mode")
|
||||
|
||||
print(f"Register check — {TODAY}\n{len(fs)} open finding(s)\n")
|
||||
print("\n".join(lines).rstrip())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
132
tools/register_index.py
Normal file
132
tools/register_index.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate REGISTER.md from the finding front-matter.
|
||||
|
||||
RISK-WP-0001-T05. The index is generated because a hand-maintained one
|
||||
drifts and then lies. Edit the findings, re-run `make register`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import sys
|
||||
|
||||
import register_lib as lib
|
||||
|
||||
TODAY = dt.date.today()
|
||||
|
||||
|
||||
def date(value) -> dt.date | None:
|
||||
if isinstance(value, dt.date):
|
||||
return value
|
||||
if isinstance(value, str) and value not in ("", "unset"):
|
||||
return dt.date.fromisoformat(value)
|
||||
return None
|
||||
|
||||
|
||||
def review_cell(f: dict) -> str:
|
||||
due = date(f.get("review_by"))
|
||||
if due is None:
|
||||
return "—"
|
||||
late = (TODAY - due).days
|
||||
if late > 0:
|
||||
return f"**{due} (overdue {late}d)**"
|
||||
return str(due)
|
||||
|
||||
|
||||
def sev_cell(f: dict) -> str:
|
||||
sev = f.get("severity", "unset")
|
||||
return f"**{sev}**" if sev in ("critical", "high") else sev
|
||||
|
||||
|
||||
def esc_cell(f: dict) -> str:
|
||||
esc = f.get("escalation", "unset")
|
||||
if esc in ("none", "unset"):
|
||||
return esc
|
||||
trigger = f.get("escalation_trigger")
|
||||
status = f.get("escalation_status", "")
|
||||
tail = f" (t{trigger}{', ' + status if status else ''})" if trigger else ""
|
||||
return f"**{esc}**{tail}"
|
||||
|
||||
|
||||
def render() -> str:
|
||||
fs = lib.findings()
|
||||
ns = lib.notes()
|
||||
open_fs = [f for f in fs if f.get("status") == "open"]
|
||||
|
||||
out = [
|
||||
"# Register",
|
||||
"",
|
||||
f"Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. Last built {TODAY}.",
|
||||
"",
|
||||
f"{len(open_fs)} open of {len(fs)} findings; {len(ns)} notes below the floor.",
|
||||
"",
|
||||
"## Findings",
|
||||
"",
|
||||
"| ID | Finding | System | Severity | Disclosure | Escalation | Fix owner | Status | Review by |",
|
||||
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
||||
]
|
||||
for f in fs:
|
||||
out.append(
|
||||
"| [{id}]({path}) | {title} | {system} | {sev} | {disc} | {esc} | {owner} | {status} | {review} |".format(
|
||||
id=f["id"],
|
||||
path=f["_path"].relative_to(lib.REPO),
|
||||
title=str(f.get("title", "")).strip('"'),
|
||||
system=f.get("system", "—"),
|
||||
sev=sev_cell(f),
|
||||
disc=f.get("disclosure", "unset"),
|
||||
esc=esc_cell(f),
|
||||
owner=f.get("fix_owner", "—"),
|
||||
status=f.get("status", "—"),
|
||||
review=review_cell(f),
|
||||
)
|
||||
)
|
||||
|
||||
constraints = [f for f in fs if f.get("constraint")]
|
||||
if constraints:
|
||||
out += ["", "## Constraints", "",
|
||||
"Hazards created by acting in the wrong order. Each binds another finding's remediation.",
|
||||
"", "| From | Binds | Severity | Constraint |", "| --- | --- | --- | --- |"]
|
||||
for f in constraints:
|
||||
out.append("| {id} | {on} | **{sev}** | {text} |".format(
|
||||
id=f["id"], on=f.get("constraint_on", "—"),
|
||||
sev=f.get("constraint_severity", "—"), text=f["constraint"]))
|
||||
|
||||
embargoed = [f for f in fs if f.get("disclosure") == "embargoed"]
|
||||
if embargoed:
|
||||
out += ["", "## Embargoes", "",
|
||||
"Held from publication with a stated condition. A hold with no moving condition is a stall.",
|
||||
"", "| Finding | Since | Lifts when | Re-decided |", "| --- | --- | --- | --- |"]
|
||||
for f in embargoed:
|
||||
out.append("| {id} | {since} | {cond} | {rev} |".format(
|
||||
id=f["id"], since=f.get("embargo_since", "—"),
|
||||
cond=f.get("embargo_condition", "—"), rev=f.get("embargo_review", "—")))
|
||||
|
||||
if ns:
|
||||
out += ["", "## Notes (below the floor)", "",
|
||||
"Seen, deliberately not findings. Not graded, not reviewed, not published.",
|
||||
"", "| ID | Note | Why below the floor |", "| --- | --- | --- |"]
|
||||
for n in ns:
|
||||
out.append("| [{id}]({path}) | {title} | {why} |".format(
|
||||
id=n["id"], path=n["_path"].relative_to(lib.REPO),
|
||||
title=str(n.get("title", "")).strip('"'),
|
||||
why=str(n.get("floor_reason", "—")).strip('"')))
|
||||
|
||||
out += ["", "## How to read this", "",
|
||||
"Severity is `docs/method/severity.md`; disclosure `docs/method/disclosure.md`;",
|
||||
"escalation `docs/method/escalation.md`; review dates `docs/method/review.md`.",
|
||||
"A constraint may be graded higher than the finding that carries it — read both.",
|
||||
""]
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
text = render()
|
||||
target = lib.REPO / "REGISTER.md"
|
||||
if "--check" in sys.argv:
|
||||
current = target.read_text(encoding="utf-8") if target.exists() else ""
|
||||
if current != text:
|
||||
print("REGISTER.md is stale — run `make register`")
|
||||
raise SystemExit(1)
|
||||
print("REGISTER.md is current")
|
||||
else:
|
||||
target.write_text(text, encoding="utf-8")
|
||||
print(f"wrote {target.relative_to(lib.REPO)}")
|
||||
42
tools/register_lib.py
Normal file
42
tools/register_lib.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""Shared front-matter reading for the register tools.
|
||||
|
||||
The finding files are the source of truth. Nothing here writes to them.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import yaml
|
||||
|
||||
REPO = pathlib.Path(__file__).resolve().parent.parent
|
||||
FINDINGS = REPO / "findings"
|
||||
NOTES = REPO / "notes"
|
||||
|
||||
SEVERITIES = ["critical", "high", "medium", "low"]
|
||||
REVIEW_INTERVAL_DAYS = {"critical": 7, "high": 30, "medium": 90, "low": 180}
|
||||
STALE_MULTIPLIER = 2
|
||||
GRADED_FIELDS = ["severity", "disclosure", "escalation", "review_by", "last_reviewed"]
|
||||
|
||||
|
||||
def load(path: pathlib.Path) -> dict:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if not text.startswith("---\n"):
|
||||
raise ValueError(f"{path.name}: no front-matter")
|
||||
_, fm, _body = text.split("---\n", 2)
|
||||
data = yaml.safe_load(fm) or {}
|
||||
data["_path"] = path
|
||||
return data
|
||||
|
||||
|
||||
def findings() -> list[dict]:
|
||||
items = [load(p) for p in sorted(FINDINGS.glob("RISK-F-*.md"))]
|
||||
return sorted(items, key=lambda f: f["id"], reverse=True)
|
||||
|
||||
|
||||
def notes() -> list[dict]:
|
||||
if not NOTES.exists():
|
||||
return []
|
||||
return sorted((load(p) for p in NOTES.glob("RISK-N-*.md")), key=lambda n: n["id"], reverse=True)
|
||||
|
||||
|
||||
def sev_rank(sev: str) -> int:
|
||||
return SEVERITIES.index(sev) if sev in SEVERITIES else len(SEVERITIES)
|
||||
Loading…
Add table
Add a link
Reference in a new issue