Secure by Design - Hoofdstuk 13

Ingediend door Dirk Hornstra op 03-may-2021 21:52

In januari 2020 zijn we gestart met hoofdstuk 1 (link), in februari met hoofdstuk 2 (link), in april met hoofdstuk 3 (link), in mei met hoofdstuk 4 (link), in juni met hoofdstuk 5 (link), in juli met hoofdstuk 6 (link), in augustus hoofdstuk 7 (link), in september hoofdstuk 8 (link), in november hoofdstuk 9 (link), in februari 2021 hoofdstuk 10 (link), in maart 2021 hoofdstuk 11 (link), in april 2021 hoofdstuk 12 (link) en nu wordt het tijd voor hoofdstuk 13.

De Timo-samenvatting

Microservices zijn booming. Door processen los te koppelen zorg je dat bepaalde acties onafhankelijk van elkaar kunnen draaien, bijgewerkt kunnen worden, ontworpen zijn voor downtime en hun eigen "bounded context" hebben (dus order kan in het ene deel een winkelwagen met inhoud zijn en in het andere deel een afgeronde en betaalde order).

Maak een goed ontwerp van je microservices om te voorkomen dat er een 1 log systeem niet omgebouwd wordt naar een log systeem verdeeld over 20 microservices.

Met microservices gaat er meer verkeer over de lijn. Kunnen meer gegevens gecombineerd worden. Hou dus goed in de gaten wat "vertrouwelijke gegevens" zijn en welke gegevens "vertrouwelijk" worden op het moment dat ze gecombineerd worden. Zorg dat de stromen traceerbaar zijn. Als ik met een console-applicatie HTTP-requests afstuur op een microservice, accepteert die dan de foutieve data omdat de structuur klopt, of omdat ook de bijbehorende hash/checksums correct zijn?

Als er data gelogd wordt, scheidt deze dan naar het type data. Is het algemene fout-informatie wat developers nodig hebben om de oorzaak te vinden en het probleem op te lossen? Of is het een deel audit-log, waarbij bepaalde acties van gebruikers in het systeem vastgelegd moeten worden (creditering van facturen), zodat bij fraude-onderzoek en andere meldingen dit te onderbouwen is?

Microservices

In hoofdstuk 12 is legacy software voorbij gekomen. Dat zijn over het algemeen grote, monolitische systemen. In dit hoofdstuk richten we ons op microservices. Er is niet 1 omschrijving die uitlegt wat microservices zijn, maar Martin Fowler en Chris Richardson hebben er het volgende van gemaakt:

The microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery.

Martin Fowler, https://www.martinfowler.com/articles/microservices.html

Microservices—also known as the microservice architecture—is an architectural style that structures an application as a collection of loosely coupled services, which implement
business capabilities. The microservice architecture enables the continuous delivery/deployment of large, complex applications.

Chris Richardson, https://microservices.io

Onafhankelijke runtime

Elk los proces moet onafhankelijk van een ander proces zijn. Als je meerdere microservices hebt, moet het niet zo zijn dat eerst microservice 1 gestart moet zijn voor je microservice 2 en 3 kunt/mag starten. Als een microservice naar een andere machine moet verhuizen, dan moet dat mogelijk zijn. Dit is ook besproken in hoofdstuk 10, cloud native aspects en de 12-factor app.

Onafhankelijke updates

Je moet een microservice kunnen updaten, dus systeem down brengen, updates doorvoeren en weer online brengen. Dit mag niet de andere processen laten crashen. Tevens kan deze update ervoor zorgen dat er meer/andere functionaliteit aangeboden wordt. Ook hier moeten de aanroepende diensten mee kunnen werken.

Ontworpen voor downtime

Een microservice kan een andere service nodig hebben die down is. Bij het bouwen moet je hier meteen rekening mee houden. Er wordt verwezen naar hoofdstuk 9 waar we de bulkheads en de circuit-breakers besproken hebben.

Elke service is een "bounded context"

Het opsplitsen van je systeem naar microservices is niet makkelijk. Wat moet waar, hoe zorg je dat zaken echt onafhankelijk zijn. Het boek raadt aan  om dit als een bounded context te bekijken (hoofdstuk 3). Denk hier aan de "payment" die in ons eigen systeem een betaling was, maar bij de betaalprovider een "poging tot betaling".  Hierdoor kun je de juiste grenzen trekken en kun je als het goed is bepalen in welk deel een bepaalde feature thuis hoort. En elke keer als er contact is tussen de microservices en die hun eigen interpretatie van een begrip hebben, gebruik je de technieken uit hoofdstuk 2.

Het belang van het ontwerpen van je API

Zoals in eerdere hoofdstukken al besproken is, zorg je met je domain primitives, dat je interne structuur niet naar buiten komt. Dat geldt bij "normale" API's, maar dus ook bij de API's van microservices. In de API maak je alleen domain-acties beschikbaar, zodat je data "read-only" kan worden en de staat van het systeem blijft valide.

