Security·GitHub Actions·CI/CD

GitHub Actions security for MCP servers: OIDC tokens, minimal GITHUB_TOKEN permissions, and safe secrets injection

GitHub Actions workflows for MCP server repositories sit at the intersection of code and deployment — they build your server, run your security gate, and publish your releases. A compromised workflow can exfiltrate every secret in your repository, publish a backdoored npm package under your name, or push malicious commits with a trusted identity. Four specific hardening steps reduce the attack surface: scoped GITHUB_TOKEN permissions, pinned third-party Action dependencies, OIDC tokens for cloud authentication, and a hard boundary between untrusted PR code and privileged workflow steps.

Scope GITHUB_TOKEN to minimum permissions

By default, GitHub grants workflows a GITHUB_TOKEN with write access to most repository resources. Most workflows only need a fraction of that. Set permissions: read-all at the workflow level as a baseline, then grant write access explicitly at the job level for only the jobs that need it:

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

permissions:
  contents: read     # default — read repo contents
  # all other permissions: none

jobs:
  security-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write  # required for CodeQL / SkillAudit SARIF upload
    steps:
      - uses: actions/checkout@v4  # see pinning section below
      - name: Run SkillAudit
        uses: skillaudit/github-action@v1
        with:
          token: ${{ secrets.SKILLAUDIT_TOKEN }}

  publish:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    permissions:
      contents: write   # only this job needs write access for releases
      packages: write   # for GitHub Package Registry
    steps:
      - uses: actions/checkout@v4
      - run: npm publish

Pin third-party Actions to commit SHAs

Referencing a third-party Action by tag (actions/checkout@v4) is a trust decision: if the tag is moved to a different commit (intentionally or via a supply chain compromise), your workflow runs different code without any change on your side. Pinning to an immutable commit SHA guarantees you run exactly what you reviewed:

# Unsafe — tag can be moved to malicious commit
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

# Safe — pinned to immutable commit SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af  # v4.1.0

Use a tool like StepSecurity Harden Runner or Dependabot's GitHub Actions update feature to manage SHA pinning with automated updates when new versions are released.

OIDC tokens for cloud authentication

Long-lived AWS IAM credentials or GCP service account keys stored as GitHub secrets are a high-value target: anyone who can read your secrets (a compromised workflow, a malicious PR) gets persistent cloud access. OIDC tokens eliminate long-lived credentials: GitHub mints a short-lived token for each workflow run that your cloud provider exchanges for temporary credentials. The token is valid for the duration of the run only:

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

permissions:
  id-token: write   # required for OIDC token minting
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

      # Exchange GitHub OIDC token for AWS temporary credentials
      # No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY in secrets
      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6542e698252b07175d0e09601  # v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-mcp-server-deploy
          aws-region: us-east-1

      - name: Deploy to ECS
        run: aws ecs update-service --cluster mcp --service mcp-server --force-new-deployment

Configure the AWS IAM role's trust policy to only allow assumption from your specific repository and branch — not from any GitHub-hosted runner anywhere.

Isolate untrusted PR code from privileged secrets

The most critical GitHub Actions security rule for open-source MCP server repositories: never run code from a fork PR in a context that has access to repository secrets. The pull_request_target trigger runs in the base repository context (with secrets) but checks out the PR's code — a combination that allows a malicious PR to exfiltrate all your secrets:

# DANGEROUS — do not use pull_request_target with checkout of PR code
on:
  pull_request_target:  # has secrets
jobs:
  test:
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # attacker-controlled code
      - run: npm test  # runs attacker code with your secrets

The safe pattern: use pull_request (no secrets) for test runs on PR code, and use pull_request_target only for trusted operations that don't check out PR code (labeling, commenting):

# SAFE — run PR tests without secrets
on:
  pull_request:  # no access to secrets from fork PRs
jobs:
  test:
    steps:
      - uses: actions/checkout@v4  # checks out PR code — safe, no secrets
      - run: npm test              # no secrets available, attacker has nothing to steal

What SkillAudit checks in GitHub Actions workflows

SkillAudit's CI workflow analysis scans .github/workflows/*.yml for common security misconfigurations:

Scan your GitHub Actions workflows for security misconfigurations

Run a free audit →