123 lines
5.5 KiB
Markdown
123 lines
5.5 KiB
Markdown
|
|
---
|
||
|
|
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.
|