Manual Test Runbook — G0: Client-side scoped Reader SP bootstrap
Owner: Sagar | Time: ~20 min | Sandbox: SnowOps sandbox subscription (acts as the "client" tenant for this test)
Purpose
Validate that apps/discovery-auditor/bootstrap/bootstrap.sh creates a
service principal with only Reader + Security Reader on the target
subscription, federates it to a SnowOps GitHub Actions workflow, and is
fully revocable via teardown.sh.
Prerequisites
- Sandbox subscription access via PIM activated as Owner + Privileged Role Administrator
- Local tooling:
azv2.50+,bash,shellcheck(optional) -
az login --tenant <SANDBOX_TENANT_ID>;az account showreturns the sandbox sub - You know the SnowOps repo slug being tested (e.g.
snowops-automation/snowops-automation)
Steps
1. Lint
shellcheck apps/discovery-auditor/bootstrap/bootstrap.sh \
apps/discovery-auditor/bootstrap/teardown.sh
- No warnings
2. Run bootstrap
cd apps/discovery-auditor/bootstrap
./bootstrap.sh \
--subscription <SANDBOX_SUB_ID> \
--snowops-org snowops-automation \
--snowops-repo snowops-automation \
--workflow-ref refs/heads/main \
--expires-on $(date -v+30d +%Y-%m-%d 2>/dev/null || date -d '+30 days' +%Y-%m-%d)
- Script prints
✅ Bootstrap complete - Captured output:
AZURE_TENANT_ID,AZURE_SUBSCRIPTION_ID,AZURE_CLIENT_ID,AUDIT_WINDOW_ENDS - No client secret was printed
3. Verify read-only contract
SP_OBJECT_ID=$(az ad sp show --id <APP_ID> --query id -o tsv)
az role assignment list --assignee "$SP_OBJECT_ID" --all -o table
- Exactly two role assignments: Reader + Security Reader
- Both scoped to
/subscriptions/<SANDBOX_SUB_ID> - No Contributor / Owner / Key Vault role anywhere
4. Verify federation
- Exactly one FIC
-
subjectisrepo:snowops-automation/snowops-automation:ref:refs/heads/main -
issuerishttps://token.actions.githubusercontent.com -
audiencescontainsapi://AzureADTokenExchange -
descriptionmentions the audit window end date
5. Negative test — write attempt must fail
Run as the SP from a SnowOps workflow (or az login as the SP locally with a one-shot secret you create + delete after) and try:
- Fails with
AuthorizationFailed— SP cannot write
6. Teardown
- Prints
✅ Teardown complete -
az ad app show --id <APP_ID>returnsResourceNotFoundError -
az role assignment list --assignee <SP_OBJECT_ID>returns empty
Pass criteria
- All of steps 1–6 marked complete
- No elevated roles assigned at any point
- No secrets emitted
- Teardown leaves zero residue in the sub
Failure modes & escalation
| Symptom | Likely cause | Action |
|---|---|---|
Insufficient privileges on app create |
Caller lacks Privileged Role Admin | Activate via PIM |
PrincipalNotFound on role assignment |
AAD replication lag | Re-run; the script's 10s sleep usually covers this |
| Bootstrap aborts on elevated-roles check | A previous bootstrap left a stale Owner assignment on the SP | Run teardown for the prior APP_ID first |
Sign-off
- Tester: ___ | Date: _ | Result: PASS / FAIL / N/A
- Notes: