Enabling keyless authentication from Azure DevOps Pipelines to Google Cloud

Azure DevOps is a widely adopted CI/CD platform among enterprise Google Cloud customers and developers. When an Azure Pipelines workflow needs to provision or mutate resources on Google Cloud—such as calling Google Cloud APIs, publishing a container image to Artifact Registry, updating a Google Kubernetes Engine (GKE) cluster, or deploying infrastructure with Terraform—it must first authenticate to Google Cloud.

Traditionally, authenticating from Azure DevOps to Google Cloud required exporting a long-lived JSON service account key and storing it in Azure Pipeline Library secure files or Azure Key Vault, turning an identity management problem into a secrets management problem. Not only did this introduce security risks if the service account key were ever leaked, but it also blocked developers whose organizations enforce Google Cloud security best practices by disabling service account key creation via the constraints/iam.disableServiceAccountKeyCreation organization policy constraint.

With OpenID Connect (OIDC) in Azure DevOps Service Connections and Google Cloud Workload Identity Federation, you can authenticate from Azure Pipelines to Google Cloud without exporting, storing, or rotating a single service account key.

Why Workload Identity Federation for Azure DevOps?

  • Fine-grained scoping: Workload Identity Pools and Providers let you define fine-grained attribute mappings and Common Expression Language (CEL) conditions between the Azure DevOps OIDC token and Google Cloud IAM permissions. Whereas an exported JSON service account key is an all-or-nothing bearer credential, Workload Identity Federation restricts authentication to a specific Azure DevOps organization, project, and service connection—and you can enforce pipeline-level approvals and branch checks directly on the Service Connection in Azure DevOps.
  • Short-lived credentials: Unlike static JSON service account keys, Workload Identity Federation exchanges the per-job Azure DevOps OIDC JWT ($idToken) for a short-lived Google Cloud OAuth 2.0 access token via the Google Cloud Security Token Service (STS). By default, these credentials expire after one hour, drastically reducing the blast radius of any compromised build environment.
  • Minimal management overhead: Static keys must be securely stored, audited, and rotated across every pipeline. With Workload Identity Federation, credentials are minted on demand for each pipeline job, leaving zero static secrets to rotate or manage after the initial setup.

Step 1: Configure a Workload Identity Service Connection in Azure DevOps

Because Azure DevOps issues pipeline OIDC tokens through Azure Resource Manager (ARM) service connections, you create an ARM service connection configured for Workload identity federation.

Tip: Do I need an Azure Subscription if I only deploy to Google Cloud?
No. If you select App registration or managed identity (manual) as the identity type, Azure DevOps immediately generates the OIDC Issuer and Subject identifier on the creation screen without requiring an active Azure Subscription. If your organization already links Azure DevOps to Microsoft Entra ID and an Azure Subscription, you can also use App registration (automatic).

  1. Sign in to your Azure DevOps organization and select your project.
  2. In the bottom-left corner, click Project settings → Service connections (under Pipelines).
  3. Click New service connection, select Azure Resource Manager, and click Next.
  4. Configure the connection using either the Manual or Automatic workflow:
    • Option A — Manual (No Azure Subscription required):
      1. Under Identity type, select App registration or managed identity (manual).
      2. Under Credential, select Workload identity federation.
      3. Enter a Service connection name (for example, google-cloud-wif). Leave Grant access permission to all pipelines unchecked if you want to authorize only specific pipelines.
      4. Click Next. Copy and save the generated Issuer and Subject identifier displayed under Workload Identity federation details. (To finish saving a manual connection in Azure DevOps without an Azure subscription, enter your Entra Tenant ID and placeholder GUIDs 00000000-0000-0000-0000-000000000000 for Subscription ID and Application client ID, then click Save Without Verification).
    • Option B — Automatic (Uses Microsoft Entra ID & Azure Subscription):
      1. Under Identity type, select App registration (automatic) and set Credential to Workload identity federation.
      2. Select your Subscription (and optional Resource group), enter the Service connection name (google-cloud-wif), and click Save.
      3. Open the newly created google-cloud-wif service connection and copy the Issuer and Subject identifier from the Overview tab.

Understanding Azure DevOps Issuer, Subject, and Audience formats

Microsoft has transitioned Azure DevOps Workload Identity Federation service connections from the legacy vstoken.dev.azure.com issuer to the Microsoft Entra v2.0 issuer (login.microsoftonline.com). Depending on when your Azure DevOps organization or service connection was created, you will see one of two OIDC formats:

