From 98bdb67e647bfa3e1566cc1d86d09ea60701c425 Mon Sep 17 00:00:00 2001 From: ms280690 Date: Mon, 1 Jun 2026 14:32:48 -0700 Subject: [PATCH] feat: AWS OIDC auth composite action and trust policy docs (issue #27) Add aws-oidc-auth composite action wrapping aws-actions/configure-aws-credentials@v6.1.3. Enforces role-session-name as '{repo}-{run_id}' for CloudTrail traceability. Calling job requires id-token: write; no static credentials stored. Add docs/oidc-trust-policies.md with: - IAM trust policy templates scoped to repo/branch/environment - GitHub Environments setup for production deployment gating - Migration checklist for moving from static keys to OIDC Update approved-actions.md with aws-actions entry and aws-actions/* org allowlist pattern. Update README with usage example and link to trust policy docs. Co-Authored-By: Claude Sonnet 4.6 --- .github/actions/aws-oidc-auth/action.yml | 30 ++++++ README.md | 26 +++++ docs/approved-actions.md | 4 +- docs/oidc-trust-policies.md | 129 +++++++++++++++++++++++ 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 .github/actions/aws-oidc-auth/action.yml create mode 100644 docs/oidc-trust-policies.md diff --git a/.github/actions/aws-oidc-auth/action.yml b/.github/actions/aws-oidc-auth/action.yml new file mode 100644 index 0000000..5761afa --- /dev/null +++ b/.github/actions/aws-oidc-auth/action.yml @@ -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) }} diff --git a/README.md b/README.md index 4187361..6995133 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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@ + with: + persist-credentials: false + - uses: sparkgeo/github-actions/.github/actions/aws-oidc-auth@ + 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: diff --git a/docs/approved-actions.md b/docs/approved-actions.md index a2f35a1..fb8fee3 100644 --- a/docs/approved-actions.md +++ b/docs/approved-actions.md @@ -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 @@ -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/*", "/*" ] } @@ -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. diff --git a/docs/oidc-trust-policies.md b/docs/oidc-trust-policies.md new file mode 100644 index 0000000..7df617e --- /dev/null +++ b/docs/oidc-trust-policies.md @@ -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@ + with: + persist-credentials: false + - uses: sparkgeo/github-actions/.github/actions/aws-oidc-auth@ + 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 | + +### 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@ + 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) +```