The pipeline outranks almost everyone in the org
Make an honest list of who could push a change to production tonight, and the machine you spent this track building sits near the top of it — above most of the people, and awake at hours none of them are. A pipeline that can deploy to production can, by definition, replace what customers see, read whatever the deploy touches, and do it in seconds, unsupervised, from a trigger anyone with commit rights can pull. That is not a flaw to be patched out; it is the job you gave it. But it means the credential the pipeline carries is the most valuable thing in the building, and an attacker who reaches it does not need to breach your firewall, your VPN, or a single production server — they need only your pipeline, because your pipeline already has the keys and permission to use them.
So the last thing this bootcamp teaches you to secure is the thing you built to be powerful. Everything in the eight parts before this made the pipeline more capable — it runs unattended, it holds credentials, it reaches production. This part asks the question that capability forces: what happens the day someone who is not you gets to press the button? The answer is not a product you buy. It is a handful of decisions — how the pipeline proves who it is, how narrowly it is allowed to act, what it holds and what it must never hold — and each one is worth more, in prevented incident, than any feature you shipped to get here.
The best secret is the one that does not exist.
Service connections — how the pipeline proves it is allowed into Azure
A pipeline does not reach into your Azure subscription directly. It reaches through a named object called a service connection — a permissioned bridge, defined once in Azure DevOps project settings, that pipelines refer to by name in their YAML instead of carrying credentials of their own. Name it azure-prod, and any authorised pipeline can say "deploy using azure-prod" without ever seeing what is behind the name. The connection is the single most sensitive object in the whole track, because whatever it is allowed to do, every pipeline that references it can do too.
- Service connection
- A named, permissioned identity configured in Azure DevOps that lets a pipeline authenticate to an external system — for Azure, a Resource Manager service connection tied to an Entra identity with an RBAC scope. Pipelines reference it by name; the credential, if any, lives in the connection, never in the pipeline file.
There are two ways that connection can prove its identity to Entra ID, and the difference between them is the difference between this part being easy and this part being a recurring incident. The old way is a service principal with a stored client secret: the connection holds a password — a "Service Principal Key", in the portal's words — inside Azure DevOps's database, and presents it to Entra on each run. It works. It is also a long-lived password sitting in a vendor's store, and like every password it expires — and it will expire, reliably, on the worst possible morning, breaking every pipeline that depended on it at once, until someone notices, mints a new secret, and pastes it back in. That recurring outage has a name in half the world's backlogs, and you have already met Campux's version of it.
The current way holds no secret at all. It is called workload identity federation, and it is the OIDC pattern you built by hand in Class Twenty-Three, re-nouned for Azure DevOps: instead of storing a password, the connection carries only a trust — a federated credential on the Entra identity that says "I will believe a token that Azure DevOps signs for this exact connection." On each run, Azure DevOps mints a short-lived token proving the run is who it claims to be, hands it to Entra in a federated token exchange, and Entra answers with minutes of access. Nothing durable is stored, so nothing durable can be stolen, and nothing expires on a Sunday. Microsoft's own guidance is unambiguous: workload identity federation "eliminates the need for secrets and secret management," it is the recommended option for a new connection, and there is a built-in tool to convert existing secret-bearing connections to it.1
Use an Azure Resource Manager service connection — workload identity federation vs. service principal + secret, and the conversion tool:
learn.microsoft.com/azure/devops/pipelines/library/connect-to-azure
Least privilege — a narrow door, and a lock on who may open it
Removing the secret answers how the pipeline proves itself. It says nothing about how much the pipeline may do once inside, and that is a separate control you have to set on purpose. A federated connection scoped to the entire subscription is a keyless car that can still drive anywhere: no password to steal, but if a bad pipeline references it — a compromised task, a malicious pull-request build, a fat-fingered YAML change — it can act across every resource group the subscription contains. The identity is safe; the blast radius is not.
The fix is the same lesson Class Eight taught about RBAC, applied here without flinching. Scope the connection's role assignment to a resource group, never the whole subscription, so a connection that deploys the warehouse app can touch the warehouse resource group and nothing else. Give each environment its own connection — a warehouse-staging and a separate warehouse-prod, each scoped to its own resource group — so that a mistake in a staging pipeline cannot reach production at all, rather than being asked politely not to. And separate the two questions the connection raises: who may use it is a pipeline-permissions setting — authorise each pipeline individually rather than granting access to all pipelines — while who may administer it, change its scope, or delete it is a role on the connection itself. Conflate them and you get the default that ships too much power to too many pipelines by accident.
Secrets, and the supply chain the pipeline pulls from
Even a secret-free connection runs steps that need real secrets — a database password, an API key, a token for a third party. The rule is short and absolute: no secret ever appears in the YAML or in the logs. A password committed to azure-pipelines.yml is a password published to everyone who can read the repo and everyone who ever clones it, forever, even after you delete the line. Instead, secrets live as secret variables or, better, in a variable group linked to Azure Key Vault — the pattern from Part D — so the pipeline reads them at run time and the vault stays the source of truth. Azure DevOps masks secret values in the log output, but treat that as a safety net, not a permission: a masked secret you deliberately echo is one base64 away from readable, and masking never sees the value you wrote to a file or sent to an external service.2
The other exposure is everything your pipeline pulls in to run. A pipeline is a supply chain: it executes tasks, and those tasks execute code you did not write. Pin task versions rather than floating on @latest, so a task cannot change under you between runs; treat a third-party Marketplace task with the same caution Class Twenty-Two taught for third-party Actions, because installing one grants it the run's context and whatever your connection can reach. Put production behind protected resources — environments, service connections, and variable groups carry approvals and checks that a pull-request build from a fork cannot satisfy — and use an extends template as an enforced gate, the Part D technique: a central template every pipeline must extend, so security steps are inherited rather than remembered.
| Control | What it protects | The failure if you skip it |
|---|---|---|
| Workload identity federation | The pipeline's identity to Azure | A stored secret expires on a Sunday and breaks every pipeline; or is stolen and reused off-platform |
| Resource-group RBAC scope | The blast radius once inside | One compromised pipeline acts across the entire subscription instead of one resource group |
| Per-pipeline authorisation | Who may use the connection | Every pipeline in the org, including a malicious fork build, borrows production access |
| Key Vault variable groups | Secrets at rest and in logs | A password lands in the YAML or the log and is public to everyone with repo read |
| Pinned tasks & extends gate | The code the pipeline pulls in | A third-party task changes under you and runs with the pipeline's full permissions |
Where azure-prod finally dies
This is the beat the whole track has been walking toward. Basecamp's estate ran on a single service connection named azure-prod — one stored client secret, scoped to the entire subscription, referenced by all thirty-one pipelines — and it expired on a rough cadence, which is why the acquired team's backlog carried a standing ticket titled "deploys broken again?? (monthly)". Every few weeks a secret lapsed, every pipeline that touched production failed at once, an engineer minted a fresh secret and pasted it back, and the ticket was reopened the next cycle. Nobody had decided the arrangement was dangerous; it was simply the way things had always broken.
The reader replaces it, and replaces the shape of it. In place of one subscription-scoped, secret-bearing connection there are now several federated, resource-group-scoped connections — one per environment, each holding no secret at all and each able to touch only the resource group it deploys. There is nothing left to expire, so the monthly ticket does not get worked; it gets closed forever, because the failure it described can no longer occur. The Basecamp engineers, who arrived braced to be told their decade of work was legacy, watch the single most irritating recurring outage of their careers disappear without ceremony — and that, more than any diagram, is what wins them over. The best secret, it turns out, is the one that does not exist.
The audit nobody asked for, that everybody remembers
You will not be thanked for shipping a feature the way you will be thanked for enumerating the service connections, finding the one stored, subscription-scoped secret every pipeline shares, and quietly converting it to federation before it expires again. It is invisible in a demo and priceless in the incident that now never happens. The number for the standup — "standing secrets in the deploy path: was one, now zero" — is the most senior sentence a first-month engineer can say, and it is true.
What the pipeline says about you
This is the last teaching page of the bootcamp, and it ends where the first one started: on the line between what the provider secures and what you do. Azure will keep the token service running, rotate its own keys, and honour the RBAC you assign. It will not decide for you whether the connection is federated or secret-bearing, whether it is scoped to a resource group or a subscription, whether a password is in your YAML, or whether a stranger's Marketplace task runs with your production access. Those are the half of the model the provider will never take — and, this one last time, the half you are paid to hold.
An interviewer cannot read your intentions, but they can read your pipeline, and it tells them everything. A federated identity with no stored secret, a connection scoped to one resource group, secrets in a vault and never in a log, tasks pinned and templates enforced — that pipeline says, without a word, that the person who built it thinks about the day they are not in the room. That is the engineer a team trusts with production, and it is the engineer this bootcamp was built to make you. You have the hands now. The road is yours to keep safe.
Examination
Four drills, then two situations. The situations have no marking scheme — write your answer before you reveal the reasoning, or the exercise is worthless. Nothing is stored.
B — the safety is the absence of the secret, not a better place to keep one. Federation holds only a trust; each run mints a short-lived token, exchanges it with Entra, and gets minutes of access, so nothing durable exists to be stolen or to lapse. C is the trap that feels responsible — encrypting a stored secret still leaves a standing credential that expires on a Sunday and can be reused if exfiltrated; the whole point of federation is that there is no such object. D describes a real and important control, but a different one: scope is about how much the connection may do (§3), not how it proves itself, and federation does not set scope for you. Microsoft states it plainly — workload identity federation "eliminates the need for secrets and secret management."
B — identity and blast radius are two separate controls, and only one has been fixed. Removing the secret means no durable credential can be stolen; it does nothing to limit what the credential may do once presented. A subscription-scoped connection is a keyless car that can still drive anywhere — a compromised task, a malicious fork build, or a fat-fingered YAML change now reaches every resource group at once. A is the dangerous half-truth this drill exists to break: "no secret" is necessary, not sufficient. C invents a token-lifetime failure that Azure DevOps handles internally, and D fabricates a link between scope and masking. Scope the connection to the one resource group it deploys, give each environment its own — the door stays narrow even after the lock is gone.
Narrow scope, secrets in the vault, pinned tasks with per-pipeline authorisation. Each shrinks what a compromised run can reach or reveal — the blast radius, the secret at rest, and the code the pipeline pulls in. The two rejects are the two most common self-inflicted wounds. Putting a secret in the YAML "because logs are masked" confuses two different exposures: masking hides a value in the run output, but the YAML itself is in the repo, readable by everyone who can clone it and permanent even after you delete the line — the secret was public the moment you committed it. And granting the connection to all pipelines to save a moment's friction is exactly how a malicious fork build or a forgotten test pipeline inherits production access; authorising each pipeline is the small tax that keeps the door narrow.
# plan: retire the shared azure-prod connection
1. Split azure-prod into one connection per environment,
each scoped to a single resource group.
2. For each new connection, generate a strong client
secret and store it in the connection; add a calendar
reminder to rotate every secret once a year.
3. Authorise each pipeline to its own connection
individually; stop granting access to all pipelines.
4. Move the database password out of the YAML into a
Key Vault-linked variable group.
Line two — and the tell is that it works hard to do the wrong thing. Generating a strong secret and diligently rotating it yearly feels like security theatre done well, but it rebuilds exactly the object the whole migration exists to delete: a stored, long-lived credential that will still expire on a bad morning and can still be exfiltrated and replayed. A yearly rotation only narrows the window; federation removes the window, because there is no secret — the connection carries a trust and mints a short-lived token per run. The correct line two is "convert each connection to workload identity federation; there is no secret to store." The other steps are right: several scoped connections beat one broad one (A is wrong), per-pipeline authorisation is the point not friction (C), and the database password belongs in Key Vault, never the YAML (D inverts §4). Rotating a secret you should not have is motion mistaken for progress.
A strong answer concedes before it argues. The friction is real and worth naming out loud: waiting on authorisation and narrow scope does slow a release, and a colleague fighting a stuck deploy at the wrong hour deserves agreement that the pain is legitimate. Refuse the framing and you lose the room; concede it and you earn the right to redirect.
Then price what the shortcut actually buys. Owner-on-the-subscription, granted to all pipelines, turns the most powerful credential in the building into a master key that every pipeline in the org — including a malicious pull-request build from a fork and every forgotten test pipeline — now holds. The day one of those is compromised, the incident is not "a release was slow," it is "an attacker had Owner on the whole subscription through our CI." You are trading a recurring five-minute wait for a standing, subscription-wide breach path. That is not a trade a team that trusts you with production would make, and saying so plainly is the job.
Close with the version that solves the friction the right way. The stuck release is almost never fixed by more power; it is fixed by the correct narrow one. Give the pipeline its own connection, federated so there is no secret, scoped to just the resource group it deploys, authorised to just that pipeline. It unblocks today's release and shrinks the door instead of widening it — and it gives you the sentence that ends the debate: we don't fix a slow deploy by making a breach bigger.
The trap is the premise — that a GitHub portfolio means an Azure DevOps deficit. Do not accept it and promise to "ramp up quickly"; that concedes you are behind. Reject it by demonstrating the concepts are the same and only the nouns differ, then prove it on the spot with the one thing this class is about.
Translate a credential, live. "On GitHub I wired a deploy to Azure with OIDC and zero stored secrets — the workflow requests a short-lived token, Entra checks it against a federated credential, and grants minutes of access. In Azure DevOps that same thing is a Resource Manager service connection using workload identity federation: same token exchange, same no-secret trust, same short-lived access — a service connection instead of a workflow permission, a federated credential on the identity instead of one on an app registration. I don't have to learn the idea; I have to learn where the button is." Naming the mapping out loud is the whole answer — it shows the portable thing was never the YAML dialect.
Close by turning the two platforms into an asset. "So the honest answer is: productive on day one for the decisions, a week for the dialect. And what you're actually getting is an engineer who has built the same pipeline on both systems — which means when someone asks whether we should be on Actions or Azure DevOps for a given workload, I can answer from having shipped on each, not from a blog post." That reframes the perceived gap into the rarest thing on the market — and it is true, which is why it survives the follow-up. The gap was never the missing noun; it was the proof, and now you carry it.
Five things worth carrying out of this class
- The pipeline is the most powerful credential in the building: it can deploy to production, unattended, in seconds. An attacker who reaches it needs nothing else. Securing it is the highest-return security work a cloud engineer does.
- A service connection is the named, permissioned bridge pipelines reference by name. Its identity can be a stored client secret (old — expires on a bad morning, can be stolen) or workload identity federation (current — no secret at all, a short-lived token exchange per run).
- Removing the secret is not the same as narrowing the door. Scope the connection's RBAC to a resource group, not the subscription; give each environment its own connection; authorise each pipeline individually. Identity and blast radius are two separate controls.
- No secret ever lives in the YAML or the logs — keep them in a Key Vault-linked variable group, and treat log masking as a net, not a licence. The pipeline is a supply chain: pin task versions, distrust third-party tasks, and enforce a shared template as a gate.
- The best secret is the one that does not exist. Replacing a shared, subscription-scoped, secret-bearing connection with federated, resource-group-scoped ones closes a whole class of recurring outage forever — and it is the sentence that ends this bootcamp.
- Microsoft's guidance is current as of this writing: workload identity federation is the recommended authentication for a new Azure Resource Manager service connection, and Azure DevOps ships a built-in tool to convert secret-bearing connections to it. The mechanics — the token exchange, the federated credential, the short-lived access — are the OIDC pattern of Class Twenty-Three, re-nouned; if you understood it there you already understand it here. Verify the exact portal steps on Microsoft Learn, because the connection-creation dialog changes more often than the concept does. ↩
- Treat "the logs are masked, so it's fine" as the single most dangerous sentence in this class, and treat any specific claim about what masking catches — including this page's — with suspicion. Masking is best-effort string replacement on emitted log lines; it does not cover values you transform, write to files, or exfiltrate deliberately, and the exact boundaries shift between agent and task versions. The safe rule does not depend on the details: keep the secret in the vault and never emit it, and you never have to know precisely where masking stops. ↩