From fd7086e68ac61fb9f5f8f49f4c1dfe4538f33820 Mon Sep 17 00:00:00 2001 From: Nicolas Brieussel Date: Sat, 13 Jun 2026 15:01:16 +0200 Subject: [PATCH 1/2] feat(aws-github-oidc): keyless GitHub OIDC -> AWS for Terraform state Add a standalone `aws-github-oidc/` root that lets GitHub Actions assume an AWS IAM role via OIDC to read/write the Terraform S3 remote state, so no static AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY are needed in GitHub secrets for the S3 backend. - aws-github-oidc/: AWS-only root (S3 backend, key aws-github-oidc/ terraform.tfstate). Creates the GitHub Actions OIDC provider, a role (github-actions-terraform) whose trust policy is scoped to repo:IntegratedDynamic/infrastructure:*, and a least-privilege inline policy granting S3 access to exactly the state bucket. Exposes role_arn. - mise.toml: aws-github-oidc-{init,plan,apply} tasks; root added to `lock`. - terraform-lock.yml: aws-github-oidc added to the lock matrix. Scaleway IAM is not an OIDC relying party, so Scaleway resources keep using the static API key (github-ci/ / terraform-ci/); OIDC covers only the AWS/S3 side. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/terraform-lock.yml | 1 + aws-github-oidc/.terraform.lock.hcl | 27 ++++++++ aws-github-oidc/README.md | 97 ++++++++++++++++++++++++++++ aws-github-oidc/main.tf | 73 +++++++++++++++++++++ aws-github-oidc/outputs.tf | 8 +++ aws-github-oidc/variables.tf | 35 ++++++++++ aws-github-oidc/version.tf | 29 +++++++++ mise.toml | 18 ++++++ 8 files changed, 288 insertions(+) create mode 100644 aws-github-oidc/.terraform.lock.hcl create mode 100644 aws-github-oidc/README.md create mode 100644 aws-github-oidc/main.tf create mode 100644 aws-github-oidc/outputs.tf create mode 100644 aws-github-oidc/variables.tf create mode 100644 aws-github-oidc/version.tf diff --git a/.github/workflows/terraform-lock.yml b/.github/workflows/terraform-lock.yml index dd0f334..8dcb1d0 100644 --- a/.github/workflows/terraform-lock.yml +++ b/.github/workflows/terraform-lock.yml @@ -27,6 +27,7 @@ jobs: - cluster/local - cluster/scaleway - github-ci + - aws-github-oidc steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/aws-github-oidc/.terraform.lock.hcl b/aws-github-oidc/.terraform.lock.hcl new file mode 100644 index 0000000..fee94f1 --- /dev/null +++ b/aws-github-oidc/.terraform.lock.hcl @@ -0,0 +1,27 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.50.0" + constraints = "~> 6.0" + hashes = [ + "h1:D8uNiOpl3UkAX4zI5T47ALMiRFXTa1XfdQC+TBu3RmE=", + "h1:mNg4vBXXqbO0hY2jCxhOyKVrnjEO0viTG2EY4oAlWaQ=", + "zh:0072806bb262c6d86bc25b4a75750e469881144c14818afdba7b82db840e1588", + "zh:1ebc2dae335dad7a8b16a1985b69a63a14954282bb44fdba7d5103f77551ac7b", + "zh:2dab48fe8f3193b8216d578ac1e3674fa566435cc7dbce2953d55b72e31d0241", + "zh:2fc3d3029c2b7429472391ef339672e1fca8e6ff32c8a519bf3acedafa7e24fe", + "zh:38a36e64e7212f6cedac861ea4d449cce07131b3378de601bf9d49a99e000208", + "zh:3ac70758ed251ce78b7f541a5a79cc6fe56474412783ae1decef719bdd0f30bf", + "zh:4385d3903e685bddb2b8005b4eb7db89f030267d4d03c7d792d2f5e739cc874a", + "zh:4cce0760b87fbafd51f30faec2a737f4183b7c615f4a86557f7d3c893a610dc5", + "zh:4feaeed18694239b896c6415d9a1e5ef89e1da4f4ad60924aa0522adeb1f6599", + "zh:502fca2be1c95f443c3e67d0555601d1de65b4ca82d197c059e9c868360e3a0a", + "zh:57d037f6fdd045f2660909c3bdface9622d81165ce647479cba98d1f353c5eab", + "zh:5dc5a0b915c2ac5256d909458f5c8e40b35f78b3a36ea893c86624eaf6c54e37", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b84c87c58a320adbb2c74a4cad03ae5aac7f2eae21db26f00fdde98c8c4d4523", + "zh:c895f1d5cbcbeff77850ac99efd36bde0048d4e909b296882331b9b9ebf48cfa", + "zh:ead82831683619124597a1f170dd31e9b293e9cf22f558cb166d5e734fcd11e4", + ] +} diff --git a/aws-github-oidc/README.md b/aws-github-oidc/README.md new file mode 100644 index 0000000..1c6dc41 --- /dev/null +++ b/aws-github-oidc/README.md @@ -0,0 +1,97 @@ +# aws-github-oidc + +A standalone Terraform root that provisions **keyless GitHub-OIDC → AWS** access +for this repo's CI: an IAM OIDC identity provider, plus an IAM role GitHub +Actions assumes (via short-lived OIDC tokens) to read/write the Terraform +**remote state on AWS S3** — **no static AWS keys in GitHub secrets**. + +This is **not** under `cluster/` — it provisions no cluster. It's a CI-platform +concern, kept as its own root so its state and blast radius stay small. + +## Why OIDC here (and why Scaleway still uses a static key) + +The Terraform state backend was migrated from Scaleway Object Storage to real +**AWS S3** (`eu-west-3`, Paris). AWS IAM **is** an OIDC relying party, so the +ideal keyless flow works: GitHub mints a short-lived OIDC token, AWS STS trades +it for temporary credentials by assuming a scoped role — nothing long-lived in +GitHub secrets. + +Scaleway resources (Kapsule, VPC) are a different story: **Scaleway IAM is not an +OIDC relying party** ([feature request](https://feature-request.scaleway.com/posts/761/oidc-provider-for-external-ci-cd)), +so those keep using a scoped, static Scaleway API key (see `terraform-ci/` / +`github-ci/`). OIDC here covers **only** the AWS/S3 side. Revisit Scaleway OIDC +if/when Scaleway ships it. + +## What it creates + +- `aws_iam_openid_connect_provider.github_actions` — the GitHub Actions OIDC + provider (`https://token.actions.githubusercontent.com`, audience + `sts.amazonaws.com`). **One per AWS account** — if one already exists in the + account, import it rather than creating a duplicate. +- `aws_iam_role.github_actions` (`github-actions-terraform`) — the role CI + assumes. Its **trust policy** is scoped to exactly this repo + (`repo:IntegratedDynamic/infrastructure:*`) with audience `sts.amazonaws.com`, + so only workflows in this repo can assume it. +- `aws_iam_role_policy.github_actions_s3` (`terraform-state-s3`) — least + privilege on exactly the state bucket: `s3:ListBucket` + + `s3:GetBucketVersioning` on the bucket, and `s3:GetObject` / `s3:PutObject` / + `s3:DeleteObject` on its objects (the S3-native locking the backend uses with + `use_lockfile = true` rides on object R/W, so no DynamoDB table is needed). + +`role_arn` is exposed as a Terraform output — it's a public identifier, not a +secret. + +## Credentials + +- **AWS** provider reads creds from the **AWS SDK chain** — locally your + `aws sso login` session / profile; in CI the role itself once assumed. Nothing + hardcoded, no `*.auto.tfvars` secret needed (`nico.auto.tfvars` is a gitignored + placeholder with no sensitive values). +- The **S3 state backend** authenticates the same way. + +## Apply + +```bash +mise run aws-github-oidc-plan # terraform init && plan — review first +mise run aws-github-oidc-apply # terraform apply (creates IAM resources) +``` + +> Never `terraform apply`/`destroy` here without explicit approval. + +## Wiring CI (manual, post-apply) + +The role ARN is known only **after** apply, and workflows reference it via a repo +variable (an ARN is a public identifier, not a secret). Set it once: + +```bash +gh variable set AWS_GITHUB_ACTIONS_ROLE_ARN \ + --repo IntegratedDynamic/infrastructure \ + --body "$(terraform -chdir=aws-github-oidc output -raw role_arn)" +``` + +Workflows then assume the role with `aws-actions/configure-aws-credentials`: + +```yaml +permissions: + id-token: write # mint the OIDC token + contents: read + +steps: + - uses: aws-actions/configure-aws-credentials@ + with: + role-to-assume: ${{ vars.AWS_GITHUB_ACTIONS_ROLE_ARN }} + aws-region: eu-west-3 + - run: aws s3 ls s3://id-terraform-state20260612164136440800000001 +``` + +No `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` secrets are required. + +## Revocation + +The role and provider live entirely in this root's state. + +- **Revoke CI access** — delete the role (or tighten/detach the inline policy); + any workflow assuming it then fails. +- **Revoke everything** — `terraform destroy` removes the role and the OIDC + provider. Mind that the OIDC provider is account-wide; only destroy it if no + other role depends on it. diff --git a/aws-github-oidc/main.tf b/aws-github-oidc/main.tf new file mode 100644 index 0000000..1e558b4 --- /dev/null +++ b/aws-github-oidc/main.tf @@ -0,0 +1,73 @@ +# Keyless GitHub-OIDC -> AWS for Terraform state access. GitHub Actions mints a +# short-lived OIDC token, AWS STS trades it for temporary credentials via this +# role — so NO static AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY ever lives in +# GitHub secrets for the S3 backend. +# +# This covers ONLY the AWS/S3 side. Scaleway resources (Kapsule, VPC) still use a +# static Scaleway API key (terraform-ci/), because Scaleway IAM is not an OIDC +# relying party — see README.md. + +# OIDC identity provider for GitHub Actions. One per AWS account; this is the +# canonical GitHub OIDC issuer + the STS audience. The thumbprints are GitHub's +# CA chain leaves; AWS no longer strictly verifies them for this issuer, but they +# are kept for compatibility. Idempotent: a single provider per account. +resource "aws_iam_openid_connect_provider" "github_actions" { + url = "https://token.actions.githubusercontent.com" + client_id_list = ["sts.amazonaws.com"] + thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1", + "1c58a3a8518e8759bf075b76b750d4f2df264fcd"] +} + +resource "aws_iam_role" "github_actions" { + name = "github-actions-terraform" + description = "Assumed by GitHub Actions (IntegratedDynamic/infrastructure) via OIDC for Terraform S3 state access." + assume_role_policy = data.aws_iam_policy_document.github_actions_trust.json +} + +# Trust policy — only workflows in this specific repo (any branch/PR/tag) may +# assume the role, and only with the sts.amazonaws.com audience. +data "aws_iam_policy_document" "github_actions_trust" { + statement { + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.github_actions.arn] + } + + condition { + test = "StringLike" + variable = "token.actions.githubusercontent.com:sub" + values = ["repo:${var.github_org}/${var.github_repo}:*"] + } + + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:aud" + values = ["sts.amazonaws.com"] + } + } +} + +# Least privilege: read/write/lock on exactly the Terraform state bucket, nothing +# else. ListBucket + GetBucketVersioning on the bucket; object R/W/D on its keys. +resource "aws_iam_role_policy" "github_actions_s3" { + name = "terraform-state-s3" + role = aws_iam_role.github_actions.id + policy = data.aws_iam_policy_document.github_actions_s3.json +} + +data "aws_iam_policy_document" "github_actions_s3" { + statement { + effect = "Allow" + actions = ["s3:ListBucket", "s3:GetBucketVersioning"] + resources = [var.state_bucket_arn] + } + + statement { + effect = "Allow" + actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] + resources = ["${var.state_bucket_arn}/*"] + } +} diff --git a/aws-github-oidc/outputs.tf b/aws-github-oidc/outputs.tf new file mode 100644 index 0000000..1b82ce8 --- /dev/null +++ b/aws-github-oidc/outputs.tf @@ -0,0 +1,8 @@ +# The role ARN is what workflows pass to aws-actions/configure-aws-credentials as +# `role-to-assume`. It's known only after apply, so it's wired into CI via the +# repo variable vars.AWS_GITHUB_ACTIONS_ROLE_ARN (set by hand post-apply — see +# README). Not a secret: an ARN is a public identifier. +output "role_arn" { + description = "ARN of the IAM role GitHub Actions assumes via OIDC (set as vars.AWS_GITHUB_ACTIONS_ROLE_ARN)." + value = aws_iam_role.github_actions.arn +} diff --git a/aws-github-oidc/variables.tf b/aws-github-oidc/variables.tf new file mode 100644 index 0000000..96306f4 --- /dev/null +++ b/aws-github-oidc/variables.tf @@ -0,0 +1,35 @@ +# AWS region the role/provider operate in. IAM is global, but the provider still +# wants a region; keep it aligned with the state bucket's region (eu-west-3). +variable "region" { + description = "AWS region the provider operates in. Matches the state bucket region." + type = string + default = "eu-west-3" +} + +# The trust policy is scoped to exactly one repo: repo:/:* . Only +# workflows in this repo can assume the role, regardless of branch/PR/tag. +variable "github_org" { + description = "GitHub organization that owns the repo allowed to assume the role." + type = string + default = "IntegratedDynamic" +} + +variable "github_repo" { + description = "GitHub repository whose workflows may assume the role (sub claim is scoped to it)." + type = string + default = "infrastructure" +} + +# The shared org Terraform state bucket (same one every root's backend points at). +# Used to scope the role's S3 policy to exactly that bucket. +variable "state_bucket_name" { + description = "Name of the shared S3 bucket holding the org's Terraform remote state." + type = string + default = "id-terraform-state20260612164136440800000001" +} + +variable "state_bucket_arn" { + description = "ARN of the state bucket — arn:aws:s3:::." + type = string + default = "arn:aws:s3:::id-terraform-state20260612164136440800000001" +} diff --git a/aws-github-oidc/version.tf b/aws-github-oidc/version.tf new file mode 100644 index 0000000..f60c11b --- /dev/null +++ b/aws-github-oidc/version.tf @@ -0,0 +1,29 @@ +terraform { + # Remote state on the shared org S3 bucket (same bucket every other root uses), + # under this root's own key so its state/blast-radius stay isolated. + backend "s3" { + bucket = "id-terraform-state20260612164136440800000001" + region = "eu-west-3" + workspace_key_prefix = "aws-github-oidc" + key = "terraform.tfstate" + encrypt = true + use_lockfile = true + } + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + } +} + +# Credentials are resolved by the AWS SDK chain — NOT hardcoded here: +# - local: `aws sso login` -> the provider defaults to your SSO session +# - CI: this very role, assumed via GitHub OIDC by +# aws-actions/configure-aws-credentials, which sets AWS_* env vars +# +# No Scaleway / Infisical / time provider here: this root only manages AWS IAM. +provider "aws" { + region = var.region +} diff --git a/mise.toml b/mise.toml index fc22d02..e154621 100644 --- a/mise.toml +++ b/mise.toml @@ -103,6 +103,23 @@ description = "terraform apply on the github-ci root (provisions the Scaleway CI dir = "github-ci" run = "terraform apply" +# ── AWS GitHub OIDC identity (aws-github-oidc/) ────────────────────────────── + +[tasks.aws-github-oidc-init] +description = "terraform init on the aws-github-oidc root" +dir = "aws-github-oidc" +run = "terraform init" + +[tasks.aws-github-oidc-plan] +description = "terraform plan on the aws-github-oidc root" +dir = "aws-github-oidc" +run = "terraform init && terraform plan" + +[tasks.aws-github-oidc-apply] +description = "terraform apply on the aws-github-oidc root (provisions the GitHub OIDC provider + IAM role)" +dir = "aws-github-oidc" +run = "terraform apply" + # ── Provider locks ─────────────────────────────────────────────────────────── [tasks.lock] @@ -112,4 +129,5 @@ terraform -chdir=terraform-state-bucket providers lock -platform=darwin_arm64 -p terraform -chdir=cluster/local providers lock -platform=darwin_arm64 -platform=linux_amd64 terraform -chdir=cluster/scaleway providers lock -platform=darwin_arm64 -platform=linux_amd64 terraform -chdir=github-ci providers lock -platform=darwin_arm64 -platform=linux_amd64 +terraform -chdir=aws-github-oidc providers lock -platform=darwin_arm64 -platform=linux_amd64 """ From e1a461cba794fc41e502f64279e4fb323b26e4ad Mon Sep 17 00:00:00 2001 From: Nicolas Brieussel Date: Sat, 13 Jun 2026 15:54:36 +0200 Subject: [PATCH 2/2] feat(aws-github-oidc): refactor to iam modules + add OIDC auth-check CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the hand-written OIDC provider/role/policy with the terraform-aws-modules/iam modules (iam-oidc-provider + iam-role with enable_github_oidc) — more concise and consistent. Trust stays scoped to repo:/:*; S3 access is account-wide read for now (AmazonS3ReadOnlyAccess). - fix outputs.role_arn to reference module.iam_role_github_oidc.arn (was the whole module object) - order the role after the provider via depends_on (trust policy needs the provider ARN to exist first) - relock providers for darwin_arm64 + linux_amd64 (adds hashicorp/tls) Add .github/workflows/aws-auth-check.yml: a PR smoke test that assumes the role via GitHub OIDC (no static keys) and runs `aws s3 ls`. Wired to the new repo variable AWS_GITHUB_ACTIONS_ROLE_ARN. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/aws-auth-check.yml | 43 +++++++++++++++ aws-github-oidc/.terraform.lock.hcl | 24 +++++++- aws-github-oidc/main.tf | 82 ++++++++++------------------ aws-github-oidc/outputs.tf | 2 +- 4 files changed, 96 insertions(+), 55 deletions(-) create mode 100644 .github/workflows/aws-auth-check.yml diff --git a/.github/workflows/aws-auth-check.yml b/.github/workflows/aws-auth-check.yml new file mode 100644 index 0000000..c68a66e --- /dev/null +++ b/.github/workflows/aws-auth-check.yml @@ -0,0 +1,43 @@ +name: AWS Auth Check + +# Validates the aws-github-oidc/ IAM role end to end: assume it via GitHub OIDC +# (no static AWS keys) and list S3 buckets. This is a smoke test, not part of any +# pipeline yet. +# +# Runs on PRs that touch the AWS OIDC identity so it can be validated before merge. + +on: + pull_request: + paths: + - '.github/workflows/aws-auth-check.yml' + - 'aws-github-oidc/**' + +permissions: + contents: read + +jobs: + list-buckets: + runs-on: ubuntu-latest + permissions: + id-token: write # mint the OIDC token exchanged for temporary AWS creds + contents: read + steps: + # Fail fast and clearly if the role ARN variable isn't set, instead of + # letting configure-aws-credentials emit a confusing assume-role error. + - name: Assert role ARN is configured + env: + ROLE_ARN: ${{ vars.AWS_GITHUB_ACTIONS_ROLE_ARN }} + run: | + if [ -z "$ROLE_ARN" ]; then + echo "::error::vars.AWS_GITHUB_ACTIONS_ROLE_ARN is not set. Apply aws-github-oidc/ and set it (see aws-github-oidc/README.md)." + exit 1 + fi + + - name: Configure AWS credentials via OIDC + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 + with: + role-to-assume: ${{ vars.AWS_GITHUB_ACTIONS_ROLE_ARN }} + aws-region: eu-west-3 + + - name: List S3 buckets + run: aws s3 ls diff --git a/aws-github-oidc/.terraform.lock.hcl b/aws-github-oidc/.terraform.lock.hcl index fee94f1..aac167e 100644 --- a/aws-github-oidc/.terraform.lock.hcl +++ b/aws-github-oidc/.terraform.lock.hcl @@ -3,7 +3,7 @@ provider "registry.terraform.io/hashicorp/aws" { version = "6.50.0" - constraints = "~> 6.0" + constraints = "~> 6.0, >= 6.28.0" hashes = [ "h1:D8uNiOpl3UkAX4zI5T47ALMiRFXTa1XfdQC+TBu3RmE=", "h1:mNg4vBXXqbO0hY2jCxhOyKVrnjEO0viTG2EY4oAlWaQ=", @@ -25,3 +25,25 @@ provider "registry.terraform.io/hashicorp/aws" { "zh:ead82831683619124597a1f170dd31e9b293e9cf22f558cb166d5e734fcd11e4", ] } + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.3.0" + constraints = ">= 3.0.0" + hashes = [ + "h1:5bCU/c+2HUh7GhclzNSH6gAuoCS4inW3obEtRAwu6WQ=", + "h1:j/BqLS2N2AScZyotd9nZpHdieJ7e5S8y+A+ZfIu8kL8=", + "zh:0ab58d6f8991d436c7d2dbd89ed814709b949b07ac5a54ee53b0aec1fa772a8b", + "zh:60b347abcb56f45d97c56f14d895069cd15a83993f199777f571b79fea3642ee", + "zh:6889be32640349230de3f23856e6f04e0e9ced4a84a27d3f552fa54684448218", + "zh:73f8e1ecf7135033165fb14b7e8bf4d656f3ce13065ec35762ea0481975328c7", + "zh:94ce25ee253eca0b42cae9c856b36bca8103b6453012d1b279c3623c805f2d42", + "zh:96bc6de9fd67bc446fd11257872e1ffb1029a996ed1d65a3f6b43f6d408ad9ab", + "zh:97c609a310a51bfd504d704e036d72064a84bf0bdb36cc08cd4cc66098212b41", + "zh:a12c16e94533c5bd123f75032576b9dc91dd5d5ccd5f7cf331d0f2e1adc55cf8", + "zh:c4f014f876adf7af57188795050bda5b0029d8c7d7773031102b6c36dcf1fc21", + "zh:d9b0a21583aaa3df3a95394fb949a3c515ff71c2ff5a1fc4a73d364aa90bfca5", + "zh:da510d22f0c6d71ad19a76406f106b782448f512375787ecfabb338ed1e311a7", + "zh:f0e9447a9ce3a24cdaa113089e65663c836d8b9bfdb915a1c0284e0112cab5c0", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/aws-github-oidc/main.tf b/aws-github-oidc/main.tf index 1e558b4..772b8e8 100644 --- a/aws-github-oidc/main.tf +++ b/aws-github-oidc/main.tf @@ -4,70 +4,46 @@ # GitHub secrets for the S3 backend. # # This covers ONLY the AWS/S3 side. Scaleway resources (Kapsule, VPC) still use a -# static Scaleway API key (terraform-ci/), because Scaleway IAM is not an OIDC +# static Scaleway API key (github-ci/), because Scaleway IAM is not an OIDC # relying party — see README.md. # OIDC identity provider for GitHub Actions. One per AWS account; this is the -# canonical GitHub OIDC issuer + the STS audience. The thumbprints are GitHub's -# CA chain leaves; AWS no longer strictly verifies them for this issuer, but they -# are kept for compatibility. Idempotent: a single provider per account. -resource "aws_iam_openid_connect_provider" "github_actions" { - url = "https://token.actions.githubusercontent.com" - client_id_list = ["sts.amazonaws.com"] - thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1", - "1c58a3a8518e8759bf075b76b750d4f2df264fcd"] -} +# canonical GitHub OIDC issuer. The audience (sts.amazonaws.com) and GitHub's +# thumbprints are defaulted by the module. +module "iam_oidc_provider" { + source = "terraform-aws-modules/iam/aws//modules/iam-oidc-provider" + version = "6.6.1" -resource "aws_iam_role" "github_actions" { - name = "github-actions-terraform" - description = "Assumed by GitHub Actions (IntegratedDynamic/infrastructure) via OIDC for Terraform S3 state access." - assume_role_policy = data.aws_iam_policy_document.github_actions_trust.json + url = "https://token.actions.githubusercontent.com" + + tags = { + Terraform = "true" + Environment = "dev" + } } -# Trust policy — only workflows in this specific repo (any branch/PR/tag) may -# assume the role, and only with the sts.amazonaws.com audience. -data "aws_iam_policy_document" "github_actions_trust" { - statement { - effect = "Allow" - actions = ["sts:AssumeRoleWithWebIdentity"] +# Role GitHub Actions assumes via OIDC. Trust is scoped to this repo only +# (repo:/:* — the module prefixes "repo:" itself). S3 read access is +# account-wide for now (intentional; tighten to the state bucket later). +module "iam_role_github_oidc" { + source = "terraform-aws-modules/iam/aws//modules/iam-role" + version = "6.6.1" - principals { - type = "Federated" - identifiers = [aws_iam_openid_connect_provider.github_actions.arn] - } + name = "github-actions-terraform" + description = "Assumed by GitHub Actions (${var.github_org}/${var.github_repo}) via OIDC for Terraform S3 state access." + enable_github_oidc = true - condition { - test = "StringLike" - variable = "token.actions.githubusercontent.com:sub" - values = ["repo:${var.github_org}/${var.github_repo}:*"] - } + oidc_wildcard_subjects = ["${var.github_org}/${var.github_repo}:*"] - condition { - test = "StringEquals" - variable = "token.actions.githubusercontent.com:aud" - values = ["sts.amazonaws.com"] - } + policies = { + S3ReadOnly = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess" } -} - -# Least privilege: read/write/lock on exactly the Terraform state bucket, nothing -# else. ListBucket + GetBucketVersioning on the bucket; object R/W/D on its keys. -resource "aws_iam_role_policy" "github_actions_s3" { - name = "terraform-state-s3" - role = aws_iam_role.github_actions.id - policy = data.aws_iam_policy_document.github_actions_s3.json -} -data "aws_iam_policy_document" "github_actions_s3" { - statement { - effect = "Allow" - actions = ["s3:ListBucket", "s3:GetBucketVersioning"] - resources = [var.state_bucket_arn] + tags = { + Terraform = "true" + Environment = "dev" } - statement { - effect = "Allow" - actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"] - resources = ["${var.state_bucket_arn}/*"] - } + # The trust policy references the OIDC provider by ARN; it must exist first. + depends_on = [module.iam_oidc_provider] } diff --git a/aws-github-oidc/outputs.tf b/aws-github-oidc/outputs.tf index 1b82ce8..7baf9e2 100644 --- a/aws-github-oidc/outputs.tf +++ b/aws-github-oidc/outputs.tf @@ -4,5 +4,5 @@ # README). Not a secret: an ARN is a public identifier. output "role_arn" { description = "ARN of the IAM role GitHub Actions assumes via OIDC (set as vars.AWS_GITHUB_ACTIONS_ROLE_ARN)." - value = aws_iam_role.github_actions.arn + value = module.iam_role_github_oidc.arn }