Format OIDC Issuer (iss) OIDC Audience (aud) OIDC Subject Identifier (sub) Length & Google Cloud Impact
Microsoft Entra Issuer (Default since Nov 2025) https://login.microsoftonline.com/<TENANT_ID>/v2.0 fb60f99c-7a34-4190-8149-302f77469936 (Entra v2.0 App ID for api://AzureADTokenExchange) /eid1/c/pub/t/<TENANT_B64>/a/<APP_B64>/sc/<ORG_ID>/<SC_ID> ~138 bytes — Exceeds Google Cloud’s 127-byte google.subject limit unless extracted via CEL (assertion.sub.extract('/sc/{sc_id}')).
Legacy Azure DevOps Issuer (Deprecated July 2026; retires July 2027) https://vstoken.dev.azure.com/<ORG_ID> api://AzureADTokenExchange sc://<ORG_NAME>/<PROJECT_NAME>/<CONNECTION_NAME> ~40–60 bytes — Fits within 127 bytes directly.

The gcloud configuration in Step 2 below handles both formats seamlessly using --allowed-audiences="api://AzureADTokenExchange,fb60f99c-7a34-4190-8149-302f77469936" and a single CEL expression.


Step 2: Set Up Workload Identity Federation in Google Cloud

Now configure Google Cloud to trust the OIDC tokens issued by your Azure DevOps service connection.

1. Set environment variables and enable required APIs

Open a terminal with the Google Cloud CLI (gcloud) authenticated to your target project and export the variables below using the Issuer and Subject identifier copied from Azure DevOps. Then enable the IAM, Security Token Service (STS), and Resource Manager APIs:

export PROJECT_ID="your-gcp-project-id"
export PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)")
export POOL_ID="azure-devops-pool"
export PROVIDER_ID="azure-devops-provider"
export SERVICE_ACCOUNT_NAME="my-service-account"

# Paste the exact Issuer and Subject identifier from your Azure DevOps Service Connection:
export ADO_ISSUER_URI="https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0"
export ADO_FULL_SUBJECT="/eid1/c/pub/t/YOUR_TENANT_B64/a/YOUR_APP_B64/sc/YOUR_ORG_ID/YOUR_SC_ID"

# Enable required Google Cloud APIs
gcloud services enable \
  iam.googleapis.com \
  iamcredentials.googleapis.com \
  sts.googleapis.com \
  cloudresourcemanager.googleapis.com \
  --project="${PROJECT_ID}"

2. Create the Google Cloud Service Account and grant least-privilege roles

Create a dedicated Google Cloud Service Account for your Azure DevOps pipeline and grant it only the IAM roles required for your workload (for example, roles/secretmanager.secretAccessor to read secrets or roles/storage.admin to manage Cloud Storage buckets):

gcloud iam service-accounts create "${SERVICE_ACCOUNT_NAME}" \
  --project="${PROJECT_ID}" \
  --display-name="Azure DevOps Workload Identity Service Account"

export SA_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

# Grant resource permissions to the Service Account (follow least privilege)
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --member="serviceAccount:${SA_EMAIL}" \
  --role="roles/secretmanager.secretAccessor"

3. Create the Workload Identity Pool and OIDC Provider

Google Cloud’s google.subject attribute has a maximum length of 127 bytes. Because Microsoft Entra-backed Azure DevOps subject identifiers (/eid1/c/pub/t/.../sc/<ORG_ID>/<SC_ID>) are 138 bytes long—and because Entra v2.0 OIDC tokens resolve api://AzureADTokenExchange to its first-party Microsoft Application ID GUID (fb60f99c-7a34-4190-8149-302f77469936) in the aud claim—we configure three controls on the OIDC provider:

  1. --allowed-audiences: Includes both api://AzureADTokenExchange and fb60f99c-7a34-4190-8149-302f77469936 so Google Cloud STS accepts tokens from both Microsoft Entra v2.0 and legacy Azure DevOps issuers.
  2. --attribute-mapping: Uses CEL assertion.sub.contains('/sc/') ? assertion.sub.extract('/sc/{sc_id}') : assertion.sub to extract the unique <ORG_ID>/<SC_ID> suffix (73 bytes) into google.subject for new Entra-backed connections, while passing legacy sc://... subjects through unchanged.
  3. --attribute-condition: Evaluates assertion.sub == '${ADO_FULL_SUBJECT}' against the entire un-truncated 138-byte sub claim before any token exchange is permitted.

Run the following commands to create the Workload Identity Pool and OIDC Provider:

# 1. Create the Workload Identity Pool
gcloud iam workload-identity-pools create "${POOL_ID}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --display-name="Azure DevOps Pool"

