Skip to content

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.

az login
az account set --subscription "<client-subscription-id>"
az account show -o table

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):
node apps/module-registry/dist/index.js auditPins --root . --format table
  • 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):
gh api -X DELETE app/installations/<installation-id>
  • 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 destroy in reverse dependency order. Mirror the Phase 8 (Core Infrastructure) dependency chain in docs/runbooks/SEQUENCING.md and 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=true resource groups remain (the Sandbox Cleanup (X7) nightly job clears these; the restore-drill in Phase 1 is its main producer).
az group list --query "[?tags.ephemeral=='true'].name" -o tsv
  • 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 destroy runs, 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