Alle Projekte

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

Diagramm: wie die pacod-Projekte zusammenpassen LLM-Aufrufe Tippe auf ein Projekt, um zu sehen, womit es verbunden ist.

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 act den 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:

  1. 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.
  2. Danach fragt es per Long Poll die Änderungen nach dieser seq ab und wartet bis zu 30 Sekunden pro Aufruf. Die Antwort kommt, sobald sich etwas ändert: PostgreSQL LISTEN weckt die wartende Anfrage.
  3. Ä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: /readyz prüft die Datenbank und den Signaturschlüssel. /metrics liefert 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 lint und 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 profile liefert name und locale.
  • Der interne Listener spricht unverschlüsseltes HTTP, und über die Drosselung bei der Anmeldung hinaus gibt es kein allgemeines Rate Limit.

Weiterlesen

Screenshots

Tech-Stack

Geschrieben in
  • Rust
  • TypeScript
  • Python
Tech-Stack
  • Rust
  • axum
  • PostgreSQL
  • OpenID Connect
  • SvelteKit
  • Docker
  • Helm

Schnellstart

Entwickeln und testen
make test-servicesmake verifymake e2e
Image bauen und starten
make imagemake compose-up

Funktioniert 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