Het boek toont een voorbeeld van een API die correct is en één die dat niet is. De business-rule in deze applicatie is dat een klant actief is als hij/zij een overeenkomst getekend heeft en een betaling gedaan heeft. In de correcte API een addLegalAgreement-functie en een addConfirmedPayment-functie. Daardoor kan de isActivated-functie een true of false terug geven. In de foutieve API is er een functie setCustomerActivated waardoor "iemand" dus het vinkje kan zetten, maar de vraag is dan of de overeenkomst wel getekend is en of de initiële betaling wel gedaan is.

Door het op deze manier te doen, is deze service bepalend of een klant nu wel of niet actief is. Dus is dit nu ook de centrale plek waar je moet zijn voor aanpassingen/uitbreidingen. Zo is er het voorbeeld dat er eerst een "welkomst-telefoontje" geweest moet zijn. Dus de functie welcomeCallPerformed is er nu bij gekomen. En isActivated geeft nu terug of alle 3 uitgevoerd zijn.

Opsplitsen van een monolitisch systeem

Het kan zijn dat je een groot systeem aan het splitsen bent naar microservices. Of een microservice is inmiddels een macro-service: te groot, dus uitsplitsen. Hou rekening met de verschillende termen en interpretaties. Het boek toont het voorbeeld van "cancelReservation" waarin een string wordt meegestuurd als het reserveringsnummer. Maar als later dat reserveringsnummer aan bepaalde eisen moet voldoen (waar je een soort Regex op kunt loslaten), dan kun je daar beter een Reservation-domain-object van maken die zijn eigen controles heeft, zodat je dit ook weer op 1 centrale plek hebt.

Semantiek en services "in ontwikkeling"

Wees alert en kritisch op wijzigingen in het model. Als een term qua betekenis verandert, probeer te voorkomen dat je zaken aan gaat passen. Als er zo'n wijziging is, vervang de term met een nieuwe term en verwijder de oude. Stel dat je een "order" hebt. Maar later komen er ook "retour-orders". Afhankelijk van de implementatie zouden die negatieve aantallen kunnen hebben. Als er vanuit het orderproces acties mee uitgevoerd worden, zou je negatieve aantallen in je winkelwagen kunnen zetten. En dan heb je de poppen aan het dansen.

Gevoelige data tussen services - CIA-T

CIA-T komt naar voren in hoofdstuk 1. Het staat voor

- Confidential (vertrouwelijk) - bepaalde informatie van jou die niet voor andere mensen bedoeld is.

- Integrity (integriteit, betrouwbaarheid) - het is belangrijk dat informatie niet verandert of alleen verandert bij speciale, geautoriseerde situaties.

- Availability (beschikbaarheid) - je gegevens zijn binnen afzienbare tijd beschikbaar. Als de brandweer naar een brand wordt gestuurd moeten ze de locatie weten, als dat pas na 2 uur wordt aangeleverd is het te laat.

- Traceability - weet wie wat aangepast heeft.

Confidentiality: omdat verkeer tussen microservices van component naar component gaat en dan ook nog vaak asynchroon, is vaak niet duidelijk wat de bron van het verzoek is. Het boek geeft dan ook de aanbeveling om een token mee te sturen, zoals dat met OAuth 2 gaat. Het JWT token gaat vervolgens de hele pijplijn door.

