← Alle berichten

Gedistribueerd vertrouwen met service-to-service-authenticatie

Op deze pagina

In Service-oriented Architectures (SOA) zijn de “services” de fundamentele bouwstenen van je applicatie. Services zijn autonoom, losjes gekoppeld en meestal stateless. Autonoom, omdat ze niet afhangen van de state of context van andere services. Losjes gekoppeld, omdat ze niet afhangen van de technologieën van andere services. Stateless, omdat ze tussen requests geen state bewaren.

Services zijn de bouwstenen van je applicatie en ze zijn de bouwstenen van je beveiligingsarchitectuur.

Vertrouwen verdelen

In een SOA zijn services onafhankelijk van elkaar. Dat geldt dus ook voor de beveiligingsmechanismen van andere services.

Dat is goed.

Je kunt voor elke service een ander beveiligingsmechanisme kiezen. Andere authenticatie, andere autorisatie, andere versleuteling, enzovoort.

Dat is ook slecht.

Je moet al die verschillende beveiligingsmechanismen ook beheren. Andere authenticatie, andere autorisatie, andere versleuteling, enzovoort.

Service-to-service-authenticatie

In een SOA zijn services onafhankelijk van elkaar. Je ziet het patroon.

Requests tussen services authenticeren is een uitdaging. Je kunt niet dezelfde mechanismen gebruiken als voor gebruikers. Geen cookies, geen sessies en je wilt zeker niet afhangen van een Identity Provider (IdP).

Bij Platform-as-a-Service (PaaS) zoals Cloudflare Workers, Heroku, Cloud Foundry en Vercel kun je zelfs niet rekenen op de onderliggende infrastructuur voor een beveiligd kanaal tussen services.

Je hebt een manier nodig om requests tussen services te authenticeren die los staat van de onderliggende infrastructuur en los van de beveiligingsmechanismen van andere services.

Daar komen JWT’s en JWKS’s om de hoek kijken.

JSON Web Tokens (JWT’s)

JSON Web Tokens (JWT’s) zijn een compacte, URL-veilige manier om claims tussen twee partijen uit te wisselen. De claims in een JWT zijn gecodeerd als een JSON-object. Dat object dient als payload van een JSON Web Signature (JWS) of als platte tekst van een JSON Web Encryption (JWE). Zo kunnen de claims digitaal worden ondertekend, worden beschermd met een Message Authentication Code (MAC) en/of worden versleuteld.

JWT-structuur

Een JWT is een string met drie delen: de header, de payload en de signature. De header en de payload zijn JSON-objecten. De signature is een Base64-gecodeerde string.

<base64url-encoded header>.<base64url-encoded payload>.<base64url-encoded signature>

JWT-header

De header bestaat meestal uit twee delen: het type token, dus JWT, en het gebruikte signing-algoritme, zoals HMAC SHA256 of RSA.

{
  "alg": "HS256",
  "typ": "JWT"
}

JWT-payload

De payload bevat de claims. Claims zijn uitspraken over een entiteit (meestal de gebruiker) en extra metadata. Er zijn drie soorten claims: registered, public en private.

  • Registered claims: Een set vooraf gedefinieerde claims. Ze zijn niet verplicht, maar wel aanbevolen, zodat je een set bruikbare, uitwisselbare claims hebt. Enkele daarvan zijn: iss (issuer), exp (expiration time), sub (subject), aud (audience) en andere.
  • Public claims: Die kunnen gebruikers van JWT’s zelf bepalen. Om botsingen te voorkomen moet je ze definiëren in het IANA JSON Web Token Registry of als een URI met een botsingsvrije namespace.
  • Private claims: Eigen claims om informatie te delen tussen partijen die afspreken ze te gebruiken.
{
  "sub": "1234567890",
  "iss": "https://my-experience-api.willhackett.xyz",
  "aud": "https://my-resource-api.willhackett.xyz"
}

De claims iss en aud geven respectievelijk de issuer en de audience van de JWT aan. De claim sub geeft het subject van de JWT aan. Hier is het subject de gebruikers-ID namens wie het request wordt gedaan. In een echte situatie raad ik aan de JWT van de aanvragende gebruiker opnieuw in te pakken in de nieuwe JWT. Zo kan de ontvangende service de identiteit van de gebruiker controleren en nagaan of de gebruiker op je resource-laag de juiste rechten heeft voor de gevraagde actie.

