Just-in-time admin elevation
Reduce standing admin privileges by elevating only when needed
Standing admin access is a security risk: if compromised, attacker has high privileges constantly. Just-in-time (JIT) elevation: be a regular user by default, request admin when needed (with strong auth + audit).
Pattern
1. Regular user. No admin privileges.
2. User clicks "Request admin access" → enters reason.
3. Approval required (auto or another admin).
4. Admin role granted for 1 hour.
5. After 1 hour, expires automatically.Reduces blast radius: stolen credentials of an "admin user" don't have admin scope unless they've recently elevated.
Data model
CREATE TABLE admin_elevations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
identity_id UUID NOT NULL,
reason TEXT NOT NULL,
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
approved_at TIMESTAMPTZ,
approved_by UUID,
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ
);
CREATE INDEX ix_active_elevations
ON admin_elevations(identity_id, expires_at)
WHERE revoked_at IS NULL AND approved_at IS NOT NULL;Check on each admin endpoint
async function requireAdmin(req) {
const session = await getSession(req);
if (session.identity.traits.base_role !== "admin_capable") {
throw new Forbidden("not_admin_capable");
}
const active = await db`
SELECT * FROM admin_elevations
WHERE identity_id = ${session.identity.id}
AND approved_at IS NOT NULL
AND revoked_at IS NULL
AND expires_at > NOW()
LIMIT 1
`.first();
if (!active) {
throw new StepUpRequired("elevation_required");
}
}User has the capability to be admin (configured globally). Currently elevated? Only if there's an active elevation.
Request flow
"use client";
export function RequestElevationButton() {
const [reason, setReason] = useState("");
async function request() {
await fetch("/api/admin-elevation", {
method: "POST",
body: JSON.stringify({ reason }),
});
window.location.reload();
}
return (
<Modal>
<h2>Request admin access</h2>
<p>You'll have admin privileges for 1 hour.</p>
<textarea value={reason} onChange={(e) => setReason(e.target.value)}
placeholder="Why do you need admin? (logged)" required />
<Button onClick={request}>Elevate</Button>
</Modal>
);
}Approval modes
Mode A: Self-approval
User clicks → immediate elevation. But:
- Require MFA at request.
- Reason logged.
- Time-limited.
For trusted admins, this is the most ergonomic. The audit trail provides accountability.
Mode B: Peer approval
Another admin must approve:
1. User requests elevation.
2. Slack/email to other admins: "Alice wants admin for 1h. Reason: [X]. Approve?"
3. Another admin clicks approve.
4. Elevation granted.For higher-stakes systems. Slower but harder to abuse.
Mode C: Manager approval
Specific manager approves their reports' requests. Hierarchy.
MFA on elevation request
Even with self-approval, require MFA:
if (!session.recent_mfa) {
return forceStepUp();
}A compromised non-MFA'd session can't elevate.
Expiration
UPDATE admin_elevations
SET revoked_at = NOW()
WHERE expires_at < NOW() AND revoked_at IS NULL;Daily cron, or on each access check. Auto-expire, user must re-request.
Manual revoke
User finishes their work; they can voluntarily end their elevation:
async function endElevation(elevationId: string, identityId: string) {
await db`
UPDATE admin_elevations
SET revoked_at = NOW()
WHERE id = ${elevationId} AND identity_id = ${identityId} AND revoked_at IS NULL
`;
}Encourage this, reduces window of risk.
Audit
Every elevation event:
INSERT INTO security_audit (event_type, actor_id, metadata)
VALUES (
'admin_elevation_requested',
$admin_id,
'{"reason": "$reason", "expires_in_minutes": 60}'
);And every admin action during elevation:
INSERT INTO security_audit (event_type, actor_id, metadata)
VALUES (
'admin_action',
$admin_id,
'{"action": "deleted_user", "target": "$target_id", "elevation_id": "$elev_id"}'
);Trace: this action happened during this elevation.
Notification
Notify on elevation:
- Slack channel for security team.
- Email to all-admins or the user themselves.
Subject: Admin elevation granted
Alice@your-corp.com elevated to admin for 1 hour.
Reason: "Migrating user data"
Expires: 2026-05-13 15:00 UTCWhen NOT to use JIT
- 24/7 ops needs (someone always needs admin to respond to incidents). Pair with on-call rotation; on-call has standing admin.
- Small teams (1-2 admins) where the friction outweighs benefit.
For larger teams (> 5 admins) or compliance environments, JIT is valuable.
In Athena
Athena's settings UI can manage elevations:
// app/admin/elevations/page.tsx
const elevations = await db`SELECT * FROM admin_elevations ORDER BY requested_at DESC LIMIT 50`;
return (
<table>
{elevations.map(e => (
<tr>
<td>{e.identity.email}</td>
<td>{e.reason}</td>
<td>{e.approved_at ? "active" : "pending"}</td>
<td>{e.expires_at}</td>
<td><Button onClick={() => revoke(e.id)}>Revoke</Button></td>
</tr>
))}
</table>
);Active elevations visible. Force-revoke if needed.
Standing vs JIT
You CAN combine: some admins (CEO, founder) have standing access (trust). Others have JIT (junior engineers).
function isStandingAdmin(identity) {
return ["founder", "cto"].includes(identity.traits.role);
}Tune to your team's needs.