Hi Beekeepers,
I've been building a small automation project that connects Password Safe Cloud to Azure Key Vault for programmatic credential availability, and I'd love your feedback, ideas, and stress-testing.
Happy to share the full GitHub repo (currently private — DM me and I'll grant access).
At my organization, we use BeyondTrust Password Safe Cloud for privileged breakglass credentials, but several downstream consumers (Azure Functions, Logic Apps, Automation Accounts, third-party SaaS) need to pull those same credentials from Azure Key Vault — because that's what they natively integrate with.
The pre-existing process was:
Manual retrieval from Password Safe
Manual paste into Key Vault
No enforced rotation confirmation
No audit trail for SOX / NYDFS
I wanted an automation layer that respects BeyondTrust as the source of truth for the credential lifecycle, but pushes the rotated secret into Key Vault with proper version discipline and evidence.
What I Built — S.H.A.N.I.
Safeguard & Harmonize Accounts via Network Integration
A locally-hosted PowerShell REST bridge with a static HTML dashboard.
Components
| File | Language | Role |
|---|---|---|
SHANI-Bridge.ps1 | PowerShell 7.x (also runs on 5.1) | Local REST server on http://127.0.0.1:8888 |
SHANI-Dashboard.html | HTML + CSS + JS | Operator UI, served by the bridge itself |
Start-SHANI.ps1 | PowerShell | Mints token, launches bridge |
Stop-SHANI.ps1 | PowerShell | Graceful shutdown |
Per-Account Flow
Rotate → Confirm → Retrieve → Upload → Disable Old → Log
- Trigger rotation on the managed account (
PUT /ManagedAccounts/{id}/Credentials) - Poll
LastChangeDatefor up to 60s to confirm rotation actually happened - Request checkout and retrieve the new credential (
POST /Requests→GET /Credentials/{id}) - Upload to Azure Key Vault as a new secret version (
Set-AzKeyVaultSecret) - Disable the prior Key Vault version (
Update-AzKeyVaultSecret -Enable $false) - Check-in the BT request
- Log to structured audit tables
Batch Behavior
- Reads a CSV (
systemName,accountName) — supports batch mode for many accounts - Per-row try/catch so one bad account doesn't kill the batch
- After completion, prompts operator to download 3 CSVs:
shani_full_log_*.csv— complete console traceshani_rotation_log_*.csv— rotation confirmation evidence (System, Account, Rotated=YES/NO, Retrieved=YES/NO, Result)shani_vault_log_*.csv— Key Vault version lifecycle evidence (SecretName, NewVersion, ActivationDate, OldVersionDisabled)
BeyondTrust Configuration Used
| Component | Value / Setting |
|---|---|
| Admin Account | BTControl_SA (local BT account, member of API group) |
| API Group | Control API User |
| API Group Permissions | 1. API Registration selected 2. Smart Groups (Breakglass 2-2 rules) 3. Credential Management — Full Control (Feature) 4. Password Safe Account Management (Feature) |
| Auth pattern | PS-Auth key=<32-char>; runas=<user>; |
| API version | Public REST v3 |
Azure Configuration Used
| Component | Setting |
|---|---|
| Service Principal | Azure AD App Registration |
| K | Get / List / Set on secrets only (no Delete — least privilege) |
| Vault Naming | breakglassaccount-<MACHINENAME> for 1:1 traceability with BT |
| Vault Hardening | Soft-Delete + Purge Protection required |
| Auth pattern | Connect-AzAccount -ServicePrincipal (no browser popup, no interactive login) |
Security Hardening Applied (v1.2)
Went through two rounds of InfoSec review. Every fix is backed by a CWE / OWASP / Microsoft reference:
Token & Authentication
- 256-bit cryptographic RNG token (not GUID)
- Never printed to console — copied to clipboard or masked fallback
- DPAPI-encrypted at rest via
ConvertFrom-SecureString— file ACL scoped to running user - Cleared from memory on process exit
- Constant-time compare via SHA-256 +
CryptographicOperations.FixedTimeEquals(with byte-XOR fallback for PS 5.1) - Rate-limited: rolling 60-second window, 10 failures triggers throttle +
SECURITYalert log
Listener Binding
- Bound to literal
127.0.0.1(notlocalhost) — no DNS/hosts-file dependency - Startup self-check aborts if bound prefix isn't exactly loopback IPv4
- Prevents IPv6 loopback (
::1) mismatch between listener and dashboard
CORS / CSRF Protection
- Removed
Access-Control-Allow-Origin: * - Removed
Access-Control-Allow-Private-Network: true - Dashboard is now served same-origin from the bridge at
http://127.0.0.1:8888/(not opened asfile://) - Server-side Origin header check — rejects anything not matching the bridge's own address
- Single-use rotating nonce (
X-SHANI-Nonce) on every sensitive call, issued fresh viaX-SHANI-Next-Nonceresponse header - Content-Type validation — POST bodies must be
application/jsonor rejected with 415
Cloud Privilege
- Service Principal reduced to
Get / List / Seton secrets —Deleteremoved (rotation only disables prior versions, never deletes)
Compatibility
- Works on Windows PowerShell 5.1 AND PowerShell 7 without modification
- Auto-fallback for
.NET 5+APIs that don't exist on 5.1




