Just-in-Time Elevation Instead of Standing Admin
Access reviews find privilege that should never have persisted; just-in-time elevation stops it persisting in the first place. This page is a focused part of Tenant Access Reviews & Certification: making the privileged role a temporary grant that expires by itself, so certification becomes a small exercise rather than the only line of defence.
Problem Framing
Standing admin exists because it is convenient. Someone needs to change a billing setting once a quarter, so they hold the admin role permanently — and for the other eighty-nine days it is a credential that can be phished, reused by a stale session, or inherited by whoever takes over the account. Every control downstream, from certification campaigns to orphan detection, exists to manage the consequences of that decision.
Just-in-time elevation removes the standing state. The principal holds a base role permanently and requests elevation when a task actually requires it: a reason, a duration bounded to hours, and an approval path appropriate to the risk. The grant is written with an expiry, the token carries that expiry, and nothing has to run for it to end.
The property that makes this work operationally is that expiry is a read-time fact rather than a scheduled job. A grant with expires_at in the past simply stops matching the query that resolves permissions. There is no cleanup task to fail silently, which is the failure mode that turns time-bound access back into standing access.
Step-by-Step Guide
1. Model elevation as a request with a mandatory expiry
CREATE TABLE elevation_request (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL,
principal_id uuid NOT NULL,
role_key text NOT NULL, -- 'admin' | 'billing_owner' | 'security_admin'
reason text NOT NULL,
ticket_ref text,
requested_at timestamptz NOT NULL DEFAULT now(),
approved_by uuid,
approved_at timestamptz,
starts_at timestamptz,
expires_at timestamptz,
revoked_at timestamptz,
CONSTRAINT bounded CHECK (expires_at IS NULL OR expires_at <= starts_at + interval '8 hours'),
CONSTRAINT approved_shape CHECK ((approved_by IS NULL) = (approved_at IS NULL))
);
CREATE INDEX ON elevation_request (tenant_id, principal_id, expires_at)
WHERE revoked_at IS NULL;
The eight-hour ceiling in the schema is the control that stops elevation drifting back toward permanence one exception at a time.
2. Resolve effective roles at read time, expiry included
CREATE OR REPLACE VIEW effective_role AS
SELECT tenant_id, principal_id, role_key, 'standing' AS source
FROM tenant_role_grant
WHERE revoked_at IS NULL AND role_key NOT IN (SELECT role_key FROM privileged_role)
UNION ALL
SELECT tenant_id, principal_id, role_key, 'elevated' AS source
FROM elevation_request
WHERE revoked_at IS NULL
AND approved_at IS NOT NULL
AND now() >= starts_at AND now() < expires_at; -- expiry is a predicate, not a job
Privileged roles are deliberately excluded from the standing half of the union, so a legacy standing admin grant cannot quietly coexist with the new model.
3. Route approval by risk, and never to the requester
// approve.ts
const APPROVERS: Record<string, ApproverRule> = {
admin: { kind: 'other_tenant_admin', fallback: 'internal_control_owner' },
billing_owner: { kind: 'other_tenant_admin', fallback: 'internal_control_owner' },
security_admin: { kind: 'internal_control_owner' }, // always two-party
};
export async function approve(requestId: string, approverId: string) {
const req = await getRequest(requestId);
if (req.principalId === approverId) throw new Error('self-approval is not permitted');
if (!await canApprove(approverId, req)) throw new Error('approver lacks authority');
const startsAt = new Date();
await db.query(
`UPDATE elevation_request
SET approved_by = $1, approved_at = now(), starts_at = $2,
expires_at = $2::timestamptz + make_interval(mins => $3)
WHERE id = $4 AND approved_at IS NULL`,
[approverId, startsAt, req.requestedMinutes, requestId],
);
await bumpAuthEpoch(req.tenantId, req.principalId); // force a fresh token
}
Bumping the authorization epoch at approval is what makes the new privilege take effect immediately, using the same mechanism that revocation uses in invalidating tenant sessions on role change.
4. Bind the token's lifetime to the grant's
// mint.ts
export async function mintElevated(principal: Principal, grant: Elevation, key: CryptoKey) {
const expSeconds = Math.floor(grant.expiresAt.getTime() / 1000);
return new SignJWT({
sub: principal.id,
tid: grant.tenantId,
roles: [grant.roleKey],
elev: grant.id, // ties the token to one elevation record
epoch: principal.authEpoch,
})
.setProtectedHeader({ alg: 'EdDSA', kid: 'auth-2026-07' })
.setIssuedAt()
.setExpirationTime(Math.min(expSeconds, nowSeconds() + 900)) // ≤15 min, and never past expiry
.sign(key);
}
Capping the token at fifteen minutes while the grant may last hours means the elevated capability is re-derived from the database several times during a session — so a revocation mid-window takes effect on the next refresh rather than at the end of the grant.
5. Make requesting frictionless, because friction is what creates exceptions
// request.ts — the path a tenant admin actually uses
export async function requestElevation(input: {
tenantId: string; principalId: string; roleKey: string; reason: string; minutes: number;
}) {
if (input.minutes > 480) throw new Error('maximum elevation is 8 hours');
const req = await createRequest(input);
const approvers = await resolveApprovers(input.tenantId, input.roleKey, input.principalId);
await notifyAll(approvers, 'elevation_requested', { req, approveUrl: url(req) });
return req;
}
If elevation takes twenty minutes to obtain, teams will ask for a standing exception and get one. A request that reaches an approver instantly, and is approvable in one click from a phone, is what keeps the model intact under operational pressure.
Verification
-- 1. Nobody holds a privileged role permanently.
SELECT g.tenant_id, g.principal_id, g.role_key
FROM tenant_role_grant g
JOIN privileged_role p USING (role_key)
WHERE g.revoked_at IS NULL;
-- expected: no rows once migration is complete
-- 2. No elevation outlived its window.
SELECT id, principal_id, expires_at
FROM elevation_request
WHERE revoked_at IS NULL AND expires_at < now()
AND EXISTS (SELECT 1 FROM privileged_action a
WHERE a.elevation_id = elevation_request.id AND a.at > elevation_request.expires_at);
-- expected: no rows — an action after expiry means the predicate was bypassed somewhere
-- 3. No self-approvals.
SELECT id FROM elevation_request WHERE approved_by = principal_id;
-- expected: no rows
// jit.spec.ts
it('loses the role the moment the window closes', async () => {
const grant = await elevate('acme', 'sam', 'admin', { minutes: 15 });
await asUser('sam').post('/api/settings/billing').expect(200);
await advanceTime(minutes(16));
await refreshToken('sam');
await asUser('sam').post('/api/settings/billing').expect(403);
});
Failure Modes & Gotchas
-
Symptom: elevation expires but the user keeps working. Cause: the token outlives the grant. Fix: cap the token below the grant's expiry and re-derive roles on refresh; never mint a token whose lifetime exceeds the elevation.
-
Symptom: teams request standing exceptions. Cause: the request path is slow or approvals stall. Fix: treat time-to-approval as a product metric, notify all eligible approvers at once, and make approving a one-click action.
-
Symptom: the ceiling is always requested. Cause: the default in the interface is the maximum. Fix: default to the shortest useful duration and require an extra step to exceed it; measure the distribution.
-
Symptom: a sole-admin tenant cannot get approval. Cause: the only eligible approver is the requester. Fix: route to the internal control owner as a documented fallback, and prompt those tenants to add a second admin.
FAQ
Does this replace access reviews entirely? No, it shrinks them. Certification still has to cover base roles, service principals and identity-provider mappings, none of which are elevation-shaped. What disappears is the largest and least useful part of a campaign: pages of standing admin grants that reviewers approve without evidence. When privileged access is measured in hours per quarter, the review that remains is small enough to be done properly.
What about emergency access when the approval path is unavailable? Keep an explicit break-glass path with a different shape: self-approved, heavily alerted, capped at one hour, and reviewed by a human within one business day. The point is not to prevent emergency access but to make it visibly different from the routine path, so break-glass use is countable and its growth is a signal rather than a mystery.
Is just-in-time elevation worth it for a small platform? The database work is a table, a view and an approval endpoint — perhaps a week. The ongoing cost is the approval friction, which is real but bounded. The payoff arrives at the first security questionnaire that asks how long privileged access persists, and again at every certification cycle that no longer has to relitigate a hundred standing grants. For a platform with regulated customers it pays for itself almost immediately.
How should elevation work for the platform's own staff? The same way, with a shorter ceiling and a heavier audit trail. Staff elevation into a tenant is impersonation, so it carries the additional requirement of being visible to the customer, and its approval should route to an internal control owner rather than to the tenant. Sharing the request, approval and expiry machinery between the two cases is worth doing — it means one implementation to review, one set of evidence to produce, and one place where the ceiling is enforced.
Does this pattern apply to database and infrastructure access too? It applies especially well there, and the mechanics are identical: a request with a reason, a second approver, a short window and an expiry enforced at read time. The difference is where the grant is enforced — a database role granted temporarily, or a cloud role assumed with a session duration — but the record, the approval and the evidence look the same. Keeping one elevation model across application, database and infrastructure access means an auditor sees one control rather than three, which is materially less work to evidence.