Zum Hauptinhalt springen

Deploy-Secrets: OIDC-Föderation

fremforge will deine Produktions-Secrets nicht verwahren. Langlebige Deploy-Zugangsdaten (AWS_ACCESS_KEY_ID, AZURE_CLIENT_SECRET, GCP_SA_KEY, …) in den Secret-Speichern einer CI bedeuten dauerhaften Rotationsaufwand und einen dauerhaft großen Schadensradius bei einem Einbruch. Der zeitgemäße Ansatz ist OIDC-Föderation: Dein Workflow fordert beim CI-Server ein kurzlebiges Identitätstoken an, das IAM deiner Cloud prüft das Token und gibt kurzlebige Zugangsdaten zurück. fremforge liefert das durchgängig.

So funktioniert es

Jeder CI-Job, der auf fremforge läuft, erhält ein frisch ausgestelltes JSON Web Token, signiert vom OIDC-Aussteller für Runner von fremforge unter https://frem.sh/_app/runner/oidc. Das Token trägt Claims, die den Kontext des Jobs beschreiben: Repository, Ref, Workflow und optional die Umgebung. Die Vertrauensrichtlinie im IAM deiner Cloud entscheidet anhand dieser Claims, ob sie Zugangsdaten ausgibt.

    sequenceDiagram
    autonumber
    actor User
    participant FF as fremforge<br/>runner-OIDC issuer
    participant Pod as runner VM
    participant Cloud as Your cloud's<br/>IAM • STS / WIF / …

    User->>FF: git push
    FF->>Pod: dispatch job — mint OIDC JWT
    Pod->>Cloud: "Here's my JWT, give me credentials"
    Cloud->>Cloud: Verify JWT against fremforge JWKS<br/>+ check trust policy claims
    Cloud->>Pod: 1h credentials
    Pod->>Cloud: deploy / read / write
  

Claims, auf die deine Cloud in der Vertrauensrichtlinie prüfen kann:

ClaimBeispielWas er identifiziert
isshttps://frem.sh/_app/runner/oidcfremforge selbst
audsts.amazonaws.combei welcher Cloud du dich authentifizierst
subrepo:my-org/my-repo:environment:production (wenn environment: angegeben ist)
repo:my-org/my-repo:ref:refs/heads/main (ohne Umgebung)
welcher Workflow-Lauf
repositorymy-org/my-repoName des Repositorys
repository_ownermy-orgSlug der Organisation
refrefs/heads/mainGit-Ref
environmentproductionim Workflow angegebener Name der Umgebung
actoraliceForgejo-Benutzername, der den Lauf ausgelöst hat

Diese Claims folgen der Form von GitHub Actions, sodass bestehende Vertrauensrichtlinien und die Actions actions/configure-aws-credentials / azure/login / google-github-actions/auth ohne Änderung funktionieren.

Discovery-URL

Richte deine Cloud auf diese Adressen aus:

  • Issuer-URL: https://frem.sh/_app/runner/oidc
  • JWKS-URL: https://frem.sh/_app/runner/oidc/.well-known/jwks.json
  • Discovery-Dokument: https://frem.sh/_app/runner/oidc/.well-known/openid-configuration

Alle drei sind anonym lesbar.

Workflow-Muster

Eine Umgebung angeben

jobs:
  deploy:
    runs-on: fremforge-runner
    environment: production
    permissions:
      id-token: write    # required to mint the OIDC JWT
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/fremforge-deploy-prod
          aws-region: eu-central-1
      - run: aws s3 sync ./build s3://my-prod-bucket/

environment: production ändert den OIDC-Claim sub zu repo:my-org/my-repo:environment:production, sodass deine Vertrauensrichtlinie den Zugriff nur Jobs gewähren kann, die production angeben.

Ohne Umgebung

Gibst du kein environment: an, fällt sub auf repo:<owner>/<repo>:ref:<ref> zurück. Vertrauensrichtlinien, die auf ref prüfen, funktionieren genauso wie bei GitHub. Die Umgebungssperre auf fremforge-Seite greift dann nicht (sie gilt nur für Workflows mit angegebener Umgebung), die Vertrauensrichtlinie in der Cloud ist also deine einzige Schutzlinie.

Beispiele für Vertrauensrichtlinien

AWS

IAM-Vertrauensrichtlinie für eine per OIDC föderierte Rolle:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::123456789012:oidc-provider/frem.sh/_app/runner/oidc"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "frem.sh/_app/runner/oidc:aud": "sts.amazonaws.com"
      },
      "StringLike": {
        "frem.sh/_app/runner/oidc:sub": "repo:my-org/my-repo:environment:production"
      }
    }
  }]
}

Einmalige Einrichtung: fremforge als OIDC-Provider registrieren:

aws iam create-open-id-connect-provider \
  --url https://frem.sh/_app/runner/oidc \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list $(curl -s https://frem.sh/_app/runner/oidc/.well-known/jwks.json | jq -r '.keys[0].x5t')

Azure (Workload Identity Federation)

Registriere die föderierte Anmeldeinformation an einer App-Registrierung in Entra ID:

az ad app federated-credential create \
  --id $APP_OBJECT_ID \
  --parameters '{
    "name": "fremforge-prod-deploy",
    "issuer": "https://frem.sh/_app/runner/oidc",
    "subject": "repo:my-org/my-repo:environment:production",
    "audiences": ["api://AzureADTokenExchange"]
  }'

Workflow:

- uses: azure/login@v2
  with:
    client-id: ${{ vars.AZURE_CLIENT_ID }}
    tenant-id: ${{ vars.AZURE_TENANT_ID }}
    subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

GCP (Workload Identity Federation)

gcloud iam workload-identity-pools providers create-oidc fremforge-provider \
  --workload-identity-pool=fremforge-pool \
  --issuer-uri="https://frem.sh/_app/runner/oidc" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.environment=assertion.environment" \
  --attribute-condition="attribute.environment == 'production'"

Workflow:

- uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/fremforge-pool/providers/fremforge-provider
    service_account: deploy@my-project.iam.gserviceaccount.com

T Cloud Public (OTC IAM Identity Provider)

T Cloud Public verwendet IAM Identity Provider und einen Token-Austausch:

  1. Identity Provider in der OTC Console → IAM → Identity Providers → Create:
    • Name: fremforge-runner-oidc
    • Protocol: OpenID Connect
    • Issuer URL: https://frem.sh/_app/runner/oidc
    • JWKS URL: https://frem.sh/_app/runner/oidc/.well-known/jwks.json
    • Client ID: iam.eu-de.otc.t-systems.com (oder die STS-Audience deines Projekts)
  2. Mapping-Regeln übersetzen Claims in einen föderierten Benutzer. Beispiel, das ein Subjekt repo:my-org/my-repo:environment:production der OTC-Gruppe deploy-prod zuordnet:
[
  {
    "local": [
      { "user": { "name": "{0}" } },
      { "group": { "name": "deploy-prod" } }
    ],
    "remote": [
      { "type": "sub" },
      { "type": "environment", "any_one_of": ["production"] }
    ]
  }
]
  1. Workflow: Tausche das JWT über den Token-Austausch-Endpunkt von IAM gegen ein OTC-Token und verwende das zurückgegebene X-Subject-Token für beliebige Aufrufe der OTC-API.

Festlegen, welche Refs wohin deployen dürfen

OIDC-Föderation macht das SPEICHERN langlebiger Secrets überflüssig, aber du musst trotzdem entscheiden, welche Branches in welche Umgebung deployen dürfen. Ein Workflow auf einem Feature-Branch KANN ein JWT ausstellen lassen, das environment: production behauptet; die Sperre ist das, was verhindert, dass dieses JWT anerkannt wird.

fremforge bietet zwei Sperren, und du solltest beide als gestaffelte Verteidigung nutzen.

Sperre 1: Vertrauensrichtlinie in der Cloud (immer nötig)

Die Vertrauensrichtlinie im IAM deiner Cloud ist die letzte Instanz. Lege den Claim sub auf die Form Repository plus Umgebung fest UND den Claim ref auf den oder die Branches, aus denen diese Umgebung deployen darf. Beispiel für AWS:

{
  "Condition": {
    "StringEquals": {
      "frem.sh/_app/runner/oidc:sub": "repo:my-org/my-repo:environment:production"
    },
    "StringLike": {
      "frem.sh/_app/runner/oidc:ref": "refs/heads/main"
    }
  }
}

