Least-privilege TerraformApplier for Spacelift OIDC across multiple AWS accounts

Executive summary. I can eliminate the need for attaching AdministratorAccess to a Terraform apply role by treating the system as three coupled controls: (a) OIDC trust hardening (who can assume the role), (b) permission modularisation + ABAC (what the role can do, without a permission-drip treadmill), and (c) organisation guardrails (what can never happen, even if (a) or (b) fail). The biggest practical risk is not “OIDC breaks”; it’s mis-scoped trust conditions and over-broad role permissions turning any compromised Spacelift run context into cross-account compromise via AssumeRoleWithWebIdentity. citeturn0search6turn5search0turn15view0turn13view0turn0search32

Explanation — Understand the why

The AdministratorAccess problem is structural: Terraform executes imperative API calls to converge reality to HCL intent, so a single “apply” identity becomes a high-value target. If the identity is admin, a compromise becomes “full account control,” not “limited service impact.” AWS explicitly recommends starting from minimum permissions and adding only what’s needed; it also recommends using IAM Access Analyzer policy generation from CloudTrail to refine towards least privilege instead of defaulting to broad managed policies. citeturn1search28turn1search1turn1search4

Threat model: what changes when Spacelift OIDC trust is compromised. Spacelift issues an OIDC JWT token to runs; the token is available inside the run environment (including at /mnt/workspace/spacelift.oidc) and is valid for one hour. That token can be exchanged with AWS STS for temporary credentials via AssumeRoleWithWebIdentity if the IAM role trust policy accepts the token’s claims. citeturn12view1turn13view0turn0search6

Attacker capabilities (practical, not hypothetical).

How AWS makes this observable (and why you should care). AWS documents that CloudTrail logs authenticated IAM and STS API calls and also logs non-authenticated requests to STS actions including AssumeRoleWithWebIdentity, including information provided by the identity provider. This is essential for detection, tuning, and rollback during migration. citeturn5search0turn5search8

Context

This report targets teams running Terraform via entity[“company”,“Spacelift”,“iac automation platform”] against entity[“company”,“Amazon Web Services”,“cloud provider”] across multiple accounts, typically with Terraform distributed by entity[“company”,“HashiCorp”,“terraform vendor”]. Your “TerraformApplier” role is assumed using Spacelift-issued OIDC tokens (AssumeRoleWithWebIdentity) and must support common workflows like plan/apply, state backends (S3 + optional DynamoDB locking), and service provisioning (ECR, EKS, RDS) while avoiding a permissions maintenance cliff. citeturn12view1turn0search6turn1search0

What Spacelift actually gives you (claims you can enforce). Spacelift’s OIDC token includes:

Important nuance: scope is advisory. Spacelift states that the scope claim (and other claims) are “merely advisory” and it is up to you to decide whether/how to control external access based on claims like scope, space, caller, and run type. This means your security posture is determined by your IAM trust/policy design—not by Spacelift defaults. citeturn13view0turn12view1

Unspecified assumptions I will not invent. You did not specify: AWS account naming conventions, OU layout / organisation structure, environment boundaries (dev/stage/prod patterns), tagging standards (required tags, keys, enforcement), region strategy, or whether you use a hub (“tooling”) account to broker into member accounts vs per-account direct OIDC roles. I’ll provide designs that work for multiple shapes and call out where you must choose. citeturn1search11turn1search2

Concepts and mental model

I recommend modelling this as three concentric enforcement rings:

flowchart TB
  subgraph Ring1["Ring 1: OIDC Trust (Who can assume?)"]
    A[OIDC provider: <SPACELIFT_HOST>]
    B[Role trust policy checks: aud + sub + optional tag/session controls]
  end

  subgraph Ring2["Ring 2: IAM Permissions (What can it do?)"]
    C[Base policies: state + locking + KMS]
    D[Capability policies: ECR / EKS / RDS / limited IAM]
    E[ABAC: session tags & resource tags]
  end

  subgraph Ring3["Ring 3: Guardrails (What can never happen?)"]
    F[SCPs + permission boundaries + monitoring]
  end

  A --> B --> C --> D --> E --> F

This aligns with AWS’s separation between trust policies (assume conditions) and identity policies/boundaries, and with Spacelift’s design of exposing claims/tags for you to consume. citeturn10view0turn0search3turn13view0turn15view2

Principles for least-privilege CI/CD OIDC roles

Scope by account and environment first (blast radius). AWS Organisations SCPs are designed as account/OU guardrails; they don’t grant permissions but define maximum permissions boundaries across accounts, which is a perfect fit for constraining CI/CD roles organisation-wide. citeturn1search2turn1search8

Scope by workspace/stack identity (who/where in CI/CD). Spacelift’s sub claim format intentionally includes space, stack/module, run type, and scope. Spacelift also documents wildcarding rules and custom subject templates (space path) so you can build either per-stack trust or hierarchical “all prod under this branch” trust. citeturn12view1turn15view0turn0search1turn6search20

Scope by action and resource type, but design for sustainability.