Authority — break-glass operator access (AUTH-11 design)

Sprint: SPRINT_20260722_016 AUTH-11 (round-22, owner-approved R8). Status: DESIGN — awaiting owner sign-off. Nothing here is implemented. Author’s position in one line: a break-glass path already ships, it satisfies the hardest constraint better than anything we would build, and it fails three of the other four — so the work is to harden it, not to add a second one.


1. The finding that reframes the task

AUTH-11 was written as a greenfield design (“design + implement a documented, tested emergency access path”). It is not greenfield. Measured against src/ on 2026-08-11:

stella admin users password-set <username> --confirm-authority-access --password-stdin (src/Cli/StellaOps.Cli/Commands/Admin/AuthorityUserPasswordSetCommand.cs), run as the one-shot container devops/compose/docker-compose.authority-password-tool.yml, opens Authority’s database directly and executes:

UPDATE authority.users
   SET password_hash = @password_hash,
       password_salt = '',
       password_algorithm = @password_algorithm,
       failed_login_attempts = 0,
       locked_until = NULL,
       ...
 WHERE tenant_id = @tenant_id AND username = @username

That is a break-glass path in everything but name: it bypasses Authority entirely, clears the lockout, and mints a working administrator credential on an estate where nothing else works.

Scored against AUTH-11’s own five constraints:

ConstraintExisting mechanism
local-only, never network-reachablesatisfied, and maximally — there is no listener at all; the operator must already have the database credential and host access
loudly audited (every use emits an evidence/timeline event)nothing. The only trace is updated_by written into the row it overwrites — which the next legitimate password change erases. grep -i audit over the command file: zero hits
time-boxedthe credential is permanent
incapable of silent persistenceit is silent persistence — a durable admin account, created without any normal flow
exercised by a drillthe seven tests are CLI parse/validation tests against a fake store; none touches a database, and none simulates a lockout

So the honest statement of the current posture is not “Authority has no break-glass path”. It is “Authority has an unaudited, unbounded, undrilled one.” That is worse than having none, because it is already in the runbooks and nobody is watching it.

2. Why hardening beats building

The obvious shape — an explicit bootstrap-mode flag plus a localhost/socket listener, which the task text offers as one option — is strictly worse on this host, and the reason is specific rather than aesthetic:

  1. It adds a listener to the bootstrap root. Authority already binds 80, 443 and 8440 (the TryAddStellaOpsLocalBinding finding, D-AUTH5B-18). A “localhost-only” endpoint is one ASPNETCORE_URLS, one reverse-proxy rule or one container-network misconfiguration away from being reachable, and this estate has already shipped exactly that class of mistake (BypassNetworks masking a scope-catalog lockout).
  2. It requires Authority to be running. The scenario AUTH-11 exists for is “Authority itself is broken”. A recovery path hosted inside the thing that is broken is not a recovery path. The deployment freeze this family lived through (2026-07-29 → 2026-08-10, Authority crash-looping at exit 139 on a migration checksum mismatch) is the worked example: no in-process endpoint would have answered.
  3. The existing path already has the property both of those problems are about. Its authentication factor is possession of the database credential and shell on the host — the strongest local-only proof available, and one an attacker who has it does not need break-glass to abuse.

3. Proposed design

Four changes, in dependency order. All are to the CLI command and Authority’s own schema; none adds a listener, a scope, a client or an identity provider.

3.1 A local, durable audit record — in the SAME transaction

New forward migration on Authority’s live chain (per AUTH-3/D-AUTH3-3: appended, every object schema-qualified, checked for name collisions against the existing chain first):

authority.break_glass_events
  id              uuid    primary key
  occurred_at     timestamptz not null
  tenant_id       text    not null
  subject         text    not null   -- the account whose credential was reset
  actor           text    not null   -- OS user + host, captured by the CLI
  reason          text    not null   -- REQUIRED free text, see 3.3
  tool_version    text    not null
  reconciled_at   timestamptz null   -- set when the timeline emitter drains it

P13 retention class: evidence (append-only; no UPDATE except reconciled_at, no DELETE — the same BEFORE DELETE guard shape AUTH-3 landed for operator signing keys).

The insert and the UPDATE authority.users run in one transaction and the audit insert comes first. If the audit write fails, the password change does not happen. That ordering is the whole control: an unauditable break-glass must be an unusable break-glass, otherwise the audit is advisory and the first person who needs it to be advisory will make it so.

