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¶
- Workflow requests an OIDC token from GitHub's provider
- Token contains claims (repo, branch, workflow, environment)
- Cloud provider validates token against trust policy
- Cloud provider issues short-lived access credentials
- 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)¶
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)
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:
- Verify the action's repository is active and maintained
- Review the action's source code for suspicious behaviour
- Check for
node_modulesor binary downloads at runtime - Verify the SHA matches a tagged release on the action's repo
- 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