risk-nexus/findings/RISK-F-0002-ops-warden-sign-ungated.md
tegwick 7f1424dbcf Sweep risk inbox and reconcile findings
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
2026-09-01 02:41:32 +02:00

407 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: RISK-F-0002
type: finding
title: "ops-warden signs SSH certificates with no authorization decision, and its unblock is now unsafe"
status: fixed
owner: risk-nexus
reported_by: ops-warden
reported_via: ops-warden
routed_by: ops-warden
date_reported: "2026-08-18"
system: ops-warden
environment: production
fix_owner: ops-warden
fix_tracking: ZONE-WP-0001 / WARDEN-WP-0032-T02 (finished 2026-08-22)
fix_tracking_superseded: "WARDEN-WP-0007 (archived 2026-07-08) / FLEX-WP-0007 (finished 2026-06-29)"
related: [RISK-F-0001]
# 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: lifted
constraint: "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."
disclosure: public
publication: pending-handover
publication_id: risk-f-0002-ops-warden-signing-authorization-gap
publication_path: "findings/ops-warden-signing-authorization-gap/v1/index.html"
publication_subtitle: "Production SSH signing once had no per-request authorization decision; the retired global switch is now replaced by an enforced zone-aware gate."
revision: "fixed-1"
last_reviewed: "2026-09-01"
review_interval: 6m
embargo_lifted: "2026-08-22 — zone-aware authorization replaced the retired global gate and passed the live authenticated-caller check"
embargo_was_since: "2026-08-19"
escalation: withdrawn
escalation_trigger: 6
escalation_status: withdrawn-hazard-window-closed
date_fixed: "2026-08-22"
last_checked: "2026-09-01T00:32:44Z"
next_check: "2026-09-01T00:32:44Z"
cadence: instant
clean_streak: 0
graded_by: risk-nexus
ruling: RISK-RULING-2026-08-19
checked_by: "codex/risk-nexus"
---
# RISK-F-0002 — the SSH signing gate is off, and turning it on is now the more dangerous move
## What is true
`ops-warden` issues short-lived SSH certificates for `adm`/`agt`/`atm` actors
against production OpenBao. It ships a pre-sign authorization gate that asks
`flex-auth` whether a given actor may sign. **That gate is disabled in
production and has been since it shipped.**
Verifiable in this estate's own files:
- `ops-warden/examples/warden.production.example.yaml:22``policy.enabled: false`
- `ops-warden/src/warden/policy.py:33``if not cfg.enabled: return None`
Every production `warden sign` therefore proceeds with no authorization
decision. What still constrains it: the actor must exist in `inventory.yaml`,
TTL is enforced per actor type (`adm` 48h, `agt` 24h, `atm` 8h), the caller must
hold a scoped `VAULT_TOKEN`, and every issuance is logged. What does not
constrain it: any per-request judgement about whether this actor should be
getting this certificate right now. Possession of the signing token is the
whole authorization model.
This is a known, deliberate state, not a discovery. It is filed because
"deliberate and known" is exactly the condition that stops being tracked once
the person who decided it stops looking — and because the reason it is still
true has just changed.
## Why this is filed now rather than in June
The gate's stated blocker has always been `FLEX-WP-0007`: `flex-auth` is not
deployed at a reachable URL, so enabling a `fail_closed: true` gate would break
all signing. That is a availability blocker, and it is boring.
`RISK-F-0001` changes the shape of it. The production config points the gate at:
```
flex_auth_url: http://flex-auth.flex-auth.svc.cluster.local:8080
```
That is precisely the ClusterIP surface `RISK-F-0001` reports as authenticating
no caller. So the sequencing is no longer "enable it once flex-auth is up". It
is:
> **Enabling the gate before `RISK-F-0001` is fixed would make things worse, not
> better.**
Today, an unauthorized signing attempt is unauthorized and unrecorded as such.
With the gate on and the oracle forgeable, an attacker who can reach the
ClusterIP can obtain a genuine `allow` and the signature log will carry a
`policy_decision_id` attesting that the issuance was authorized. The gate would
convert an absent control into a **false attestation** — and the audit trail,
which currently makes no claim, would start making one that is wrong.
An authorization check that can be forged is worse than no authorization check,
because only one of the two lies in the record afterwards.
## What ops-warden is doing about it
The mechanism recommendation for `RISK-F-0001` was answered on 2026-08-17
(`ops-warden/wiki/NetKingdomSecurityMap.md`, "Service-to-service caller
authentication"): projected, audience-scoped Kubernetes ServiceAccount tokens
verified by TokenReview, with the caller's asserted `system` bound to the
authenticated ServiceAccount. `ops-warden` implements the calling side and has
committed to a warn-only rollout on `flex-auth`'s schedule.
The ordering constraint is recorded there and restated here because it is a
risk statement, not an architecture one:
```
flex-auth warn-only -> ops-warden gate presents its SA token -> logs clean
-> flex-auth fail-closed -> ops-warden policy.enabled: true
```
`policy.enabled` must not flip anywhere while `/v1/check` answers
unauthenticated callers. Nothing else is asked of `flex-auth` by this record.
## What this repo is asked to decide
1. **Severity.** Note the two states are not equally bad and the register should
probably say which it is scoring: the gate being *off* (a missing control,
honestly represented) versus the gate being *turned on prematurely* (a
present control that lies). The second is the one worth a severity.
2. **Whether this is one finding or a dependency on `RISK-F-0001`.** It is
filed separately because the fix owner differs and because the "off" state
has its own standing regardless of how `RISK-F-0001` resolves. If this repo
would rather carry it as a consequence of `RISK-F-0001` than as a peer,
that is a reasonable call and `ops-warden` will not re-file it.
3. **Escalation.** `ops-warden` does not think this needs the operator: it is
known, owned, and its dangerous failure mode is a *sequencing* error that is
now written down in both repos. Recorded so the judgement is this repo's and
not assumed.
## The general point, offered for the register's design
`RISK-F-0001` observed that four live defects were found by repos reading their
own code against a ladder, none by monitoring. This finding is a fifth, found
the same way — by reading an answer we had just given and noticing it changed
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.
## Constraint lifted, escalation withdrawn — 2026-08-19 (same day)
`RISK-F-0001` was fixed hours after this register graded it: both `flex-auth`
production Deployments now enforce ADR-0004 TokenReview, with live
unbound-request probes returning 401.
**The attestation hazard is gone.** The constraint recorded this morning —
that enabling `policy.enabled` against a forgeable oracle would write a
`policy_decision_id` asserting an authorization nobody made — required
`/v1/check` to answer unauthenticated callers. It no longer does. Enabling the
gate is now an ordinary availability question for `ops-warden`: does the
calling side present its ServiceAccount token, and does signing survive a
`fail_closed: true`. That is `ops-warden`'s sequencing to run, not a risk
this register holds.
**The trigger-6 escalation is withdrawn.** The hazard window closed before the
operator was asked to hold the ordering. `ops-warden`'s own judgement that this
did not need the operator was, in the end, right — and the register still holds
that it was right for the wrong reason. The disagreement was never about the
severity of the hazard; it was about whether "written down in both repos"
keeps a thing held. It did not: the window closed by `flex-auth` shipping, not
by anyone re-reading the note.
**The finding stays open, unchanged at `medium`.** The gate is still off, and
`warden sign` still proceeds with no per-request judgement. Nothing about this
morning's news changes that.
**The general point earns another instance.** This finding argued that a
blocker is a claim about the world at a date, and that its own blocker had been
invalidated in a day with nothing re-checking it. That has now happened twice
to the same finding, in the same week, in the same direction. The second time,
the thing that had gone stale was this register's own grading, four hours old.
## Reviews
- **2026-08-19** — constraint lifted, escalation withdrawn, severity unchanged.
Open at review: has `ops-warden` presented SA tokens and enabled the gate;
is `FLEX-WP-0007` still the stated blocker or has it too gone stale.
## Check — 2026-08-20: a new availability fact, and it is the one that matters now
`RISK-V-0001` verified `flex-auth`'s NetworkPolicies against the live cluster
and found a third policy nobody had mentioned:
```
flex-auth-ops-warden, created 2026-08-19T12:47:18Z
policyTypes: [Ingress, Egress]
ingress: no rules at all
```
`Ingress` in `policyTypes` with zero rules means deny all ingress. On its face,
nothing reaches that pin.
**This is now the live question on this finding.** The attestation hazard
lifted when `flex-auth` shipped; what remained was whether enabling
`policy.enabled` breaks signing on availability grounds. If the pin
`ops-warden` calls admits no ingress, a `fail_closed: true` gate against it
fails closed — every `warden sign` stops.
The register is **not** concluding that, and said so to both owners: the policy
may be mid-rollout, it may not be the pin `ops-warden` targets, and another
policy may admit the traffic. Both were told on 2026-08-20, before either flips
a switch, which is the entire reason this finding is carried as a peer rather
than folded into `RISK-F-0001`.
The severity is unchanged at `medium`. What changed is the evidence, and it
changed in the direction of "do not enable this yet" for a completely different
reason than the one this finding was filed for. That is the second time in two
days that this finding's blocker turned out to be a claim about the world at a
date.
- **2026-08-20** — not clean: RISK-V-0001 found the flex-auth-ops-warden policy admits no ingress; the live question is now availability, not attestation. Cadence instant → instant; checked again immediately.
## Check — 2026-08-21: both tracked records were already closed when this was filed
`RISK-WP-0005-T01` made this register read the fix records it had been writing
down without reading. The first run answered a question nobody had asked:
| Tracked | State | Last changed |
| --- | --- | --- |
| `WARDEN-WP-0007` | **archived** | 2026-07-08 |
| `FLEX-WP-0007` | **finished** | 2026-06-29 |
This finding was filed on **2026-08-18** and states that the gate is "blocked
on `FLEX-WP-0007`". That workplan had read `finished` for seven weeks by then.
`flex-auth` restated the same blocker to this register on 2026-08-19 —
"`policy.enabled` remains off (`FLEX-WP-0007`)" — while their own authoritative
record said it was done.
**The register is not concluding that the blocker is gone.** Three readings
fit and only the owners can say which: the deployment finished and was later
reverted or never applied; "finished" means something narrower than the
sentence implies; or the blocker is genuinely stale and two repos have been
repeating it.
What can be said without either owner's help is that **the stated blocker has
not matched the stated record since June**, and nothing would have caught that
before this register started reading fix state.
### This is the third time on the same finding
`RISK-F-0002` argued, when it was filed, that a blocker is a claim about the
world at a date, and that its own had been invalidated in a day with nothing
re-checking it. Since then:
1. `RISK-F-0001` was fixed hours after this register graded the ordering hazard;
2. `RISK-V-0001` found a NetworkPolicy that may make enabling the gate fail
closed for an entirely different reason;
3. and now both tracked records turn out to have been closed before the finding
was written.
The finding was right about the estate and right about itself. Severity is
unchanged at `medium` — none of this changes what the gate does today — but
what is *blocking* it has been wrong in the record for two months.
### The ask, sharpened
Previously: probe whether the flex-auth pin admits ingress. Now, additionally:
**what actually blocks `policy.enabled` today?** If the answer is "nothing
except the ingress question", that is a much shorter path than the one both
repos have been describing.
- **2026-08-21** — not clean: Fix tracking read for the first time: WARDEN-WP-0007 archived 2026-07-08, FLEX-WP-0007 finished 2026-06-29 — both closed before the finding was filed. Cadence instant → instant; checked again immediately.
## Check — 2026-08-21: reading (c) confirmed — the blocker was stale, and the control is being retired
`ops-warden` answered: **nothing blocks `policy.enabled`. It is off by
decision, not by blocker.**
The gate is ready and verified — `flex-auth`'s `flex-auth-ops-warden` pin runs
`callerAuth.mode: enforce`, confirmed by both sides
(`decision:f3f7c88f9585582a`, anonymous `/v1/check` returns 401), and
`ops-warden` shipped the calling identity in `WARDEN-WP-0031`. `FLEX-WP-0007`
is finished, and so is `FLEX-WP-0016`, which `flex-auth` closed on 2026-08-19
recording explicitly that the flip it was named after is not coming.
**What replaced the blocker is a decision, not an obstacle.** `ops-warden`
`ADR-0006` (accepted): enforcement is zone-scoped, never a global flag.
`policy.enabled` is a single estate-wide boolean, and flipping it would enforce
uniformly across an estate under deep refactor. So it is not waiting to be
turned on — **it is being retired and replaced** by a per-zone control
(`ZONE-WP-0001`, with `WARDEN-WP-0032` consuming).
### The disposition: the framing is superseded, the risk is not
`ops-warden` proposed closed-by-supersession and left the grade here. Ruled:
- **The framing is superseded.** "Gate shipped, disabled, blocked on
`FLEX-WP-0007`" describes a world that ended in June. Recording it as blocked
remediation misdescribes it, and they are right about that.
- **The risk stands, unchanged at `medium`.** Every production `warden sign`
still proceeds with no per-request authorization decision. That is what the
finding is about, and no part of it improved — a control retired before its
replacement exists is still an absent control.
So the finding stays `open`, with a corrected fix path. `fix_tracking` becomes
`ZONE-WP-0001` / `WARDEN-WP-0032`. Nothing about the estate is worse today than
yesterday; what changed is that the register now knows why it is not better.
### The successor's blocker is real, and it is the same missing join
`ZONE-WP-0001-T03` cannot model stance because **26 of 27 credential lanes have
no identifiable workload** to attach a maturity ladder to. Escalated by
`ops-warden` to `repo-manager` and `net-kingdom` on 2026-08-20, unanswered.
That is `RISK-N-0004` — the zone lookup this register routed as a note — with a
number on it. Three findings already wanted that facility; now the estate's
per-zone enforcement is blocked on it too. **The note is one instance short of
being a finding**, and the missing instance is somebody stating that the join
cannot be built.
### Four stale blockers in twelve hours, self-reported
`ops-warden` volunteered three more, all found the same day: an OpenBao token
recorded as expired that was valid; a verification script recorded as "ready"
that had never been written; and a ten-day blocker against `secrets-engine`
answerable from that repo's source.
Their diagnosis is the one this register has been circling since `RISK-F-0001`:
**a blocker is written once, as prose, and then read as fact forever, because
nothing re-derives it and nothing expires it.** They have asked to adopt
whatever staleness convention this register settles rather than inventing a
second one. That is worth answering properly and is recorded as an open item
for the next round.
- **2026-08-21** — not clean: owner replied; see the dated check section Cadence instant → instant; checked again immediately.
## Closure — 2026-09-01: the successor control is live
`WARDEN-WP-0032-T02` is done. The production configuration rejects the retired
global `policy.enabled` and `policy.fail_closed` switches; the signing PEP now
uses compiled security-zone membership, the stance returned by `flex-auth`, and
a local per-zone failure mode. The owner re-ran the authenticated caller path
against the migrated operator configuration and received a live allow decision
with decision id `decision:f3f7c88f9585582a`.
Risk Nexus also ran the current policy, configuration, routing, and read-boundary
regressions: **137 passed**. The defect this finding carried—production signing
proceeding with no per-request authorization decision—is gone. The finding is
`fixed`; the embargo lifts and publication is handed over.
- **2026-09-01** — not clean: the zone-aware successor is implemented and live-verified; status fixed and embargo lifted. Cadence instant → instant; checked again immediately.