# 2. Create the OIDC Provider with CEL extraction, full subject condition, and both Entra v2.0 + legacy audiences
gcloud iam workload-identity-pools providers create-oidc "${PROVIDER_ID}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="${POOL_ID}" \
  --display-name="Azure DevOps OIDC Provider" \
  --issuer-uri="${ADO_ISSUER_URI}" \
  --allowed-audiences="api://AzureADTokenExchange,fb60f99c-7a34-4190-8149-302f77469936" \
  --attribute-mapping="google.subject=assertion.sub.contains('/sc/') ? assertion.sub.extract('/sc/{sc_id}') : assertion.sub" \
  --attribute-condition="assertion.sub == '${ADO_FULL_SUBJECT}'"

4. Allow the Azure DevOps identity to impersonate the Service Account

Compute the mapped subject (<ORG_ID>/<SC_ID> for Microsoft Entra issuers, or sc://... for legacy issuers) and bind roles/iam.workloadIdentityUser on the Service Account using the singular principal:// prefix:

# Compute the mapped google.subject value (<= 127 bytes)
if [[ "${ADO_FULL_SUBJECT}" == *"/sc/"* ]]; then
  export MAPPED_SUBJECT="${ADO_FULL_SUBJECT#*/sc/}"
else
  export MAPPED_SUBJECT="${ADO_FULL_SUBJECT}"
fi

echo "Mapped google.subject (${#MAPPED_SUBJECT} bytes): ${MAPPED_SUBJECT}"

# Grant roles/iam.workloadIdentityUser on the Service Account to the federated principal
gcloud iam service-accounts add-iam-policy-binding "${SA_EMAIL}" \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/subject/${MAPPED_SUBJECT}"

Important: Always use singular principal:// (not principalSet://) when referencing /subject/${MAPPED_SUBJECT}. principalSet:// is only valid when matching custom attributes (/attribute.<name>/<value>), groups (/group/<value>), or all pool identities (/*).


Step 3: Authenticate from Azure Pipelines to Google Cloud

With Workload Identity Federation configured in both Azure DevOps and Google Cloud, your Azure Pipelines jobs can now authenticate without any stored keys. Below are two common patterns:

  • Option 1 (CLI & REST APIs): Authenticate gcloud and mint a short-lived OAuth 2.0 access token to call Google Cloud REST APIs directly via curl.
  • Option 2 (Application Default Credentials with Client Libraries & Terraform): Export GOOGLE_APPLICATION_CREDENTIALS so tools like Terraform and Google Cloud Client Libraries authenticate automatically.

Option 1: Authenticating gcloud CLI and calling Google Cloud REST APIs

In the pipeline below, the AzureCLI@2 task with addSpnToEnvironment: true exposes the signed OIDC JWT in $idToken. The script saves $idToken to a local file in $(Pipeline.Workspace), generates an external account credential configuration file with gcloud iam workload-identity-pools create-cred-config, and mints a short-lived OAuth 2.0 access token to invoke the Secret Manager API:

# azure-pipelines.yml
trigger:
- main

pool:
  vmImage: 'ubuntu-latest'

variables:
  ADO_SERVICE_CONNECTION: 'google-cloud-wif'
  GCP_PROJECT_ID: 'your-gcp-project-id'
  GCP_PROJECT_NUMBER: '123456789012'
  GCP_WIF_POOL: 'azure-devops-pool'
  GCP_WIF_PROVIDER: 'azure-devops-provider'
  GCP_SERVICE_ACCOUNT: 'my-service-account@your-gcp-project-id.iam.gserviceaccount.com'
  GOOGLE_APPLICATION_CREDENTIALS: '$(Pipeline.Workspace)/.gcp_wif_credentials.json'
  OIDC_TOKEN_FILE: '$(Pipeline.Workspace)/.ado_oidc_token.jwt'

steps:
- task: AzureCLI@2
  displayName: 'Authenticate to Google Cloud via Workload Identity Federation'
  inputs:
    azureSubscription: $(ADO_SERVICE_CONNECTION)
    addSpnToEnvironment: true
    scriptType: 'bash'
    scriptLocation: 'inlineScript'
    inlineScript: |
      set -euo pipefail
      if [ -z "${idToken:-}" ]; then
        echo "ERROR: idToken is empty. Ensure the service connection uses Workload Identity Federation and addSpnToEnvironment is true."
        exit 1
      fi
      printf "%s" "$idToken" > "$(OIDC_TOKEN_FILE)"
      chmod 600 "$(OIDC_TOKEN_FILE)"

      # Generate the Google Cloud external_account credential configuration file
      gcloud iam workload-identity-pools create-cred-config \
        "projects/$(GCP_PROJECT_NUMBER)/locations/global/workloadIdentityPools/$(GCP_WIF_POOL)/providers/$(GCP_WIF_PROVIDER)" \
        --service-account="$(GCP_SERVICE_ACCOUNT)" \
        --credential-source-file="$(OIDC_TOKEN_FILE)" \
        --output-file="$(GOOGLE_APPLICATION_CREDENTIALS)"

      # Authenticate the gcloud CLI
      gcloud auth login --cred-file="$(GOOGLE_APPLICATION_CREDENTIALS)" --quiet
      gcloud auth list

      # Mint a short-lived OAuth 2.0 access token and call a Google Cloud REST API (e.g., Secret Manager)
      ACCESS_TOKEN=$(gcloud auth application-default print-access-token)
      curl -sS "https://secretmanager.googleapis.com/v1/projects/$(GCP_PROJECT_ID)/secrets/my-secret/versions/latest:access" \
        --header "Authorization: Bearer ${ACCESS_TOKEN}"

Option 2: Authenticating client libraries and Terraform via Application Default Credentials

Because GOOGLE_APPLICATION_CREDENTIALS points to the generated Workload Identity Federation configuration file in $(Pipeline.Workspace), any tool built on Google Cloud Application Default Credentials (ADC)—including Google Cloud client libraries (Go, Python, Java, Node.js, .NET) and Terraform—automatically exchanges the OIDC token in the background with zero code or provider block changes.

For example, define a main.tf file without any hardcoded credentials field:

# main.tf — No "credentials" attribute needed; Terraform automatically uses GOOGLE_APPLICATION_CREDENTIALS
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 5.0"
    }
  }
}

