Agent-native Authentifizierung
fremforge akzeptiert drei Authentifizierungsmuster nach OAuth2 / OIDC für nicht-menschliche Aufrufer (Agenten, Skripte, KI-Assistenten, CI-Runner, Integrationen von Anbietern). Alle drei stützen sich auf dieselbe Scope-Allowlist pro Mandant und dieselbe Audit-Kette wie die Anmeldung von Benutzern: kein Schattenweg, kein separater Vertrauensspeicher.
Die gemeinsame Voraussetzung für jeden Ablauf, an dem der IdP des Kunden beteiligt ist: Registriere deinen IdP als OIDC-Auth-Quelle unter Org-Admin → SSO → Auth-Quellen, bestätige die Domain und konfiguriere auf der Bearbeitungsseite der Auth-Quelle unter Exchangeable scopes die Scope-Allowlists pro App. Dieselbe Registrierung trägt die Föderation der Anmeldung von Personen, sodass ein Kunde, der OIDC-SSO nutzt, die Grundlage bereits hat.
Hinweis zur Implementierung: Die Endpunkte
/_app/api/v1/auth/token-exchangeund/_app/api/v1/oauth/tokenwerden von der Control Plane von fremforge ausgeliefert, sie sind keine nativen Forgejo-Endpunkte. SDK-Autoren solltenhttps://frem.sh/_app/api/v1/*genau wie dokumentiert ansprechen.
Die drei Abläufe im Überblick
| Ablauf | Spezifikation | Rolle des Kunden-IdP | Am besten für |
|---|---|---|---|
| 1. Direktes JWT-Bearer | — (informell, „Bearer JWT“) | Stellt das JWT aus; fremforge prüft es | Workload-Identitäten (Entra Workload Identity, Okta-Dienstkonto, Google-Dienstkonto), die bereits an einen Mandanten gebundene JWTs ausstellen |
| 2. Token-Austausch (RFC 8693) | RFC 8693 | Stellt das subject_token aus (+ optional actor_token für OBO) | CI-Workflows, geplante Agenten, On-Behalf-Of-Abläufe (OBO), bei denen das Audit die Person und den für sie handelnden Agenten nennen muss |
3. client_credentials (RFC 6749 §4.4) | RFC 6749 | Keine: fremforge stellt client_id und client_secret aus | Apps, die gar keinen IdP haben, oder wenn der Kunde fremforge als IdP bevorzugt |
Alle drei Abläufe erzeugen ein kurzlebiges (≤ 1 Stunde) ffp_-Zugriffstoken, gebunden an einen Mandanten und einen Satz Scopes. RBAC, Ratenbegrenzung, Audit-Ausgabe und die Prüfungen der Mandantentrennung sind identisch mit einem von einer Person erstellten PAT.
Ablauf 1: JWT aus dem Kunden-IdP direkt als Bearer
Der Agent legt sein vom IdP ausgestelltes JWT direkt als Authorization: Bearer <jwt> an einem beliebigen /api/v1/*-Endpunkt vor. fremforge prüft das JWT gegen deine registrierte Auth-Quelle, bildet die Schnittmenge der Scopes aus dem App-Grant der Quelle und liefert die Antwort: kein zusätzlicher Umlauf, keine gespeicherten Zugangsdaten.
Endpunkt: jede Route frem.sh/_app/api/v1/*.
Einrichtung (einmalig, pro Auth-Quelle):
- Org-Admin → SSO → eine OIDC-Quelle für deinen IdP registrieren (oder eine bestehende für die Anmeldung von Personen wiederverwenden).
- Scope-Allowlists pro App unter Auth-Quelle bearbeiten → App-Grants konfigurieren. Die Allowlist bindet eine IdP-Anwendung (abgeglichen über den Claim
azpoderaud[0]des JWT) an einen maximalen Satz Scopes. - Org-Admin → Programmatischer Zugriff → JWT-Bearer und Austausch verwalten → Direktes JWT-Bearer zulassen für die Quelle einschalten.
- (Nur bei mandantenübergreifenden IdPs) Eine Claim-Assertion eintragen, die die Quelle an deinen Mandanten bindet:
{"tid": "<your-Entra-tenant-guid>"}für Entra/common/,{"hd": "yourdomain.com"}für Google Workspace usw. Das Admin-Formular weigert sich, direktes Bearer für eine mandantenübergreifende Issuer-URL ohne Claim-Assertions einzuschalten.
Anforderungen an das Token:
issentspricht der registrierten URL der Quelle (normalisiert, ohne Beachtung der Groß- und Kleinschreibung, ein abschließender Schrägstrich ist erlaubt).audentspricht entweder der konfigurierten Audience-Überschreibung der Quelle ODER der kanonischen Audience pro Mandanthttps://frem.sh/<your-tenant-slug>.expliegt in der Zukunft,iatnicht unangemessen weit in der Vergangenheit (jose erlaubt 30 Sekunden Uhrenabweichung).- Alle an der Quelle konfigurierten
claim_assertionspassen auf den geprüften Payload. - Der Signaturschlüssel wird über OIDC Discovery + JWKS aufgelöst (10 Minuten zwischengespeichert, vom Kunden über die übliche Schlüsselrotation des IdP steuerbar).
Beispiel (Entra Workload Identity):
# 1. Acquire a JWT from your IdP (managed identity, federated identity,
# or workload-identity SDK) with aud=https://frem.sh/<your-slug>
ACCESS_TOKEN=$(az account get-access-token \
--resource https://frem.sh/acme \
--query accessToken -o tsv)
# 2. Call any fremforge API directly
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
https://frem.sh/_app/api/v1/orgs/acme/reposSicherheitseigenschaften:
- Das JWT ist die Zugangsberechtigung. In der Datenbank von fremforge wird keine PAT-Zeile angelegt. Widerruf =
expablaufen lassen ODER die IdP-Quelle entfernen bzw. direktes Bearer ausschalten (die nächste Anfrage wird abgewiesen). - Eine mandantenübergreifende Bindung ist konstruktionsbedingt unmöglich: Eine Audience pro Mandant bindet das JWT an einen Slug, und die Tabelle auth_sources erzwingt auf Datenbankebene eine UNIQUE-Bedingung, die verhindert, dass zwei Mandanten denselben Issuer für direktes Bearer registrieren.
- Die Scopes werden durch den App-Grant der Quelle begrenzt. Scopes, die Geld bewegen (
billing:write,sso:write,policy:write,orgs:write), erfordern ausdrückliche Einträge in der Allowlist pro App: Eine NULL-Allowlist gewährt sie NIE.
Ablauf 2: Token-Austausch nach RFC 8693 (mit OBO)
Der Agent legt ein vom IdP ausgestelltes JWT als subject_token vor und tauscht es gegen ein kurzlebiges fremforge-PAT (ffp_). Der Parameter actor_token ermöglicht die Delegation im Auftrag einer Person: Der Agent handelt für eine benannte Person. fremforge kennt keine Agenten-Identitäten: Die Person ist der Akteur im Audit, und der Agent ist ein Detail jeder Audit-Zeile.
Endpunkt: POST frem.sh/_app/api/v1/auth/token-exchange.
Beispiel (einfacher Austausch):
curl -X POST https://frem.sh/_app/api/v1/auth/token-exchange \
-H "Content-Type: application/json" \
-d '{
"subject_token": "<customer-idp-jwt>",
"tenant_slug": "acme",
"scopes": ["orgs:read", "findings:write"],
"name": "ci-build-bot"
}'Beispiel (OBO: Der Agent handelt im Auftrag von Alice):
curl -X POST https://frem.sh/_app/api/v1/auth/token-exchange \
-H "Content-Type: application/json" \
-d '{
"subject_token": "<alice-idp-jwt>",
"actor_token": "<agent-idp-jwt>",
"tenant_slug": "acme",
"scopes": ["findings:read", "audit:read"]
}'Fordere nur Scopes an, die der Token-Austausch gewähren kann: die austauschbare Menge (orgs:read, orgs:write, audit:read, exports:read, exports:write, findings:read, findings:write, components:read, deployments:report, runners:read, runners:write, sboms:write, seats:read, seats:write, billing:read, billing:write, sso:read, sso:write, policy:read, policy:write), eingeschränkt durch den App-Grant an deiner Auth-Quelle. Ein Scope außerhalb einer der beiden wird verworfen, und eine Anfrage, bei der nichts übrig bleibt, wird mit no-scopes-grantable abgewiesen. Zugriff auf Repositorys und Issues ist nicht Teil der Scopes der fremforge-API und kann daher nicht eingetauscht werden.
Jedes Audit-Ereignis, das das ausgestellte PAT schreibt, nennt Alice als actor, als oidc:idp:<alice-sub> (der geprüfte sub des Subject-Tokens). Der Agent ist ein Detail der Zeile: fields.agent ist oidc:agent:<agent-sub> (der geprüfte sub des Actor-Tokens), und fields.via_token ist die ID des ausgestellten PAT. Der Austausch selbst wird als api.token.exchange.obo erfasst, ebenfalls mit Alice als Akteurin und dem Agenten in fields.agent. Beide Werte stammen nur aus den geprüften JWTs; nichts im Request-Body kann die Person benennen oder ändern.
Ohne actor_token nennt ein Austausch mit dem Kunden-IdP trotzdem die Person als Akteur, mit via_token und ohne agent. Ein Austausch über Runner-OIDC hat keine Person (ein CI-Job ist keine), daher behalten seine Ereignisse actor=token:<id>.
Compliance-Abfragen wie „jede Aktion, die mein Anthropic-Agent im letzten Quartal im Auftrag von Alice ausgeführt hat“ filtern das Audit-Log auf Alice und lesen fields.agent. Der Akteur-Filter trifft den Akteur, die Spalte on_behalf_of und fields.agent, sodass ein Filter auf oidc:agent:<agent-sub> alles findet, was dieser Agent getan hat, für jede Person. Im Audit-Log der Administration trifft der Filter Teilzeichenketten, und solche Zeilen tragen eine Markierung über Agent; GET /orgs/{org}/audit?actor= trifft genau. Ereignisse, die vor dem 2026-10-23 geschrieben wurden, nannten den Agenten als Akteur und Alice in on_behalf_of. Die Hash-Kette deckt sie ab, daher behalten sie diese Form, werden als im Auftrag von Alice angezeigt, und dieselben Filter finden sie.
Unterschied zu Ablauf 1: Der Token-Austausch legt ein gespeichertes PAT an (es kann also eine ausdrückliche Teilmenge von Scopes und einen eigenen Namen tragen). Ablauf 1 ist flüchtig. Wähle den Austausch, wenn der Agent viele Aufrufe in einem einzigen Workflow-Lauf macht; wähle direktes Bearer, wenn jeder Aufruf für sich steht.
Ablauf 3: client_credentials (fremforge als IdP)
Für Apps ohne IdP oder wenn der Kunde einen einzigen Satz Zugangsdaten bevorzugt, der in fremforge verwaltet wird. Der Mandanten-Admin erzeugt ein Paar (client_id, client_secret), die App authentifiziert sich mit Authorization: Basic (oder per POST-Formular) und erhält ein kurzlebiges ffp_-PAT.
Einrichtung:
- Org-Admin → Programmatischer Zugriff → JWT-Bearer und Austausch verwalten → Neuen Client anlegen (im Abschnitt
client_credentials). - Wähle einen Namen, eine Obergrenze für Scopes und optional eine Audience-Allowlist.
- Kopiere das
client_secretsofort: Es wird einmal gezeigt und nie wieder angezeigt.
Endpunkt: POST frem.sh/_app/api/v1/oauth/token.
Beispiel:
curl -X POST https://frem.sh/_app/api/v1/oauth/token \
-u "ffc_<client-id>:ffs_<client-secret>" \
-d "grant_type=client_credentials" \
-d "scope=findings:write runners:read"Antwort:
{
"access_token": "ffp_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "findings:write runners:read",
"audience": "https://frem.sh/acme"
}Rotation: Klicke jederzeit in der Admin-Oberfläche auf Rotieren. Das aktuelle Secret bleibt 24 Stunden lang neben dem neuen gültig, sodass du deine Konfiguration ohne Ausfall umstellen kannst. Nach Ablauf der 24 Stunden authentifiziert nur noch das neue Secret.
Widerruf: Ein Klick auf Widerrufen macht den Client ungültig UND widerruft in einer einzigen Transaktion kaskadierend jedes ffp_-Token, das er je ausgestellt hat: ausfallsicher, selbst wenn der Operator-Pod mitten im Widerruf beendet wird.
Was die drei Abläufe gemeinsam haben
| Eigenschaft | Wie sie durchgesetzt wird |
|---|---|
| Mandantentrennung | Jedes ausgestellte Token hat tenant_id gesetzt; requireTenantBinding weist mandantenübergreifende Aufrufe mit 404 ab. Ein UNIQUE auf Datenbankebene verhindert, dass ein Issuer von zwei Mandanten für direktes Bearer beansprucht wird. |
| Scope-Obergrenze | Alle drei bilden die Schnittmenge mit einer vom Betreiber kontrollierten Menge austauschbarer Scopes UND der Allowlist des Kunden pro IdP bzw. pro Client. Scopes, die Geld bewegen (billing:write, sso:write, policy:write, orgs:write), erfordern ausdrückliche Einträge in der Allowlist und werden nie standardmäßig gewährt. |
| TTL | Alle ausgestellten Tokens sind auf 1 Stunde begrenzt. Kunden, die länger gültige Zugangsdaten wollen, nutzen den Weg über die Admin-Oberfläche (Personal Access Tokens). |
| Audit-Kette | Jede Ausstellung schreibt ein audit_event mit actor, Aktion und Scopes. Bei einem Austausch mit dem Kunden-IdP ist der Akteur die Person und der Agent (bei OBO) steht in fields.agent; jedes spätere Ereignis, das das Token schreibt, trägt die Person als Akteur sowie agent und via_token. Der Hash der Kette deckt fields_json ab, einschließlich beider. |
| Ratenbegrenzung | Die Token-Endpunkte sind pro IP begrenzt. /auth/token-exchange und /oauth/token teilen sich eine Grenze von 30/min, um die CPU-Kosten der JWT-Prüfung bei unsinnigen Eingaben zu begrenzen. |
| Erreichbarkeit von JWKS | Alle ausgehenden Abrufe von JWKS und Discovery laufen über ein eingeschränktes Egress-Gateway: SSRF, private IPs und Cloud-Metadaten werden am Netzwerkübergang blockiert. JWKS wird 10 Minuten zwischengespeichert. |
| EU-Datenresidenz | Die Pipeline erreicht nie einen in den USA gehosteten Endpunkt von Sigstore / Auth0 / Okta: Die Prüfung findet vollständig innerhalb des Footprints von fremforge in T Cloud Public eu-de statt. Egress zum Kunden-IdP läuft nur über den per SSRF-Proxy abgesicherten Abruf. |
Zusammenfassung des Bedrohungsmodells
| Bedrohung | Gegenmaßnahme |
|---|---|
Ein Kunde registriert eine mandantenübergreifende Entra-URL /common/ ohne Claim-Assertion tid | Das Admin-Formular weist es ab; die Hilfsfunktion isMultiTenantIssuerUrl() erkennt Entra (v1 + v2), Google Workspace, Microsoft Gov / China Clouds |
Zwei Mandanten teilen sich eine IdP-URL iss | Partieller UNIQUE-Index auf (LOWER(RTRIM(iss,'/'))) WHERE direktes Bearer eingeschaltet: Die zweite Registrierung wird auf Datenbankebene abgewiesen |
| Ein JWT aus dem Kunden-IdP wird gegen den falschen Mandanten wiederverwendet | Audience pro Mandant https://frem.sh/<slug>: Ein Token, das für aud=https://frem.sh/acme ausgestellt wurde, kann die Audience-Prüfung des Mandanten initech schlicht nicht bestehen |
Durchgesickertes client_secret | Der Mandanten-Admin führt Widerrufen aus → die kaskadierende Transaktion beendet den Client UND jedes PAT, das er ausgestellt hat; die Rotation unterstützt ein 24-Stunden-Fenster mit zwei Secrets für einen Wechsel ohne Ausfall |
| Gefälschte Claims im OBO-actor_token | jose prüft Signatur + iss + aud gegen dieselbe Quelle wie beim Subject; sub ist unveränderlich, email/name wird nie vertraut |
| Veralteter JWKS-Cache nach Schlüsselrotation beim IdP | Feste TTL von 10 Minuten: betrieblich begrenzt |
| Netzwerkinterner Hostname im Discovery-Dokument des IdP (DNS-Rebind / SSRF) | Das eingeschränkte Egress-Gateway blockiert den Abruf; eine Prüfung auf https:// im Code bietet zusätzliche Tiefenverteidigung |
Kurzreferenz
| Pfad | Methode | Auth | TTL | Body |
|---|---|---|---|---|
/_app/api/v1/auth/token-exchange | POST | Das JWT IST die Authentifizierung | 1h | subject_token (+ optional actor_token), tenant_slug, scopes, name |
/_app/api/v1/oauth/token | POST | Basic ODER Formular-Body | 1h | grant_type=client_credentials, client_id, client_secret, scope, audience |
/api/v1/* (beliebig) | beliebig | Authorization: Bearer <jwt or ffp_*> | laut exp / laut PAT-Zeile | — |
Verwandte Themen
- SSO-Einrichtung (OIDC): Registriere deinen IdP für die Anmeldung von Personen und für Agent-Auth
- Workflow-OIDC für Cloud-Authentifizierung: die ausgehende Richtung (Runner-VM → Vertrauensrichtlinien in AWS/GCP/Azure)
- Audit-Log + Kette: prüfe, was jeder Agent getan hat, gefiltert nach der Person oder nach dem Agenten
- API-Tokens (PATs): von Personen erstellte, langlebige Tokens für dieselbe Oberfläche
/api/v1/*; erstellt unter Benutzereinstellungen → API-Token, siehe Authentifizierungsrichtlinie zu maximaler Lebensdauer und Rotationsrichtlinie