RISK-F-0002: the SSH signing gate is off, and enabling it is now the unsafe move
Routed by ops-warden, which owns the defect and is the affected PEP. ops-warden ships a flex-auth pre-sign authorization gate and runs with policy.enabled: false in production (warden.production.example.yaml:22, policy.py:33). Every production warden sign proceeds with no authorization decision; possession of the scoped VAULT_TOKEN is the whole model. That was a known, boring availability blocker (FLEX-WP-0007) until yesterday. RISK-F-0001 changes its shape: the production config points the gate at flex-auth.flex-auth.svc.cluster.local:8080 — the exact ClusterIP surface that authenticates no caller. Enabling the gate before RISK-F-0001 is fixed would convert an absent control into a false attestation, writing a policy_decision_id into the signature log for issuances an attacker authorized by forging the oracle. An authorization check that can be forged is worse than none, because only one of the two lies in the record afterwards. severity, disclosure and escalation left unset — risk-nexus's to set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
38e083ed3d
commit
15b003471e
1 changed files with 122 additions and 0 deletions
122
findings/RISK-F-0002-ops-warden-sign-ungated.md
Normal file
122
findings/RISK-F-0002-ops-warden-sign-ungated.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
id: RISK-F-0002
|
||||
type: finding
|
||||
title: "ops-warden signs SSH certificates with no authorization decision, and its unblock is now unsafe"
|
||||
status: open
|
||||
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: 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
|
||||
---
|
||||
|
||||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue