Tenki’s startup program is live: up to $50K in credits and grants.Apply
Security

CI Security Is Focused on the Wrong Layer

Hayssem Vazquez-Elsayed
Hayssem Vazquez-Elsayedproduct

Share Article:

Every major CI security vendor in 2026 leads with the same pitch: sandboxed runners, ephemeral VMs, locked-down network policies. It sounds right. Runners execute untrusted code, so isolating them should be the priority.

The problem is that none of the high-profile supply chain attacks from the past year actually exploited the runner host. They exploited the workflow dependency graph: unpinned action references, mutable tags, implicit trust chains between reusable workflows, and over-scoped tokens that granted write access to things the workflow had no business touching.

Runner isolation is necessary. Nobody is arguing you should run CI on bare metal with root access. But treating it as the primary security control is like putting a deadbolt on the front door while leaving the dependency graph wide open.

The Attacks That Actually Happened

Consider the incidents that defined CI security in the past twelve months. Each one targeted the workflow layer, not the runner.

tj-actions/changed-files was used in over 22,000 public repositories when an attacker compromised it in March 2025. The attack didn't break into a runner. It mutated a tag that thousands of workflows referenced, injecting code that base64-encoded secrets and dumped them to workflow logs. The attack spread through a chain of four sequentially compromised actions, as Wiz documented. Coinbase was the original target, but collateral damage hit thousands of repos.

Ultralytics had cryptominers injected into its PyPI releases in late 2024. The root cause: a script injection vulnerability in a custom composite action. The attacker opened a PR with a malicious branch name that got interpolated directly into a bash run block, exfiltrating GitHub tokens and PyPI publishing credentials. No runner escape needed.

Trivy's supply chain was breached through a pull_request_target workflow that the authors believed was secure. The workflow checked out attacker-controlled code from a fork, then executed it through an internally authored action. The attacker stole a highly-privileged organization-level token. The runner was functioning perfectly. The workflow trust model wasn't.

GhostAction, discovered by GitGuardian in September 2025, compromised 327 GitHub users across 817 repositories. Attackers injected malicious workflows that exfiltrated 3,325 secrets, including PyPI, npm, and DockerHub tokens, via HTTP POST requests to a remote endpoint.

Every one of these attacks would have succeeded on the most hardened, sandboxed, air-gapped runner you can configure. The runner wasn't the vulnerability. The workflow graph was.

What the Workflow Dependency Graph Actually Looks Like

A typical GitHub Actions workflow doesn't just run your code. It pulls in a tree of third-party actions, each of which can pull in its own dependencies. A single workflow file might reference:

  • Direct action references via uses: that resolve at runtime to whatever a mutable tag points to
  • Composite actions that nest other actions, creating transitive dependencies invisible to the caller
  • Reusable workflows called with workflow_call that inherit secrets and permissions from the caller
  • Event-chained workflows via workflow_run that consume artifacts from parent workflows
  • GITHUB_TOKEN with default write permissions that flow to every step, even those that only need read access

That's the dependency graph. And until very recently, GitHub provided no tooling to resolve it, audit it, or lock it down. You couldn't even see the full transitive tree of what a composite action pulled in.

As Wiz put it in their GitHub Actions threat model: the fundamental security challenge is who controls what code runs, and with what permissions. The trust boundary sits between repository owners who can modify workflows and external actors who can influence execution through PRs, issues, and event triggers. When that boundary gets blurred by a pull_request_target trigger or a reusable workflow that implicitly inherits credentials, the graph becomes the attack surface.

GitHub Knows This. Their 2026 Roadmap Proves It.

GitHub's 2026 Actions security roadmap, published in March, organizes its response around three layers: ecosystem, attack surface, and infrastructure. The interesting part is that two of those three are workflow-level concerns. Only infrastructure (the egress firewall, runner telemetry) is about the runner itself.

Here's what they're building at the graph level:

Workflow-level dependency locking. A new dependencies: section in workflow YAML that locks all direct and transitive dependencies to specific commit SHAs. GitHub compares it to Go's go.mod + go.sum for your CI pipeline. Dependency changes show up as diffs in pull requests. Hash mismatches halt execution before jobs run. Public preview is expected within three to six months.

Policy-driven execution protections. Built on GitHub's ruleset framework, these let organizations define centrally who can trigger workflows and which events are permitted. No more reasoning about trust per YAML file. An org could restrict workflow_dispatch to maintainers, prohibit pull_request_target entirely, and enforce these policies across every repo in the org.

Scoped secrets. Secrets can be bound to specific repositories, branches, environments, workflow identities, or trusted reusable workflows. Reusable workflows won't automatically inherit credentials from callers. Repository write access will no longer grant secret management permissions. That function gets a dedicated custom role.

The roadmap does include runner-level controls too, specifically the native egress firewall that operates at Layer 7 outside the runner VM and remains immutable even if an attacker gains root inside the container. That's genuinely useful. But notice the sequencing: dependency locking and execution policies are on a faster timeline (three to six months to preview) than the egress firewall (six to nine months). GitHub is prioritizing the graph.

Why AI-Generated Workflows Make This Worse

There's a compounding factor that most CI security discussions haven't caught up with yet: AI agents writing workflow configuration.

Copilot, Cursor, and similar tools generate YAML from templates and training data. They produce functional workflows fast. But they don't verify action provenance. They don't pin to commit SHAs by default. They don't audit whether the GITHUB_TOKEN permissions scope is wider than necessary. And they definitely don't check whether a composite action nests three other actions, each referenced by mutable tag.

Wiz's research explicitly calls this out. Their Part 2 threat model explores how AI-powered actions that process user input (issue content, PR descriptions) introduce prompt injection vectors. An attacker can craft a PR description that manipulates an AI action into executing unintended commands within the workflow's trusted context.

The combination is toxic: AI generates insecure-by-default workflow configurations at scale, while other AI-powered actions create new attack surfaces that traditional YAML audits wouldn't catch. Runner sandboxing does exactly nothing about either of these problems.

Securing the Graph vs. Securing the Runner

These two security postures look different in practice. Here's what each emphasizes:

Runner-focused security prioritizes ephemeral VMs, network egress restrictions, filesystem isolation, process-level monitoring, and rootless containers. These are all valid infrastructure controls. They limit blast radius after something has already gone wrong.

Graph-focused security prioritizes pinning every action to immutable commit SHAs, resolving and auditing transitive dependencies, scoping GITHUB_TOKEN to minimum required permissions, blocking dangerous triggers like pull_request_target at the org level, separating secret management from code contribution access, and reviewing dependency diffs in PRs before merge.

The difference is timing. Runner controls are reactive: they limit damage after malicious code is already executing. Graph controls are preventive: they stop the malicious code from entering the execution path in the first place.

Concretely, here's what a graph-first configuration looks like in your workflow files today, even before GitHub ships dependency locking:

# Pin to exact commit SHA, not a mutable tag
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

# Restrict token permissions to minimum needed
permissions:
  contents: read
  pull-requests: write

# Never use pull_request_target with checkout of PR head
# Use pull_request instead — it runs without secret access
on:
  pull_request:
    branches: [main]

Compare that to the typical AI-generated workflow you'll find in most repos:

# Mutable tag — could point to anything tomorrow
- uses: actions/checkout@v4

# No permissions block — defaults to write-all
# on: pull_request_target — secrets exposed to fork PRs

The second version is shorter, cleaner, and exactly what an LLM will produce. It's also the configuration that every supply chain attack in the past year exploited.

Where Tenki Fits

This is the gap Tenki's code reviewer is built to catch. When a PR modifies workflow YAML, Tenki's AI review doesn't just check syntax. It flags unpinned action references, over-scoped token permissions, dangerous trigger patterns, and reusable workflow chains that grant implicit trust. The review happens at PR time, before the runner ever starts, which is exactly where graph-level security decisions need to be enforced.

That matters because GitHub's dependency locking and execution policies are still months from general availability. Teams need graph-level review now, not in Q4.

What to Do Right Now

You don't need to wait for GitHub's roadmap to ship. Most of the graph-level controls are available today through discipline and tooling.

  1. Audit every uses: reference. Replace mutable tags with commit SHAs. Add a comment noting the version for readability. Tools like Dependabot can automate SHA updates when new action versions ship.
  2. Add explicit permissions blocks to every workflow. If a job only reads code, it should only have contents: read. No exceptions.
  3. Ban pull_request_target unless you have a documented, reviewed exception. If you must use it, never check out the PR head and execute it in the same workflow.
  4. Treat workflow YAML changes as security-sensitive diffs. Any PR that touches .github/workflows/ should require a security-aware reviewer. Automate this with CODEOWNERS or an AI code reviewer that understands workflow semantics.
  5. Audit reusable workflow chains for implicit secret inheritance. If a calling workflow passes secrets: inherit, know exactly where those credentials end up.

Runner isolation isn't going anywhere. Keep your sandboxes, your ephemeral VMs, your egress rules. But if your security strategy starts and ends at the runner layer, you're defending against the attacks that aren't happening while leaving the door open to the ones that are.

The workflow graph is the attack surface. Start securing it there.

Tags

#github-actions-security-tag#supply-chain-attack#egress-firewall#github-runners

Recommended for you

What's next in your stack.