provider "google" {
  project = var.project_id
  region  = "us-central1"
}

resource "google_storage_bucket" "demo_bucket" {
  name                        = var.bucket_name
  location                    = "US"
  uniform_bucket_level_access = true
}

Then add the Terraform steps after the Workload Identity Federation authentication step in your azure-pipelines.yml:

# Add these steps after the "Authenticate to Google Cloud via Workload Identity Federation" step:
- task: TerraformInstaller@1
  displayName: 'Install Terraform'
  inputs:
    terraformVersion: 'latest'

- script: |
    set -euo pipefail
    terraform init
    terraform plan \
      -var="project_id=$(GCP_PROJECT_ID)" \
      -var="bucket_name=ado-wif-demo-$(GCP_PROJECT_ID)-$(Build.BuildId)" \
      -out=tfplan
    terraform apply -auto-approve tfplan
  displayName: 'Terraform Init, Plan, and Apply'

Note: Using a Manual Service Connection without an Azure Subscription?
The AzureCLI@2 task always runs az login --service-principal against Microsoft Entra ID before executing your inlineScript. If you saved your Manual Service Connection with placeholder GUIDs (00000000-0000-0000-0000-000000000000) because you do not have an Azure Subscription, use a Bash@3 step that requests the OIDC token directly from Azure DevOps’s built-in $(System.OidcRequestUri) REST endpoint:

jobs:
- job: AuthenticateToGCP
  uses:
    endpoints:
    - $(ADO_SERVICE_CONNECTION)
  steps:
  - task: Bash@3
    displayName: 'Fetch Azure DevOps OIDC Token & Configure Google Cloud WIF'
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)
    inputs:
      targetType: 'inline'
      script: |
        set -euo pipefail
        OIDC_URL="$(System.OidcRequestUri)?api-version=7.1&serviceConnectionId=$(ADO_SERVICE_CONNECTION_ID)"
        ID_TOKEN=$(curl -sS -X POST -H "Authorization: Bearer ${SYSTEM_ACCESSTOKEN}" \
          -H "Content-Type: application/json" -d '{}' "${OIDC_URL}" | jq -r '.oidcToken')
        printf "%s" "${ID_TOKEN}" > "$(OIDC_TOKEN_FILE)"
        chmod 600 "$(OIDC_TOKEN_FILE)"
        gcloud iam workload-identity-pools create-cred-config \
          "projects/$(GCP_PROJECT_NUMBER)/locations/global/workloadIdentityPools/$(GCP_WIF_POOL)/providers/$(GCP_WIF_PROVIDER)" \
          --service-account="$(GCP_SERVICE_ACCOUNT)" \
          --credential-source-file="$(OIDC_TOKEN_FILE)" \
          --output-file="$(GOOGLE_APPLICATION_CREDENTIALS)"

Authenticating from Azure DevOps Pipelines to Google Cloud without a long-lived JSON service account key eliminates one of the most common sources of credential leakage in multi-cloud CI/CD environments. By combining Azure DevOps OIDC service connections with Google Cloud Workload Identity Federation and CEL attribute conditions, your pipelines gain short-lived, cryptographically verifiable credentials with zero secret rotation overhead.

To learn more, check out the Workload Identity Federation documentation and best practices for using Workload Identity Federation.

1 Like