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
92 changes: 92 additions & 0 deletions .github/workflows/scaleway-plan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Scaleway Plan (via s3-lister role)

# Plans cluster/scaleway using the org-wide s3-lister IAM role, assumed DIRECTLY
# via GitHub OIDC — no static AWS keys, and no routing through the bootstrap CI
# role (that role exists only to create/update roles, not to power pipelines).
#
# The s3-lister role is READ-ONLY on S3, which shapes the whole job:
# - state is read but never written -> plan only, never apply
# - the S3-native lock can't be written -> -lock=false
# - the workspace already exists in S3 -> select it via TF_WORKSPACE;
# never `terraform workspace new` (a write the read-only role can't do)
# - bootstrap_argocd=false -> skip the Infisical data source
# and the in-cluster helm/kubernetes providers (no cluster, no secrets in CI)
#
# Scaleway API access for the refresh still uses the static SCW key from secrets
# (Scaleway is not an OIDC relying party) — same creds as scaleway-auth-check.yml.

on:
pull_request:
paths:
- 'cluster/scaleway/**'
- 's3-lister-role/**'
- '.github/workflows/scaleway-plan.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
plan:
runs-on: ubuntu-latest
# Scope SCW secret usage to a dedicated environment (matches
# scaleway-auth-check.yml; satisfies zizmor's secrets-outside-env audit).
environment: scaleway
permissions:
id-token: write # mint the OIDC token exchanged for temporary AWS creds
contents: read
env:
# Select the existing remote workspace without `workspace new` (a write the
# read-only role can't perform). The default workspace would collide with
# the state bucket's own state at the bucket root.
TF_WORKSPACE: infrastructure
# Scaleway provider creds for the refresh. Keys are secrets; the public
# org/project IDs are repo variables (not secrets).
SCW_ACCESS_KEY: ${{ secrets.SCW_ACCESS_KEY }}
SCW_SECRET_KEY: ${{ secrets.SCW_SECRET_KEY }}
SCW_DEFAULT_ORGANIZATION_ID: ${{ vars.SCW_DEFAULT_ORGANIZATION_ID }}
SCW_DEFAULT_PROJECT_ID: ${{ vars.SCW_DEFAULT_PROJECT_ID }}
# The infisical provider authenticates eagerly even with bootstrap_argocd
# off (the gated data source still pulls the provider into the graph), so
# it needs valid machine-identity creds just to configure. With
# bootstrap_argocd=false no secret is actually read.
TF_VAR_infisical_client_id: ${{ secrets.INFISICAL_CLIENT_ID }}
TF_VAR_infisical_client_secret: ${{ secrets.INFISICAL_CLIENT_SECRET }}
steps:
# Fail fast and clearly if creds/vars aren't set, instead of letting the
# tooling emit a confusing assume-role or provider auth error.
- name: Assert credentials are present
env:
ROLE_ARN: ${{ vars.AWS_S3_LISTER_ROLE_ARN }}
run: |
if [ -z "$ROLE_ARN" ]; then
echo "::error::vars.AWS_S3_LISTER_ROLE_ARN is not set. Set it to the s3-lister role ARN (see s3-lister-role/README.md)."
exit 1
fi
if [ -z "$SCW_ACCESS_KEY" ] || [ -z "$SCW_SECRET_KEY" ]; then
echo "::error::SCW_ACCESS_KEY and/or SCW_SECRET_KEY are not set (see github-ci/README.md)."
exit 1
fi

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
with:
# Keep in sync with mise.toml [tools] terraform version.
terraform_version: "1.14"

# Direct OIDC -> s3-lister. Works because the role's trust now includes a
# GitHub-OIDC statement scoped org-wide (repo:IntegratedDynamic/*).
- name: Assume the s3-lister role via OIDC
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
with:
role-to-assume: ${{ vars.AWS_S3_LISTER_ROLE_ARN }}
aws-region: eu-west-3

- name: Terraform plan (read-only role -> no lock, no apply)
run: |
terraform -chdir=cluster/scaleway init -input=false
terraform -chdir=cluster/scaleway plan \
-lock=false -input=false -var bootstrap_argocd=false
29 changes: 23 additions & 6 deletions s3-lister-role/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,36 @@
# aws-github-oidc/iam-ci.tf): the role must sit under the managed `path` and
# carry the `permissions_boundary`. Omit either and the apply is denied.
#
# Trust: assumable by ANY principal in our AWS Organization. We don't list
# account IDs — instead the trust condition pins aws:PrincipalOrgID, so every
# (and only) caller whose credentials belong to org `var.org_id` may assume it.
# Trust: two doors, both org-scoped, that the module ORs together.
# 1. AWS principals in our AWS Organization — aws:PrincipalOrgID pins the org,
# so any caller whose credentials already belong to `var.org_id` may assume
# it (e.g. an SSO session, or a role assumed elsewhere in the org).
# 2. GitHub Actions in our GitHub org, DIRECTLY via OIDC web identity — no
# routing through the bootstrap CI role. `enable_github_oidc` adds an
# sts:AssumeRoleWithWebIdentity statement; `oidc_wildcard_subjects` scopes
# the token `sub` to `repo:<org>/*` (the module prepends `repo:`). This is
# the GitHub-org analogue of aws:PrincipalOrgID: any repo in the org, any
# branch, can assume the role keylessly.
#
# `use_name_prefix = false` keeps the role name EXACTLY `var.role_name` (no random
# suffix) so its ARN is stable and a workflow can name it in configure-aws-credentials.
module "s3_lister" {
source = "terraform-aws-modules/iam/aws//modules/iam-role"
version = "6.6.1"

name = var.role_name
path = var.role_path
description = "Org-wide read-only S3 lister. Created by CI; capped by the tf-managed permissions boundary."
name = var.role_name
use_name_prefix = false
path = var.role_path
description = "Org-wide read-only S3 lister. Created by CI; capped by the tf-managed permissions boundary."

permissions_boundary = var.permissions_boundary_arn

# Door 2: keyless GitHub-OIDC, org-wide. `var.github_oidc_subjects` are
# org/repo globs; the module prepends `repo:` and matches the token `sub`.
enable_github_oidc = true
oidc_wildcard_subjects = var.github_oidc_subjects

# Door 1: AWS principals in our AWS Organization.
trust_policy_permissions = {
OrgWideAssume = {
actions = ["sts:AssumeRole"]
Expand Down
11 changes: 11 additions & 0 deletions s3-lister-role/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,14 @@ variable "org_id" {
type = string
default = "o-f9lb1e5es9"
}

# GitHub OIDC subjects allowed to assume the role via web identity. Values are
# org/repo globs; the iam-role module prepends `repo:` and matches the token
# `sub` claim with StringLike. The org-wide default (`<org>/*`) lets ANY repo on
# ANY branch in the GitHub org assume the role keylessly — the GitHub-org
# analogue of the aws:PrincipalOrgID trust. Tighten to specific repos if needed.
variable "github_oidc_subjects" {
description = "GitHub OIDC sub globs (org/repo, module prepends `repo:`) allowed to assume the role."
type = list(string)
default = ["IntegratedDynamic/*"]
}
3 changes: 3 additions & 0 deletions s3-lister-role/workspace/infrastructure.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ permissions_boundary_arn = "arn:aws:iam::503577850357:policy/tf-managed-boundary

# Anyone in this AWS Organization may assume the role.
org_id = "o-f9lb1e5es9"

# Any repo in this GitHub org may assume the role directly via OIDC web identity.
github_oidc_subjects = ["IntegratedDynamic/*"]
Loading