A personal access token in an Azure DevOps variable group is a standing secret with a person’s name on it. When that person leaves, the token often stays. When the token is scoped broadly, every pipeline that inherited the group can reach AWS or the repo as that person. Rutagon builds Azure DevOps PAT elimination as a pipeline cutover to workload identity federation: the job assumes an AWS role with an OIDC token, then the PAT that existed to mint AWS keys—or to act as a standing cloud identity—is deleted.
We work in commercial Azure DevOps organizations. We do not claim Azure Government delivery. The AWS side is the same OIDC provider and role model we use for other CI systems. The Azure DevOps service connection and the subject claims are the part we wire for this buyer.
This is not a tour of every Azure DevOps feature. It is PAT removal on the pipelines that still inject AWS_ACCESS_KEY_ID or use a PAT as the only identity to call AWS.
Azure DevOps PAT Elimination With Workload Identity Federation
Azure DevOps PAT elimination starts with an inventory of variable groups, service connections, and library secrets that look like PATs or IAM user keys. We map each to the pipelines that consume them. A PAT that only clones a private module is a different row than a PAT that was used to generate long-lived AWS keys. Both can go. The AWS row is the one this pilot closes first.
Workload identity federation here means the pipeline presents an OIDC token and AWS STS returns temporary credentials for a role. The service connection is configured for that federation. The AWS provider or CLI in the job uses the assumed role. No PAT is required for that assume.
We already document replacing AWS access keys with OIDC as the general pattern. This article is the Azure DevOps-shaped instance: subjects, audiences, and the place the PAT used to live.
What We Replace on the Pipeline
Jobs that called aws configure with stored keys stop doing that. Jobs that used a PAT to hit an internal broker that then handed out keys stop doing that too. The broker was a PAT with extra steps. The role trust lists the Azure DevOps issuer and the repository or project subjects you intend.
We keep plan and apply, or build and deploy, on separate roles when the org already splits those stages. We do not collapse them to save IAM objects. A build job that can produce production credentials is the same class of mistake as a shared PAT.
GitLab OIDC is a sibling for teams that run both GitLab and Azure DevOps. We do not copy a GitLab trust policy onto an Azure DevOps issuer. Wrong issuer, failed assume, PAT left “just for now.”
pipeline: deploy-prod
was:
variable_group: aws-keys
pat: ado-aws-broker
now:
service_connection: aws-oidc-prod
role: ado-deploy-prod
prove:
pat_status: revoked
key_status: deleted
AWS documents OIDC identity providers for the trust. That is the AWS object we create. Azure DevOps owns the federated service connection on its side.
Deleting the PAT Is the Close
A green pipeline with the PAT still in the library is not elimination. We revoke the PAT in Azure DevOps, remove it from variable groups, and search remaining pipelines for the old secret names. We deactivate the IAM user keys the PAT was protecting. Evidence is the revoked PAT id (or last four), the empty variable group, CloudTrail for the role, and the key deletion time.
If a human still needs Azure DevOps API access for a break-glass script, that is a short-lived PAT with a ticket and an expiry, not a library value inherited by twenty pipelines. The standing one is the finding.
AWS cloud infrastructure is how we place the roles when deploy accounts are separate from the CI identity account.
Library groups, templates, and the PAT in a classic release
Azure DevOps will happily hide a PAT one layer below the YAML you reviewed. Variable groups linked as “library,” templates in another repo, and classic release definitions all inherit secrets. We expand every pipeline that deploys or applies infrastructure and we list every secret name that looks like a PAT or an AWS key. A YAML file that contains no secrets can still load a group that does. The inventory is the expanded graph, not the pretty YAML.
Classic releases are the usual residue. Teams migrate build YAML and leave a release pipeline on a PAT because “it still works.” That release is in the exception list with a retirement date or it is converted in the same engagement. We do not mark PAT elimination done while a classic definition can still publish with the old token.
Environment approvals do not replace federation. An approval gate in front of a PAT still sends the PAT. Approvals are fine as change control. They are not an identity. The job that runs after approve must assume the role.
We also search git history and secure files for PAT strings that were committed “just once.” Those need revoke even if the variable group is clean. A leaked PAT in an old commit is still valid until Azure DevOps says it is not. Revoke is the close; rewriting history is optional.
Self-hosted agents that store a PAT in a .env on disk are a third store. We treat the agent as a secret location. Federation should not require a disk token. If an agent still needs a token to talk to Azure DevOps itself, that token is scoped to agent registration, not to AWS, and it is not copied into the AWS variable group.
When the same org also runs GitLab, we keep the two OIDC providers separate. A shared IAM role trusted by both issuers is possible and usually unwise. Separate roles keep a stolen Azure DevOps token from matching a GitLab subject. The GitLab OIDC pattern stays on GitLab subjects.
After revoke we run a canary pipeline that must fail if someone pastes the old PAT back into a group. The canary looks for the old secret names and for AWS_ACCESS_KEY_ID in the job env. A green canary that still prints access key IDs is a failed close.
Frequently Asked Questions
Does this require Azure Government?
No. This write-up and the delivery it describes are for commercial Azure DevOps talking to AWS. We do not claim Azure Government experience. If your issuer URL differs, the trust policy must match the issuer you actually use.
Can we keep a PAT for Terraform module downloads only?
You can, and it should not be the same PAT that could change AWS. Scope it to the package or repo permission it needs, put an expiry on it, and keep it out of the AWS deploy group. The AWS path still federates.
What if some classic pipelines cannot use the federated connection?
Then those pipelines are on the exception list with an owner and a retirement date, or they get converted. Leaving classic releases on a PAT forever is how elimination fails. We do not pretend a YAML pipeline elsewhere covers them.
Is a service principal with a client secret better than a PAT?
A standing client secret is still a standing secret. Federation is the target. A secret is a bridge with a deletion date, the same as an IAM user key.
Talk to us about your credential elimination pilot. Call 907-841-8407, email contact@rutagon.com, or use rutagon.com/contact.