ActionBoxBlog

SecurityPlatformSREDevOps

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.

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.

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

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

Try this workflow in minutes

Create a free Source, then run the exact commands from this post against the live API — no approval infrastructure to build.

S
Suson Sapkota

Founder, Actionbox