Jonathan's Blog
Azure DevOps Pulumi Cloud OIDC Setup

Using OIDC Authentication Between Azure DevOps and Pulumi Cloud

Published

Why

When integrating Azure DevOps (ADO) with Pulumi Cloud, a common approach is to generate a Pulumi access token and store it as a secret in your pipeline. While this works, it introduces another credential that must be managed, rotated, and protected.

Modern CI/CD platforms are increasingly moving away from long-lived secrets in favor of federated identity. ADO supports OpenID Connect (OIDC), allowing pipelines to request short-lived identity tokens at runtime. Instead of storing a Pulumi access token, ADO can prove its identity directly to Pulumi Cloud and receive temporary credentials for the duration of the pipeline run.

This approach provides several benefits:

  • Eliminates long-lived Pulumi access tokens from your pipeline
  • Reduces the risk of credential leakage
  • Uses short-lived credentials that automatically expire
  • Simplifies credential management across multiple projects
  • Follows modern cloud authentication best practices

In this article, we'll configure a trust relationship between ADO and Pulumi Cloud so that pipeline executions can be authenticated without storing any Pulumi secrets.

For simplicity, I'm going to assume you already have a federated service connection in ADO that authenticates with Azure.


How

Here's how we will do this:

  1. Configure Pulumi Cloud to trust ADO
  2. Set a policy in Pulumi to validate the token
  3. Request an OIDC token in the pipeline
  4. Log in with this token to Pulumi
  5. Finally, run Pulumi commands in the pipeline

Before we start clicking through configuration screens, it helps to understand the authentication flow. Azure DevOps already has a trust relationship with Azure through the federated service connection. We'll leverage that same identity to authenticate to Pulumi Cloud without storing a Pulumi access token.

---
config:
  theme: 'dark'
---
flowchart LR
    AZ[Azure]
    ADO[Azure DevOps Pipeline]
    PC[Pulumi Cloud]

    AZ -->|Federated Service Connection| ADO
    ADO -->|OIDC Token| PC

    ADO -->|pulumi up| AZ
    PC -->|Authentication & State| ADO

What

Take a look at the service connection in Azure DevOps that you use to connect to Azure (Azure Resource Manager).

Getting to the service connection details

You can do that by going to the project settings [1] and "Service connections" [2].

The "Issuer" (iss) and "Subject identifier" sub will be listed here [3]. These details, along with the "Audience" (aud) can also be on the App Registration [4] itself under the "Certificates & secrets" blade and the "Federated credentials" tab.

Note: I found that since I had left the Audience field blank when I created the App Reg, it defaulted to fb60f99c-7a34-4190-8149-302f77469936 which is a magic GUID for api://AzureADTokenExchange

Armed with this information, we can head over to the Pulumi Cloud platform and set up the OIDC issuer.

How to get to the Access management screen in Pulumi Cloud
  1. Make sure that you have the correct organization or user selected
  2. Select Settings
  3. Select "Access management"
Register a new issuer

On the access management screen:

  1. Select "OIDC Issuers"
  2. Then "Register issuer"
OIDC Issuer details

The name field can be whatever makes sense to your organization and is for display purposes only.

The URL field is the Issuer from ADO.

You can read more about the "Max expiration" and "Thumbprints" from Pulumi's documentation.

Note: You cannot change these fields after the issuer has been created. You can regenerate the thumbprints.

Next, we need to set the access policies. These policies are how Pulumi and ADO will agree that the token is valid.

Set the "Decision" to "Allow". Select the appropriate "Token type". Set "Role" to "Member".

For the rules, I added two. One was aud for the audience and iss for the issuer. You could probably add more like the sub , but these two will most likely be sufficient.

Save the policy, and we're done with Pulumi Cloud for now.

Pipelines

Now we can turn our attention to the pipelines.

Create a pipeline with an AzureCLI task. Make sure to enable access to the service principal. In YAML you can do this with addSpnToEnvironment: true ⁣under the inputs. In the classic release pipelines, you find that under "Advanced" and check the box labeled "Access service principal details in script".

If you plan to use Pulumi's CLI directly, then this script shows how to log in to Pulumi Cloud.

Note that there's also --oidc-team, --oidc-user instead of --oidc-org.

