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:
| Constraint | Existing mechanism | |
|---|---|---|
| local-only, never network-reachable | satisfied, 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-boxed | the credential is permanent | ❌ |
| incapable of silent persistence | it is silent persistence — a durable admin account, created without any normal flow | ❌ |
| exercised by a drill | the 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:
- It adds a listener to the bootstrap root. Authority already binds 80, 443 and 8440 (the
TryAddStellaOpsLocalBindingfinding, D-AUTH5B-18). A “localhost-only” endpoint is oneASPNETCORE_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 (BypassNetworksmasking a scope-catalog lockout). - 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.
- 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:
- set
password_expires_aton the break-glass reset (trivial); - 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:
- converge a fresh database and confirm the admin can log in;
- manufacture the lockout by exhausting
failed_login_attemptsthrough the real/connect/tokenpath — not by writinglocked_untildirectly, or the drill proves nothing about the state operators actually hit; - confirm login now fails;
- run the recovery store against that database;
- confirm login succeeds, exactly one
authority.break_glass_eventsrow exists with the reason and actor, and the audit row is undeletable; - 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
- No new scope, client, or identity provider. Every one of those is a live auth surface on the estate’s bootstrap root; this family has already recorded what a scope-catalog lockout costs.
- No second recovery mechanism. Two break-glass paths means two audit stories and one of them will rot.
- No change to
--confirm-authority-access. It works and operators know it. - It does not retroactively audit past uses. There is no record to recover; the migration starts the ledger at zero and the runbook should say so rather than implying completeness.
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.
- 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.
- Doctor goes CRITICAL on an unreconciled break-glass use — YES. While a
break_glass_eventsrow 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. - 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
- The expiry is an options value with a 15-minute default and MUST be stamped into the audit event.
- The doctor check needs an explicit acknowledgement surface; Critical-until-acknowledged means the reconciliation path is part of the feature, not a follow-up.
- Tenant validation rejects non-structural tenants at the tool boundary with an actionable message — fail closed, and say which tenants are permitted.
- Two measurements from the design pass still stand and are worth re-stating here because they change what “done” means:
authority.users.password_expires_athas no reader anywhere insrc/(the schema advertises a control the product does not implement), and turning it on would lock nobody out today (0 of 80 users carry a non-null value).
6. Status of AUTH-11’s completion criteria
- [x] Mechanism designed with the security argument written down; owner sign-off. — SIGNED OFF 2026-08-14 (“ok, but document it”): the design and argument are this document, and §5 now records the three answers that were blocking code — 15 minutes configurable, doctor Critical until reconciled, structural tenants only. Implementation is UNBLOCKED as of this ruling.
- [ ] Recovery drill green on a scratch stack; runbook chapter written. — now the only open criterion, and it carries the owner’s documentation condition: the runbook chapter (drill spec in §3.4, ready to implement directly) plus the doctor check’s Critical-until-acknowledged semantics stated where operators meet them. Until both land, the sign-off is recorded but not discharged. The drill is specified in §3.4 in enough detail to implement directly once approved.