Integrity: in plaats van data steeds te kopiëren, probeer zoveel mogelijk bij de bron van de data te blijven. En valideer dat de data nog steeds correct is (met bijvoorbeeld een checksum, certificaat of andere oplossing.

Availability: als een service down is, moet er op basis van een gecachte waarde of de standaard default een aanvaardbaar alternatief geboden worden. Ook hier wordt verwezen naar de circuit breaker.

Traceability: dit is niet makkelijk, het boek komt er later op terug.

Wat is "gevoelige data"

Hierbij denk je gauw aan een creditkaart nummer, je BSN-nummer. Maar het zou ook je nummerplaat van je auto kunnen zijn, want in combinatie met geo-locatie data kan misschien bepaald worden waar jij was op een bepaald moment. Het boek komt met het voorbeeld van een hotel. Dat jij in kamer 20 de nacht van 20 of 21 mei hebt doorgebracht is vertrouwelijke data. Die hoeft verder met niemand gedeeld te worden. Maar als de volgende dag een jas in die kamer gevonden wordt en jij haalt die later op, dan moet wel de match gemaakt worden dat jij die 20e op de kamer was en het dus jouw jas is. En het boek geeft aan, ook delen van code waar data verzameld wordt kan een plek zijn waar data die los niet vertrouwelijk is, juist door het combineren wel vertrouwelijk kan worden.

Ga bij data de volgende vragen stellen:

  • is deze informatie in een andere context gevoelige data?
  • is deze informatie in een andere context afhankelijk van hoge beschikbaarheid en betrouwbaarheid?
  • als deze informatie met gegevens uit andere services gecombineerd wordt, is het dan wel vertrouwelijk?


In het boeks staat nog een kort blokje over "passing data over the wire". In je monolitische systeem zat al die code in je C# applicatie en "kon er niemand bij". Nu we microservices hebben, waarbij veel data via HTTPS verstuurd wordt, moet goed gekeken worden of het binnen een gesloten subnet zit, of het via het publiek toegankelijke deel van het internet getransporteerd wordt en als er firewalls onderschept worden, wat kan dat voor impact hebben?

Logging in microservices

In de hoofdstukken is al eerder benoemd dat je voorzichtig moet zijn met het loggen van data. Er kan gevoelige data lekken. Door misbruik van bepaalde zaken kan het loggen helemaal fout lopen. In dit hoofdstuk gaan we kijken naar hoe belangrijk"traceability" is en hoe de integriteit en vertrouwelijkheid van log-data gewaarborgd is.

Integriteit van geaggregeerde log-data

In eerdere hoofdstukken werd al afgeraden om logs naar een bestand op een lokale schijf op te slaan. Lekker makkelijk om remote in te loggen en daar te spitten, maar als je meerdere microservices hebt, die ook nog eens naar een andere server verplaatst kunnen worden is een centrale log-locatie essentieel. Je moet de data dus verzamelen op een bepaalde plek (aggregatie). Om die data binnen te krijgen moet het een genormaliseerd, gestructureerd format hebben, als voorbeeld wordt JSON genoemd. De eerste horde hierbij is dat er "unchecked string" in je data terecht kan komen. Het normaliseren van de data wordt meestal in een soort tijdelijk proces uitgevoerd, wat dan weer in conflict is het met "repavement-principe" van de 3 R's uit hoofdstuk 10. In het boek zien we eerst een fout die optreedt en met een globale "common normalization step" omgezet wordt naar JSON. Het boek raadt aan om elke microservice zelf te laten zorgen dat er genormaliseerde data aangeleverd wordt. Hierdoor kan de microservice zelf een hash toevoegen, waarmee het logging-systeem kan bepalen of het een valide melding is. En je hebt minder 3-rd party afhankelijkheden nodig.

Traceability in logs

Het boek komt met 2 betaalservices, A en B waarbij A versie 1.0 heeft en B heeft versie 1.1. B is backward compatibel met A, maar heeft wat extra functionaliteit. Daar zit echter een bug in, er treden fouten op bij het afronden van getallen. En nu is de vraag, is het service A of B die de bug bevat?

In het boek staat in een blokje nog even een toelichting op versienummers, zoals dat door Tom Preston-Werner, uitvinder van Gravatar en co-founder van Github bedacht is. Meer te lezen op https://semver.org/

Om die A of B te kunnen bepalen, moeten we bij het loggen meenemen:

  • een service moet uniek identificeerbaar zijn op basis van naam, versie nummer en instantie ID.
  • een transactie moet over verschillende systemen traceerbaar zijn.


Een transactie die van system A, naar system B naar system C gaat, moet op system C te zien zijn als de transactie die A en B ook bezocht heeft. Het boek geeft Dapper van Google en Magpie van Microsoft als voorbeelden die dat kunnen. Maar dat kan overkill zijn. Je kunt ook zelf je trace-ID's maken en die mee geven.

Vertrouwelijkheid via een domein-geörienteerde logger API

In hoofdstuk 10 zijn voorbeelden gegeven van logging, om er bepaalde tags aan te hangen (INFO, DEBUG, FATAL). Maar dat zijn maar labeltjes, in een INFO-melding kan ook vertrouwelijke informatie staan. Dus ook hier geldt dat je zelf moet nadenken over de indeling.

Het boek geeft het voorbeeld van een hotel. Er worden kamers geboekt, er worden schoonmaakspullen gekocht. We krijgen de cancelBooking-interface te zien. De logger die de gegevens logt accepteert strings, dus die maakt het niet uit wat je erin gooit. In de cancelationFailed-exceptie wordt de logger.log daarom 2x aangeroepen. 1x met een auditData(id, result, user) en een behaviourData(result). Zodat na de tijd in de audit gekeken kan worden wie de actie uitgevoerd heeft, maar in de "behaviourData" de algemene systeemfout die opgelost moet worden kan worden bekeken.

De vraag is of je deze gegevens in hetzelfde log op wilt slaan. Als iemand het voor elkaar krijgt om de JSON te escapen, zou je mogelijk data zichtbaar kunnen maken die niet voor jouw ogen bedoeld is. Ook kun je dan makkelijker data opruimen (fout-logs zul je na een x-periode willen opschonen), terwijl je audit-log misschien voor een aantal jaar wilt kunnen bewaren.

Zorg dus dat je die stromen scheidt.