Security

GitHub Actions security for monorepos: permissions, OIDC, and path gating

Jorge de los Santos, CTO & Co-Founder · April 23, 2026 · 10 min read

A monorepo CI pipeline runs fifty workflows a day across a dozen services. One over-privileged token compromises them all. Here's the 2026 playbook.

GitHub Actions security for monorepos: permissions, OIDC, and path gating

Why Monorepos Change the Threat Model

A polyrepo team that takes GitHub Actions security seriously applies the baseline — pin SHAs, minimize permissions, use OIDC, require reviews — once per repo and moves on. A monorepo team cannot. In a monorepo, a single compromised workflow can write to any service in the codebase. A misconfigured GITHUB_TOKEN with write permission does not compromise one project; it compromises the platform.

The 2026 monorepo reality is that many Series-B and up teams are operating out of a single repository — often a Nx, Turborepo, Bazel, or Pants-managed codebase with 10–50 services, hundreds of contributors, and a CI system that fires fifty workflows per merge. This architecture has strong ergonomic and dependency-management advantages, but the security model for CI has to catch up.

This post is the 2026 monorepo-specific playbook for GitHub Actions security. The controls below go beyond the standard advice and address the specific failure modes that show up when fifty workflows share a single repository, a single secret store, and a single token scope.

The Four Monorepo-Specific Threats

Cross-service token scope bleed. A workflow that runs for changes to services/billing inherits permissions that the workflow for services/frontend should not have. Without path-based gating, the billing workflow runs on PRs to frontend and vice versa.

Over-broad secret access. Monorepos tend to have a flat secret store. Every workflow has access to every secret by default. A compromised workflow in a leaf service — perhaps via a malicious dependency — can exfiltrate production database credentials belonging to a different service.

Reusable workflow injection. Monorepos share a lot of CI code — typically in a .github/workflows/reusable/ directory or a separate actions/ repo. A compromise of a reusable workflow affects every service that consumes it. Monorepos amplify this.

Path filter false security. paths-ignore and paths filters look like isolation. They are not security boundaries. A workflow can be triggered regardless of path filters via workflow_dispatch, schedule, or pull_request_target. Treating path filters as access controls is a common monorepo mistake.

Each of these has a mitigation. Most monorepos in 2026 implement one or two and are surprised when the threat modeling exercise surfaces the others.

Control 1: Default-Deny Workflow Permissions at the Repo Level

Set the repository-wide default for GITHUB_TOKEN permissions to read-all, with explicit per-workflow escalation only where required. This is the single most impactful control for a monorepo because it limits blast radius on every new workflow added by default.

In the GitHub repository settings (Settings → Actions → General → Workflow permissions), set “Read repository contents and packages permissions” as the default. Then each workflow declares its required permissions explicitly at the top:

permissions:
  contents: read
  pull-requests: write
  id-token: write  # for OIDC

Reject any workflow that uses permissions: write-all. In a monorepo this is non-negotiable — a write-all workflow can modify any service’s code, not just its own.

For workflows that legitimately need write access (release automation, generated-file updaters), scope the permission to the job, not the workflow:

jobs:
  release:
    permissions:
      contents: write
    runs-on: ubuntu-latest

This creates an artifact that security review can grep for: any job in the repo with contents: write gets a code-owner review from the platform-security team.

Control 2: Pin Third-Party Actions by SHA, Not Tag

Every third-party action should be referenced by full commit SHA, not by tag or branch. uses: some-org/action@v3 trusts some-org forever; uses: some-org/[email protected] trusts that specific commit.

Monorepos typically have many more third-party actions than polyrepos because each service team imports their own. The tj-actions compromise of March 2025 — and the follow-on compromises through the rest of 2025 and early 2026 — showed that the “respected action” trust model is not safe. Every organization that adopted SHA pinning before those compromises was unaffected; every organization that relied on tag pinning had an incident.

The monorepo-specific tooling:

  • Dependabot for GitHub Actions is enabled (updates.package-ecosystem: "github-actions" in dependabot.yml). Dependabot will open PRs to update pinned SHAs when the upstream publishes new tags.
  • StepSecurity’s step-security/harden-runner action is a reasonable second layer — it blocks egress from the runner to non-allowlisted domains during the workflow run. Attackers who breach a third-party action lose the ability to exfiltrate.
  • A repository-level policy blocks any uses: that references a tag or branch. pinact and ratchet are two CLI tools that validate this in pre-commit. gha-pin-check as a required status check enforces it at merge.

Control 3: Reusable Workflows With Versioned Inputs

Every monorepo ends up with a reusable workflow library — standard CI steps (lint, test, build, publish) that every service consumes. Monorepos should treat these as library code, not as shared configuration.

The 2026 baseline:

  • Reusable workflows live in .github/workflows/reusable/ or a dedicated internal actions/ repo
  • Consumed by SHA, not by branch (uses: ./.github/workflows/reusable/build.yml@abc1234)
  • Every reusable workflow has explicit inputs: and secrets: declarations — no implicit inheritance
  • secrets: inherit is banned. Every reusable workflow declares exactly which secrets it takes
  • Reusable workflow changes require code-owner review from the platform-security team (enforced via CODEOWNERS)

The secret-inheritance point matters. When a calling workflow uses secrets: inherit, the reusable workflow receives every secret in scope — whether it needs them or not. A reusable workflow that only needs a deploy token should declare secrets: { DEPLOY_TOKEN } explicitly, and the caller should pass it explicitly.

Control 4: Path-Based Triggering Is Ergonomics, Not Security

paths: and paths-ignore: filters are important for CI performance in monorepos — you do not want to run the billing service tests when someone changes services/frontend. They are not security controls.

Any workflow that needs to be restricted to a specific service — for example, a deploy workflow that should only run from changes under services/billing — must implement the restriction in the job itself, not just in the trigger:

jobs:
  deploy-billing:
    if: contains(github.event.pull_request.labels.*.name, 'deploy:billing')
    steps:
      - uses: actions/checkout@abc1234...
      - name: Verify path scope
        run: |
          if ! git diff --name-only origin/main | grep -q '^services/billing/'; then
            echo "This workflow requires changes under services/billing/"
            exit 1
          fi

This moves the check into the workflow run itself, where it cannot be bypassed by workflow_dispatch or by a crafted trigger payload.


See the IAN team run on your cloud. We connect to your AWS account via a scoped read-only role, run the Observe-tier agents, and leave you with a concrete audit report — cost waste, security exposure, compliance gaps, and a labor-offset estimate. You keep the findings regardless of next steps. Get a free infrastructure audit →


Control 5: OIDC With Per-Service Scoped Roles

OIDC federation from GitHub Actions to AWS, GCP, Azure, or any other cloud provider is the correct 2026 default — no long-lived access keys, tokens minted per-workflow with short TTLs, and a trust policy that ties the token to a specific repository and workflow.

In a monorepo, the trust policy should go one step further: tie the role to the specific workflow file and, where possible, to the specific service directory via custom claims.

The GitHub OIDC token includes a sub claim of the form repo:ORG/REPO:ref:refs/heads/main or similar. The per-service scoping technique uses a custom claim in the subject — GitHub Actions supports this via core.setOutput('audience', ...) or by customizing the sub pattern at the repository level.

A more pragmatic 2026 pattern: one IAM role per service, each with a trust policy that conditions on the workflow file path:

{
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:sub":
        "repo:acme/monorepo:environment:prod-billing"
    }
  }
}

And in the workflow:

jobs:
  deploy:
    environment: prod-billing
    permissions:
      id-token: write
    steps:
      - uses: aws-actions/configure-aws-credentials@abc1234...
        with:
          role-to-assume: arn:aws:iam::123456789012:role/deploy-billing
          aws-region: us-east-1

The environment: prod-billing declaration — combined with GitHub environment protection rules — means this role can only be assumed from a workflow that runs in the prod-billing environment. Other services cannot assume the role even if they execute in the same repository.

Control 6: Secret Isolation by Environment and Team

Flat repository-wide secret stores are the worst architecture for a monorepo. In 2026, organize secrets using GitHub environments — one environment per service or per service-environment pair. Each environment has its own secret scope, its own required reviewers, and its own deployment protection rules.

For example:

  • prod-billing environment contains only the billing production secrets
  • staging-billing environment contains the billing staging secrets
  • prod-frontend, staging-frontend — each scoped similarly

