Verteiltes Vertrauen mit Service-to-Service-Authentifizierung
Auf dieser Seite
In serviceorientierten Architekturen (SOA) sind die „Services“ die Bausteine deiner Anwendung. Services sind autonom, lose gekoppelt und meist zustandslos. Autonom, weil sie unabhängig vom Zustand und Kontext anderer Services sind. Lose gekoppelt, weil sie unabhängig von den Technologien anderer Services sind. Zustandslos, weil sie zwischen zwei Anfragen keinen Zustand halten.
Services sind die Bausteine deiner Anwendung. Und sie sind die Bausteine deiner Sicherheitsarchitektur.
Vertrauen verteilen
In einer SOA sind Services unabhängig voneinander. Damit sind sie auch unabhängig von den Sicherheitsmechanismen anderer Services.
Das ist gut.
Du kannst für verschiedene Services verschiedene Sicherheitsmechanismen einsetzen. Andere Authentifizierung, andere Autorisierung, andere Verschlüsselung und so weiter.
Das ist auch schlecht.
Du musst verschiedene Sicherheitsmechanismen für verschiedene Services verwalten. Andere Authentifizierung, andere Autorisierung, andere Verschlüsselung und so weiter.
Service-to-Service-Authentifizierung
In einer SOA sind Services unabhängig voneinander. Du erkennst das Muster.
Anfragen zwischen Services zu authentifizieren, ist schwierig. Die Mechanismen, mit denen du Nutzer authentifizierst, kannst du hier nicht verwenden. Keine Cookies, keine Sessions, und auf einen Identity Provider (IdP) willst du dich erst recht nicht verlassen.
Bei Platform-as-a-Service-Angeboten (PaaS) wie Cloudflare Workers, Heroku, Cloud Foundry und Vercel kannst du nicht einmal darauf zählen, dass die darunterliegende Infrastruktur einen sicheren Kanal zwischen den Services stellt.
Du brauchst eine Möglichkeit, Anfragen zwischen Services zu authentifizieren, die unabhängig von der Infrastruktur ist und unabhängig von den Sicherheitsmechanismen anderer Services.
Hier kommen JWTs und JWKSs ins Spiel.
JSON Web Tokens (JWTs)
JSON Web Tokens (JWTs) sind ein kompaktes, URL-sicheres Format, um Claims zwischen zwei Parteien zu übertragen. Die Claims eines JWT sind als JSON-Objekt kodiert. Dieses Objekt dient als Payload einer JSON-Web-Signature-Struktur (JWS) oder als Klartext einer JSON-Web-Encryption-Struktur (JWE). So lassen sich die Claims digital signieren oder mit einem Message Authentication Code (MAC) gegen Manipulation schützen und/oder verschlüsseln.
JWT-Aufbau
Ein JWT ist ein String aus drei Teilen: Header, Payload und Signatur. Header und Payload sind JSON-Objekte. Die Signatur ist ein Base64-kodierter String.
<base64url-encoded header>.<base64url-encoded payload>.<base64url-encoded signature>
JWT-Header
Der Header besteht üblicherweise aus zwei Teilen: dem Typ des Tokens, also JWT, und dem verwendeten Signaturalgorithmus, etwa HMAC SHA256 oder RSA.
{
"alg": "HS256",
"typ": "JWT"
}
JWT-Payload
Die Payload enthält die Claims. Claims sind Aussagen über eine Entität (meist den Nutzer) und zusätzliche Metadaten. Es gibt drei Arten von Claims: Registered, Public und Private Claims.
- Registered Claims: Ein Satz vordefinierter Claims. Sie sind nicht verpflichtend, aber empfohlen, damit es nützliche, interoperable Claims gibt. Dazu gehören iss (Issuer), exp (Ablaufzeit), sub (Subject), aud (Audience) und weitere.
- Public Claims: Wer JWTs nutzt, kann sie frei definieren. Um Kollisionen zu vermeiden, sollten sie aber in der IANA JSON Web Token Registry stehen oder als URI mit einem kollisionssicheren Namensraum definiert sein.
- Private Claims: Eigene Claims, die Informationen zwischen Parteien austauschen, die sich auf deren Nutzung geeinigt haben.
{
"sub": "1234567890",
"iss": "https://my-experience-api.willhackett.xyz",
"aud": "https://my-resource-api.willhackett.xyz"
}
Die Claims iss und aud bezeichnen den Aussteller und die Zielgruppe des JWT. Der Claim sub bezeichnet das Subject des JWT. Hier ist das die ID des Nutzers, in dessen Auftrag die Anfrage läuft. In der Praxis würde ich empfehlen, das JWT des anfragenden Nutzers in das neue JWT einzupacken. So kann der empfangende Service die Identität des Nutzers prüfen und feststellen, ob der Nutzer auf deiner Ressourcenebene die nötigen Rechte für die gewünschte Aktion hat.
Aussteller und Zielgruppe spielen später bei den Vertrauensgrenzen eine Rolle.
JWT-Signatur
Für die Signatur nimmst du den kodierten Header, die kodierte Payload, ein Secret und den im Header angegebenen Algorithmus und signierst das Ganze.
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);
Die Signatur stellt sicher, dass die Nachricht unterwegs nicht verändert wurde. Bei Tokens, die mit einem privaten Schlüssel signiert sind, belegt sie außerdem, dass der Absender des JWT der ist, der er zu sein vorgibt.
JSON Web Key Sets (JWKSs)
JSON Web Key Sets (JWKSs) sind Mengen von Schlüsseln. Sie enthalten die öffentlichen Schlüssel, mit denen du jedes JSON Web Token (JWT) prüfst, das der Autorisierungsserver ausgestellt und mit dem Algorithmus RS256 signiert hat.
JWKS-Aufbau
Ein JWKS ist ein JSON-Objekt, das eine Menge von JWKs beschreibt. Das JSON-Objekt MUSS ein Member „keys“ haben, ein Array von JWKs. Das ist das JWK-Set-Format.
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use": "enc",
"kid": "1"
},
{
"kty": "RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFb.....<remainder of RSA key omitted>.....ZU3e_TURo9-F1bp-hqOY_BLPbEx-Rmc6XaXk",
"e": "AQAB",
"alg": "RS256",
"kid": "2011-04-29"
}
]
}
Uns interessieren hier nur die öffentlichen Schlüssel. Mit ihnen prüfst du die Signatur der JWTs.
Vertrauensmodell
Das Vertrauensmodell ist einfach. Dem Service, der das JWT ausstellt, vertraust du, dass er gültige JWTs ausstellt. Dem Service, der das JWT empfängt, vertraust du, dass er es prüft. Dem empfangenden Service vertraust du nicht, im Namen des aufrufenden Services gültige JWTs auszustellen.
Wenn Service A ein JWT an Service B ausstellt, darf Service B es gegen das JWKS von Service A prüfen. Service B kann nicht selbst ein JWT signieren und erwarten, dass Service C ihm vertraut. Hier kommen die Vertrauensgrenzen ins Spiel.
Vertrauensgrenzen
Vertrauensgrenzen sind die Grenzen zwischen Services, die einander vertrauen.
If Service A issues a JWT to Service B, Service B is trusted to verify the JWT against the JWKS provided by Service A.
Jedes JWT enthält einen Claim iss und einen Claim aud. Die aud ist die vorgesehene Zielgruppe des JWT. Damit prüfst du, ob die Anfrage für den richtigen Service bestimmt ist. Die iss ist der Aussteller des JWT. Damit prüfst du, ob ein vertrauenswürdiger Service das JWT ausgestellt hat.
Service B vertraut Service A, gültige JWTs auszustellen. Angenommen, der Hostname von Service A ist service-a.willhackett.xyz und der von Service B ist service-b.willhackett.xyz. Dann wäre der Issuer-Claim https://service-a.willhackett.xyz und der Audience-Claim https://service-b.willhackett.xyz.
Service B würde Tokens vertrauen, die mit dem Issuer-Claim https://service-a.willhackett.xyz und dem Audience-Claim https://service-b.willhackett.xyz ausgestellt sind. Service B würde den JWKS-Endpunkt von Service A aufrufen, um die öffentlichen Schlüssel zu holen, mit denen er die von Service A ausgestellten JWTs prüft.
Service Mesh
Du fragst dich vielleicht, warum nicht einfach ein Service Mesh. Service Meshes sind großartig, aber kein Allheilmittel. Leider passen sie nicht zu jedem Anwendungsfall. Je mehr Webentwicklung zu Entwicklung für den Edge oder für eine Plattform wird, desto weniger helfen Service Meshes.
Innerhalb deiner eigenen Infrastruktur ist ein Service Mesh eine gute Lösung. Container mit einem Sidecar-Proxy auszuliefern, ist ein hervorragender Weg, die Kommunikation zwischen Services zu standardisieren. Aber was, wenn du auf eine Plattform wie Cloudflare Workers, Heroku, Cloud Foundry oder Vercel deployst?
Auf Plattformen musst du dich auf Standards verlassen.
Weitere Gedanken
Bei diesem verteilten Vertrauensmodell musst du sicherstellen, dass die RSA-Schlüssel, die deine Runtime erzeugt, beim horizontalen Skalieren konsistent bleiben. Nur so liefert der JWKS-Aufruf immer dieselben öffentlichen Schlüssel.
Das ist nur ein Standard für HTTP-Service-to-Service-Authentifizierung. Du könntest stattdessen auf Mutual TLS (mTLS), Shared Secrets und Ähnliches setzen. Wichtig ist, dass du einen Standard hast, der unabhängig von der darunterliegenden Infrastruktur ist und unabhängig von den Sicherheitsmechanismen anderer Services.