Run GraphWarden proxy deployments from Azure DevOps Pipelines

A complete Azure DevOps YAML pipeline that builds, tests, and smoke-tests a GraphWarden proxy deployment. Variable groups hold credentials and a banner comment prevents accidental commits of real secrets. The template below is runnable after replacing the marked values with your organization's tenant, client id, and secret.

Azure DevOps Pipelines with variable groups

Prerequisites

- An Azure DevOps organization and project where you can edit pipelines and create variable groups - A variable group named `graphwarden-secrets` containing `GRAPHWARDEN_PROXY_URL`, `GRAPHWARDEN_CLIENT_ID`, and `GRAPHWARDEN_CLIENT_SECRET` (covered on the secrets-management page in this section) - A Microsoft-hosted agent (the `ubuntu-latest` image used below) or a self-hosted agent that can reach your GraphWarden proxy URL

Add a GraphWarden smoke test with an AI assistant

Add a GraphWarden smoke test to my existing Azure DevOps YAML pipeline.

I want a pipeline stage that:
1. Acquires a token from my GraphWarden proxy's /oauth2/v2.0/token endpoint
2. Fails the build if token acquisition fails
3. Uses variable-group-backed secrets (not inline values)

Reference documentation: https://graphwarden.com/llms.txt

Ask me for my existing azure-pipelines.yml and whether my variable group is already configured.

Reference: llms.txt

The pipeline, end to end

The YAML below is a complete Azure DevOps pipeline. Copy it into a file named azure-pipelines.yml at the root of your repository. Do not edit the variable references — the pipeline reads every secret from the graphwarden-secrets variable group at run time.

# DO NOT COMMIT REAL SECRETS TO THIS FILE.
# Use the Azure DevOps UI to create a variable group "graphwarden-secrets"
# with GRAPHWARDEN_PROXY_URL, GRAPHWARDEN_CLIENT_ID, GRAPHWARDEN_CLIENT_SECRET.

trigger:
  branches:
    include:
      - main

variables:
  - group: graphwarden-secrets
  - name: dotnetVersion
    value: '8.0.x'

stages:
  - stage: BuildAndTest
    jobs:
      - job: Build
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: UseDotNet@2
            inputs:
              version: $(dotnetVersion)
          - script: dotnet restore
            displayName: 'Restore dependencies'
          - script: dotnet build --configuration Release --no-restore
            displayName: 'Build proxy'
          - script: dotnet test --configuration Release --no-build --logger trx
            displayName: 'Run proxy tests'
          - task: PublishTestResults@2
            condition: succeededOrFailed()
            inputs:
              testResultsFormat: 'VSTest'
              testResultsFiles: '**/*.trx'

  - stage: IntegrationTest
    dependsOn: BuildAndTest
    jobs:
      - job: SmokeTest
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: PowerShell@2
            displayName: 'Run GraphWarden smoke test'
            inputs:
              targetType: 'inline'
              script: |
                # Verify the proxy responds before promoting the build.
                $token = Invoke-RestMethod -Method Post `
                  -Uri "$(GRAPHWARDEN_PROXY_URL)/oauth2/v2.0/token" `
                  -Body @{
                    grant_type = 'client_credentials'
                    client_id = "$(GRAPHWARDEN_CLIENT_ID)"
                    client_secret = "$(GRAPHWARDEN_CLIENT_SECRET)"
                    scope = 'https://graph.microsoft.com/.default'
                  }
                if (-not $token.access_token) {
                  Write-Error "Token acquisition failed against GraphWarden."
                  exit 1
                }
                Write-Host "GraphWarden smoke test passed."
            env:
              GRAPHWARDEN_PROXY_URL: $(GRAPHWARDEN_PROXY_URL)
              GRAPHWARDEN_CLIENT_ID: $(GRAPHWARDEN_CLIENT_ID)
              GRAPHWARDEN_CLIENT_SECRET: $(GRAPHWARDEN_CLIENT_SECRET)
The `# DO NOT COMMIT REAL SECRETS` banner at the top of the pipeline is there on purpose. A commit that replaces `$(GRAPHWARDEN_CLIENT_SECRET)` with a real value leaks the credential to every clone of the repository and every log line the secret shows up in. Keep the variable-group references unchanged.

