🛸 Hitchhiker's Guide — Phase 08: CI/CD Pipelines & Secure Supply Chain
Read this if: you can make a
.ymlturn green but "why did the deploy skip," "how does keyless auth actually work," and "how do you roll back automatically" still sound like other people's problems. They're yours now. This is the compressed tour the WARMUP derives slowly. Skim it, then read the WARMUP.
0. The 30-second mental model
A pipeline is a DAG with a security boundary bolted on. Three things to own:
- The graph — stages depend on stages; the engine runs them in topological order; a
stage runs iff its dependencies satisfied its condition and any required
approval was granted. Failure propagates: a failed/skipped/blocked dep is not
succeeded, so the default-condition dependents skip. - The identity — the pipeline auths to Azure with no stored secret via OIDC
workload-identity federation: it presents a short-lived OIDC token; Entra exchanges it
only if a federated credential matches
iss+sub+audexactly. The subject pins the branch/environment — the wrong subject is denied. That denial is the security. - The rollout — blue-green (instant swap, instant rollback) or canary (ramp %, gate on health, auto-rollback on the first failing step). The gate exists before the incident.
One sentence to tattoo: a stage runs iff every dependency satisfied its condition and its approval was granted — and the pipeline proves who it is with a subject-scoped OIDC token, not a stored secret.
1. The execution model, one breath
topo-order the DAG → for each stage:
condition not satisfied by deps? → SKIPPED
needs approval, not granted? → BLOCKED
else run it → SUCCEEDED / FAILED (failure propagates to dependents)
succeeded = all deps green (default) · always = run no matter what (notify/cleanup) ·
failed = run only if a dep FAILED (rollback). Skipped ≠ failed ≠ blocked — three
statuses, three causes.
2. The OIDC exchange, one breath
run starts → CI mints OIDC JWT { iss, sub=repo:org/app:ref:refs/heads/main, aud } (lives ~minutes)
→ POST to Entra as client_assertion (jwt-bearer)
→ Entra verifies signature (JWKS) + exp, then MATCHES iss & sub & aud to a federated cred
→ match? mint Azure access token (scoped by RBAC) : DENY (AADSTS700213)
No secret stored, anywhere. The subject is the branch/environment knob. Deny by default.
3. The numbers & rules to tattoo on your arm
| Thing | Number / rule |
|---|---|
| Default stage condition | succeeded — needs all deps green |
| The three conditions | succeeded / always / failed |
| OIDC token lifetime | minutes (exp), minted per run |
| Entra exchange audience | api://AzureADTokenExchange |
| Match requirement | iss AND sub AND aud, exact, deny by default |
| Federated creds per identity | ~20 max |
| GitHub OIDC permission | permissions: id-token: write (+ contents: read) |
| Wrong-branch error | AADSTS700213 (no matching federated credential) |
| Blue-green rollback | instant (swap back; old slot stays warm) |
| Canary on failure | roll back to 0% at the first failing step |
| Stored secret = | the worst CI security smell — don't store it, federate |
4. az / gh one-liners you'll actually run
# Create an Entra app + service principal to federate (no secret created!)
az ad app create --display-name "ci-deployer"
APP_ID=$(az ad app list --display-name ci-deployer --query "[0].appId" -o tsv)
# Add a FEDERATED credential pinned to main (this is the security boundary)
az ad app federated-credential create --id "$APP_ID" --parameters '{
"name": "github-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:contoso/app:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
# Give the identity ONLY the role it needs, at the narrowest scope (P04)
az role assignment create --assignee "$APP_ID" --role Contributor \
--scope /subscriptions/<sub>/resourceGroups/<rg>
# GitHub Actions: log in to Azure with OIDC, NO secret in the repo
# permissions: { id-token: write, contents: read }
# - uses: azure/login@v2
# with: { client-id: ..., tenant-id: ..., subscription-id: ... }
# App Service blue-green: swap staging → production (instant cutover + rollback)
az webapp deployment slot swap -g <rg> -n <app> --slot staging --target-slot production
# Azure Pipelines: a condition that only runs a rollback stage on failure
# - stage: rollback
# dependsOn: deploy
# condition: failed()
# List a pipeline's runs / re-run / approve an environment check
az pipelines runs list --output table
gh run list ; gh run rerun <run-id> ; gh run watch <run-id>
5. War story shapes you'll relive
- "The deploy stage just didn't run, no error." → a dependency failed (or skipped), so
its default
succeededcondition wasn't met → it skipped. Skipped ≠ failed. Check the upstream results and the stage'scondition. - "Rollback fired on a green build." → someone wrote
condition: always()where they meantfailed().alwaysruns no matter what;failedonly on a real failure. - "A fork's PR got our cloud credential." → the federated-credential subject was too
broad (matched PRs / any branch). Pin it to
...:ref:refs/heads/mainor...:environment:production; never federatepull_requestfor privileged deploys. - "Our secret leaked in a build log and someone's mining crypto in our sub." → you were storing a long-lived SP secret. The fix isn't "redact logs harder," it's federate — remove the secret entirely.
- "The bad release took prod down for 40 minutes." → no canary, no health gate, no auto- rollback. A canary would've caught it at 5% and rolled back to 0% with nobody paged.
- "It only fails on Tuesdays / when re-run." → non-determinism in the build (unsorted parallelism, time/random without a seed). CI must be reproducible.
6. Vocabulary that signals you've shipped
- DAG /
dependsOn/needs— the pipeline is a graph; stages are nodes, deps are edges. - Condition —
succeeded/always/failed; the gate that decides skip vs run. - Skipped vs blocked vs failed — condition-unmet vs approval-withheld vs ran-and-failed.
- Environment / approval gate — a protected target that pauses for a human or a check.
- Matrix — one job spec → the Cartesian product of variables → N parallel jobs.
- OIDC / workload-identity federation — keyless auth; trust a token's iss/sub/aud, no secret.
- Federated credential — the (issuer, subject, audience) trust object on an Entra identity.
- Subject (
sub) — the branch/environment the token is for; the security boundary. - Blue-green / canary / rolling — swap two slots / ramp+gate / batch-replace.
- Health gate — the automated check that triggers an automatic rollback.
- SBOM / provenance / attestation / SLSA — what's in it / who built it / signed proof / the levels.
- Keyless signing (Sigstore) — sign with the pipeline's OIDC identity, no key to manage.
7. Beginner mistakes that mark you in interviews
- Calling a pipeline "a script" — missing that it's a DAG with conditions and gates.
- Confusing
always()andfailed()(and being surprised when rollback runs on success). - Thinking "skipped" means "failed" — they're different statuses with different causes.
- Storing a long-lived service-principal secret in CI instead of federating with OIDC.
- Setting the federated-credential subject too broad (any branch / PRs) — a fork PR then owns your cloud.
- Treating "deploy" as a copy, with no canary/health gate/rollback path defined up front.
- Saying "it's signed so it's safe" — signed ≠ attributable to my hardened builder (that's provenance + SLSA).
- A matrix that explodes into hundreds of jobs and a surprise bill — prune to what catches bugs.
8. How this phase pays off later
- The DAG + conditions model is how you reason about every pipeline you'll ever debug, including the IaC pipelines from P01/P02 and the container builds from P07.
- OIDC federation is the keyless-auth pattern that also underpins managed identities (P12) and is the answer to half of "how do you secure X without a secret."
- Blue-green / canary + health gates are the reliability primitives that connect straight into P14 (HA, rollback, circuit breakers) and the capstone (P15).
- The supply-chain framing is the security-design language for any architecture review.
Now read the WARMUP slowly, then build the engine. Once run_pipeline, exchange_oidc_token,
and canary_rollout are green, you can explain a deploy that skipped, an auth with no
secret, and an automatic rollback — which is the whole Round-2/Round-5 signal for this phase.