Automatically keep your Azure PIM (Privileged Identity Management) roles activated. Runs as a systemd user timer, checks every 30 minutes, and re-activates roles as they expire — no manual clicks in the Azure portal.
Azure PIM roles expire after a fixed duration (typically 8 hours). If you have 10+ eligible roles across multiple subscriptions, manually re-activating them through the portal several times a day is painful. Forgetting to re-activate breaks your workflows — failed deployments, denied storage access, cluster jobs that can't submit.
Uses a reconciliation loop pattern (inspired by Kubernetes controllers): observe the actual state, diff against desired state, act to converge, repeat.
- Observe — Fetches eligible roles (
az-pim list) and active roles (az-pim list --active) - Diff — Computes which eligible roles are missing from the active set
- Act — Activates missing roles, respecting a per-role cooldown to avoid hammering Azure during its async provisioning window (5–7 min)
- Schedule — Queries the Azure REST API for exact role expiration timestamps and schedules a one-shot timer to fire 5 minutes after the earliest expiry
Key design choice: never explicitly deactivates roles. Azure PIM deactivation is asynchronous and takes 5–7 minutes of unpredictable delay. Attempting deactivate-then-activate creates a race condition with a gap where roles are neither active nor fully deactivated. Instead, roles are allowed to expire naturally, then re-activated cleanly.
| Tool | Install | Purpose |
|---|---|---|
Azure CLI (az) |
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash |
Azure authentication and REST API calls |
azure-pim-cli (az-pim) |
cargo install azure-pim-cli |
PIM role listing and activation |
| jq | sudo apt install jq |
JSON processing |
| systemd | (included in most Linux distros) | Timer scheduling |
You must be logged into Azure CLI before installing: az login.
git clone https://github.com/bsantraigi/PRIMA.git
cd PRIMA
./prima installThis will:
- Check all prerequisites are installed and
azis logged in - Generate a systemd user service file with correct absolute paths for your machine
- Symlink the timer unit to
~/.config/systemd/user/ - Enable and start the 30-minute heartbeat timer
- Enable linger (
loginctl enable-linger) so the timer runs even without an active login session
The service file is generated, not checked in — it contains absolute paths specific to where you cloned the repo. If you move the repo, run ./prima install again.
./prima install # First-time setup (or after moving the repo)
./prima status # Show timer, last run, one-shot, and cooldown state
./prima run # Run activation manually right now
./prima logs # Show recent journal logs (last 50 entries)
./prima disable # Pause the timer (roles will expire naturally)
./prima enable # Resume the timer
./prima uninstall # Stop timers, remove unit files# Via prima
./prima logs
# Or directly via journalctl
journalctl --user -u prima.service -f # follow live
journalctl --user -u prima.service --since today./prima runThis runs the full reconciliation loop once — useful for immediate activation after a fresh az login or to test changes.
Environment variables (set in your shell or export before running):
| Variable | Default | Description |
|---|---|---|
PIM_JUSTIFICATION |
Work |
Justification string sent with each activation request |
PIM_DURATION |
8 hours |
How long to activate roles for (Azure PIM max is typically 8h) |
PIM_ACCOUNT_PREFIX |
sc- |
Only allow accounts whose UPN starts with this prefix. Set to empty string to disable |
To make these permanent for the systemd service, add them to the generated service file's Environment= line after installing.
┌─────────────────────────────┐
│ systemd timer (30 min) │
│ + one-shot at expiry+5min │
└──────────┬──────────────────┘
│ triggers
▼
┌─────────────────────────────┐
│ prima.sh │
│ (reconciliation loop) │
└──────────┬──────────────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ az-pim list │ │ az-pim list │ │ az rest │
│ (eligible) │ │ --active │ │ (expiry times) │
└──────────────┘ └──────────────┘ └──────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ Reconcile │
│ missing = eligible - active │
│ for each missing role: │
│ if not in cooldown → activate │
│ if all active → check expiry → schedule one-shot │
└─────────────────────────────────────────────────────┘
│
▼
┌──────────────────┐
│ cooldown.json │ (per-role timestamps)
│ ~/.local/state/ │
│ prima/ │
└──────────────────┘
| File | Description |
|---|---|
prima.sh |
Main reconciliation script (~130 lines of bash) |
prima.timer |
Systemd timer unit (static — no machine-specific paths) |
prima |
CLI entrypoint — install/uninstall/lifecycle management |
~/.config/systemd/user/prima.service |
Generated at install time with correct paths |
~/.local/state/prima/cooldown.json |
Per-role cooldown timestamps (safe to delete) |
| Scenario | API calls | Time |
|---|---|---|
| All roles active, expiry far away | 3 (list × 2 + az rest) |
~15s |
| All roles active, expiry soon | 3 + one-shot scheduled | ~15s |
| Some roles missing, activation needed | 3 (list × 2 + activate) |
~15s |
| Recently activated (cooldown) | 2 (list × 2, skips activate) |
~10s |
The cooldown file tracks when each role was last requested for activation. This prevents re-requesting activation during Azure's 5–7 minute async provisioning window. The key is "RoleName|/full/scope/path" and the value is a Unix epoch timestamp. Entries older than 10 minutes are ignored (not pruned — they're harmless). Deleting the file entirely is safe; the system self-corrects on the next tick.
When all eligible roles are active and the earliest expiry is within 35 minutes (the next heartbeat might miss it), the script schedules a transient systemd timer via systemd-run --user --on-active=Xs. This fires at expiry + 5 minutes, runs the same reconciliation script, and catches the expired roles immediately. If the one-shot misses (reboot, systemd issue), the 30-minute heartbeat catches it as a fallback.
az-pim not found in PATH — The systemd service sets its own PATH. If az-pim is installed somewhere unusual, update the Environment=PATH=... line in ~/.config/systemd/user/prima.service.
az CLI not logged in — Run az login in your terminal. The systemd service uses the same credential cache (typically ~/.azure/).
Timer not running after reboot — Check loginctl show-user $USER | grep Linger. If Linger=no, run sudo loginctl enable-linger $USER.
Roles not activating — Check ./prima logs for errors. Common causes: expired az token, az-pim version mismatch, Azure throttling.
Want to change the heartbeat interval? — Edit OnUnitActiveSec=30min in prima.timer, then ./prima install to reload.
MIT — see LICENSE.