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
expis 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.
Consent mode
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
ISubjectDataSourceis wrapped inBreakGlassCappingDataSource: 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.loginsecurity event, asecurity_alertmail to every operator, and abreakglass.rotation_requiredevent — 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.