steps:
  - task: AzureCLI@2
    displayName: 'Pulumi Cloud OIDC'
    inputs:
      azureSubscription: 'Pulumi-ADO-Federated-Connection'
      scriptType: pscore
      scriptLocation: inlineScript
      addSpnToEnvironment: true
      visibleAzLogin: false
      inlineScript: |
        $idToken = $env:idToken

        Write-Host "OIDC token returned. Setting output var 'PulumiOidcToken'"
        Write-Host "##vso[task.setvariable variable=PulumiOidcToken]$idToken"

        Write-Host "Attempting login"
        pulumi login --oidc-token $idToken --oidc-org JpPulumiBlog

        Write-Host "Who Am I?"
        pulumi whoami

        Write-Host "Listing projects"
        pulumi project ls

Pulumi also has a dedicated ADO task that you can use. To use it, you need to set the token as an output variable and provide that variable to the "Login Args". That looks something like this:

steps:
  - task: AzureCLI@2
    displayName: 'Pulumi Cloud OIDC'
    inputs:
      azureSubscription: 'Pulumi-ADO-Federated-Connection'
      scriptType: pscore
      scriptLocation: inlineScript
      addSpnToEnvironment: true
      visibleAzLogin: false
      inlineScript: |
        $idToken = $env:idToken

        Write-Host "OIDC token returned. Setting output var 'PulumiOidcToken'"
        # This sets a pipeline variable called 'PulumiOidcToken' to the toke value
        Write-Host "##vso[task.setvariable variable=PulumiOidcToken]$idToken"
  - task: pulumi.build-and-release-task.custom-build-release-task.Pulumi@1
    displayName: 'Run pulumi preview'
    inputs:
      azureSubscription: 'Pulumi-ADO-Federated-Connection'
      command: preview
      loginArgs: '--oidc-token $(PulumiOidcToken) --oidc-org JpPulumiBlog'
      stack: dev
      # You will need to point this to the folder where your Pulumi.yaml file lives
      cwd: '[path to Pulumi project]'

Troubleshooting

If authentication isn't working as expected, there are a few common areas to check.

Token exchange failed

If you get an error OIDC token exchange failed: Post "/api/oauth/token": unsupported protocol scheme "" then you may need to explicitly set the backend with pulumi login https://api.pulumi.com ....

Invalid issuer

If Pulumi reports that the issuer is invalid or untrusted, verify that the issuer URL configured in Pulumi Cloud exactly matches the iss claim in the token issued by ADO.

Audience mismatch

If authentication fails due to an audience validation error, inspect the aud claim in the token and compare it with the value used in your Pulumi access policy. In many Azure DevOps federated credential configurations, the audience defaults to api://AzureADTokenExchange (represented internally by the GUID fb60f99c-7a34-4190-8149-302f77469936).

Access denied

If the token is accepted but access is denied, review the access policy associated with the OIDC issuer. Ensure the policy decision is set to Allow, the correct token type is selected, and the assigned role has sufficient permissions for the actions being performed.

Inspecting the token

When troubleshooting, it can be helpful to inspect the token claims directly. You can decode the JWT and verify the values for:

  • iss (Issuer)
  • aud (Audience)
  • sub (Subject)

These values should match the expectations configured in your Pulumi OIDC issuer and access policies.

Note: ADO will mask these values in the logs so it's better to get them from the service connection, the app registration or by sending the JTW to a local API to decode it.

Verify authentication independently

Before running pulumi up, consider validating authentication separately pulumi whoami.

If this command succeeds, Pulumi has successfully authenticated and any subsequent issues are likely related to your stack configuration or Azure permissions rather than OIDC authentication.


Conclusion

By configuring Pulumi Cloud to trust ADO through OIDC, we've eliminated the need to store a long-lived Pulumi access token in our pipeline. Instead, ADO can obtain a short-lived identity token from the existing federated service connection and use that token to authenticate with Pulumi Cloud at runtime.

While the initial setup requires a little more configuration than generating an access token, the result is a more secure and maintainable authentication model. There are no Pulumi secrets to rotate, no credentials to distribute across projects, and a significantly lower risk of a leaked token granting long-term access.

PulumiAzure DevOpsSecurityOIDC

Remember to share this post!

X LinkedIn

Jonathan Peterson

Fifteen years of web development experience on the Microsoft tech stack creating both internal enterprise applications and public-facing websites.