Variable group setup

The pipeline reads three values from a variable group. Create it once per project.

  1. In Azure DevOps, open Pipelines > Library > + Variable group.
  2. Name the group graphwarden-secrets (the name the pipeline references).
  3. Add three variables: GRAPHWARDEN_PROXY_URL, GRAPHWARDEN_CLIENT_ID, GRAPHWARDEN_CLIENT_SECRET.
  4. Click the lock icon next to GRAPHWARDEN_CLIENT_SECRET to mark it as secret. Azure DevOps masks secret values in logs and prevents them from being displayed after saving.
  5. On the variable group page, open Pipeline permissions and grant access to the pipeline that runs azure-pipelines.yml.
  6. Save the group. The pipeline picks up the values on its next run.

The variable group is organization-scoped; multiple pipelines can reference the same group. For separate environments (staging, production), create one variable group per environment and use a conditional reference in variables: to select the correct group at run time.

Handling ruleset-denied calls in CI

A smoke test that calls real Graph endpoints through a real GraphWarden deployment can hit a ruleset denial (403 Forbidden with a ruleId in the response body). Ruleset denials are expected behaviour: the customer's security policy has blocked the call. A CI pipeline that treats every 403 as a silent success hides real misconfigurations.

The pattern that works:

  • For the smoke test above, use the /oauth2/v2.0/token endpoint rather than a Graph endpoint. Token acquisition does not require a specific rule; if it fails, the proxy is unreachable or the credentials are wrong — both are real build failures.
  • If you add Graph-endpoint assertions, record the exact endpoints and methods your pipeline exercises. Add a test that asserts a specific rule permits them. A flexible approach: let the smoke test call an endpoint, assert the response status is either 200 or a known-expected 403 with a specific ruleId. Anything else is a failure.
  • continueOnError: true silences denials but also silences network outages, token misconfiguration, and real regressions. Use it only when the denial is expected for the branch under test (for example, a pull-request pipeline that runs against a hardened production ruleset).

Troubleshooting

The pipeline reports Variable group 'graphwarden-secrets' could not be accessed

The variable group exists but the pipeline does not have permission to read it. Open the variable group in Pipelines > Library, click Pipeline permissions, and add the pipeline to the allow list. For classic pipelines not yet migrated to YAML, the UI asks for permission on the first run; click Permit in the pipeline summary.

dotnet restore fails with Unable to resolve host 'api.nuget.org'

The agent lacks outbound internet access. Microsoft-hosted agents always have internet; self-hosted agents need outbound TCP 443 to api.nuget.org, www.nuget.org, and your private NuGet feeds. If you run behind a corporate proxy, set the HTTP_PROXY and HTTPS_PROXY environment variables on the self-hosted agent service.

Invoke-RestMethod returns The remote server returned an error: (404) Not Found

The proxy URL in GRAPHWARDEN_PROXY_URL is wrong, or the token endpoint path is wrong. The pipeline uses /oauth2/v2.0/token which is the proxy's standard token path; confirm your GraphWarden deployment exposes the same path (the default) and that the URL includes the scheme (https://proxy.example.com, not proxy.example.com). A trailing slash on GRAPHWARDEN_PROXY_URL produces a double-slash path that some proxies reject.

Token acquisition failed against GraphWarden with an HTTP 401

The GRAPHWARDEN_CLIENT_ID and GRAPHWARDEN_CLIENT_SECRET pair is not the pair registered in the GraphWarden App Admin for this CI pipeline's App Identity. Rotate the credentials through GraphWarden Admin, update the variable group with the new values, and re-run the pipeline. Do not reuse App Identity credentials across environments.

The pipeline succeeds locally but fails on the hosted agent

The hosted agent cannot reach your on-premise proxy. ubuntu-latest runs in Microsoft's Azure infrastructure; an on-premise proxy behind a corporate firewall is not reachable from there. Either expose the proxy on a publicly routable URL (behind its own authentication and TLS) or switch to a self-hosted agent running inside your network.

Last reviewed:

Last reviewed: .