AI & Machine Learning DevOps & Infrastructure

Hardening GitHub Actions with Runner Deprecation APIs

A self-hosted runner can become noncompliant long before it visibly breaks. GitHub can block registration. Brownouts interrupt jobs, and a runtime cutoff can turn an ordinary deployment into an incident. GitHub Actions runner deprecation APIs, released on 3 September 2026, make the risk measurable instead of anecdotal.

The timing is not accidental. The March 2025 compromise of tj-actions/changed-files affected over 23,000 repositories after mutable action tags were redirected to malicious code. CVE-2025-30066 documented the result: a Python payload extracted secrets from runner-worker process memory and emitted them into Actions logs. Runner fleets, token permissions, and action references now sit on the same security boundary.

GitHub’s September 2026 changes provide useful primitives: structured runner end-of-life data, a narrowly scoped vulnerability-alerts permission for GITHUB_TOKEN, job context properties for reusable-workflow identity checks, and cache-mode controls. Each feature is tidy on its own. Together, they form a CI/CD control plane that is versioned, observable, and harder to over-privilege.

GitHub Actions Runner Deprecation APIs Turn Dates into Policy

The runner deprecation API answers a question infrastructure teams once handled with spreadsheets and optimism: when will this exact runner version stop working? GitHub exposes GET /actions/runners/deprecations/{version} at repository, organization, and enterprise scope, including GET /orgs/{org}/actions/runners/deprecations/{version} and GET /repos/{owner}/{repo}/actions/runners/deprecations/{version}.

A response identifies the runner_version and two different deadlines. runtime_deprecates_at is the execution cutoff: after that timestamp, jobs using the runner version fail. registration_deprecates_at is the admission cutoff: new registrations using that version are rejected. Don’t treat them as one date. A runner already attached to a group can remain available after a registration cutoff while an autoscaling pool quietly loses its ability to replace failed nodes.

GET /orgs/acme-platform/actions/runners/deprecations/2.329.0

{
  "runner_version": "2.329.0",
  "runtime_deprecates_at": "...",
  "registration_deprecates_at": "..."
}

Build a small runner-governance service around this contract. Feed it inventory from Kubernetes ARC, scale-set tooling, virtual-machine tags, or a CMDB, then query the deprecation endpoint for every distinct installed version. Its output should be a signed policy decision: permit, drain, rebuild, or page. This is a thin line between GitHub’s lifecycle data and the systems that create runners. GitHub supplies dates; your control plane decides what happens next.

A practical policy model

Store normalized records in Postgres 16: runner identifier, pool, region, image digest, runner version, last-seen time, and both deprecation timestamps. A Go service can poll GitHub hourly, while a Kubernetes controller reconciles runner pods every few minutes. Don’t query the endpoint in every job. That creates an avoidable API dependency and adds noise to the build path.

Use four states: healthy when runtime support exceeds 45 days; upgrade-required inside 45 days; drain inside 14 days; and blocked after runtime deprecation. The windows should reflect image-build lead time and change-control latency. For an immutable AMI rebuilt nightly, 14 days may be generous. For a regulated appliance image validated through several stages, 45 days may be dangerously thin.

Deprecation telemetry is not reporting. It is an input to admission control.

GitHub resumed strict minimum-version enforcement on GitHub.com and GitHub Enterprise Cloud. A runner must be at least version 2.329.0 to be configured or re-registered, and every new runner release must be installed within 30 days of publication to keep executing jobs. Enterprise Cloud enforcement culminates on 25 September 2026; Enterprise Cloud with Data Residency reached full enforcement on 31 July 2026.

Brownouts are rehearsal, not surprise. GitHub intermittently blocks registration and later job execution for unsupported versions between 11:00 AM and 3:00 PM ET on specified days, including 14 September, 16 September, and 18 September. A fleet that survives only because no replacement node has been needed is already a failed design. This is the wrong default: runners should be replaceable before GitHub proves they are not.

Minimum-Version Enforcement Demands Immutable Runner Operations

Long-lived runners invite configuration drift, retained workspaces, stale toolchains, and missed runner upgrades. Minimum-version enforcement makes the operational cost visible, though the security case existed before the deadline. Rebuild runners from a declared image pipeline; don’t patch a pet VM until its version number looks acceptable.

A mature pipeline has six stages: obtain the published runner release; build a container image or VM image; run integration jobs against representative repositories; query the deprecation endpoint for the embedded version; publish only if policy passes; then drain and replace the existing pool. Custom runner setups, generally available in March 2026, fit this model well because their composition can be versioned beside the workflow assumptions they serve.

At image-build time, reject an artifact when its runtime_deprecates_at is fewer than 45 days away. At deployment time, reject a pool whose image digest is not on the approved manifest. Runtime has its own check: drain nodes that exceed a maximum age even when their runner version remains supported. These controls are separate. Version compliance does not prove a runner has a clean workspace or that its base image has current operating-system patches.

For multi-region Enterprise Cloud deployments, keep one policy engine and attach regional constraints as data. Data Residency operations need the same minimum version, 2.329.0, and the same monthly release discipline, but their 31 July 2026 enforcement date meant the upgrade cadence had to mature earlier. A common control plane keeps one region from becoming a museum of deprecated images.

