GitHub Permissions

Purpose

GitHub’s permission model controls access to repositories, organisations, and automation tokens. Misconfigured permissions are a leading source of supply-chain vulnerabilities and accidental secret exposure. This note covers organisation/team/repo roles, token types, GITHUB_TOKEN scopes, and fine-grained permission policies.

Architecture

GitHub Organization
├── Teams (groups of users with inherited permissions)
│   ├── Read / Triage / Write / Maintain / Admin
│   └── Assigned to repos with specific roles
├── Repositories
│   ├── Collaborators (direct user → repo role)
│   └── Branch protection rules
└── Apps and Tokens
    ├── GITHUB_TOKEN (short-lived, workflow-scoped)
    ├── Classic PAT (long-lived, user-scoped, coarse-grained)
    └── Fine-grained PAT (repo-scoped, permission-scoped, expiry)

Implementation Notes

Repository and Organisation Roles

RoleCan do
ReadView/clone repo, create issues
Triage+ label/close issues, but not push
Write+ push to non-protected branches, manage labels
Maintain+ manage repo settings (not sensitive ones)
AdminFull control including deleting repo, managing members

Organisation members inherit org-level base permissions; team membership overrides base for assigned repos.

Token Types

Classic PAT (legacy)

  • Grants permissions across all repos the user has access to
  • No expiry enforcement; stored with repo, workflow, packages scopes
  • Avoid for new projects — overly broad

Fine-grained PAT (recommended)

  • Scoped to specific repos only
  • Per-repo permissions (contents: read, pull_requests: write, etc.)
  • Mandatory expiry (max 1 year)
  • Require org owner approval if org policy enforces it
GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens

GITHUB_TOKEN (for Actions)

  • Automatically minted at workflow start; expires when workflow ends
  • Scoped to the repo running the workflow
  • Default permissions configurable in org/repo settings
# Workflow: restrict to minimum necessary permissions
permissions:
  contents: read
  pull-requests: write
  id-token: write    # for OIDC

Set org/repo default to read-only and only expand per-workflow or per-job.

Branch Protection Rules

Critical for protecting main / release branches:

Repo Settings → Branches → Branch protection rules → Add rule

Recommended settings:

  • ✅ Require a pull request before merging
  • ✅ Require approvals: 1 (small team) or 2 (critical repos)
  • ✅ Dismiss stale reviews when new commits are pushed
  • ✅ Require status checks to pass before merging (link your CI jobs)
  • ✅ Require linear history (enforces squash or rebase)
  • ✅ Restrict who can push to matching branches (only release managers)
  • ✅ Include administrators (avoid “admin bypass” loophole)

Environments and Deployment Gates

Environments add an approval layer before sensitive deployments:

Repo Settings → Environments → New environment → Configure
jobs:
  deploy-prod:
    environment:
      name: production
      url: https://myapp.example.com
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh

With Required reviewers set, the workflow pauses until an approver clicks “Approve and deploy” in the GitHub UI. Combined with OIDC, this means cloud credentials are only issued after human approval.

Audit Log

Organisation admins can review all permission changes, token usage, and repo access via Organisation Settings → Audit log or the REST API:

gh api /orgs/myorg/audit-log --paginate | jq '.[] | select(.action=="org.invite_member")'

Trade-offs

PatternProCon
Fine-grained PATMinimal blast radiusMust set expiry; approval required in some orgs
Classic PATSimple setupAll repos accessible; long-lived; easy to leak
GITHUB_TOKEN with read-all defaultNo setupOver-permissioned for most workflows
GITHUB_TOKEN with permissions: blockLeast-privilege per workflowVerbose YAML
OIDC cloud authNo stored secretsPer-provider IAM setup required
Environment approvalsHuman gate before productionAdds latency to CD pipelines

References