Run GraphWarden proxy deployments from GitHub Actions
A complete GitHub Actions workflow that builds, tests, and runs a GraphWarden smoke test. Repository secrets hold credentials and a banner comment prevents accidental commits. The workflow below runs as-is after you add the required repository secrets.
GitHub Actions with repository secretsPrerequisites
The workflow, end to end
The workflow below is a complete GitHub Actions workflow. Copy it into .github/workflows/graphwarden-proxy.yml at the root of your repository. The ${{ secrets.* }} references read from the repository secret store at run time.
# DO NOT COMMIT REAL SECRETS TO THIS FILE.
# Add GRAPHWARDEN_PROXY_URL, GRAPHWARDEN_CLIENT_ID, GRAPHWARDEN_CLIENT_SECRET
# to Settings → Secrets and variables → Actions.
name: GraphWarden proxy CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --configuration Release --no-build --logger trx
smoke-test:
needs: build-and-test
runs-on: ubuntu-latest
steps:
- name: GraphWarden token acquisition
run: |
# Verify the proxy responds before promoting the build.
curl -sf -X POST \
-d "grant_type=client_credentials" \
-d "client_id=${{ secrets.GRAPHWARDEN_CLIENT_ID }}" \
-d "client_secret=${{ secrets.GRAPHWARDEN_CLIENT_SECRET }}" \
-d "scope=https://graph.microsoft.com/.default" \
"${{ secrets.GRAPHWARDEN_PROXY_URL }}/oauth2/v2.0/token" \
| jq -e '.access_token' > /dev/null
env:
GRAPHWARDEN_PROXY_URL: ${{ secrets.GRAPHWARDEN_PROXY_URL }}
GRAPHWARDEN_CLIENT_ID: ${{ secrets.GRAPHWARDEN_CLIENT_ID }}
GRAPHWARDEN_CLIENT_SECRET: ${{ secrets.GRAPHWARDEN_CLIENT_SECRET }}
Repository secrets setup
The workflow reads three values from the repository secret store. Add them once per repository.
- In GitHub, open your repository and navigate to Settings > Secrets and variables > Actions.
- Click New repository secret.
- Name the first secret
GRAPHWARDEN_PROXY_URL. Paste the full proxy URL including thehttps://scheme. Click Add secret. - Repeat for
GRAPHWARDEN_CLIENT_IDandGRAPHWARDEN_CLIENT_SECRET. - The workflow picks up the values on its next run. GitHub masks every secret value in job logs; a secret that leaks into stdout shows up as
***in the log viewer.
For environment-specific secrets (staging versus production), use GitHub Environments. Each environment has its own secret scope and can require approval before a job runs. The workflow above uses repository-scoped secrets, which apply to every job in the repository.
Protecting against secret leaks in logs
GitHub Actions auto-masks every ${{ secrets.* }} reference in job logs — a secret value that appears in stdout renders as ***. The masking is substring-based: if a secret value contains the literal string abc123, every occurrence of abc123 in the log is masked, whether it came from the secret reference or from somewhere else.
The masking does not protect against all leak vectors.
- A
run:step that prints a secret withecho "$MY_SECRET"is masked, but the same value encoded in Base64 or concatenated with other characters is not.echo "$(echo $MY_SECRET | base64)"leaks the Base64 form. - A step that writes the secret to a file and uploads the file as an artifact leaks the raw value — artifacts are not masked.
- A secret used in an HTTP request to an external service reaches that service in plaintext. If the service logs the request body, the secret leaks to the service's log.
The smoke test above uses curl -sf (silent, fail-on-HTTP-error) and pipes to jq -e '.access_token' > /dev/null, so neither the request body nor the response body is printed. Keep that pattern when you add more secret-bearing steps.
Troubleshooting
The workflow reports secret GRAPHWARDEN_CLIENT_SECRET not found
The secret name does not match the reference in the workflow. Secret names are case-sensitive. Open Settings > Secrets and variables > Actions and confirm the three secrets are spelled exactly GRAPHWARDEN_PROXY_URL, GRAPHWARDEN_CLIENT_ID, GRAPHWARDEN_CLIENT_SECRET.
The smoke test fails with curl: (7) Failed to connect
The runner cannot reach the proxy URL. ubuntu-latest runs in GitHub's cloud; an on-premise proxy behind a corporate firewall is not reachable. Either expose the proxy on a publicly routable URL (behind TLS and GraphWarden authentication) or run the workflow on a self-hosted runner that sits inside your network.
jq: error (at <stdin>:1): Cannot iterate over null on the smoke test
The token response body did not contain an access_token field. The proxy returned either an error body or an empty body. Re-run the job with the jq filter removed (| cat) to see the real response body. The most common causes are wrong credentials (the secret pair does not match the GraphWarden App Identity) or a wrong proxy URL (the request hit a server that returned a generic 401 body).
The workflow runs on push but not on pull requests from forks
GitHub does not pass secrets to workflows triggered by pull_request events from forks. This is a deliberate safeguard: a contributor could otherwise add a step that exfiltrates your secrets. If you need the smoke test on pull requests, use the pull_request_target trigger and gate the secret-bearing job on a label set by a maintainer. Read the GitHub documentation for pull_request_target carefully — the trigger has its own security model.
The secret appears as the literal string *** in the response body
The masking replaced the secret in a stdout dump, which means the secret leaked before the masking could be applied to the sink. Review the run: block for commands that print the environment (env, printenv) or the secret directly (echo). Remove the printing. The masking is a safety net, not a primary control.
Last reviewed: