Our new goMISP service offers fully managed, cloud-based and secure MISP instances – ready to use from day one.
Azure DevOps has quietly become one of the most powerful systems in many enterprises. It stores the source code, it builds and deploys the software, and it stores the credentials that are used to deploy into production. In other words:
Whoever controls Azure DevOps controls a large part of your IT landscape.
In this blog we explain:
How Azure DevOps is structured.
Which misconfigurations we find again and again during penetration tests.
Why they are dangerous and what Microsoft itself recommends to fix them.
In our article Software Build Pipeline = The Finest Hacker Target we described why CI/CD build pipelines are such an attractive target. The key points were:
A build pipeline is the assembly line of your software. If an attacker manipulates the assembly line, your company distributes malware under its own trusted brand – through its own trusted delivery channels, straight into its own highly protected production environments.
Build systems are vulnerable by design: they take code written by developers and execute it, usually with high privileges. The core security questions are therefore not if we can keep attackers out, but rather what happens if a developer account is misused. Do we have controls that contain it?
A build landscape consists of three major components: the source code repository, the build scheduler and the binary repository. The build scheduler sits in the middle and needs privileged access to everything. Which is why it must be the center of every security assessment.
Azure DevOps is especially interesting because it implements all three components in one single platform, plus the credential store that connects them to your cloud environment:
This consolidation is convenient for development teams – and convenient for attackers. In a classic landscape an attacker who compromises Jenkins still has to find a way into Artifactory and into the cloud. In Azure DevOps, the code, the build engine and the deployment credentials sit behind one permission model. A single misconfigured setting can therefore have an organization-wide effect.
For further official documentation, everything Microsoft publishes on this topic starts at Make your Azure DevOps secure and Secure your Azure Pipelines.
To understand where things go wrong, you need a rough mental model of the platform. Azure DevOps is a hierarchy, and every level has its own permissions.
Azure DevOps organizations should be connected to your Entra ID tenant. Entra ID decides who may sign in: user accounts, group memberships, multi-factor authentication, Conditional Access policies and just-in-time administration via Privileged Identity Management. (Connect your organization to Microsoft Entra ID)
Important to understand: Entra ID only controls the front door. What a user may do inside Azure DevOps is decided by Azure DevOps permissions. And those are managed by your project teams, not by your identity team.
The organization is the top-level container (historically called a "project collection"). Three things live here:
Organization policies – for example whether users may create personal access tokens (PATs), whether third-party applications may connect via OAuth, whether external guests may be invited. (Settings overview)
Organization-wide security groups – above all Project Collection Administrators, who can do practically anything in the entire organization. (Look up Project Collection Administrators)
The Project Collection Build Service – a technical identity under which pipelines can run. It is organization-wide, which means it can reach into other projects. This becomes important in misconfiguration #5. (Scoped build identities)
An organization contains many projects. Each project ships with built-in security groups: Readers, Contributors, Build Administrators, Release Administrators and Project Administrators. (Default permissions quick reference)
The group that matters most for security is Contributors. By default it is allowed to push code, to edit build pipelines and to queue builds. In other words: Every member of Contributors can define what code is executed on your build agents and then start that execution. This is not a bug, it is the design of a self-service build platform. But it means that the group membership of "Contributors" is effectively a list of people who can run arbitrary code in your build environment.
Inside a project you find:
Repositories with their own permissions and branch policies (the technical implementation of the four-eyes principle). (Branch policies)
Pipelines, either as YAML (the definition is a file inside the repository) or as Classic pipelines (clicked together in the Azure DevOps web UI). Microsoft's security model is built around YAML. (Use YAML pipelines instead of Classic pipelines)
Protected resources – variable groups, service connections, environments, agent pools and secure files. Microsoft calls them protected because access to them can be restricted to named pipelines and guarded by approvals and checks. (Resource security)
Two sentences summarise the whole security problem of the platform:
A pipeline run executes whatever its definition says: The definition is usually a file in a Git repository. At runtime, that pipeline is handed every secret and every credential it is allowed to use.
Everything that follows is about keeping those two sentences from turning into an incident.
The following five findings are the ones we see in nearly every Azure DevOps assessment. They are listed roughly in order of impact. For each one we explain the concept, how the misconfiguration typically comes about, what an attacker can do with it, and what Microsoft officially recommends.
The concept. Permissions in Azure DevOps are granted through security groups at organization, project and object level. The default project group Contributors holds three permissions that are harmless on their own but explosive in combination: Contribute (push code), Edit build pipeline and Queue builds.
How it happens. Organizations grow. New colleagues, contractors and partner companies are added to "Contributors" because that is the group that lets people work. Nobody revokes access when a project ends. On top of that, administrators often grant individual permissions directly to single users ("just this once, for this one repository") instead of using groups. Those individual entries are never reviewed again. In the assessments we performed, we regularly found 50–90 accounts with write access per project, a significant share of them external, plus well over 100 individual permission entries granted directly to named users.
Why it is risky. Anyone in this group can create a new branch, change the pipeline definition on that branch, and start the pipeline manually. No pull request, no review, no merge into main is required, because a pipeline can be run against any branch. Whatever script the attacker puts into the pipeline is executed on the build agent. Together with all secrets and cloud credentials that this pipeline is allowed to use (see #2 and #3). Microsoft states this risk explicitly: "a user with Create Branch permission at the repository level can introduce code to the repo even if that user lacks Contribute permission".
Microsoft's recommendation.
Grant permissions through security groups, never to individual users, keep the groups as small as possible, and prefer inheritance over object-level exceptions. (Make your Azure DevOps secure – Scope permissions)
Review access regularly and remove users who no longer need it. Place external guests in a dedicated Entra ID group and re-evaluate it periodically. (Make your Azure DevOps secure – Manage external guests)
Reduce the membership of Project Collection Administrators to a documented minimum and replace permanent admin rights with just-in-time access through a Microsoft Entra PIM group. (Privileged Identity Management for groups)
Separate products and teams into separate projects, so that one team's pipelines cannot reach another team's resources. (Secure your Azure Pipelines)
The concept. Pipelines need credentials: API keys, storage tokens, database passwords. Azure DevOps offers variable groups for this purpose. Every variable in a group can be marked as secret, in which case the value is encrypted, no longer displayed in the UI and masked in log output. Even better, a variable group can be linked to an Azure Key Vault so that the secrets never live in Azure DevOps at all.
How it happens. Marking a variable as secret is a single checkbox but it is optional. During troubleshooting someone needs to see the value, unchecks the box, and never checks it again. Credentials also get pasted into inline PowerShell or Bash scripts inside pipeline definitions, into release definitions, into wiki pages and into work item comments, where no masking exists at all. And because the value "works", nobody touches it again for years.
Why it is risky. A variable that is not marked as secret is readable in clear text by everyone with read access to the project library. Typically every project member, including read-only accounts and external guests and it appears unmasked in build logs. In our assessments we found, among other things, active personal access token with near-administrative permissions across the entire organization and valid Azure Storage SAS tokens stored as a plain variable. Such a token turns a harmless read-only account into an organization-wide compromise in a single step.
Microsoft's recommendation.
Never store sensitive values as plain text. Use secret variables, and manage them in Azure Key Vault wherever possible, so that access is controlled by Azure role-based access control. (Secrets in Azure Pipelines)
The best way to protect a secret is not to have one. Replace stored credentials with workload identity federation (OIDC) or managed identities, which issue short-lived tokens instead of storing a password. (Secure your Azure Pipelines – Secure service connections)
Replace long-lived personal access tokens with Microsoft Entra tokens, service principals or managed identities. Where PATs remain, keep their lifetime short and their scopes minimal. (Authentication guidance)
Audit and rotate secrets regularly, and remove the ones that are no longer needed. Any credential that has been found in clear text must be treated as compromised and rotated.
Switch on secret scanning and push protection so that credentials are detected when they enter a repository. (GitHub Advanced Security for Azure DevOps – secret scanning)
The concept. Variable groups, service connections, environments, agent pools and secure files are protected resources. Each of them can either be authorized for specific, named pipelines, or opened up to every pipeline in the project via the Open access / Grant access permission to all pipelines setting. In addition, each resource can carry approvals and checks, for example a manual approval, or a branch control check that only permits pipelines running from an approved branch to use the resource.
How it happens. When a pipeline fails with "resource not authorized", the dialogue offers a convenient "permit" button. Under time pressure, people take the broad option. A new pipeline is created, it needs the same credentials, and the resource ends up being shared with everything. Approvals and checks, by contrast, have to be configured deliberately for every single resource, so in practice they are configured for almost none.
Why it is risky. An open resource removes the last barrier in the escalation chain shown above. Anyone who can create or edit any pipeline in the project automatically has access to all shared credentials of that project. In our assessments we found projects where secrets in multiple variable groups and service connections were open to every pipeline, including an organization-wide full-access token and Terraform service principal secrets. The effect is that dozens of developers and every automated process effectively hold production cloud credentials. Microsoft's own documentation is blunt about it: "If you only name the variable group in YAML pipelines, anyone who can push code to your repository could extract the contents of secrets in the variable group." (Variable groups – authorize for pipelines)
Microsoft's recommendation.
Do not enable Open access. Microsoft states directly: "For better security, don't enable Open access, which allows all pipelines in the project to use the resource." Authorize only the specific pipelines you trust. (Resource security – permissions)
Add approvals and checks on sensitive resources, and configure them so that the person who started the run cannot approve their own run. (Define approvals and checks)
Add a branch control check so that a service connection or production environment can only be used by pipelines running from an approved branch. This extends your code review requirements to pipeline execution. (Secure your Azure Pipelines – Restrict service connections to specific branches)
Minimise the scope of service connections: scope them to a specific resource group, never grant broad Contributor rights on an entire subscription, prefer workload identity federation over stored secrets, and review the usage history to remove connections that are no longer used. (Minimize the scope of service connections)
The concept. In Azure DevOps the four-eyes principle has two independent halves:
Branch policies protect the code: they require a minimum number of reviewers on a pull request before it can be merged into a protected branch such as main.
Approvals and checks protect the execution: they require a human to approve before a pipeline may use a protected resource or deploy to an environment.
Most organizations implement the first half and skip the second.
How it happens. Branch policies are introduced once, usually on main, and then slowly eroded by day-to-day pressure: the option "Allow requestors to approve their own changes" is enabled because a small team wants to keep moving. The reset of approval votes on new pushes is switched off because re-approving is annoying. A broad group receives the permissions Bypass policies when completing pull requests or Edit policies so that urgent fixes can go through. Feature branches are left unprotected because "they get reviewed before the merge anyway".
Why it is risky. Each of these settings individually removes the separation of duties:
With self-approval enabled, one person can create, approve and merge their own change. Four eyes become two.
Without vote reset, an attacker can get a harmless change approved and then push additional commits to the same pull request.
With Bypass policies or Edit policies, a user can simply complete a pull request without review, or delete the policy.
And because pipelines can be started on any branch, an attacker does not even need to touch main: running a modified pipeline from an unprotected feature branch bypasses the code review entirely, which is exactly why the second half, approvals and checks on resources, matters.
In our assessments we regularly found all of these at once: self-approval enabled, a "power user" group holding bypass permissions on all repositories, and not a single approval check on any variable group, service connection or agent pool.
Microsoft's recommendation.
Require a minimum number of reviewers on protected branches, and switch off "Allow requestors to approve their own changes". Microsoft also offers "Prohibit the most recent pusher from approving their own changes" explicitly to enforce segregation of duties. (Branch policies – require a minimum number of reviewers)
Under When new changes are pushed, enable "Reset all approval votes" (or "Require at least one approval on every iteration") so that approvals expire when new commits arrive. (Branch policies – reset conditions)
Grant the permissions Bypass policies when pushing, Bypass policies when completing pull requests, Force push and Edit policies only in exceptional, documented cases. Never to broad groups, and not to administrators by default. (Set Git repository permissions)
Complement code review with manual approval checks and branch control checks on protected resources and environments, so that reviewing code and running privileged pipelines are two separate decisions. (Define approvals and checks)
The concept. Azure DevOps ships a small set of organization- and project-level switches under Settings → Pipelines → Settings that massively limit the blast radius of a compromised pipeline. The most important one is the job authorization scope. Every pipeline job receives an access token. That token is either project-scoped (it can only touch resources of its own project) or collection-scoped (it can touch resources in every project of the organization). (Job access tokens and job authorization scope)
How it happens. Organizations created before Microsoft changed the defaults still run with the old, permissive settings, and nobody ever revisits them. Older pipelines sometimes genuinely relied on cross-project access, so the setting was turned off once and stayed off for everything. These switches are also invisible in day-to-day work: nothing breaks when they are disabled, so nobody notices.
Why it is risky. Microsoft describes the consequence in unusually direct words: "If the scope is not restricted at either the organization level or project level, then every job in your YAML pipeline gets a collection scoped job access token. In other words, your pipeline has access to any repository in any project of your organization. If an adversary is able to gain access to a single pipeline in a single project, they will be able to gain access to any repository in your organization." In practice this means a developer in a low-criticality project can use a pipeline to read (and, where the build identity has write permissions, modify) the repositories of unrelated, business-critical projects. We have demonstrated exactly this lateral movement in client environments.
Microsoft's recommendation. Enable the following settings, ideally at organization level (Organization settings → Pipelines → Settings):
If you take away only three things from this article:
Azure DevOps is production infrastructure, not a developer tool. It holds the code, it executes it, and it stores the credentials to your cloud environment.
The individual settings look harmless. The risk comes from the combination: broad write permissions × pipelines defined in the repository × resources shared with all pipelines × hardening switches left off.
Microsoft gives you all the controls you need. Scoped permissions, Key Vault-backed secrets, named pipeline authorizations, approvals and branch control checks, project-scoped build identities. They just have to be switched on - and verified.
That verification is exactly what we do. In a security review of your Azure DevOps environment we go through the organization, the projects, the repositories, the pipelines and every protected resource, we map the findings against Microsoft's official best practices, and (where it is in scope) we demonstrate the actual attack paths, so that your teams can see what a single developer account is really able to reach.
Interested? Let's have an informal first conversation. Tell us a bit about your environment and we will walk you through how such a review works, what it covers, and what you would get out of it. No obligations.
📧 contact@wallsec.de