Departing Employee Deprovisioning Runbook
Owner: operator
Scope: Audit infrastructure access — Heroku Postgres raptor_app credential + audit_archiver SSM credential
Card: #1495 (SC-A15)
Phase: Audit Phase 2 (operational runbook — lands in parallel with Phase 2 code cards)
Depends on: SC-A2 (audit roles and credential model)
Target window: All steps completable within 4 hours by a single operator
SOC-2 ref: CC6.3 — Logical access termination
Last verified: 2026-05-19 UTC (operator dry-run on staging)
Next review due: 2026-08-19 UTC (quarterly — see SC-A16 attestation runbook)
Scope note: This runbook covers audit infrastructure access only: RBAC group removal, Console session revocation, Heroku Postgres
raptor_appcredential rotation, andaudit_archiverSSM credential rotation. It does not cover broader offboarding (GitHub org, Heroku collaborator removal, Cloudflare allowlist removal, Google Workspace suspension, etc.). Those are separate procedures.Runbook staleness: If Velvet takes over
raptor_appcredential rotation (planned post-v1), update Steps 2–3 to use the Velvet rotation flow and bump "Last verified". The SOC-2 quarterly attestation schedule (SC-A16) is the forcing function for review.
When to use this runbook
Execute within 4 hours of confirmed employee departure notice whenever the departing person held any of the following:
- Console operator account (any role)
- Direct Heroku Postgres access (e.g., ran
heroku pg:psqlfor audit operations) - Knowledge of the
raptor_appPostgres credential oraudit_archiverSSM credential
If in doubt, execute all six steps. The steps are idempotent — rotating credentials that the person did not hold causes no harm beyond a short service restart.
Prerequisites
Before starting:
- [ ] You are authenticated as an operator with
superadminrole in Console. - [ ] You have Heroku CLI authenticated:
heroku auth:whoamireturns your account. - [ ] You have AWS CLI configured for
us-east-1with sufficient SSM write access. - [ ] You have opened a FreeScout ticket to track this deprovisioning event.
Format the subject:
DEPROV-YYYY-MM-DD-<email-prefix>(example:DEPROV-2026-05-19-jsmith). Record the ticket ID — you will need it for the audit log entry in Step 6. - [ ] Note the time you started. The 4-hour window begins now.
Step 1 — Revoke Console + Antlers admin access
Estimated time: 10 minutes
1a. Remove RBAC group membership
Navigate to https://console.raxx.app/console/rbac/grants (prod) or
https://console-staging.raxx.app/console/rbac/grants (staging dry-run).
- Search for the departing employee by email.
- For each group membership shown, click Revoke and confirm.
- Verify the employee no longer appears in any group after removal.
Alternatively, if the Console grants UI is unavailable, execute directly against
the Console Postgres database (use heroku pg:psql -a raxx-console-prod):
-- Identify the user record
SELECT id, email FROM admins WHERE email = '<departing-email>';
-- Review current group memberships
SELECT g.name, gm.granted_at, gm.granted_by
FROM rbac_user_groups gm
JOIN rbac_groups g ON g.id = gm.group_id
WHERE gm.user_id = <admin_id>;
-- Remove all group memberships (run once per group_id shown above)
DELETE FROM rbac_user_groups WHERE user_id = <admin_id>;
-- Verify removal
SELECT COUNT(*) FROM rbac_user_groups WHERE user_id = <admin_id>;
-- Expected: 0
Revoking group membership automatically invalidates the RBAC session cache for the affected user on their next request. Proceed to Step 1b to force immediate session termination.
1b. Revoke active Console sessions
Navigate to https://console.raxx.app/console/admins/online (requires superadmin).
- Locate the departing employee in the active sessions list.
- For each active session shown, click Revoke and confirm.
- Verify no active sessions remain for the departing employee.
Alternatively, via direct DB:
-- List active sessions (Console Postgres: heroku pg:psql -a raxx-console-prod)
SELECT id, started_at, last_seen_at, user_agent
FROM admin_sessions
WHERE admin_id = <admin_id>
AND revoked_at IS NULL
ORDER BY last_seen_at DESC;
-- Hard-revoke all sessions
UPDATE admin_sessions
SET revoked_at = NOW()
WHERE admin_id = <admin_id>
AND revoked_at IS NULL;
-- Confirm
SELECT COUNT(*) FROM admin_sessions WHERE admin_id = <admin_id> AND revoked_at IS NULL;
-- Expected: 0
Checkpoint 1: Group memberships removed and all sessions revoked. The departing employee is locked out of Console and Antlers immediately.
Step 2 — Rotate the raptor_app Postgres credential
Estimated time: 30 minutes (including restart and smoke)
The raptor_app Heroku Postgres credential is stored in Infisical at:
/raxx/prod/raptor/RAPTOR_APP_DB_URL
Rotating this credential involves three sub-steps: generate a new password via Heroku's RDS-aware path, update Infisical, and restart Raptor dynos.
Important: Use
heroku pg:credentials:rotate, not directALTER ROLE ... PASSWORD. Heroku Postgres runs on AWS RDS; the owner credential is not inrds_passwordand cannot set passwords via raw SQL. Seedocs/ops/runbooks/raptor-postgres-roles.md— Failure mode A.
2a. Rotate via Heroku CLI
# Rotate the raptor_app credential (this generates a new password in Heroku's RDS path)
heroku pg:credentials:rotate DATABASE --name raptor_app -a raxx-api-prod
# Retrieve the new connection URL (contains the new password)
heroku pg:credentials:url DATABASE --name raptor_app -a raxx-api-prod
# Copy the "Connection URL:" value — it begins with postgres://raptor_app:...
2b. Update Infisical at the canonical path
The raptor_app credential must be kept in sync in Infisical so that Velvet
and any future automation can read it from the vault rather than the Heroku CLI.
Update the secret at /raxx/prod/raptor/RAPTOR_APP_DB_URL:
# Write to Infisical via REST (replace <NEW_URL> with the value from 2a)
# Silence stdout — the URL contains the password.
curl -sf -X PATCH "https://vault.raxx.app/api/v3/secrets/raw/RAPTOR_APP_DB_URL" \
-H "Authorization: Bearer $(cat ~/.raxx-vault-token)" \
-H "Content-Type: application/json" \
-d '{"workspaceId":"<workspace-id>","environment":"prod","secretPath":"/raxx/prod/raptor","secretValue":"<NEW_URL>"}' \
>/dev/null
If vault access is unavailable (bootstrap circularity or token expiry), store
the new URL temporarily in SSM at /raxx/prod/raptor/RAPTOR_APP_DB_URL and
sync to Infisical at the next vault session. Do not leave the credential only in
Heroku config — Infisical is the source of truth.
2c. Set the Heroku config var and restart
# Set the new URL as the Heroku config var (stdout silenced — contains password)
heroku config:set RAPTOR_APP_DATABASE_URL="<NEW_URL>" -a raxx-api-prod >/dev/null 2>&1
# Restart Raptor to pick up the new credential
heroku restart -a raxx-api-prod
# Tail logs to confirm clean startup
heroku logs -a raxx-api-prod --tail --num 30
# Watch for: no "password authentication failed for user raptor_app" errors
# Watch for: Raptor requests processing normally (200s in the log stream)
2d. Verify with smoke test
scripts/ci/run_smoke.sh --env=prod
Expected: all smoke checks pass. If the smoke test fails with a Postgres authentication error, re-check that the config var was set correctly:
heroku config:get RAPTOR_APP_DATABASE_URL -a raxx-api-prod | cut -c1-35
# Expected prefix: postgres://raptor_app:
Checkpoint 2:
raptor_appcredential rotated, Infisical updated, Raptor restarted, smoke green. The old credential is invalid.
Step 3 — Rotate the audit_archiver SSM credential
Estimated time: 20 minutes
Rotate only if the departing employee had direct Postgres access for archival operations. If the departing employee had Console-only access (no direct DB access), skip to Step 4.
The audit_archiver credential lives in AWS SSM Parameter Store under:
/raxx/prod/raptor/AUDIT_ARCHIVER_DB_URL
Per feedback_aws_workloads_use_ssm_not_vault.md, AWS-resident workload secrets
live in SSM, not Infisical.
3a. Rotate via Heroku CLI
# Rotate the audit_archiver credential
heroku pg:credentials:rotate DATABASE --name audit_archiver -a raxx-api-prod
# Retrieve the new connection URL
heroku pg:credentials:url DATABASE --name audit_archiver -a raxx-api-prod
# Copy the "Connection URL:" value — begins with postgres://audit_archiver:...
3b. Update SSM
# Write new credential to SSM (stdout silenced — contains password)
aws ssm put-parameter \
--name "/raxx/prod/raptor/AUDIT_ARCHIVER_DB_URL" \
--type SecureString \
--value "<NEW_URL>" \
--region us-east-1 \
--overwrite \
>/dev/null 2>&1
# Verify the parameter was written without exposing the value
aws ssm get-parameter \
--name "/raxx/prod/raptor/AUDIT_ARCHIVER_DB_URL" \
--with-decryption \
--region us-east-1 \
--query "Parameter.Value" \
--output text | cut -c1-35
# Expected prefix: postgres://audit_archiver:
3c. Update the Heroku Scheduler job (if applicable)
If the audit_archiver credential is referenced by a Heroku Scheduler job on
raxx-api-prod, update the job's environment:
- Navigate to the Heroku dashboard →
raxx-api-prod→ Add-ons → Heroku Scheduler. - Locate the archiver job (command:
python jobs/audit_archiver.py). - If the job reads
AUDIT_ARCHIVER_DB_URLfrom an environment variable set directly on the dyno, update it:
heroku config:set AUDIT_ARCHIVER_DB_URL="<NEW_URL>" -a raxx-api-prod >/dev/null 2>&1
- Trigger a manual run to confirm the job succeeds with the new credential:
heroku run python jobs/audit_archiver.py --dry-run -a raxx-api-prod
# Expected: job exits 0 with no authentication errors
Checkpoint 3:
audit_archiverSSM credential rotated, Heroku Scheduler job (if any) updated, manual dry-run confirmed green.
Step 4 — Verify: Raptor operates correctly with the new raptor_app credential
Estimated time: 5 minutes
Run the Raptor smoke suite against production and confirm zero Postgres authentication errors in the 10 minutes following the credential rotation.
# Full smoke suite
scripts/ci/run_smoke.sh --env=prod
# Tail prod logs for Postgres errors for 5 minutes
heroku logs -a raxx-api-prod --tail --num 100 | grep -i "authentication failed\|password\|FATAL\|ERROR" || echo "No Postgres auth errors found"
Expected: smoke suite exits 0, no Postgres auth errors in logs.
If smoke fails with a permission denied error (not an authentication error),
check for grant drift per docs/ops/runbooks/raptor-postgres-roles.md
§ "Re-apply grants if drift occurs".
Checkpoint 4: Raptor production is operating normally on the new credential.
Step 5 — Verify: departing employee Console session is invalidated
Estimated time: 5 minutes
Confirm that no active Console sessions remain for the departing employee by re-querying the Console database (the same query as Step 1b):
heroku pg:psql -a raxx-console-prod -c "
SELECT COUNT(*) AS active_sessions
FROM admin_sessions
WHERE admin_id = (SELECT id FROM admins WHERE email = '<departing-email>')
AND revoked_at IS NULL;
"
# Expected: active_sessions = 0
If the count is non-zero, return to Step 1b and revoke the remaining sessions.
Checkpoint 5: Zero active Console sessions for the departing employee.
Step 6 — Log the deprovisioning event in audit_archival_runs
Estimated time: 5 minutes
Record the deprovisioning event manually in audit_archival_runs (the audit
infrastructure manifest table) to close the audit trail for this operation.
Connect to the Raptor prod Postgres as the owner credential (migration credential):
heroku pg:psql -a raxx-api-prod
Insert the deprovisioning record:
INSERT INTO audit_archival_runs (
run_type,
operator_email,
freescout_ticket_id,
notes,
completed_at
)
VALUES (
'deprovisioning',
'<your-operator-email>',
'<DEPROV-YYYY-MM-DD-ticket-id>',
'Departing employee: <departing-email>. Steps completed: RBAC group removal, Console session revocation, raptor_app rotation, audit_archiver rotation (if applicable). Window: <start-time UTC> to <end-time UTC>.',
NOW()
);
If audit_archival_runs does not yet have a deprovisioning run_type (SC-A13
may not have landed yet), insert with run_type = 'manual_deprovisioning' and
note that the schema update is pending.
Checkpoint 6: Deprovisioning event logged in
audit_archival_runswith FreeScout ticket reference and operator email. This row is append-only (INSERT + SELECT only forraptor_app; no UPDATE or DELETE).
Summary verification checklist
Complete all items before closing the FreeScout ticket:
- [ ] Step 1a — RBAC group memberships removed for
<departing-email> - [ ] Step 1b — All Console sessions revoked (active_sessions = 0)
- [ ] Step 2 —
raptor_appcredential rotated, Infisical updated at/raxx/prod/raptor/RAPTOR_APP_DB_URL, Heroku config var updated, Raptor restarted - [ ] Step 3 —
audit_archivercredential rotated (if applicable), SSM updated at/raxx/prod/raptor/AUDIT_ARCHIVER_DB_URL, Scheduler job updated - [ ] Step 4 — Smoke suite passes on prod, no Postgres auth errors in logs
- [ ] Step 5 — Zero active Console sessions confirmed for departing employee
- [ ] Step 6 — Deprovisioning event logged in
audit_archival_runswith FreeScout ticket ID
Close the FreeScout ticket with a note: "Deprovisioning complete. Window:
Timing guidance
| Step | Estimated time | Cumulative |
|---|---|---|
| Step 1 — RBAC + session revocation | 10 min | 0:10 |
Step 2 — raptor_app rotation + smoke |
30 min | 0:40 |
Step 3 — audit_archiver rotation (if applicable) |
20 min | 1:00 |
| Step 4 — Prod smoke verification | 5 min | 1:05 |
| Step 5 — Session verification | 5 min | 1:10 |
| Step 6 — Audit log entry | 5 min | 1:15 |
Total: ~75 minutes under normal conditions. The 4-hour window exists to accommodate interruptions, credential rotation delays, and escalation paths described below.
Escalation
Stop and escalate to Kristerpher if:
heroku pg:credentials:rotatereturns an error not covered by this runbook.- The smoke suite fails after credential rotation and the cause is not clear within 15 minutes of investigation.
audit_archival_runsINSERT fails due to a schema mismatch (SC-A13 has not landed).- The departing employee's SSO / passkey credential needs to be revoked from an external IdP (future SAML phase — not applicable for v1).
Rollback: if raptor_app rotation breaks production
The credential rotation is not directly reversible (the old password is gone from Heroku's RDS once rotated). Recovery path:
- Generate another rotation immediately (Step 2a–2c again).
- If Raptor is hard-down and can't wait for credential propagation, set
FLAG_RAPTOR_APP_ROLE_SEPARATION=0to fall back to the ownerDATABASE_URL:
heroku config:set FLAG_RAPTOR_APP_ROLE_SEPARATION=0 -a raxx-api-prod >/dev/null 2>&1
heroku restart -a raxx-api-prod
This is a temporary fallback only. Re-enable the flag once the new raptor_app
credential is confirmed working.
Phase 2+ note
When Velvet's Postgres credential rotation handler ships (planned post-v1), Steps 2–3 will be replaced by a Velvet rotation flow. At that point:
- Update this runbook to reference the Velvet SOP instead of the manual CLI steps.
- Bump "Last verified" and trigger a SOC-2 quarterly attestation dry-run.
- File the update as a follow-up to SC-A16.
Cross-references
docs/ops/runbooks/raptor-postgres-roles.md—raptor_approle provisioning + grant matrixdocs/ops/runbooks/soc2-quarterly-attestation.md— CC6.3 quarterly evidence checklistdocs/ops/runbooks/velvet-operator.md— Velvet rotation flows (future: Steps 2–3)docs/ops/runbooks/rotation/aws-iam-access-key.md— AWS IAM credential rotation (not in scope here)docs/architecture/customer-audit-unified/design.md—audit_archiverrole definition- Issue: #1495 (SC-A15), #1465 (audit v2 design PR), #1482 (SC-A2), #1496 (SC-A16 attestation)
- Infisical path:
/raxx/prod/raptor/RAPTOR_APP_DB_URL - SSM path:
/raxx/prod/raptor/AUDIT_ARCHIVER_DB_URL