Client Offboarding Playbook (W4)¶
Purpose: the canonical, repeatable procedure for ending a SnowOps engagement cleanly — handing back everything the client owns, tearing down only what SnowOps operated, and leaving a complete evidence trail of access revocation.
This playbook is the inverse of onboarding. Onboarding stands a client up via the GitHub Onboarder App (
B1) (repo + branch protection + CODEOWNERS + federated OIDC) and the Azure Client Bootstrap Module (B2) (Azure AD app + service principal + GitHub-OIDC federated credentials + role assignments, no client secret ever created). Offboarding unwinds exactly those, in reverse, while respecting per-asset ownership so client-owned infrastructure is handed back rather than destroyed.
Owner: Sagar | Time: ~2–4 hours for a hand-back; +1–2 hours for a full tear-down | Milestone: M3
Preconditions / triggers¶
Run this playbook when any of the following is confirmed and in writing (contract amendment, signed SOW close-out, or ticket from the client sponsor):
- Contract end / non-renewal — the engagement term has lapsed and will not renew.
- Transition to in-house — the client is taking operations in-house and wants SnowOps access removed but the platform left running.
- Vendor change — the client is moving to another partner; repos/state/secrets must be handed to them or to the client directly.
- Engagement type decided — confirm whether this is a hand-back (platform stays, SnowOps leaves) or a tear-down (platform is decommissioned). Only tear-downs run Phase 7.
- Data-handling expectation confirmed — retention period for evidence/audit artifacts agreed with the client (drives Phase 1 and Phase 8).
- A client recipient is identified for everything being handed over (repo owner, Azure tenant admin, secrets custodian).
If any precondition is unmet, pause — do not begin teardown of identity or state until the hand-back recipient and data-handling terms are confirmed. See Rollback / abort.
Ownership recap¶
Every SnowOps asset carries an ownership tag (gap G10, taxonomy §3.7 of the asset catalog). Offboarding behavior is driven entirely by this tag — hand back what the client owns; tear down only what SnowOps operated.
| Tag | Meaning | What offboarding does |
|---|---|---|
[SO] SnowOps-Operated |
SnowOps runs it on the client's behalf (e.g. internal tooling, sandbox cleanup). | Tear down or disconnect. Remove SnowOps-side automation, jobs, and access. Nothing to hand back unless the client wants to adopt it. |
[CO] Client-Owned |
Provisioned by SnowOps but owned/operated by the client after handover (most F-/H-modules). | Hand back, do not destroy (unless a full tear-down is explicitly contracted). Transfer ownership of state, RBAC, and credentials to the client. |
[CA] Client-Adopted |
Portable assets the client adopted into their own repos/process (pipelines, pre-commit, OPA). | Confirm the client retains them. Remove SnowOps's operational involvement only. |
[SH] Shared |
Jointly used during the engagement (evidence, discovery). | Split: deliver the client's copy as an evidence package; retire SnowOps's operating access. |
[SO→CO] Hand-back assets |
Started SnowOps-operated, designed to transfer to the client (the GitHub Onboarder App (B1), the Azure Client Bootstrap Module (B2)). |
The core of offboarding. Transfer ownership, then remove SnowOps's federated trust. |
Rule of thumb:
[SO]is the only category SnowOps deletes by default.[CO]/[CA]/[SH]are handed back; tear-down of those is Phase 7 and only on a contracted decommission.
Phased checklist¶
Work the phases in order. Each phase is gated: do not start identity teardown (Phase 3) until the evidence freeze (Phase 1) and knowledge transfer (Phase 2) are complete, and do not run state/secret teardown (Phase 5) until repository hand-back (Phase 4) is done. Fill the sign-off block at the end.
Throughout: select the engagement subscription first.
Phase 1 — Freeze & snapshot¶
Capture a final, immutable evidence baseline before anything changes. This is the proof-of-state at handover.
- Final compliance snapshot via the Compliance Snapshot Collector (
E0) (read-only; Reader + Security Reader):
cd apps/evidence-collector
node dist/index.js --subscription "<client-subscription-id>" \
--out-dir compliance/snapshots --now "$(date -u +%FT%TZ)"
- Final compliance dashboard via the Compliance Dashboard (
S2) (fully offline; renders the snapshot history into the handover HTML/markdown report):
cd apps/compliance-dashboard
node dist/index.js --snapshots-dir ../evidence-collector/compliance/snapshots \
--out-dir compliance/dashboard
- Final backup + on-demand restore drill — confirm the Azure Backup Policy Module (
L1) policies are intact, then run a final recoverability check with the Automated Restore Drill (L4) (restores into an ephemeral sandbox RG, validates, tears down, reports):
cd apps/restore-drill
node dist/index.js --mode azure-cli --out-dir compliance/restore-drills \
--now "$(date -u +%FT%TZ)"
- Export the immutable discovery/audit log — deliver the WORM-immutable hash-chained log from the Discovery Audit Log (
G6) and the subscription Activity Log archive from the Audit Log Archive Module (J6). Do not delete these (see Failure modes); copy them out for the evidence package.
# Copy the WORM audit/discovery log + activity-log archive to the handover bundle
az storage blob download-batch \
--account-name "<audit-archive-account>" \
--source "<audit-container>" \
--destination ./offboarding-evidence/audit-archive \
--auth-mode login
Phase 2 — Knowledge transfer¶
Hand over everything the client needs to run the platform without SnowOps.
- Architecture diagram — regenerate the current diagram with the Diagram Generator (
V2) (terraform output -json→ d2lang) and deliver it. - Operational runbooks — regenerate per-domain runbooks with the Runbook Generator (
V3) and deliver them.
terraform -chdir=live/prod/eastus output -json > /tmp/tfout.json
node apps/diagram-generator/dist/index.js --input /tmp/tfout.json --out-dir ./handover/diagrams
node apps/runbook-generator/dist/index.js --input /tmp/tfout.json --out-dir ./handover/runbooks
- Module version pins — export the resolved module manifest and confirm every consumer is pinned to a semver tag (never
ref=main) via the Module Registry & Versioning (F11):
- Client guides — deliver the full
docs/client-guides/set (handover guides, brownfield adoption, quickstart) relevant to the client's milestone. - Confirm receipt in writing from the client recipient before proceeding.
Phase 3 — Identity teardown¶
Unwind the onboarding identity setup (the inverse of the Azure Client Bootstrap Module (B2)). Order matters: never remove the last break-glass owner before client emergency access is confirmed.
- Confirm client break-glass first. Verify the client has at least one permanent, non-PIM Global Admin / Owner they control, independent of SnowOps, before removing any SnowOps access. The Azure Resource PIM Module (
B5) and Entra PIM Templates Module (H3) both enforce a precondition of ≥1 permanent break-glass Owner — that holder must now be the client's. - Remove SnowOps federated OIDC credentials created by the Azure Client Bootstrap Module (
B2). These are the GitHub-OIDC federated identity credentials on the SnowOps-controlled app registration — removing the federation trust blocks token issuance (same mechanism as the vendor-breach IR runbook).
# List federated creds on the SnowOps bootstrap app, then remove each
az ad app federated-credential list --id <snowops-app-id> -o table
az ad app federated-credential delete --id <snowops-app-id> --federated-credential-id <cred-id>
- Rotate or transfer break-glass accounts managed by the Break-Glass Account Module (
H7). The module takes existing account object IDs as input and creates no passwords — so hand back or rotate the underlying accounts via the H7 credential ceremony (hardware FIDO2 + split-knowledge), then remove any SnowOps members from the break-glass group. - Revoke SnowOps PIM eligibility — remove SnowOps principals' eligible role assignments from the Azure Resource PIM Module (
B5) and the Entra PIM Templates Module (H3).
az role assignment list --assignee <snowops-principal-id> --all -o table
# Remove standing/eligible assignments for SnowOps principals (verify each before deleting)
- Remove SnowOps members from Conditional Access exclusion groups — the Conditional Access Module (
H2) policies all exclude the break-glass group; ensure no SnowOps identities remain in that exclusion group once SnowOps no longer holds break-glass.
Phase 4 — Repository & CI hand-back¶
Hand back or archive the repo the GitHub Onboarder App (B1) provisioned, and stop SnowOps-operated workflows.
- Transfer or archive the provisioned repo. Transfer ownership to the client org, or archive if retiring:
# Transfer to the client org
gh api -X POST repos/<snowops-org>/<client-repo>/transfer -f new_owner='<client-org>'
# …or archive if the engagement is ending without a recipient
gh repo archive <snowops-org>/<client-repo> --yes
- Hand back CODEOWNERS / branch-protection automation. Remove the GitHub Onboarder App (
B1) installation from the repo so SnowOps no longer manages branch protection and CODEOWNERS, or transfer that responsibility to the client per the GitOps Branching Standard (C4):
- Disable SnowOps-operated workflows. Disable the scheduled automation SnowOps ran on the client's behalf — drift detection (the Drift Detector (
S1),drift-detection.yml), SP rotation (the Service Principal Inventory (H5),sp-inventory-rotation.yml), and any others SnowOps operated. The client can re-enable them under their own ownership if they adopt them ([CA]).
gh workflow disable drift-detection.yml --repo <org>/<client-repo>
gh workflow disable sp-inventory-rotation.yml --repo <org>/<client-repo>
Phase 5 — State & secrets¶
Transfer ownership of Terraform state and rotate every secret SnowOps could have seen.
- Transfer Terraform state backend ownership. The State Backend Module (
F6) (wrapped per-client by the Client State Backend Module (B4)) holds the state storage account. Reassign Blob Data RBAC to the client and remove SnowOps grants — do not destroy state until hand-back is confirmed (see Failure modes).
# Grant the client owner role on the state storage account
az role assignment create --assignee <client-principal-id> \
--role "Storage Blob Data Owner" \
--scope "<state-storage-account-resource-id>"
# Remove SnowOps's data-plane grant once the client confirms access
az role assignment delete --assignee <snowops-principal-id> \
--role "Storage Blob Data Contributor" \
--scope "<state-storage-account-resource-id>"
- Rotate all secrets in Key Vault. Anything in the Key Vault Module (
F5) vault that SnowOps had access to is now considered exposed — rotate it on hand-back (mirror the recovery step in the IR runbooks):
az keyvault secret list --vault-name <client-kv-name> --query '[].name' -o tsv
# Rotate each secret the client will continue to use; the client sets new values
- Hand over or revoke service principals. Use the Service Principal Inventory (
H5) to enumerate app-registration credentials, then either transfer ownership to the client or revoke SnowOps-only SPs.
node apps/sp-inventory/dist/index.js --out-dir /tmp/sp-inventory
# For SnowOps-only SPs being retired:
az ad app delete --id <snowops-sp-app-id>
Phase 6 — Cost & monitoring¶
Reassign or retire the monitoring/cost plumbing SnowOps operated; confirm the client owns the data planes.
- Budget alerts — reassign the notification recipients on the Budget Alert Module (
U1) budget to the client, or remove the SnowOps action group. - On-call integration — disconnect the SnowOps PagerDuty/Opsgenie + Slack wiring in the On-Call Integration Module (
K2) so incidents no longer page SnowOps; hand the integration to the client if they keep it. - Alert rule pack — reassign the action groups consumed by the Alert Rule Pack (
J4) to the client's on-call, or remove SnowOps targets. - Confirm client owns the data planes — the Log Analytics Module (
J1) workspace and the Audit Log Archive Module (J6) archive are[CO]; confirm RBAC is the client's and remove SnowOps reader grants.
az monitor log-analytics workspace show -g <rg> -n <workspace> -o table
az role assignment list --scope <law-resource-id> -o table # confirm client RBAC, drop SnowOps grants
Phase 7 — Decommission (tear-down engagements only)¶
Skip this entire phase for a hand-back. Run it only when a full decommission is explicitly contracted. Everything here is destructive.
-
terraform destroyin reverse dependency order. Mirror the Phase 8 (Core Infrastructure) dependency chain indocs/runbooks/SEQUENCING.mdand destroy in reverse: compute/data stores → registry/secrets → identity bindings → networking → baseline/RG last. Destroy state-dependent stacks before the state backend itself.
# Example: destroy a stack (LIFO across stacks; never destroy the state backend first)
terraform -chdir=live/prod/eastus/<stack> destroy
- Verify sandbox cleanup — confirm no
ephemeral=trueresource groups remain (the Sandbox Cleanup (X7) nightly job clears these; the restore-drill in Phase 1 is its main producer).
- Confirm no orphaned resources — check for leftover role assignments, app registrations, and resource groups tied to SnowOps principals.
az role assignment list --assignee <snowops-principal-id> --all -o table # expect empty
az ad app list --display-name-starts-with snowops- --query '[].displayName' -o tsv
az group list -o table
Phase 8 — Final attestation¶
Close the engagement with a delivered evidence package and proof of revocation.
- Data-handling / retention confirmation — record the agreed retention for evidence/audit artifacts and confirm SnowOps's copies are handled per that agreement.
- Evidence package delivered — the Phase 1 snapshot (
E0), dashboard (S2), restore-drill report (L4), and the exported audit/discovery logs (G6/J6) are delivered to the client and receipt acknowledged. - Access-revocation proof — capture evidence that all SnowOps access is gone: empty
az role assignment list --assignee <snowops-principal-id>, removed federated credentials, removed break-glass membership, and disabled workflows. Attach to the evidence package.
Failure modes / gotchas¶
| Pitfall | Why it bites | Mitigation |
|---|---|---|
| Destroying state before hand-back | Deleting the State Backend Module (F6) / Client State Backend Module (B4) storage account before the client confirms access leaves the client unable to manage their own infra — and Terraform state is not recoverable from the resources alone. |
Phase 5 transfers RBAC and gets written confirmation before any destroy. State backend is destroyed last (Phase 7) and only on a contracted tear-down. |
| Removing the last break-glass owner | The Azure Resource PIM Module (B5) and Entra PIM Templates Module (H3) require ≥1 permanent break-glass Owner; removing SnowOps's break-glass before the client has their own can lock the subscription out entirely. |
Phase 3 confirms the client's independent break-glass holder first, then removes SnowOps. Never delete both at once. |
| Deleting the audit log prematurely | The Discovery Audit Log (G6) and Audit Log Archive Module (J6) are WORM-immutable evidence; deleting (or trying to delete under an immutability lock) breaks the evidence chain and may be a compliance violation. |
Export, never delete. Copy to the evidence package in Phase 1; leave the immutable store intact for the agreed retention. |
| Rotating secrets after revoking access | If SnowOps access is removed before secrets are rotated, the client may be unable to rotate, or stale SnowOps-known secrets stay live. | Rotate in Phase 5 while transferring ownership, before SnowOps grants are fully dropped. |
| Archiving the repo before transferring state/secrets | An archived repo is read-only; in-repo references and workflows can't be updated. | Repo hand-back (Phase 4) precedes state/secrets (Phase 5); archive only after the client has what they need. |
| Forgetting Conditional Access exclusions | Leaving SnowOps identities in the break-glass exclusion group (Conditional Access Module (H2)) leaves a CA-bypass path open after offboarding. |
Phase 3 explicitly clears SnowOps from the exclusion group. |
Rollback / abort¶
Offboarding can be paused or reversed at any point before Phase 7 (decommission), because Phases 1–6 are hand-back and revocation steps, not destruction:
- Pause: stop at the end of the current phase. Phases are gated and each leaves the platform in a consistent state. Record where you stopped in the sign-off notes.
- Reverse (re-engage): if the client re-engages before Phase 7, restore SnowOps access by re-running the onboarding path — re-add the federated OIDC credentials via the Azure Client Bootstrap Module (
B2), re-grant PIM eligibility (B5/H3), re-add SnowOps to the break-glass group (H7), and re-enable the workflows disabled in Phase 4. No data was destroyed. - Phase 7 is the point of no return. Once
terraform destroyruns, reversal means a full re-provision from state + backups, not a rollback. Do not start Phase 7 until hand-back (Phases 1–6) is signed off and the tear-down is contracted.
Sign-off¶
| Field | Value |
|---|---|
| Engagement type | ☐ Hand-back ☐ Tear-down |
| Result | ☐ PASS ☐ FAIL |
| Run by | |
| Date | |
| Phases completed | ☐ 1 ☐ 2 ☐ 3 ☐ 4 ☐ 5 ☐ 6 ☐ 7 (tear-down only) ☐ 8 |
| Evidence package delivered | ☐ Yes ☐ No |
| Notes |
Client counter-sign — the client confirms receipt of the handover artifacts and the evidence package, and acknowledges that SnowOps access has been revoked.
| Field | Value |
|---|---|
| Client representative | |
| Date | |
| Signature / acknowledgement |