Aligned translucent glass layers held by brass and blue spacers, with one layer set apart.

Explicit, narrow capabilities limit how far a compromised workflow can reach.

Use vulnerability-alerts Permission as a Precise CI Gate

The new vulnerability-alerts: read permission closes a longstanding security design gap. A workflow can read Dependabot alerts with its own GITHUB_TOKEN, without a broad Personal Access Token or a GitHub App credential carrying wider repository or security_events scope. Small permission changes can look cosmetic. This one changes the default architecture.

GitHub’s permissions model has a useful sharp edge: once any permission is explicitly specified, unspecified permissions become none. Make that behaviour standard. A job that reads only repository content and Dependabot alerts should declare both, plainly. It shouldn’t inherit a vague write-capable token because a workflow author wanted fewer YAML lines.

name: security-audit

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

permissions:
  contents: read
  vulnerability-alerts: read

jobs:
  dependabot-gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v7
        with:
          script: |
            const alerts = await github.rest.dependabot.listAlertsForRepo({
              owner: context.repo.owner,
              repo: context.repo.repo,
              state: 'open',
              severity: 'critical'
            });
            if (alerts.data.length > 0) {
              core.setFailed(`${alerts.data.length} critical Dependabot alerts open`);
            }

The example is intentionally narrow: it fails when open critical alerts exist. Production policy needs more texture. Paginate results; the REST response does not guarantee every alert fits on one page. Decide whether measurement follows the base branch, the changed manifest, or repository-wide open alerts. A repository-wide gate is clear, but it can block unrelated fixes behind old debt. For many teams, blocking new critical exposure on pull requests while enforcing zero open critical alerts on the protected default branch is the cleaner split.

GitHub Enterprise Server Dependabot alert endpoints remain beta and retain membership and organization-role requirements. Test token behaviour against the actual server version instead of assuming GitHub.com semantics. The principle holds: use GITHUB_TOKEN with vulnerability-alerts: read where available, and reserve higher-privilege identities for exceptional operations that are audited and short-lived.

I’d rather tolerate a well-defined security-gate exception than distribute a PAT with broad repo access through every pipeline. Exceptions can expire, be reviewed, and be measured. A permanently powerful CI token is an incident waiting for a convenient trigger.

GitHub Actions Security Needs Layers, Not a Single Gate

Dependabot gating cannot protect a workflow that executes a compromised action before the gate runs. The tj-actions/changed-files incident made the mechanism painfully clear: mutable tags such as v35 and v44.5.1 were repointed to a malicious commit. Pin critical actions to full 40-character commit hashes. A tag is a release label, not an immutable security identity.

The displayed actions/github-script@v7 reference above is readable, but it is not the hardened final form. Replace it with a reviewed full SHA in a real workflow, then keep the human-readable version in an adjacent YAML comment or lockfile. GitHub’s 26 March 2026 Actions security roadmap points toward dependency lockfiles, scoped secrets, and an egress firewall. SHA pinning is available now; it shouldn’t wait for the rest of that programme.

Pinning has a transitive limit. Composite actions may invoke other actions by mutable tags, so a top-level SHA does not make every dependency immutable. Use repository policy, Renovate or Dependabot updates for GitHub Actions references, and code review that inspects composite-action internals. The awkward detail matters: a lock on the front door does little when an included build script leaves a side entrance open.

Use OpenID Connect for cloud access rather than long-lived cloud credentials stored in repository settings. OIDC has been generally available since April 2026 and permits an identity provider to issue short-lived tokens according to claims such as repository, branch, environment, and audience. Scope cloud roles to exact audiences and subjects. A token stolen from process memory is still harmful, but a brief, audience-restricted credential has a dramatically smaller reuse window.

September 2026 cache-mode belongs in the same design review. Shared cache access can become a cross-workflow trust problem when a compromised workflow can read or corrupt artifacts intended for another process. Apply the most restrictive cache-mode that permits the job’s purpose. Pair it with explicit job permissions. Protected environments for deployments and reusable-workflow source identity checks using the newly available job context properties complete the boundary.

Operate the Control Plane Before the Brownout

The useful weekly report is compact: installed runner versions by pool, days until runtime_deprecates_at, image age and replacement success rate, plus repositories with open critical Dependabot alerts. Feed it to an operational dashboard, then make it executable. A failed policy check should open a tracked upgrade task; a 14-day threshold should drain a pool; a critical alert on the default branch should stop promotion.

Run a controlled failure drill before GitHub’s next brownout window. Disable a canary runner pool and force replacement registration, then verify the published image is current enough to join the group and execute a pinned workflow. Use a revoked test secret while validating OIDC-based deployment and confirming that cache-mode prevents unintended cache access. The test is modest. The assurance is not.

Sunlit modern corridor with repeating maintenance doors and one blue-edged gold-latched door.

Continuous renewal replaces emergency upgrades with an engineered operating rhythm.

CI/CD security becomes credible when its guarantees are encoded in the machinery that builds software. Query the runner deprecation API, enforce 2.329.0 and the 30-day release cadence, grant vulnerability-alerts: read only where it is needed, and pin what executes. Make the next deployment prove those controls before it gets production access.