Ein Feature-Branch KANN das JWT ausstellen lassen, aber AWS gibt keine Zugangsdaten aus, weil ref refs/heads/feature/foo statt refs/heads/main ist. Das ist das von GitHub empfohlene Grundmuster und funktioniert ab dem ersten Tag.

Sperre 2: Deployment-Branch-Muster pro Umgebung in fremforge (empfohlen)

Unter Organisationsverwaltung → Umgebungen (frem.sh/<org>/_admin/environments) konfigurierst du jede Umgebung wie bei GitHub. Pro Umgebung gibt es drei Bereiche:

Branches und Tags für Deployments: Glob-Muster, die gegen die auslösende Git-Ref geprüft werden. Beispiele (Kurzformen für Branches erhalten automatisch das Präfix refs/heads/):

NameMusterWirkung
productionmainnur der Branch main
stagingmain
develop
release/*
die Branches main, develop und release/*
pr-preview*jeder Branch (die Sperre in der Cloud ist die eigentliche Instanz)
releaserefs/tags/v[0-9]+.*nur Versions-Tags (ausdrückliches Präfix refs/tags/)

Workflows, die environment: production angeben, werden beim Dispatch abgewiesen, wenn die auslösende Ref nicht passt. Workflows OHNE Angabe von environment: laufen uneingeschränkt, sodass die normale CI auf Feature-Branches weiter funktioniert.

Umgebungs-Secrets: verschlüsselt, auf die Umgebung beschränkt und als ${{ secrets.NAME }} im Workflow UND als normale Umgebungsvariablen eingefügt, wenn das environment: des Workflows passt. Deckt die Secrets ab, die nichts mit Cloud-IAM zu tun haben (API-Schlüssel von Drittanbietern, Signaturschlüssel, Webhook-Secrets) und die OIDC-Föderation nicht löst.

Umgebungsvariablen: nicht geheime, auf die Umgebung beschränkte Werte. Gleiches Einfügemodell.

Und Freigabesperren?

Environments in GitHub Actions haben die Funktion „Required reviewers“: Ein Job pausiert nach dem Merge, bis N Personen in der Actions-Oberfläche auf Approve klicken. fremforge liefert das bewusst nicht, weil Forgejo an derselben Stelle im Ablauf bereits eine stärkere Sperre hat: Branch-Schutz auf main mit CODEOWNERS für .forgejo/workflows/* verlangt ein Pull-Request-Review, bevor ein Workflow, der deployen kann, gemergt wird. Für 99 % der Fälle (1 Pull Request = 1 Deployment) IST das Pull-Request-Review in Forgejo die Freigabe des Deployments. Kunden, die den GitHub-Fall brauchen (1 Pull Request kann N unabhängige Deployments mit jeweils eigenen Freigebenden auslösen), legen ein Issue an, und wir priorisieren Freigebende zum Deploy-Zeitpunkt (geplant) nach tatsächlichem Bedarf.

Die Sperre auf fremforge-Seite verhindert, dass das JWT überhaupt ausgestellt wird. Die Sperre in der Cloud verhindert, dass ein ausgestelltes JWT anerkannt wird. Kombiniere beide: Selbst wenn ein künftiger Fehler in fremforge einen abgewiesenen Dispatch durchließe, gibt das IAM der Cloud trotzdem keine Zugangsdaten aus. Selbst wenn die Vertrauensrichtlinie eines Kunden in der Cloud einen Tippfehler hat und ein breiteres Ref-Muster als beabsichtigt akzeptiert, hat fremforge das JWT trotzdem nicht ausgestellt.

Schrittweises Onboarding

Gibst du in einem Workflow environment: x an, hast aber noch keine Umgebung x konfiguriert, lässt fremforge den Dispatch zu, statt ihn abzuweisen. Das ist Absicht, damit die Workflows eines neuen Kunden nicht in dem Moment brechen, in dem er eine Umgebung angibt; die Vertrauensrichtlinie in deiner Cloud ist das Sicherheitsnetz, bis du die Umgebung in fremforge anlegst. Sobald sie existiert, wird die Sperre durchgesetzt.

Abriegelung: nur geschützte Branches

Für Umgebungen mit hohen Sicherheitsanforderungen, in denen CI auf ungeschützten Refs VOLLSTÄNDIG verboten sein soll (nicht nur die Deploy-Zugangsdaten gesperrt), schalte Organisationsverwaltung → Umgebungen → Organisationsweite Sperre → Workflows nur auf geschützten Branches ausführen ein:

Workflows nur auf geschützten Branches ausführen

Ist das eingeschaltet, weist die Plattform den Dispatch JEDES Workflows ab, dessen auslösender Branch NICHT in der Liste der geschützten Branches des Repositorys steht (konfiguriert über die Forgejo-eigene Einstellung Einstellungen → Branches → Neue Regel hinzufügen). Das betrifft auch die normale CI (Tests, Lint): Auf ungeschützten Branches läuft nichts. Nützlich für regulierte Umgebungen, in denen außerhalb geprüfter Refs kein Code ausgeführt werden soll; für den normalen Entwicklungsablauf nicht empfohlen, weil es das Testen auf Feature-Branches verhindert.

Wenn du das einschaltest, schützt du in Forgejo typischerweise die Muster main, develop und release/*, damit deine üblichen Entwicklungs-Branches weiter CI ausführen dürfen.

Empfohlene Konfiguration

Für typische SaaS- und Produktteams:

  1. Schalte die Sperre „nur geschützte Branches“ nicht ein, damit die normale CI auf Feature-Branches läuft.
  2. Konfiguriere Umgebungen unter Organisationsverwaltung → Umgebungen mit passenden Mustern für Branches und Tags für Deployments für jede (production, staging usw.).
  3. Konfiguriere Vertrauensrichtlinien in der Cloud mit Bedingungen sowohl für sub (an die Umgebung gebunden) ALS AUCH für ref (an den Branch gebunden).
  4. Schütze main in Forgejo mit Branch-Schutz und CODEOWNERS für .forgejo/workflows/*, damit Änderungen an Workflows ein Review brauchen.

Für regulierte oder abgeriegelte Umgebungen (Banken, Verteidigung, Gesundheitswesen):

  1. Schalte die Sperre „nur geschützte Branches“ ein.
  2. Konfiguriere zusätzlich Umgebungen für eine feinere Sperre pro Umgebung.
  3. Dieselbe Konfiguration der Vertrauensrichtlinien in der Cloud wie oben.

Diese Kombination entspricht dem Deploy-Sperrmodell, das GitHub Enterprise und AWS empfehlen (Umgebungen für Deployments verlangen plus nur geschützte Branches), ergänzt um den optionalen Abriegelungsschalter für regulierte Mandanten.

Vergleich: was fremforge speichert und was deine Cloud

Langlebige SecretsOIDC-Föderation (diese Seite)
Wo die Deploy-Zugangsdaten liegenEinstellungen → Secrets in fremforgeIm IAM deiner Cloud. fremforge sieht sie nie.
Lebensdauer der ZugangsdatenBis du sie rotierst1 Stunde (pro Vertrauensrichtlinie konfigurierbar)
Wenn fremforge kompromittiert wirdDer Angreifer hat deine AWS-SchlüsselDer Angreifer kann OIDC-JWTs ausstellen, aber die Vertrauensrichtlinie in der Cloud begrenzt, was diese JWTs tun können (pro Umgebung, pro Repository, pro Ref)
RotationsaufwandVierteljährlich pro Secret und CloudKeiner, Schlüssel wechseln mit jedem Job
Audit-SpurManuell: wer das Secret wann exportiert hatJede OIDC-Ausstellung steht in audit_events und im CloudTrail/Activity Log deiner Cloud

Einschränkungen

  • environment: in Objektform (mit name: + url:) wird über den Namen unterstützt; url: wird gelesen, aber derzeit nicht als Claim ausgegeben.
  • Per Matrix erweiterte Umgebungsnamen (environment: ${{ matrix.target }}) werden beim Ausstellen des OIDC-Tokens nicht aufgelöst, sodass kein Claim environment ausgegeben wird. Verwende pro Matrix-Job ein fest eingetragenes environment:, wenn du Vertrauensrichtlinien auf Basis des Umgebungs-Claims brauchst.
  • Sperren mit erforderlichen Freigebenden pro Umgebung (wie die Funktion „wait for approval“ der GitHub Environments) sind als Folgearbeit für Phase 2 vorgemerkt; die heutige Durchsetzung ist binär (geschützter Branch ja/nein).

Siehe auch