Skip to content

GitHub Actions — Security

OWNER: alex, tjitte
ALSO_USED_BY: leon, marta, iwona
LAST_VERIFIED: 2026-03-26
GE_STACK_VERSION: GitHub Actions (github-hosted runners, ubuntu-latest)


Overview

GitHub Actions security in GE: OIDC for cloud authentication, pinned
action versions for supply chain security, permission scoping, and
audit logging. All practices align with ISO 27001 and SOC 2 Type II.


OIDC for Cloud Authentication

GE uses OpenID Connect (OIDC) tokens for cloud provider authentication.
No long-lived credentials stored as GitHub secrets for cloud access.

How OIDC Works

  1. Workflow requests an OIDC token from GitHub's provider
  2. Token contains claims (repo, branch, workflow, environment)
  3. Cloud provider validates token against trust policy
  4. Cloud provider issues short-lived access credentials
  5. Credentials expire when the job ends

UpCloud OIDC Configuration

jobs:  
  deploy:  
    runs-on: ubuntu-latest  
    permissions:  
      id-token: write    # Required for OIDC  
      contents: read  
    environment: staging  
    steps:  
      - name: Authenticate to UpCloud  
        uses: upcloud/auth-action@{sha}  # Pinned to SHA  
        with:  
          audience: "https://api.upcloud.com"  
          role: "ge-deployer-staging"  

CHECK: permissions.id-token: write is set on jobs using OIDC
CHECK: OIDC trust policies are scoped to specific repos and branches
CHECK: production OIDC trust requires environment: production claim

IF: OIDC is not available for a cloud service
THEN: use environment-scoped secrets with short expiry
THEN: rotate secrets quarterly at minimum


Pinned Action Versions

CRITICAL: All third-party actions MUST be pinned to full commit SHA.

Tags like v4 or v4.2.2 are mutable — the action author can change
the underlying code without changing the tag. SHA pins are immutable.

Correct (SHA-pinned)

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2  
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684  # v4.2.3  
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020  # v4.4.0  

Wrong (tag-pinned)

# ANTI_PATTERN: mutable tag reference  
- uses: actions/checkout@v4  
- uses: actions/cache@v4  

CHECK: every uses: line references a full 40-character SHA
CHECK: human-readable version is in a trailing comment
CHECK: Dependabot is configured to update pinned SHAs

ANTI_PATTERN: pinning to @v4 or @main
FIX: pin to full SHA with version comment


Permission Scoping

Every workflow MUST declare minimum permissions.
Start with permissions: read-all and grant write only where needed.

permissions:  
  contents: read  
  pull-requests: write   # Only if workflow comments on PRs  
  id-token: write        # Only if using OIDC  
  packages: write        # Only if publishing packages  

CHECK: top-level permissions block on every workflow
CHECK: no workflow uses permissions: write-all
CHECK: each permission is justified with a comment if non-obvious

IF: a workflow needs write access
THEN: grant at the job level, not the workflow level (narrower scope)

jobs:  
  test:  
    permissions:  
      contents: read  
  deploy:  
    permissions:  
      contents: read  
      id-token: write  

Supply Chain Security

Dependabot for Actions

# .github/dependabot.yml  
version: 2  
updates:  
  - package-ecosystem: "github-actions"  
    directory: "/"  
    schedule:  
      interval: "weekly"  
    commit-message:  
      prefix: "chore(ci):"  

CHECK: Dependabot configured for github-actions ecosystem
CHECK: Dependabot PRs reviewed before merging — verify SHA matches release

Action Allowlisting

GitHub supports restricting which actions can run in your organisation.
GE maintains an allowlist of approved actions.

CHECK: only actions from actions/* (GitHub official) and approved vendors allowed
CHECK: new third-party actions require security review by alex or tjitte

Verifying Action Integrity

Before adding a new action:

  1. Verify the action's repository is active and maintained
  2. Review the action's source code for suspicious behaviour
  3. Check for node_modules or binary downloads at runtime
  4. Verify the SHA matches a tagged release on the action's repo
  5. Add to organisation allowlist

Fork PR Security

IF: workflow triggered by pull_request from a fork
THEN: the workflow runs with read-only permissions
THEN: secrets are NOT available (by design)

ANTI_PATTERN: using pull_request_target to give fork PRs write access
FIX: use pull_request and have maintainers trigger deploy workflows

IF: fork PR needs write access (e.g., comment on PR)
THEN: use a two-workflow approach:
1. pull_request workflow runs tests (read-only, no secrets)
2. workflow_run workflow posts results (triggered after PR workflow completes)


Secret Masking

GitHub automatically masks secrets in logs. But dynamic secrets
(generated at runtime) need manual masking.

- run: |  
    TOKEN=$(curl -s https://auth.example.com/token)  
    echo "::add-mask::$TOKEN"  
    echo "TOKEN=$TOKEN" >> $GITHUB_ENV  

CHECK: all runtime-generated secrets are masked with ::add-mask::
CHECK: never echo secrets without masking first
CHECK: secret values never appear in step names or job names


Audit Logging

GitHub provides audit logs for all Actions activity.
GE retains logs for 90 days (compliance requirement).

IF: investigating a security incident involving CI/CD
THEN: check GitHub audit log for workflow runs
THEN: check deployment history for the environment
THEN: correlate with cloud provider logs

CHECK: audit log retention is configured in GitHub org settings
CHECK: webhook notifications enabled for failed production deploys


Cross-References

READ_ALSO: wiki/docs/stack/github-actions/index.md
READ_ALSO: wiki/docs/stack/github-actions/patterns.md
READ_ALSO: wiki/docs/stack/github-actions/pitfalls.md
READ_ALSO: wiki/docs/stack/github-actions/checklist.md
READ_ALSO: wiki/docs/stack/terraform-upcloud/index.md
READ_ALSO: wiki/docs/stack/kubernetes/security.md