← Microsoft articles

Security operations

GHAS + Azure runners without ops debt

When GitHub Advanced Security delivers value on hosted runners, and how to host ephemeral self-hosted runners on Azure when you really need them.

Shubham Anandani··9–11 min read

Problem

Teams want code, secret and dependency scanning wired into the pull request, not bolted on afterwards. GitHub Advanced Security (GHAS) does that, and for most repositories it runs fine on GitHub-hosted runners. The trouble starts when a build needs custom images, private network access or specialized hardware. Then you are running your own runners, and with them come token handling, scaling and isolation problems that did not exist before.

Options and trade-offs

  • GitHub-hosted runners with GHAS: the fastest path. There is no infrastructure to patch and each job gets a clean VM. The limit is that you take the images and network position GitHub gives you.
  • Self-hosted runners on Azure Virtual Machine Scale Sets (VMSS): you control the image, the network and the hardware. In return you own scaling, patching, and the credential used to register runners.
  • Persistent self-hosted runners: simple to set up, and the worst option for security. State and credentials from one job can leak into the next. Avoid them for anything that builds untrusted code.
  • Scope the GHAS features separately: secret scanning and push protection, code scanning with CodeQL, and dependency review. They have different costs and different operational weight, so turn them on in the order that gives the most value for the least friction.

My recommendation

Start on hosted runners and turn on code scanning and secret scanning. Only add self-hosted capacity for a specific reason you can name, and when you do, make every runner ephemeral: one job, then the VM is gone. Keep the registration credential out of the image and fetch it at boot through a managed identity.

Minimal working example

The flow: a VMSS instance boots, signs in with its managed identity, reads a registration secret from Key Vault, registers itself as an ephemeral runner, runs one job, and exits.

Boot script (cloud-init or a custom script extension):

#!/usr/bin/env bash
set -euo pipefail

ORG="my-org"
VAULT="my-runner-vault"
RUNNER_DIR=/opt/actions-runner

# 1. Sign in with the VM's managed identity (no secret on disk)
az login --identity --output none

# 2. Read the credential used to request registration tokens
GH_TOKEN=$(az keyvault secret show --vault-name "$VAULT" --name gh-runner-admin --query value -o tsv)

# 3. Ask GitHub for a short-lived registration token
REG_TOKEN=$(curl -fsS -X POST \
  -H "Authorization: Bearer $GH_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/orgs/$ORG/actions/runners/registration-token" | jq -r .token)

# 4. Register as an ephemeral runner in a dedicated group, then run
cd "$RUNNER_DIR"
./config.sh --url "https://github.com/$ORG" --token "$REG_TOKEN" \
  --ephemeral --unattended --runnergroup azure-vmss --labels azure-vmss,linux
./run.sh

Target the pool from a workflow, and add code scanning alongside it:

name: ci
on: [pull_request]

jobs:
  build:
    runs-on: [self-hosted, linux, azure-vmss]
    steps:
      - uses: actions/checkout@v4
      - run: ./build.sh

  codeql:
    runs-on: ubuntu-latest        # hosted runner is enough for scanning most repos
    permissions:
      security-events: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: java
      - uses: github/codeql-action/analyze@v3

Test it: queue a job, confirm a new VM registers, runs the job, and disappears from the runner list afterwards. Then check that a second job lands on a different VM.

Checklist

  • Confirm which GHAS features your plan includes and how active committers are counted before you roll out widely.
  • Make every self-hosted runner ephemeral, and put them in a dedicated runner group restricted to the repositories that need them.
  • Fetch the registration credential at boot through a managed identity and Key Vault. Never bake it into the image.
  • Prefer a GitHub App over a personal access token for requesting registration tokens, so access is scoped and not tied to a person.
  • Isolate the runner network. Allow only the egress the builds need.
  • Autoscale on queue depth, and cap the maximum instance count so a burst cannot become a bill.
  • Use hosted runners for CodeQL unless the build needs something they cannot provide.

Where this doesn't apply

Standard build stacks fit GitHub-hosted runners, and you should use them. Self-hosted capacity is justified by a real constraint (private network, custom hardware, compliance), not by habit.

Sources

  • GitHub security features overview — https://docs.github.com/en/code-security/getting-started/github-security-features
  • Self-hosted runners — https://docs.github.com/en/actions/hosting-your-own-runners
  • GH-500 study guide — https://learn.microsoft.com/en-us/credentials/certifications/resources/study-guides/gh-500
  • ---