Skip to content

JIT privileged access approval workflow: grant, expire, revoke

A copy-paste JIT privileged access approval workflow with bounded grants, automatic revocation, expiry, and an audit trail without standing access.

Combination lock resting on a computer keyboard

A developer with permanent admin access is a breach waiting for a phishing email. The blast radius is the standing part, not the privilege itself: anyone who compromises the account inherits access that never expires. Just-in-time (JIT) access flips the model — nobody holds elevated permissions by default; they request them for a task, a human approves with the justification attached, the grant expires automatically, and every step is audited.

Architecture Flow
Approve Reject / timeout Request role Verify identity + reason Actionbox approval Grant bounded access No access Auto-revoke at expiry

JIT privileged access approval workflow: request, grant, revoke

ElementWhat it doesFails closed when…
RequestWho, what system, why, for how longNo request, no access
ApprovalNamed approver, on recordNo approver → auto-deny after SLA
Time boundGrant carries an explicit expiryExpiry enforced by the platform, not the user
Auto-revokeEntitlement removes itselfTask done early → user can end the grant

Step 1: the request creates the decision

Your JIT platform (AWS SSM, Google PAM, BeyondTrust, or your own script) stops at the approval boundary and asks — instead of granting:

bash
#!/usr/bin/env bash
# request-jit.sh — request temporary access to a production node
set -euo pipefail

NODE="${1:?node name}"
ROLE="${2:?role to elevate}"
DURATION_H="${3:-4}"   # routine ops: 4h, incident response: 8h, maintenance: 12h
REASON="${4:?business justification}"
EXPIRES_AT=$(date -u -d '+15 minutes' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u -v+15M '+%Y-%m-%dT%H:%M:%SZ')

actionbox ask \
  "Grant ${ROLE} on ${NODE} for ${DURATION_H}h?" \
  --option approve="Approve (auto-revokes in ${DURATION_H}h)" \
  --option deny="Deny" \
  --context-json "[{\"type\":\"key_value\",\"items\":{\"requester\":\"$(whoami)\",\"node\":\"${NODE}\",\"role\":\"${ROLE}\",\"duration_h\":\"${DURATION_H}\",\"reason\":\"${REASON}\"}}]" \
  --expires "$EXPIRES_AT" \
  --on-expire-json '{"type":"resolve","response":{"type":"single_choice","value":"deny"},"reason":"approval SLA exceeded"}' \
  --callback-url https://ops.acme.com/actionbox/jit-callback

The callback does the actual elevation only when the decision is approve. Timeout is a denial — the request vanishes instead of lingering.

Step 2: the grant is time-bounded and self-revoking

The approver's "yes" is not a permanent key. The callback grants the role with an explicit expiry, and revocation is the platform's job:

bash
# jit-callback.sh — called with the decision from Actionbox
DECISION="$1"

if [ "$DECISION" = "approve" ]; then
   # Call your PAM/provider grant API with the approved role, node, and
   # bounded duration. Use the provider's documented command/API here.

  # The access duration is enforced by SSM — no cleanup script needed.
  # Audit: session recording + approval record both reference this request.
else
  echo "Access denied: $DECISION"
  exit 1
fi

Auto-revocation prevents the classic failure mode — users request elevation "for a quick fix" and never release it, accumulating residual privilege. The timer expires, session ends, the audit log closes the record.

Step 3: every grant is an audit record

JIT without an audit trail is just permanent elevation with extra steps. The record per grant is what supports forensic reconstruction later:

bash
# Monthly review: use Actionbox History/API filters for the Source and
# resolved status. The CLI `get` command requires one Action ID.

Watch three metrics: approval latency (median under two minutes for routine elevations), break-glass usage (under 0.1% of total elevations), and approver engagement (evidence they actually examined the request). When latency drifts up, people start routing around the system — that's how standing privilege sneaks back in.

Full example: break-glass with a reason, not a backdoor

Every JIT system needs a fallback when the platform is down. Make it a recorded path, not a shared password:

bash
#!/usr/bin/env bash
# break-glass.sh — emergency elevation, logged even when the queue is down
set -euo pipefail

reason="${1:?incident ticket required}"

actionbox ask \
  "BREAK-GLASS: emergency elevation by $(whoami)" \
  --option approve="Authorize for the incident window" \
  --option deny="Route through the normal JIT queue" \
  --context-json "[{\"type\":\"key_value\",\"items\":{\"mode\":\"break-glass\",\"ticket\":\"${reason}\",\"expiry\":\"auto-revoke 1h\"}}]" \
  --callback-url https://ops.acme.com/actionbox/break-glass-callback

Unapproved break-glass is how the fallback becomes the new standing-privilege backdoor. Approved-and-logged break-glass is how you keep an emergency lane without the hole.

Why this beats "everyone is an admin"

  • Blast radius shrinks to the task — a compromised account owns nothing for longer than its last grant
  • Approval latency is a product feature — routine requests target a two-minute median, not a ticket queue
  • Expiry is enforced, not hoped for — revocation doesn't depend on user discipline
  • Compliance evidence is one query — who had what, when, why, and who approved — on record forever

Sources & Research

Try it

  1. Install the CLI on your bastion or automation host
  2. Create a Source → ACTIONBOX_TOKEN
  3. Put the ask in front of your elevation command

Create a free Source · CLI + callback reference · Secret rotation sign-off · Cron jobs that ask before acting

About the author

Suson Sapkota

Suson founded ActionBox and works in software and data engineering. He writes about approval workflows, background jobs, and how to verify what happened after a human decision.

Turn the next risky operation into a reviewable decision.

Create a free Source, run the example from this guide, and keep the decision and execution outcome connected.