A workflow that targets an environment (environment: prod-billing) gets only the secrets in that environment. A compromise of a prod-frontend workflow does not expose prod-billing secrets.

Combined with OIDC, this means your long-lived secret count in a well-architected monorepo is nearly zero. All cloud credentials are federated; the only repo-stored secrets are third-party API keys (Datadog, PagerDuty, vendor integrations), each scoped to its environment.

Control 7: pull_request_target Is Still the Trap

pull_request_target runs with the base-branch workflow definition but against the PR’s head commit. This is useful for workflows that need repository secrets (common in monorepos for e2e tests) but it is the single most abused trigger in GitHub Actions.

The 2026 rule: never check out the PR head commit from a pull_request_target workflow unless the PR is from a verified maintainer. If you need PR code executed with secrets, do it in two workflows — one pull_request workflow that runs untrusted code in a sandbox, and one pull_request_target workflow that processes the artifact from the sandboxed run.

Better: for monorepos, most e2e and integration test patterns do not require pull_request_target. Use workflow_run — a workflow triggered by the completion of another workflow — with access to the original PR context but running on a trusted commit.

Control 8: Required Status Checks and Ruleset Enforcement

Define required status checks at the ruleset level (not classic branch protection) and version the ruleset JSON in the repository. A monorepo typically has a different required-checks set per service path — a change to services/billing requires the billing test suite to pass, not the frontend one.

GitHub’s ruleset-per-path feature (GA throughout 2025, refined in 2026) lets you define:

  • For PRs touching services/billing/**: require billing-tests, billing-sast, billing-iac-check
  • For PRs touching services/frontend/**: require frontend-tests, frontend-sast, frontend-accessibility
  • For PRs touching .github/workflows/**: require workflow-lint, workflow-security-review, plus mandatory code-owner review from platform-security

This last one is the highest-leverage monorepo control — every change to the CI pipeline itself goes through a stricter review than every other change. It catches the class of incident where an attacker adds a “harmless” workflow that exfiltrates secrets.

The Audit Pack for Monorepos

Enterprise customers asking about your CI security will want evidence. The monorepo-specific evidence pack contains:

  1. The JSON export of repository rulesets (one artifact per service path)
  2. The list of all workflows with permissions: write-* and the justification for each
  3. The dependabot configuration showing github-actions ecosystem enabled
  4. A sample OIDC trust policy showing the subject condition
  5. The list of GitHub environments and their protection rules
  6. A sample of the reusable workflow directory structure with CODEOWNERS assignment

Export this quarterly. Keep it updated. The first prospect who asks for CI security documentation will save you a week.

How IAN Helps

IAN audits your GitHub Actions configuration across every repository — including monorepos — and checks against the full control catalog above. Over-broad permissions, tag-pinned third-party actions, flat secret stores, missing OIDC, pull_request_target misuse, missing ruleset coverage on CI paths.

When IAN finds a gap, it opens a pull request with the fix — pinning SHAs, tightening permissions, restructuring environments, adding ruleset JSON — so the repair lands on your normal review path. For monorepos specifically, IAN produces a per-service security report so each team sees the scope that applies to them.

The Monorepo Rollout Order

If you are starting today:

  1. Set default workflow permissions to read-all at the repo level. Every existing write-all workflow surfaces immediately.
  2. Pin all third-party actions by SHA. Use a CLI tool to automate the conversion, then enable Dependabot for github-actions.
  3. Move secrets into environments. Create one environment per service-environment pair. Delete the repo-level secrets.
  4. Add OIDC to each cloud. One role per service; trust policies scoped by environment.
  5. Version-control the reusable workflow library. Move shared workflows to .github/workflows/reusable/, add CODEOWNERS, pin by SHA.
  6. Audit pull_request_target usage. Eliminate or convert to workflow_run where possible.
  7. Define rulesets per service path. Export to JSON, commit the JSON, make it the source of truth.

This is one sprint for a platform team. The audit pack falls out of it automatically.

Get a free infrastructure audit → | See pricing →

Next step: talk to the team

30 minutes. We'll look at your cloud together and scope what we'd take off your plate — see pricing.

Related Posts