Notiz

OIDC → AWS STS: CI/CD ohne langlebige Schlüssel

Föderierte Identität ersetzt AWS_ACCESS_KEY_ID im CI/CD: ein Muster für GitHub, GitLab und Atlantis — ohne Rotation, mit echter CloudTrail-Attribution.

AWS_ACCESS_KEY_ID in CI-Variablen war 2018 Standard. 2026 ist es ein Antimuster. Föderierte Identität über OIDC liefert der Pipeline kurzlebige STS-Credentials, eingegrenzt durch den sub-Claim des Tokens, und CloudTrail protokolliert nicht mehr einen namenlosen Machine-User, sondern den konkreten Workflow mit Branch und Repository. Dasselbe Muster trägt für GitHub Actions, GitLab CI und ein selbst gehostetes Atlantis.

Was am langlebigen Schlüssel falsch ist

Ein aws_iam_access_key lebt Monate. Er steht in den secrets des Repositories oder in CI-Variablen, und das erzeugt drei Probleme, die keine „bessere Passwortrichtlinie“ heilt:

  1. Manuelle Rotation. Den Schlüssel neu auszustellen heißt: ein PR in der SaaS-Plattform, ein neuer Wert in jedem Secret jedes Repositories und die Hoffnung, dass niemand ihn in ~/.aws/credentials auf seinem Laptop liegen hat.
  2. Blast Radius bei Leck. Ein Schlüssel mit Recht auf terraform apply funktioniert ebenso aus der Shell eines Angreifers wie aus Ihrem CI. Die Netzwerkgrenze ist 0.0.0.0/0.
  3. CloudTrail ohne Attribution. Sie sehen eventSource=ec2.amazonaws.com, userIdentity.userName=ci-deployer-prod, aber keinerlei Bezug dazu, welcher PR oder wessen Klick es ausgelöst hat.

OIDC als Ersatz: drei Akteure

Der föderierte Ablauf ersetzt „Schlüssel weitergeben“ durch „Token gegen kurze Session tauschen“. Er hat drei Akteure:

  • Der CI-Anbieter stellt ein signiertes JWT aus (das id_token) mit Claims zu Repository, Branch, Environment und Projekt-ID.
  • Ein IAM OIDC Identity Provider in AWS — einer pro Anbieter, ARN in der Form arn:aws:iam::<acc>:oidc-provider/<issuer-host>. Audience ist immer sts.amazonaws.com.
  • Eine IAM-Rolle mit Trust Policy — der Federated-Principal zeigt auf den OIDC-Provider, die Condition filtert Anfragen über den sub-Claim.

Wenn ein Job startet, legt das CI das JWT in eine temporäre Variable und ruft sts:AssumeRoleWithWebIdentity auf. AWS prüft die Signatur, vergleicht aud und sub mit der Trust Policy und liefert AccessKeyId/SecretAccessKey/SessionToken, gültig für eine Stunde oder weniger. CloudTrail zeigt nicht mehr iam-user/ci-prod, sondern arn:aws:sts::…:assumed-role/<role>/<run-id> mit dem sub-Claim in webIdFederationData.

Minimales Setup auf drei Anbietern

GitHub Actions — Issuer token.actions.githubusercontent.com, Sub-Format repo:org/repo:ref:refs/heads/main (oder :environment:prod, oder :pull_request):

permissions:
  id-token: write     # erforderlich
  contents: read
jobs:
  deploy:
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<acct>:role/gha-deploy
          aws-region: eu-central-1
      - run: terraform apply -auto-approve

GitLab CI (SaaS) — Issuer https://gitlab.com, Sub-Format project_path:group/project:ref_type:branch:ref:main. Das JWT kommt als id_tokens an, der Tausch ist ein expliziter aws sts assume-role-with-web-identity-Schritt.

Selbst gehostetes Atlantis — Issuer ist entweder Atlantis selbst oder das dahinterliegende IDP. Scope per Projekt-IAM-Rolle, nicht eine gemeinsame „atlantis-runner“-Rolle.

Die Trust Policy ist bis auf Issuer und Sub-Pattern identisch:

resource "aws_iam_role" "ci_deploy" {
  assume_role_policy = jsonencode({
    Statement = [{
      Effect = "Allow"
      Action = "sts:AssumeRoleWithWebIdentity"
      Principal = { Federated = "arn:aws:iam::${var.acc}:oidc-provider/${var.issuer}" }
      Condition = {
        StringEquals = { "${var.issuer}:aud" = "sts.amazonaws.com" }
        StringLike   = { "${var.issuer}:sub" = var.sub_pattern }
      }
    }]
  })
}

Was Sie auch dann nicht tun dürfen, wenn es „läuft“

Drei Antimuster tauchen in öffentlichen Repositories immer wieder auf und neutralisieren den Gewinn aus OIDC:

  • AdministratorAccess an der OIDC-Rolle „für die Demo“. Eine föderierte Rolle muss least-privilege sein. Andernfalls erhält ein Angreifer bei kompromittiertem Build-Skript (npm-Abhängigkeit, Supply-Chain-Hop) exakt das, was Ihr iam-user ihm zuvor gab — nur mit besserer Attribution. Kein Gewinn.
  • Ein Job führt npm build und terraform apply aus. Der Build läuft auf einem Default-Runner, kann beliebige Abhängigkeiten ziehen, und derselbe Prozess erhält danach ein temporäres Token mit Prod-Rechten. Die richtige Form: ein Build-Job assumiert keine Rolle und schiebt das Artefakt in eine Registry, ein separater Apply-Job ruft configure-aws-credentials auf. Eine kompromittierte Abhängigkeit reicht nur bis zu den Pull-Rechten.
  • AWS_ACCESS_KEY_ID im Runner-Environment lebt „aus Kompatibilität“ weiter. Environment-Variablen überschreiben AWS_PROFILE in der aws-CLI; ein vergessenes AWS_ACCESS_KEY_ID=… in Group-Level-Variablen umgeht die OIDC-Session stillschweigend. Vor terraform in der CI-Shell immer unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN.

Eine zusätzliche Falle für Terraform: module { source = "s3::…" } respektiert AWS_PROFILE nicht — Module werden mit den Default-Credentials geladen. Zwei Korrekturen: vor terraform init aws configure export-credentials --format env ausführen, oder Module in Git/Registry statt in S3 halten.

Was Sie aufhören werden zu tun

Nach der Migration verschwindet die Routine: Schlüsselrotation, Reviews darüber, wer wann AWS_* in secrets abgelegt hat, das Off-Boarding ehemaliger Teammitglieder mit Schlüsseln auf Laptops. Die Zahl der dauerhaften Geheimnisse im CI fällt auf null. Die Angriffsfläche von CI/CD schrumpft auf das Fälschen der JWT-Signatur von GitHub oder GitLab — was im realistischen Bedrohungsmodell der Kompromittierung des Anbieters selbst gleichkommt.

Das ist die seltene Security-Migration, die Verfahren entfernt statt sie hinzuzufügen.