De issuer en audience komen later terug, als ik het over trust boundaries heb.

JWT-signature

Om de signature te maken neem je de gecodeerde header, de gecodeerde payload, een secret en het algoritme uit de header, en onderteken je dat.

HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);

Met de signature controleer je dat het bericht onderweg niet is gewijzigd. Bij tokens die met een private key zijn ondertekend, controleer je er ook mee dat de afzender van de JWT is wie hij zegt te zijn.

JSON Web Key Sets (JWKS’s)

JSON Web Key Sets (JWKS’s) zijn sets sleutels met de public keys waarmee je elke JSON Web Token (JWT) verifieert die de autorisatieserver heeft uitgegeven en met het RS256-algoritme heeft ondertekend.

JWKS-structuur

Een JWKS is een JSON-object dat een set JWK’s voorstelt. Het JSON-object MOET een “keys”-lid hebben, een array van JWK’s. Dat is het JWK Set-formaat.

{
  "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"
    }
  ]
}

Wij hebben alleen de public keys nodig. Daarmee verifieer je de signature van de JWT’s.

Vertrouwensmodel

Het vertrouwensmodel is simpel. De service die de JWT uitgeeft, krijgt het vertrouwen om geldige JWT’s uit te geven. De service die de JWT ontvangt, krijgt het vertrouwen om de JWT te verifiëren. De ontvangende service krijgt niet het vertrouwen om namens de aanroepende service geldige JWT’s uit te geven.

Geeft Service A een JWT uit aan Service B, dan vertrouwen we Service B om de JWT te verifiëren tegen de JWKS van Service A. Service B kan geen JWT ondertekenen en verwachten dat Service C die vertrouwt. Daar komen de trust boundaries om de hoek kijken.

Trust boundaries

Trust boundaries zijn de grenzen tussen services die elkaar vertrouwen.

If Service A issues a JWT to Service B, Service B is trusted to verify the JWT against the JWKS provided by Service A.

Elke JWT bevat een iss-claim en een aud-claim. De aud is het beoogde publiek van de JWT. Daarmee controleer je of het request voor de juiste service bedoeld is. De iss is de uitgever van de JWT. Daarmee controleer je of een vertrouwde service de JWT heeft uitgegeven.

Service B vertrouwt Service A om geldige JWT’s uit te geven. Stel dat de hostname van Service A service-a.willhackett.xyz is en die van Service B service-b.willhackett.xyz. Dan is de issuer-claim https://service-a.willhackett.xyz en de audience-claim https://service-b.willhackett.xyz.

Service B vertrouwt tokens met de issuer-claim https://service-a.willhackett.xyz en de audience-claim https://service-b.willhackett.xyz. Service B roept het JWKS-endpoint van Service A aan om de public keys op te halen waarmee de JWT’s van Service A worden geverifieerd.

Service mesh

De vraag ligt voor de hand: waarom geen service mesh? Service meshes zijn prachtig, maar ze zijn geen wondermiddel. Helaas passen ze niet bij elk gebruik. Nu webontwikkeling steeds meer ontwikkeling voor de edge of op een platform wordt, worden service meshes steeds minder bruikbaar.

Binnen je eigen infrastructuur is een service mesh een goede oplossing. Containers deployen met een sidecar-proxy is een prima manier om service-to-service-communicatie te standaardiseren. Maar wat als je deployt op een platform als Cloudflare Workers, Heroku, Cloud Foundry of Vercel?

Op platforms moet je op standaarden vertrouwen.

Overige gedachten

Met dit gedistribueerde vertrouwensmodel moet je zorgen dat de RSA-sleutels die je runtime genereert consistent blijven als je horizontaal schaalt. Dan geeft de JWKS-aanroep altijd dezelfde public keys terug.

Dit is maar één standaard voor HTTP service-to-service-authenticatie. Je kunt ook mutual TLS (mTLS), shared secrets en dergelijke gebruiken. Het belangrijkste is dat je een standaard hebt die los staat van de onderliggende infrastructuur en los van de beveiligingsmechanismen van andere services.

Bronnen