Articles · Operations & Administration

Break-glass & impersonation

Try out the running example

Every support team eventually does it: logs in as the customer. The only question is whether that happens through a shared password in a wiki (“the ops account”) or through a mechanism that is fenced, time-boxed, consent-aware and impossible to hide from the audit trail. Same for the 3 a.m. emergency: you will have a break-glass account — the design decision is whether its power is capped structurally or by good intentions.

Impersonation: a token that says who’s really there

Two registrations on top of any Sentinel host:

services.AddSentinelImpersonation();   // + o.RequireTargetConsent = true for consent mode
app.MapSentinelImpersonation();        // /sentinel-admin/impersonation/*

An admin holding sentinel:global:impersonate (or the org-scoped sentinel:org:impersonate — evaluated per-target-org, like every delegated-admin fence) starts one:

POST /sentinel-admin/impersonation/start
{ "targetUserId": "", "reason": "support case 42" }

The reason is mandatory — it goes on the audit chain. What comes back is an access token minted for the target, with three properties worth staring at:

  • act — the RFC 8693 actor claim: "act": { "sub": "<admin id>" }. The subject is the target; who is really behind the token is in the token itself, so every downstream audit line can name both.
  • Time-boxed — the impersonation expires (30 minutes by default), and the token’s exp is clipped to the box: it never outlives the impersonation record.
  • mfa: "none" and no session — an impersonation token can’t pass step-up checks as the user, and there is no refresh token. When the box closes, it’s over.

One active impersonation per actor; POST …/end closes it early. Self-impersonation is rejected, targets in other realms are 404 (never “denied” — that would leak existence), and only Active users can be impersonated.

The banner the UI can’t skip

GET /profile/me on an impersonation token answers as the target — plus a block that exists only on impersonated requests:

{
  "id": "…target…",
  "email": "taylor@clinic.sample",
  "impersonation": { "actorId": "…admin…", "expiresAt": "2026-08-14T12:30:00Z" }
}

Frontends render that as the “You are viewing as Taylor” banner. Because it rides the same profile call every app already makes, there is no separate is-impersonating endpoint to forget. The sample’s Impersonation_start_mints_act_token_and_the_profile_carries_the_banner test walks the whole loop over real HTTP.

Some products (and some regulators) require the user to approve being impersonated. Flip one flag — globally or per realm:

services.AddSentinelImpersonation(o => o.RequireTargetConsent = true);

Now start only requests: the record is pendingconsent, no token exists, and the target gets a security_alert mail carrying a single-purpose consent token (a signed JWT with its own type — it can’t be replayed as anything else). The target approves via the public endpoint:

POST /sentinel-admin/impersonation/consent/approve   { "token": "<from the mail>" }

…and only then does GET …/active hand the actor an access token. The consent window (one hour by default) bounds how long the request stays approvable.

Break-glass: power that is capped, loud, and rehearsed

A break-glass account is deliberately boring: an ordinary user with one attribute —

user.Attributes[BreakGlassPolicy.BreakGlassAttribute] = true;   // "sentinel:break_glass"

— and deliberately broad grants (*:*:* in the sample). The interesting part is what the registration does around it:

services.AddSentinelBreakGlass(policy =>
{
    policy.CappedPatterns = ["sentinel:global:*", "records:global:read"];
    policy.OperatorEmails = ["ops@clinic.sample"];
    policy.DrillIntervalDays = 90;
});

AddSentinelBreakGlass decorates two seams instead of adding hooks:

  • The registered ISubjectDataSource is wrapped in BreakGlassCappingDataSource: when a flagged account’s grants are loaded, every allow is intersected with the capped patterns (the same pattern-intersection the owner-capped API keys use) and every deny is preserved verbatim. The cap runs at snapshot-build time, so every evaluation path — HTTP handler, embedded evaluator, profile endpoint — sees only the capped set. There is no login hook to forget.
  • The login gate is wrapped so a successful login by a flagged account triggers the alarms: a breakglass.login security event, a security_alert mail to every operator, and a breakglass.rotation_required event — every use flags the credential for mandatory rotation.

The order matters: call it after your subject source and AddSentinel() are registered, because decoration wraps whatever is already there — it throws if there is nothing to wrap. In the sample, /profile/permissions for the break-glass account shows exactly ["sentinel:global:*", "records:global:read"] — the stored *:*:* never reaches an evaluator (Break_glass_login_alerts_operators_and_grants_evaluate_capped).

The drill, health-checked

An emergency account that nobody has tested is a false sense of security with a password. Sentinel makes the rehearsal a readiness concern:

services.AddHealthChecks().AddSentinelBreakGlassHealthCheck();
app.MapHealthChecks("/health");

No drill within DrillIntervalDays → the check reports Degraded, with lastDrillAt/lastUseAt in the health data. Running a drill is a real break-glass login (alarms and all) followed by stamping the marker:

POST /sentinel-admin/break-glass/drill-login-marker    # requires sentinel:global:manage
GET  /sentinel-admin/break-glass/status                # { drillStale: false, … }

The capped sentinel:global:* covers sentinel:global:manage, so the emergency session itself can stamp the drill — by design: the drill is the emergency procedure, executed on a calm Tuesday. The sample’s Drill_marker_flips_the_health_check_from_degraded_to_healthy test shows /health flipping Degraded → Healthy on the marker.

What lands on the record

Every transition writes to the audit ledger: security events on the target’s timeline (impersonation.requested, .started, .ended), admin-chain entries attributed to the actor, and the break-glass trio (breakglass.login, breakglass.rotation_required, breakglass.drill_completed) — plus the same kinds through the event sink for your webhooks. The mechanism doesn’t trust the humans operating it to also narrate it; the narration is built in.