WardenAuthAgent Security
PricingDocsCompareBlogLearnChangelog
Sign inGet started free
All posts
Deep Dive
July 19, 2026
12 min read

Just-in-Time Access: Granting Temporary Permissions Without the Overhead

Standing permissions are a liability. Just-in-time access grants permissions only when needed, for exactly as long as needed. Architecture patterns for JIT elevation, time-boxed role assignments, and the audit trail that proves it worked.


Standing permissions — roles assigned permanently, never reviewed, never revoked — are the norm in most systems. They are also the single biggest contributor to privilege creep. Just-in-time (JIT) access flips the model: permissions are granted only when needed, for exactly as long as needed, and then automatically revoked. This post is about making JIT practical — fast enough that users don't route around it, auditable enough that compliance teams trust it, and automated enough that it doesn't create support tickets.

The problem: standing permissions are a liability

Every permanently assigned role widens your blast radius. When a user with production deploy access needs it twice a year, the other 363 days are unnecessary exposure. A compromised credential with standing admin access can do catastrophic damage. JIT access reduces the window of vulnerability from "forever" to "the duration of the task."

Rule of thumb: if a user exercises a permission less than once per month, it should be JIT-granted, not permanently assigned. Your audit log will tell you which permissions these are.

Three JIT patterns

Pattern 1: Time-boxed role assignment

The simplest JIT pattern: assign a role with an expiration timestamp. The role grants its permissions from the moment of assignment until the expiration — at which point the system automatically removes it.

typescript
// Grant temporary production access for 2 hours
await accessPolicy.create({
  subjectId: 'user_123',
  scopeId: 'acme-corp',
  roles: ['prod-deployer'],
  expiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
  reason: 'Hotfix deployment for issue #4512',
})

// Access check evaluates both role membership AND expiry:
async function checkAccess(req) {
  const policy = await getPolicy(req.subjectId, req.scopeId)
  const activeRoles = policy.roles.filter(r => !r.expiresAt || r.expiresAt > new Date())
  return evaluatePermissions(activeRoles, req.resource, req.action)
}

Pattern 2: Approval-gated elevation

For sensitive operations, a second approver must authorize the role assignment before it is active. This enforces separation of duties — the same user cannot both request and approve their own elevation.

typescript
// Request elevation — creates a pending approval
await approvalRequest.create({
  subjectId: 'user_123',
  scopeId: 'acme-corp',
  requestedRoles: ['prod-deployer'],
  duration: '2h',
  reason: 'Hotfix deployment',
  requiredApprovers: 1,
})

// Approver grants it — role becomes active with expiry
await approvalRequest.approve({
  approvalId: 'approval_789',
  approverId: 'user_456', // different user
})

// Rejection is also recorded — full audit trail

Pattern 3: Break-glass emergency access

In emergencies (production down, security incident), normal approval workflows are too slow. Break-glass access allows immediate elevation but with mandatory post-hoc review. Every break-glass event is logged with the reason, reviewed by security within 24 hours, and alerts the on-call team.

Automatic expiry: the Lambda sweeper

Expired roles don't revoke themselves — something has to enforce the expiry. We use a scheduled Lambda (EventBridge cron, every 5 minutes) that scans for expired role assignments and removes them:

typescript
// Sweeper Lambda — runs every 5 minutes via EventBridge
export async function handler() {
  const expired = await queryExpiredPolicies() // GSI on expiresAt

  for (const policy of expired) {
    await removeExpiredRoles(policy.subjectId, policy.scopeId)
    await auditLog.record({
      event: 'jit.role-expired',
      subjectId: policy.subjectId,
      scopeId: policy.scopeId,
      roles: policy.expiredRoles,
    })
  }
}

Audit trail: proving JIT worked

JIT without audit is just access with an expiry. You need to prove: who requested elevation, who approved it, what roles were granted, when they were active, and when they expired. Each state transition produces an audit event:

  • jit.elevation-requested: user requested temporary access, with reason
  • jit.elevation-approved: approver granted the access, with timestamp
  • jit.elevation-denied: approver rejected the request
  • jit.role-activated: temporary role became active
  • jit.role-expired: temporary role automatically expired
  • jit.role-revoked: temporary role manually revoked before expiry
  • jit.break-glass: emergency elevation used, flagged for review

What we learned

The biggest adoption barrier to JIT is not technical — it is user friction. If requesting temporary access takes 10 minutes and an approval, users will demand permanent roles. We reduced the request flow to one API call with an optional reason field and a 2-hour default duration. For non-sensitive roles, auto-approval skips the approval gate entirely — making JIT the path of least resistance.


Back to blogTry WardenAuth free →
© 2026 ecarrizo. All rights reserved.
PricingDocsCompareBlogLearnChangelogStatusGlossaryContactTermsPrivacy