GitHub Actions Tutorial — Security Considerations

2 minute read

Security Considerations

Self-hosted runners execute code from your repository — including pull requests from forks. Without proper security, a malicious PR can access your network, secrets, and infrastructure. This page covers the threat model and mitigations.


The Threat Model

Attacker submits PR with malicious workflow
         │
         ▼
Self-hosted runner executes the code
         │
         ├── Access to private network
         ├── Access to runner's file system
         ├── Access to environment variables
         ├── Potential credential theft
         └── Lateral movement to other systems

GitHub-hosted runners are disposable VMs — they’re destroyed after each job. Self-hosted runners persist, making them a higher-value target.


Critical Rules

1. Never Use Self-Hosted Runners on Public Repos

Anyone can fork a public repository and submit a PR. If your self-hosted runner processes that PR, the attacker’s code runs on your machine.

Repository Self-Hosted Runners Recommendation
Public ❌ Use GitHub-hosted runners
Private (trusted contributors) ✅ Acceptable with controls
Private (external contributors) ⚠️ Use with fork restrictions

2. Use Ephemeral Runners

Ephemeral runners eliminate persistent state:

./config.sh --ephemeral --url ... --token ...

No credentials or data persist between jobs.

3. Restrict Fork PR Workflows

For private repos with external contributors:

Repository Settings → Actions → General:

  • Fork pull request workflows: Require approval for all outside collaborators
  • Fork pull request workflows from outside collaborators: Require approval for first-time contributors

4. Use Runner Groups for Access Control

Limit which repositories can use sensitive runners:

Group: production-runners
  Access: my-org/deploy-service ONLY
  
Group: general-runners
  Access: All repositories

Network Security

Isolate Runner Networks

┌─────────────────────────┐
│  Runner Network (DMZ)   │
│  ┌─────────────────┐    │
│  │ Self-hosted      │    │
│  │ runner           │    │
│  └─────────────────┘    │
│         │ restricted     │
│         ▼               │
│  Allowed outbound:      │
│  - github.com           │
│  - npm/pip registries   │
│  - Deploy targets       │
│                         │
│  Blocked:               │
│  - Internal databases   │
│  - Admin panels         │
│  - Other services       │
└─────────────────────────┘
  • Place runners in an isolated network segment
  • Use firewall rules to limit what they can access
  • Only allow necessary outbound connections

Secret Management

Practice Detail
Use GitHub Secrets Don’t store credentials on the runner
Use OIDC Short-lived tokens instead of long-lived keys
Rotate tokens Regularly rotate runner registration tokens
Ephemeral runners Secrets don’t persist between jobs

Workflow Security Practices

Pin Action Versions by SHA


# ❌ Tag can be moved by the action author
- uses: actions/checkout@v4

# ✅ SHA is immutable
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

Limit GITHUB_TOKEN Permissions


permissions:
  contents: read          # minimum needed
  pull-requests: write    # only if needed

Audit Third-Party Actions

Before using any action, review:

  • Source code in the action’s repository
  • Whether it’s from a verified creator
  • Recent activity and maintenance status

Common Mistakes

Mistake Fix
Self-hosted runners on public repos Move to GitHub-hosted or restrict fork PRs
Persistent runners with secrets Use ephemeral mode
No network isolation Place runners in DMZ with firewall rules
Using @v1 tags on untrusted actions Pin to full SHA

Quick Reference

Practice Priority
No self-hosted on public repos 🔴 Critical
Ephemeral runners 🔴 Critical
Restrict fork PR workflows 🟡 High
Runner groups for access control 🟡 High
Network isolation 🟡 High
Pin actions by SHA 🟢 Recommended
Minimal GITHUB_TOKEN permissions 🟢 Recommended

← Previous: Ephemeral Runners

Next: Practice Exercises →

Updated: