Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/actions/aws-oidc-auth/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: "AWS OIDC Auth"
description: |
Assumes an AWS IAM role via GitHub OIDC — no static credentials stored.
Required permissions on calling job: id-token: write, contents: read

Enforces role-session-name as '{repo-name}-{run_id}' so CloudTrail entries
are always traceable back to a specific workflow run. Override with
role-session-name input only if your trust policy requires a fixed prefix.

The calling job's trust policy must scope the sub claim to the specific
repo and ref (or environment). See docs/oidc-trust-policies.md.
inputs:
role-arn:
description: "IAM role ARN to assume (e.g. arn:aws:iam::123456789012:role/my-role)"
required: true
aws-region:
description: "AWS region (e.g. ca-central-1)"
required: true
role-session-name:
description: "Override the session name. Defaults to '{repo}-{run_id}'. Must match any trust policy condition on sts:RoleSessionName."
default: ""
runs:
using: "composite"
steps:
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@99214aa6889fcddfa57764031d71add364327e59 # v6.1.3
with:
role-to-assume: ${{ inputs.role-arn }}
aws-region: ${{ inputs.aws-region }}
role-session-name: ${{ inputs.role-session-name != '' && inputs.role-session-name || format('{0}-{1}', github.event.repository.name, github.run_id) }}
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ gh api repos/sparkgeo/github-actions/commits/main --jq '.sha'
| Dependency Review | [`dependency-review`](.github/actions/dependency-review/action.yml) | Blocks PRs introducing dependencies with known vulnerabilities or non-permitted licenses; posts a summary comment. Also supports non-PR invocation via `base-ref`/`head-ref`. | `fail-on-severity` (default: `high`), `allow-licenses` (default: `MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Unlicense, CC0-1.0`), `comment-summary-in-pr` (default: `on-failure`), `base-ref` (default: `""`), `head-ref` (default: `""`) |
| Storage Optimizer | [`storage-optimizer`](.github/actions/storage-optimizer/action.yml) | Frees disk space on GitHub-hosted runners by removing unused toolchains (JDK, .NET, Swift, Android SDK, etc.) and pruning Docker | None |
| Terramate + OpenTofu Setup | [`terramate-opentofu-setup`](.github/actions/terramate-opentofu-setup/action.yml) | Installs Terramate and OpenTofu, validates generated files are up to date, initialises changed stacks, and lists changed stacks | `opentofu_version` (default: `1.10.0`), `terramate_version` (default: `0.14.7`) |
| AWS OIDC Auth | [`aws-oidc-auth`](.github/actions/aws-oidc-auth/action.yml) | Assumes an IAM role via GitHub OIDC — no static credentials stored; enforces traceable session name `{repo}-{run_id}` | `role-arn` (required), `aws-region` (required), `role-session-name` (default: `{repo}-{run_id}`) |

### GitHub Actionlint

Expand Down Expand Up @@ -130,6 +131,31 @@ steps:

The action will fail the job if `terramate generate` produces uncommitted output, ensuring generated files are always in sync with the source of truth.

### AWS OIDC Auth

Exchanges a GitHub OIDC token for short-lived AWS credentials — no static key stored as a secret. See [docs/oidc-trust-policies.md](docs/oidc-trust-policies.md) for IAM trust policy setup and migration checklist.

```yaml
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # required for OIDC token exchange
contents: read
steps:
- uses: actions/checkout@<SHA>
with:
persist-credentials: false
- uses: sparkgeo/github-actions/.github/actions/aws-oidc-auth@<SHA>
with:
role-arn: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: ca-central-1
# AWS credentials now available in environment
- run: aws sts get-caller-identity
```

Store the role ARN as a repository or environment **variable** (not a secret — ARNs are not sensitive). For production deployments, add a `environment: production` key to the job and configure required reviewers in Settings → Environments.

## Security

This repo is part of the Sparkgeo GitHub Actions security programme. The pillars are:
Expand Down
4 changes: 3 additions & 1 deletion docs/approved-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This is enforced at: **Org Settings → Actions → General → Allow selected a
| `zizmorcore/zizmor-action` | zizmorcore | `5f14fd08` (v0.5.6) | `zizmor` | Zizmor static security analysis; uploads SARIF | 2026-05-21 |
| `opentofu/setup-opentofu` | OpenTofu | `847eaa4a` (v2.0.1) | `terramate-opentofu-setup` | Install OpenTofu CLI | 2026-05-21 |
| `terramate-io/terramate-action` | Terramate | `c5a13758` (v3.0.0) | `terramate-opentofu-setup` | Install Terramate CLI | 2026-05-21 |
| `aws-actions/configure-aws-credentials` | AWS (Amazon) | `99214aa6` (v6.1.3) | `aws-oidc-auth` | Assume IAM role via GitHub OIDC; exchanges OIDC token for short-lived AWS credentials | 2026-06-01 |

## Security review criteria

Expand Down Expand Up @@ -54,7 +55,7 @@ Before approving a new action, verify all of the following:
"github_owned_allowed": true,
"verified_allowed": false,
"patterns_allowed": [
"ossf/*", "reviewdog/*", "zizmorcore/*", "opentofu/*", "terramate-io/*",
"ossf/*", "reviewdog/*", "zizmorcore/*", "opentofu/*", "terramate-io/*", "aws-actions/*",
"<new-publisher>/*"
]
}
Expand All @@ -74,6 +75,7 @@ reviewdog/*
zizmorcore/*
opentofu/*
terramate-io/*
aws-actions/*
```

GitHub-owned actions (`actions/*`, `github/*`) are covered by `github_owned_allowed: true` and do not need explicit patterns.
129 changes: 129 additions & 0 deletions docs/oidc-trust-policies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# OIDC Trust Policies

GitHub Actions supports OIDC federation: workflows exchange a GitHub-signed identity token for a short-lived cloud credential. No static key is stored as a secret.

## Why OIDC over static credentials

| | Static key | OIDC |
|---|---|---|
| Expiry | Never (until manual rotation) | 1 hour max |
| Leak blast radius | Exploitable until rotated | Useless after job ends |
| Rotation burden | Manual | None |
| Audit trail | Secret name only | Full OIDC claims (repo, branch, run ID, actor) |

## AWS

### Composite action

```yaml
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@<SHA>
with:
persist-credentials: false
- uses: sparkgeo/github-actions/.github/actions/aws-oidc-auth@<SHA>
with:
role-arn: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: ca-central-1
# AWS credentials are now available in the environment
- run: aws sts get-caller-identity
```

The action enforces `role-session-name: {repo-name}-{run_id}`, making every CloudTrail event traceable to a specific run.

### IAM trust policy

Bind to a specific repo and branch — never the whole org:

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:sparkgeo/REPO_NAME:ref:refs/heads/main"
}
}
}
]
}
```

**Scope conditions — from most to least restrictive:**

| Condition on `sub` | Allows |
|---|---|
| `repo:sparkgeo/app:ref:refs/heads/main` | Main branch only |
| `repo:sparkgeo/app:environment:production` | Any branch deploying to `production` environment |
| `repo:sparkgeo/app:ref:refs/heads/*` | Any branch (never use for production roles) |
| `repo:sparkgeo/*` | Any repo in org — avoid |
Comment on lines +70 to +71

### Required AWS setup (one-time per account)

1. Create the OIDC provider in IAM:
```bash
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
```
2. Create the IAM role with the trust policy above.
3. Attach only the permissions the role needs (least privilege).
4. Store the role ARN as a repo/environment variable (not a secret — ARNs are not sensitive):
```
AWS_DEPLOY_ROLE_ARN = arn:aws:iam::123456789012:role/sparkgeo-app-deploy
```

## GitHub environments and environment-scoped secrets

For production deployments, gate the job on a GitHub environment with protection rules:

```yaml
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # requires approver review before job runs
permissions:
id-token: write
contents: read
steps:
- uses: sparkgeo/github-actions/.github/actions/aws-oidc-auth@<SHA>
with:
role-arn: ${{ vars.AWS_DEPLOY_ROLE_ARN }} # environment-scoped variable
aws-region: ca-central-1
```

Environment configuration (Settings → Environments → production):
- Required reviewers: `@sparkgeo/platform-team`
- Deployment branches: `main` only
- Environment variables: `AWS_DEPLOY_ROLE_ARN` (prod ARN, different from staging)

**Secret scoping rule:**
- Org secrets: non-sensitive shared config only (`AWS_ACCOUNT_ID`, `SONAR_HOST_URL`)
- Environment secrets: sensitive values scoped to their environment — staging jobs cannot access production credentials

## Migration checklist

Use this when onboarding a repo from static credentials to OIDC:

```
[ ] Identify static AWS keys stored as repo/org secrets
[ ] Create IAM role with trust policy scoped to the specific repo + branch/environment
[ ] Set up GitHub environment protection rules for production roles
[ ] Replace static key usage with aws-oidc-auth composite action
[ ] Remove the static key secret from GitHub
[ ] Disable/delete the IAM user the key belonged to
[ ] Verify CloudTrail shows sts:AssumeRoleWithWebIdentity events (not IAM user events)
```