Why a local table rather than the existing emitter. Authority’s audit path is AddAuditEmission → an HTTP POST to the Timeline service (IAuditEventEmitter). Break-glass runs exactly when the platform is down, so a network sink is unavailable in the case that matters. The row is written locally and reconciled to the timeline afterwards by a small drain (the same outbox shape AUTH-4 already built for the tenants catalog — this is not a new mechanism, and eventing is already homed in stellaops_authority).

3.2 The credential is entry, not an account

authority.users already carries password_expires_at(verified live in stellaops_authority, declared in 001_v1_authority_baseline.sql:106). The reset sets it to occurred_at + 15 minutes rather than leaving it null. The operator gets in and must complete a normal password change through Authority; walking away leaves an expired credential rather than a permanent admin.

Measured, and it changes the cost of this item: password_expires_at HAS NO READER. A tree-wide grep for PasswordExpiresAt across src/ returns the EF entity, the DbContext mapping, the compiled model and three migration files — and nothing else. No authentication path evaluates it. It is a column, an ORM property, and a lie: an operator looking at the schema would reasonably conclude Authority enforces password expiry, and it does not.

So this item is not “set a column that already works”. It is two pieces of work, and the second is the real one:

  1. set password_expires_at on the break-glass reset (trivial);
  2. make the password grant path honour it, red-proven — a test that sets the column in the past and asserts the grant is refused, verified to fail before the enforcement lands.

Piece 2 has blast radius beyond break-glass: switching on an expiry check that has never run could refuse existing accounts if any row already carries a stale value. Measured read-only on this estate 2026-08-11 and the news is good: of 80 rows in authority.users, 0 carry a non-null password_expires_at and therefore 0 are already expired, so turning the check on locks nobody out here. Re-measure per estate before shipping — the query is SELECT count(*) FILTER (WHERE password_expires_at < now()) FROM authority.users. Note that the unread column is itself worth reporting to the owner independently of AUTH-11: it is a security-relevant control the schema advertises and the product does not implement.

3.3 A required reason, captured at the tool

--reason "<text>" becomes mandatory alongside --confirm-authority-access. It costs the operator five seconds during an incident and it is the difference between an audit row that answers “who and when” and one that answers “why” — which is the only question a post-incident review actually asks.

3.4 The drill

A fresh-DB integration test, in the AUTH-7 DoctorAuthorityFactory style (real host, real PostgreSQL), that runs the full loop and is therefore a test of the scenario, not of the command:

  1. converge a fresh database and confirm the admin can log in;
  2. manufacture the lockout by exhausting failed_login_attempts through the real /connect/token path — not by writing locked_until directly, or the drill proves nothing about the state operators actually hit;
  3. confirm login now fails;
  4. run the recovery store against that database;
  5. confirm login succeeds, exactly one authority.break_glass_events row exists with the reason and actor, and the audit row is undeletable;
  6. advance the clock past the expiry and confirm the recovered credential stops working.

Step 6 is the one that will be tempting to drop. It is the only step that tests the constraint the current mechanism most clearly violates.

4. What this design deliberately does NOT do

5. RULED by the owner (2026-08-14) — sign-off recorded

Owner ruling, verbatim: “ok, but document it” — the three questions below were signed off AS RECOMMENDED, with documentation made an explicit condition of the sign-off. AUTH-11’s criterion 1 is therefore RULED; it stays unticked only until the runbook chapter and the doctor-check semantics land (criterion 2’s drill), because the owner’s condition is part of the sign-off, not separate from it.

  1. Expiry: 15 minutes, CONFIGURABLE. Long enough to change a password through a UI on a degraded estate; short enough that an abandoned break-glass is not a standing account. Configurable because the air-gapped case is real: an operator with no console may need longer, and that must not require a code change. The configured value is recorded in the audit event, so a stretched window is visible after the fact rather than invisible.
  2. Doctor goes CRITICAL on an unreconciled break-glass use — YES. While a break_glass_events row is unacknowledged, the doctor check reports Critical. This deliberately promotes an operational-hygiene issue to a health signal, and that is the point: “someone used break-glass and never told anyone” becomes a visible fault instead of folklore. The check clears on explicit reconciliation, never on a timer — a Critical that ages out would train operators to wait rather than reconcile.
  3. Tenant scope: STRUCTURAL TENANTS ONLY (installation / default). Break-glass is an installation-recovery mechanism, not a per-tenant convenience. Restricting it shrinks the blast radius of the estate’s most privileged path, and a per-tenant emergency is a support question answered through the normal admin surface, not through the auth root.

Implementation consequences of these answers

6. Status of AUTH-11’s completion criteria