Skip to content
Merged
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
43 changes: 43 additions & 0 deletions .github/workflows/aws-auth-check.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .github/workflows/terraform-lock.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
- cluster/local
- cluster/scaleway
- github-ci
- aws-github-oidc
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand Down
49 changes: 49 additions & 0 deletions aws-github-oidc/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

97 changes: 97 additions & 0 deletions aws-github-oidc/README.md
Original file line number Diff line number Diff line change
@@ -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@<pinned-sha>
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.
49 changes: 49 additions & 0 deletions aws-github-oidc/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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 (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 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"

url = "https://token.actions.githubusercontent.com"

tags = {
Terraform = "true"
Environment = "dev"
}
}

# Role GitHub Actions assumes via OIDC. Trust is scoped to this repo only
# (repo:<org>/<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"

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

oidc_wildcard_subjects = ["${var.github_org}/${var.github_repo}:*"]

policies = {
S3ReadOnly = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}

tags = {
Terraform = "true"
Environment = "dev"
}

# The trust policy references the OIDC provider by ARN; it must exist first.
depends_on = [module.iam_oidc_provider]
}
8 changes: 8 additions & 0 deletions aws-github-oidc/outputs.tf
Original file line number Diff line number Diff line change
@@ -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 = module.iam_role_github_oidc.arn
}
35 changes: 35 additions & 0 deletions aws-github-oidc/variables.tf
Original file line number Diff line number Diff line change
@@ -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:<org>/<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:::<state_bucket_name>."
type = string
default = "arn:aws:s3:::id-terraform-state20260612164136440800000001"
}
29 changes: 29 additions & 0 deletions aws-github-oidc/version.tf
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 18 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
"""
Loading