Break-Glass Access
Emergency local-admin credential that activates only when LDAP is the active backend and the LDAP probe is currently failing. Designed for the case where the directory goes down and the only operator account is LDAP-resolved.
Configure
auth:
backend: ldap
ldap:
# ... normal LDAP config ...
breakGlass:
username: emergency-admin
passwordHash: "${HORIZON_BREAK_GLASS_HASH}"
roles: [admin]
Fields
| Field | Type | Default | Required | Notes |
|---|---|---|---|---|
breakGlass.username |
string (min 1) | — | yes (if block present) | Break-glass login name. Choose something not in the directory. |
breakGlass.passwordHash |
string (min 1) | — | yes (if block present) | Argon2id hash. Generate via pnpm --filter bff cli:hash. |
breakGlass.roles |
string[] | ['admin'] |
no | Roles granted during a break-glass session. Defaults to admin because the purpose is recovery. |
The block is optional — leave it out (or commented) to disable break-glass entirely.
Activation conditions
Break-glass is honored at login only when both are true:
auth.backend: ldap(the block is unused in local mode — a startup warning is logged if both are present).ldapHealth.isUnhealthy()returns true at the moment of login. The probe runs continuously; “unhealthy” means the last probe (TCP / bind / search) failed.
When activated:
- The login form accepts
breakGlass.username+ the password matchingbreakGlass.passwordHash. - The created session carries
breakGlass.roles. - An audit event with
outcome: break-glassis written. - A WARN log line is emitted:
auth: break-glass login granted (LDAP unhealthy).
When LDAP is healthy again, the break-glass username is rejected at login — even if you type the right password. The session that was already opened during the outage remains valid until TTL or explicit logout.
Verification
apps/bff/src/user/break-glass.ts:
- Same Argon2id verifier as the local backend.
- Timing-safe — a wrong username still incurs the argon2 cost (verify against a dummy hash) to prevent leaking which break-glass username is configured via timing.
Audit
Every successful break-glass login is recorded twice:
-
Audit log (
horizon-audit.jsonl):{ "ts": "2026-05-18T14:29:33.456Z", "actor": "emergency-admin", "action": "auth.login.break-glass", "outcome": "break-glass", "fromIp": "192.0.2.10", "sessionId": "...", "details": { "backend": "ldap" } } -
Application log at WARN level.
This double-logging is deliberate: the audit log is for compliance / forensics; the WARN log is for noticing in real time. Wire your log alerting to surface auth.login.break-glass events.
Operational guidance
- Hash, never plaintext. Never put the bare password in
horizon.yaml. - Use env-var interpolation for the hash:
passwordHash: "${HORIZON_BREAK_GLASS_HASH}". Keeps the literal hash out of files that may end up in version control or backup tarballs. - Test the path before you need it. Block LDAP at the network level (firewall rule, mock unreachable), confirm the login form accepts break-glass, then restore LDAP.
- Rotate the break-glass password on a schedule (quarterly is a common cadence). After rotation, verify the new hash loads via the admin Auth Status page.
- Limit the role list. The default
[admin]is appropriate for genuine emergencies, but consider[operator]if your team’s break-glass needs do not include user / role management. - Alert on use. Any
auth.login.break-glassevent in production should page the on-call channel — break-glass is for outages, and an unexpected use is either someone testing in prod (still worth knowing) or a misuse.
What break-glass cannot do
- It does not work when
backend: local— local mode has no health failure mode, so the trigger condition cannot be met. - It does not bypass RBAC. The session has whatever roles you grant via
breakGlass.roles; if you set[viewer], the session can only read. - It does not persist beyond the LDAP outage’s session — once LDAP is healthy again, no new break-glass logins succeed. Existing sessions live until TTL.
Common mistakes
breakGlassblock configured butbackend: local. Block is ignored; warning at startup. Either switch to LDAP backend or remove the block.- Same
usernameas an LDAP user. Works but is confusing for audit-log reading. Choose a name not in the directory (e.g.,emergency-admin,glass-break). - Hash stored in version-controlled file. Use
${ENV_VAR}interpolation instead.