Identität und Zugriff
pacod-id
Ein Ort, an dem sich Menschen und Maschinen anmelden — und an dem festgehalten wird, was sie in all deinen Produkten dürfen.
pacod-id ist ein OpenID Connect Provider für eigene Anwendungen, mit Konten, Organisationen, Rollenzuweisungen und einem Änderungs-Feed, dem Produkte folgen, um Zugriffe lokal zu entscheiden. Geschrieben in Rust, mit Client-Bibliotheken für Rust, TypeScript und Python.
- Apache-2.0
- Version 0.1.0
- Vorschau
Funktionen
-
OpenID Connect Provider
Anmeldung für deine eigenen Anwendungen, geprüft mit der Konformitäts-Testsuite der OpenID Foundation.
-
Konten und Organisationen
Personen, Organisationen, Rollenzuweisungen, Servicekonten, Mail und ein Audit-Trail.
-
Eine Regel-Engine, drei Portierungen
Produktmanifeste, Scopes, allowed, has_role und touches — in Rust, TypeScript und Python, die alle dieselben YAML-Testvektoren bestehen.
-
Autorisierungs-Feed
Produkte folgen einem Änderungs-Feed der Rollenzuweisungen und entscheiden Zugriffe lokal, ohne Roundtrip pro Anfrage.
-
Tokens, richtig gemacht
Compact JWS mit RS256 und ES256 auf ring, JWKS, Introspection und Token Exchange.
-
Gebrandete Anmeldung
Gehostete Anmelde- und Kontoseiten mit Branding pro Produkt, hell und dunkel, in vier Sprachen.
-
Clients zum Einbinden
Ein Rust-Client-Crate, ein Python-Paket mit FastAPI-Dependencies und ein SvelteKit-Anmeldeablauf als Backend-for-Frontend.
-
Startklar
Ein Docker-Image, ein Entwicklungs-Stack mit Mail und einem Google-Mock sowie ein Helm-Chart.
Einordnung
Anmeldung für eine ganze Produktfamilie
pacod-id ist der Identitätsdienst hinter den pacod-Produkten. Personen melden sich einmal an und erreichen mit demselben Konto die Gateway-Konsole, die Agentenplattform, das CMS-Studio und die Wissensdatenbank. Auch Maschinen melden sich an, als Servicekonten.
Es gibt ihn, damit kein Produkt eigene Passwörter, Konto-Mails und Admin-Seiten betreiben muss.
Alle Konten liegen in einem gemeinsamen Pool: Das sub einer Person ist in jedem Realm dasselbe.
Was sich von Produkt zu Produkt unterscheidet, ist das Aussehen der Anmeldung und wer dort was
darf — und pacod-id hält beides fest.
pacod-id ist ein OpenID Connect Provider für Ihre eigenen Anwendungen. Jeder Client steht in der Konfiguration: Es gibt keine dynamische Registrierung und keinen Zustimmungsdialog.
Realms, Clients und Tokens
Ein Realm ist ein Issuer-Host mit eigener Marke, eigenem Anmelde-Cookie, eigenen Sprachen
und Richtlinien: Passwortlänge, Sitzungsdauer, ob sich jeder registrieren darf, welche Rollen
Konten verwalten. pacod-id wählt den Realm anhand des Hosts der Anfrage, sodass sich zwei Marken
einen Kontenpool teilen und trotzdem wie zwei Produkte aussehen können. Die Marke eines Realms
ist eine von fünf eingebauten: gateway, cms, kb sowie shop und bazaar für das fiktive
Produkt, das Dokumentation und Tests verwenden.
Ein Client ist eine Anwendung, von einer von drei Arten:
| Art | Typischer Einsatz | Grants |
|---|---|---|
confidential |
Das Backend-for-Frontend eines Produkts, das ein Secret hält | authorization_code, refresh_token |
public |
Eine native oder mobile App; ihre Codes schützt allein PKCE | authorization_code, refresh_token |
service |
Der Server eines Produkts, der mit pacod-id spricht | client_credentials, Token Exchange |
Angeboten wird nur der Authorization Code Flow, und PKCE (S256) ist für jeden Client Pflicht, auch für vertrauliche.
- Access Tokens sind JWTs (RFC 9068), je nach Client RS256 oder ES256, standardmäßig 5 Minuten gültig (2 bis 15). Sie sagen, wer, in welcher Sitzung und mit welchem Scope — Rollen tragen sie nicht.
- id_tokens leben 5 Minuten und tragen Adresse und Namen, wenn die Scopes danach fragen.
- Refresh Tokens sind opak und werden bei jeder Nutzung rotiert. Wird nach einer Kulanzfrist von 30 Sekunden ein bereits ersetzter vorgelegt, enden alle Sitzungen des Kontos, und der Kontoinhaber erhält eine Mail.
- Token Exchange (RFC 8693) lässt den Dienst eines Produkts im Namen einer Person handeln,
innerhalb ihrer Rechte, wobei
actden Dienst nennt.
Die Anmeldung mit Google lässt sich je Realm einschalten. Endet eine Sitzung, erhält jeder Client, der darin einen Grant hatte und eine Back-Channel-Logout-URI hat, ein Logout-Token.
Zugriff lokal entscheiden
Rollen stehen bewusst nicht in den Access Tokens. Jedes Produkt schickt beim Start ein Manifest:
seine Scope-Typen, seine Rollen mit ihren Berechtigungen und wer was vergeben darf. Scopes bilden
einen Baum, von platform, wahlweise über organisation:<id>, bis zu den eigenen Typen des Produkts, etwa
gw.workspace:ws_1. Eine Zuweisung ist eine Rolle an einem Scope, und ihre Berechtigungen
gelten nach unten: Eine Rolle an einer Organisation zählt in jedem Workspace darunter.
product: shop
scope_types:
profile: { parent: platform }
roles:
admin:
at: [platform]
permissions: [admin.read, admin.write, profiles.assign]
protected: true
profile_editor:
at: [profile]
permissions: [profile.edit]
grant:
default: admin.write
pacod-id speichert die Zuweisungen und prüft jede Änderung daran: Wer eine Rolle vergibt, braucht
die Vergabeberechtigung des Manifests an diesem Scope, und eine Rolle mit protected verliert nie
ihren letzten Inhaber.
Die Regel-Engine beantwortet allowed, has_role, touches und can_grant. Es gibt sie in
Rust, TypeScript und Python, und alle drei bestehen dieselben YAML-Testvektoren
(policy/engine-vectors/). Die Vektoren sind die Spezifikation, sodass eine Entscheidung überall
gleich ausfällt, wo sie getroffen wird.
Der Autorisierungs-Feed
Jedes Produkt hält seine eigene Kopie der Zuweisungen und folgt den Änderungen:
- Beim Start lädt es einen Snapshot: seinen Katalog, die Organisationen und seine Scopes,
die Zuweisungen seiner Rollen, deaktivierte und gelöschte Konten sowie kürzlich beendete
Sitzungen, alles zum Stand einer Sequenznummer,
seq. - Danach fragt es per Long Poll die Änderungen nach dieser
seqab und wartet bis zu 30 Sekunden pro Aufruf. Die Antwort kommt, sobald sich etwas ändert: PostgreSQLLISTENweckt die wartende Anfrage. - Änderungen werden 7 Tage aufbewahrt. Ein älterer Cursor bekommt eine 410, und das Produkt lädt den Snapshot neu.
Die Ereignisse betreffen Zuweisungen, Konten, Sitzungen, Service-Tokens, Scopes und Kataloge.
Schreibende Aufrufe antworten mit der seq des Feeds, sodass ein Produkt, das seinen eigenen
Schreibvorgang lesen muss, wartet, bis seine Kopie aufgeholt hat. Die Client-Bibliotheken für
Rust und Python erledigen all das in ihrem Zuweisungs-Cache.
Gemessen mit make measure auf einem Entwicklerrechner (Docker Desktop auf Apple Silicon)
erreichte eine Änderung einen bereits wartenden Follower mit einem p99 von etwa 10 ms — das Ziel
liegt unter einer Sekunde.
Die Bausteine
| Teil | Was |
|---|---|
crates/pacod-id |
Der Dienst: OpenID Connect Provider, Konten, Organisationen, Rollenzuweisungen, der Autorisierungs-Feed, Mail, Audit |
crates/pacod-id-authz |
Die Regel-Engine |
crates/pacod-id-jose |
Compact JWS (RS256, ES256) auf ring, und JWKS |
crates/pacod-id-client |
Token-Prüfung, der Zuweisungs-Cache, Introspection, Token Exchange für Rust-Produkte |
python/ |
Das Paket pacod_id: Regel-Engine, Token-Prüfung, FastAPI-Dependencies |
packages/* |
@pacod/id-authz, @pacod/ui, @pacod/id-client, @pacod/id-sveltekit |
ui/ |
Die gehosteten Anmelde- und Kontoseiten |
Der Dienst ist ein einziges Rust-Binary mit drei Listenern:
| Listener | Port | Für |
|---|---|---|
| öffentlich | 8070 | Browser und Apps, über einen TLS-Reverse-Proxy: die OpenID-Connect-Endpunkte und die gehosteten Seiten |
| intern | 8071 | Nur die Server der Produkte: Tokens, der Feed, Introspection, Beenden von Sitzungen, die Admin-API |
| Metriken | 9170 | /healthz, /readyz und /metrics |
Die gehosteten Anmelde-, Konto- und Admin-Seiten sind ein statischer SvelteKit-Build, den das
Binary selbst ausliefert. @pacod/ui enthält die Design-Tokens, Marken-Themes und
Anmeldekomponenten, die jedes pacod-Produkt nutzt — auch diese Website.
Sicherheitsmodell
Die wichtigsten Schutzmaßnahmen laut Bedrohungsmodell:
- Passwörter: Argon2id auf dem OWASP-Minimum (19 MiB, 2 Durchläufe), standardmäßig 12 Zeichen und eine Sperrliste gängiger Passwörter. Die Drosselung der Anmeldeversuche liegt in PostgreSQL und gilt für alle Instanzen gemeinsam: pro Konto, pro bekanntem Browser und pro IP-Adresse.
- Kein Ausforschen von Konten: Anmeldung, Passwort-Zurücksetzen und Registrierung antworten gleich, ob es zu einer Adresse ein Konto gibt oder nicht.
- Signaturschlüssel: von pacod-id selbst erzeugt, in der Datenbank versiegelt unter einem
Key-Encryption-Key (
PID_KEK), alle 90 Tage rotiert, wobei der nächste Schlüssel einen Tag vorher veröffentlicht wird. Eine Notfallrotation ist ein einziger Befehl. - Gespeicherte Geheimnisse: Refresh Tokens und Mail-Links als SHA-256, Client-Secrets und Service-Tokens als HMAC mit einem eigenen Pepper. Die eigenen Logzeilen von pacod-id enthalten nie ein Passwort, ein Token oder einen Schlüssel.
- Erreichbarkeit: Der öffentliche Listener bedient keine internen oder Admin-Routen, und die NetworkPolicy des Helm-Charts lässt den internen Port nur von den Quellen zu, die Sie angeben. Am gemeinsamen Edge des vollständigen pacod-Stacks hat nur der öffentliche Listener eine Route.
- Image: ein statisches musl-Binary auf distroless, ohne Root-Rechte und ohne Shell.
Betrieb
Realms und Clients stehen in einer TOML-Datei, Geheimnisse nie. Das Secret eines Clients kommt
aus der Variable, die sein secret_env nennt.
[[realm]]
name = "gateway"
issuer = "https://id.example.com"
brand = "gateway"
locales = ["en", "bg", "de", "ru"]
self_signup = false
[[client]]
id = "pacod-gw-console"
realm = "gateway"
kind = "confidential"
secret_env = "PGW_UI_CLIENT_SECRET"
redirect_uris = ["https://console.example.com/auth/callback"]
audience = "pacod-gw"
pacod-id check-config prüft die Datei ohne Datenbank. Darüber hinaus braucht der Dienst
PID_DATABASE_URL, PID_KEK und PID_TOKEN_PEPPER, jeweils als Variable, als Datei oder über
ein Secrets-Verzeichnis.
- Datenbank: PostgreSQL, mit einer eigenen Rolle, die kein Superuser ist. Migrationen fügen nur hinzu und laufen bei jedem Start unter einem Advisory Lock.
- Instanzen: beliebig viele teilen sich eine Datenbank. Jede verschickt fällige Mails und Logouts und lädt Schlüssel und Kataloge neu; eine, die das Lease hält, rotiert die Schlüssel und entfernt alte Zeilen.
- Mail: Keine Anfrage wartet auf SMTP. Mails gehen in eine Outbox, in Marke und Sprachen des Realms, und werden etwa acht Stunden lang erneut versucht.
- Erster Administrator: wird beim Start aus
PID_BOOTSTRAP_ADMIN_*angelegt. - Beobachtung:
/readyzprüft die Datenbank und den Signaturschlüssel./metricsliefert Anzahl und Dauer der Anfragen pro Listener. Die Logs sind JSON, ohne Zeile pro Anfrage. Anmeldungen, Rollenänderungen und Aktionen von Administratoren landen im Audit-Log, das 400 Tage aufbewahrt wird und auf den Admin-Seiten lesbar ist.
Ein Gateway, das Personen bisher selbst angemeldet hat, kann sie mit pacod-id import gw
übergeben: Passwörter, Admin-Tokens und Einladungslinks funktionieren weiter.
make compose-up startet einen Entwicklungs-Stack in pacod-net, dem Docker-Netzwerk, dem
jedes pacod-Produkt beitritt. Die Compose-Konfiguration besteht aus zwei Dateien.
docker-compose.yml enthält nur pacod-id. docker-compose.standalone.yml ergänzt eigene Kopien
der gemeinsamen Komponenten, die pacod-id braucht: PostgreSQL (pacod-postgres),
pacod-db-init, das bei jedem Start die Rolle pacod_id und ihre Datenbank anlegt, falls sie
fehlen, und einen Mail-Sink (pacod-mailpit), dazu den Google-Mock (id-mock-idp). Der Stack
lauscht nur auf 127.0.0.1, mit dem Issuer unter http://127.0.0.1:26070.
Der vollständige pacod-Stack, pacod-demo (in Betrieb
unter demo.pacod.io), lädt nur die erste Datei. Er betreibt ein
PostgreSQL, einen Mail-Sink, einen Edge und ein pacod-id für alle Produkte, mit einem einzigen
Realm für sie alle. platform/ enthält den Anteil von pacod-id an dieser Konfiguration: sein
Datenbankskript, seine Route am gemeinsamen Caddy und seinen Prometheus-Job. Auf einem gemeinsam
genutzten PostgreSQL-Server rät das Bedrohungsmodell zu einer eigenen Rolle ohne
Superuser-Rechte für jedes Produkt: Ein Produkt, das sich als Superuser verbindet, kann die
Tabellen von pacod-id lesen.
Was noch fehlt
- pacod-id 0.1.0 ist noch nicht veröffentlicht. Upgrades, Backup und Wiederherstellung wurden noch nicht durchgespielt.
- Das Helm-Chart besteht
helm lintund die Schemaprüfungen, wurde aber noch auf keinem Cluster installiert. Ein echter SMTP-Server und das echte Google wurden nicht ausprobiert, der Mock der Entwicklungsumgebung schon. - Die Konformitätstests der OpenID Foundation liefen lokal: die Pläne Config, Basic und RP-Initiated Logout ohne eine einzige verletzte Bedingung, während ein Back-Channel-Logout-Test am eigenen Zertifikat der Testsuite hängen blieb. Lokale Läufe sind keine Zertifizierung.
- Noch keine MFA und keine Passkeys und damit kein
acr-Claim; die Dokumentation führt sie als geplant. - Keine Claims für Adresse und Telefon; der Scope
profileliefertnameundlocale. - Der interne Listener spricht unverschlüsseltes HTTP, und über die Drosselung bei der Anmeldung hinaus gibt es kein allgemeines Rate Limit.
Weiterlesen
- Runbook: Installation und Betrieb.
- HTTP-API: jeder Endpunkt und die Ereignisse des Feeds.
- Bedrohungsmodell: Schutzmaßnahmen und verbleibende Risiken.
- Messungen: Latenz des Token-Endpunkts und des Feeds.
- Konformitätsergebnisse und die Testvektoren der Regel-Engine.
Screenshots
Tech-Stack
- Geschrieben in
- Rust
- TypeScript
- Python
- Tech-Stack
- Rust
- axum
- PostgreSQL
- OpenID Connect
- SvelteKit
- Docker
- Helm
Schnellstart
make test-servicesmake verifymake e2emake imagemake compose-upFunktioniert mit
-
pacod-gw
Die Konsole des Gateways kann Personen über pacod-id anmelden und prüft Workspace-Rollen mit dessen Regel-Engine.
-
pacod-ap
App und Admin-Konsole der Agentenplattform melden sich als Confidential Client über pacod-id an.
-
pacod-cms
Das Studio meldet Personen über pacod-id an und verwaltet dort Rollen pro Organisation und pro Website.
-
pacod-kb
Die Oberfläche der Wissensdatenbank ist ein OIDC-Client von pacod-id; die Rollen Viewer, Editor und Admin werden dort vergeben.
Lizenz
Apache-2.0