Kapitel 4. Kompromisse bei der Gestaltung
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Du wirst also ein (Software-)Produkt entwickeln! Auf dieser komplexen Reise, die von der Planung bis zum Einsatz des Codes reicht, musst du an viele Dinge denken.
Normalerweise beginnst du mit einer groben Vorstellung davon, was das Produkt oder die Dienstleistung leisten soll. Das kann z. B. ein grobes Konzept für ein Spiel oder eine Reihe von Geschäftsanforderungen für eine cloudbasierte Produktivitätsanwendung sein. Du entwickelst auch einen Plan, wie das Angebot finanziert werden soll.
Je weiter du in den Entwurfsprozess einsteigst und je konkreter deine Vorstellungen von der Form des Produkts werden, desto mehr Anforderungen und Beschränkungen für den Entwurf und die Umsetzung der Anwendung tauchen auf. Es gibt spezifische Anforderungen an die Funktionalität des Produkts und allgemeine Beschränkungen wie Entwicklungs- und Betriebskosten. Du wirst auch auf Anforderungen und Einschränkungen in Bezug auf Sicherheit und Zuverlässigkeit stoßen: Dein Dienst wird wahrscheinlich bestimmte Anforderungen an die Verfügbarkeit und Zuverlässigkeit stellen, und du hast vielleicht Sicherheitsanforderungen zum Schutz sensibler Nutzerdaten, die von deiner Anwendung verarbeitet werden.
Einige dieser Anforderungen und Einschränkungen können miteinander in Konflikt stehen, und du musst Kompromisse eingehen und das richtige Gleichgewicht zwischen ihnen finden.
Gestaltungsziele und Anforderungen
Die Anforderungen an die Funktionen deines Produkts werden in der Regel ganz andere Merkmale aufweisen als die Anforderungen an die Sicherheit und Zuverlässigkeit. Schauen wir uns die Arten von Anforderungen genauer an, mit denen du bei der Entwicklung eines Produkts konfrontiert wirst.
Merkmal Anforderungen
Funktionsanforderungen, auch bekannt als funktionale Anforderungen,1 legen die Hauptfunktion eines Dienstes oder einer Anwendung fest und beschreiben, wie ein Nutzer eine bestimmte Aufgabe erfüllen oder ein bestimmtes Bedürfnis befriedigen kann. Sie werden oft in Form von Use Cases, User Stories oder User Journeysausgedrückt - Abfolgenvon Interaktionen zwischen einem Nutzer und dem Dienst oder der Anwendung. Kritische Anforderungen sind die Teilmenge der Funktionsanforderungen, die für das Produkt oder die Dienstleistung unerlässlich sind. Wenn ein Entwurf eine kritische Anforderung oder eine kritische Benutzergeschichte nicht erfüllt, hast du kein lebensfähiges Produkt.
Die Anforderungen an die Funktionen sind in der Regel der Hauptgrund für deine Designentscheidungen. Schließlich versuchst du, ein System oder eine Dienstleistung zu entwickeln, das/die eine bestimmte Gruppe von Bedürfnissen der von dir anvisierten Nutzer/innen befriedigt. Oft musst du Kompromisse zwischen den verschiedenen Anforderungen eingehen. Aus diesem Grund ist es sinnvoll, zwischen kritischen Anforderungen und anderen Anforderungen zu unterscheiden.
Normalerweise gelten eine Reihe von Anforderungen für die gesamte Anwendung oder Dienstleistung. Diese Anforderungen tauchen oft nicht in den User Stories oder den Anforderungen für einzelne Funktionen auf. Stattdessen werden sie einmal in der zentralen Anforderungsdokumentation genannt oder sogar implizit vorausgesetzt. Hier ist ein Beispiel:
- Alle Ansichten/Seiten der Web-UI der Anwendung müssen:
- Befolge gemeinsame Richtlinien zur visuellen Gestaltung
- Beachte die Richtlinien für Barrierefreiheit
- Eine Fußzeile mit Links zu den Datenschutzrichtlinien und den AGBs (Terms of Service)
Nichtfunktionale Anforderungen
Mehrere Kategorien von Anforderungen konzentrieren sich auf allgemeine Eigenschaften oder Verhaltensweisen des Systems und nicht auf bestimmte Verhaltensweisen. Diese nicht-funktionalen Anforderungen sind für unseren Schwerpunkt - Sicherheit und Zuverlässigkeit - relevant. Zum Beispiel:
-
Unter welchen Umständen darf jemand (ein externer Benutzer, ein Kundendienstmitarbeiter oder ein Betriebsingenieur) Zugang zu bestimmten Daten haben?
-
Wie lauten die Service Level Objectives (SLOs) für Kennzahlen wie die Betriebszeit oder die 95- und 99-prozentige Antwortlatenz? Wie reagiert das System bei einer Last, die einen bestimmten Schwellenwert überschreitet?
Bei der Abwägung der Anforderungen kann es hilfreich sein, gleichzeitig auch die Anforderungen in anderen Bereichen als dem eigentlichen System zu berücksichtigen, da Entscheidungen in diesen Bereichen erhebliche Auswirkungen auf die Kernanforderungen des Systems haben können. Zu diesen weiter gefassten Bereichen gehören die folgenden:
- Effizienz und Schnelligkeit der Entwicklung
- Wie effizient können Entwickler angesichts der gewählten Implementierungssprache, der Anwendungsframeworks, der Test- und Build-Prozesse neue Funktionen iterieren? Wie effizient können Entwickler bestehenden Code verstehen und ändern oder debuggen?
- Geschwindigkeit des Einsatzes
- Wie lange dauert es von der Entwicklung einer Funktion bis zu dem Zeitpunkt, an dem diese Funktion den Nutzern/Kunden zur Verfügung steht?
Merkmale versus aufkommende Eigenschaften
Bei Funktionsanforderungen gibt es in der Regel eine ziemlich direkte Verbindung zwischen den Anforderungen, dem Code, der diese Anforderungen erfüllt, und den Tests, die die Implementierung überprüfen. Zum Beispiel:
- Spezifikation
- In einer User Story oder Anforderung kann festgelegt werden, wie ein angemeldeter Benutzer einer Anwendung die persönlichen Daten seines Benutzerprofils (z. B. seinen Namen und seine Kontaktinformationen) einsehen und ändern kann.
- Umsetzung
-
Eine Web- oder Mobilanwendung, die auf dieser Spezifikation basiert, würde typischerweise einen Code enthalten, der sich sehr spezifisch auf diese Anforderung bezieht, wie zum Beispiel den folgenden:
-
Strukturierte Typen zur Darstellung der Profildaten
-
UI-Code zur Darstellung und Änderung der Profildaten
-
Serverseitige RPC- oder HTTP-Action-Handler, um die Profildaten des angemeldeten Benutzers aus einem Datenspeicher abzufragen und um aktualisierte Informationen zu akzeptieren, die in den Datenspeicher geschrieben werden sollen
-
- Validierung
- Normalerweise gibt es einen Integrationstest, der die angegebene User Story Schritt für Schritt durchläuft. Der Test könnte einen UI-Testtreiber verwenden, um das Formular "Profil bearbeiten" auszufüllen und abzuschicken und dann zu überprüfen, ob die abgeschickten Daten in dem erwarteten Datenbankeintrag erscheinen. Wahrscheinlich gibt es auch Unit-Tests für einzelne Schritte in der User Story.
Im Gegensatz dazu sind nicht-funktionale Anforderungen - wie Zuverlässigkeit und Sicherheit - oft viel schwieriger festzulegen. Es wäre schön, wenn dein Webserver ein --enable_high_reliability_mode
Flag hätte und du deine Anwendung zuverlässig machen könntest, indem du das Flag umlegst und deinem Hosting- oder Cloud-Provider eine zusätzliche Gebühr zahlst. Aber es gibt kein solches Flag und kein bestimmtes Modul oder eine Komponente im Quellcode einer Anwendung, die Zuverlässigkeit "implementiert".
Beispiel: Google Design Dokument
Google verwendet eine Vorlage für Designdokumente, um das Design neuer Funktionen anzuleiten und um Feedback von den Interessengruppen einzuholen, bevor ein Entwicklungsprojekt gestartet wird.
Die Abschnitte der Templating-Vorlage, die sich auf die Zuverlässigkeit und die Sicherheit beziehen, erinnern die Teams daran, über die Auswirkungen ihres Projekts nachzudenken und gegebenenfalls den Prozess der Produktionsbereitschaft oder der Sicherheitsüberprüfung einzuleiten. Entwurfsprüfungen finden manchmal mehrere Quartale vor der offiziellen Einführung statt.
Anforderungen ausgleichen
Da die Attribute eines Systems, die Sicherheits- und Zuverlässigkeitsaspekte erfüllen, größtenteils emergente Eigenschaften sind, interagieren sie in der Regel sowohl mit der Umsetzung von Funktionsanforderungen als auch untereinander. Daher ist es besonders schwierig, über Kompromisse bei der Sicherheit und Zuverlässigkeit als eigenständiges Thema nachzudenken.
In diesem Abschnitt findest du ein Beispiel, das dir zeigt, welche Kompromisse du unter Umständen eingehen musst. Einige Teile dieses Beispiels gehen ziemlich tief in technische Details, die an sich nicht unbedingt wichtig sind. Alle Überlegungen zur Einhaltung von Vorschriften, gesetzlichen Bestimmungen und geschäftlichen Aspekten, die bei der Entwicklung von Zahlungsverarbeitungssystemen und deren Betrieb eine Rolle spielen, sind für dieses Beispiel ebenfalls nicht wichtig. Vielmehr geht es darum, die komplexen Abhängigkeiten zwischen den Anforderungen zu veranschaulichen. Mit anderen Worten: Der Schwerpunkt liegt nicht auf den Details zum Schutz von Kreditkartennummern, sondern auf den Überlegungen, die bei der Entwicklung eines Systems mit komplexen Sicherheits- und Zuverlässigkeitsanforderungen angestellt werden.
Beispiel: Zahlungsabwicklung
Stell dir vor, du entwickelst einen Online-Dienst, der Widgets an Verbraucher verkauft.2 Die Spezifikation des Dienstes enthält eine Benutzergeschichte, die besagt, dass ein Benutzer über eine mobile oder Webanwendung Widgets aus einem Online-Katalog auswählen kann. Anschließend kann der Nutzer die ausgewählten Widgets kaufen, wozu er Angaben zu einer Zahlungsmethode machen muss.
Überlegungen zu Sicherheit und Zuverlässigkeit
Die Annahme von Zahlungsinformationen stellt hohe Anforderungen an die Sicherheit und Zuverlässigkeit des Systems und der organisatorischen Abläufe. Namen, Adressen und Kreditkartennummern sind sensible persönliche Daten, die besondere Sicherheitsvorkehrungen erfordern3 und können je nach Gerichtsbarkeit behördliche Auflagen nach sich ziehen. Die Annahme von Zahlungsinformationen kann auch dazu führen, dass der Dienst die branchenweiten oder gesetzlichen Sicherheitsstandards wie PCI DSS einhalten muss.
Eine Kompromittierung dieser sensiblen Nutzerdaten, insbesondere von personenbezogenen Daten, kann schwerwiegende Folgen für das Projekt und sogar für die gesamte Organisation/das Unternehmen haben. Du könntest das Vertrauen deiner Nutzer/innen und Kunden/innen verlieren und infolgedessen auch ihr Geschäft. In den letzten Jahren hat der Gesetzgeber Gesetze und Verordnungen erlassen, die Unternehmen, die von Datenschutzverletzungen betroffen sind, potenziell zeitaufwändige und teure Verpflichtungen auferlegen. Wie in Kapitel 1 beschrieben, mussten einige Unternehmen aufgrund eines schwerwiegenden Sicherheitsvorfalls sogar ganz ihren Betrieb einstellen.
In bestimmten Szenarien könnte ein übergeordneter Kompromiss auf der Ebene des Produktdesigns die Anwendung von der Verarbeitung von Zahlungen befreien - zum Beispiel kann das Produkt in ein werbebasiertes oder gemeinschaftsfinanziertes Modell umgewandelt werden. Für unser Beispiel bleiben wir bei der Prämisse, dass die Annahme von Zahlungen eine wichtige Voraussetzung ist.
Nutzung eines Drittanbieters für den Umgang mit sensiblen Daten
Die beste Möglichkeit, Sicherheitsbedenken in Bezug auf sensible Daten zu zerstreuen, besteht oft darin, diese Daten gar nicht erst zu speichern (weitere Informationen zu diesem Thema findest du in Kapitel 5). Möglicherweise kannst du dafür sorgen, dass sensible Daten niemals deine Systeme durchlaufen, oder zumindest die Systeme so gestalten, dass sie die Daten nicht persistent speichern.4 Du kannst aus verschiedenen kommerziellen Zahlungsdienst-APIs wählen, um sie in die Anwendung zu integrieren und die Handhabung von Zahlungsinformationen, Zahlungstransaktionen und damit zusammenhängenden Angelegenheiten (wie z. B. Betrugsbekämpfungsmaßnahmen) an den Anbieter zu übertragen.
Vorteile
Je nach den Umständen kann die Inanspruchnahme eines Zahlungsdienstes das Risiko und das Ausmaß, in dem du internes Fachwissen zur Bewältigung von Risiken in diesem Bereich aufbauen musst, verringern, indem du dich stattdessen auf die Expertise des Anbieters verlässt:
-
Die sensiblen Daten befinden sich nicht mehr in deinen Systemen, wodurch das Risiko sinkt, dass eine Schwachstelle in deinen Systemen oder Prozessen zu einer Datenkompromittierung führen könnte. Natürlich könnte eine Kompromittierung des Drittanbieters immer noch die Daten deiner Nutzer/innen gefährden.
-
Je nach den spezifischen Umständen und geltenden Anforderungen können deine vertraglichen Verpflichtungen und die Einhaltung der Sicherheitsstandards der Zahlungsindustrie vereinfacht werden.
-
Du musst keine Infrastruktur aufbauen und pflegen, um die ruhenden Daten in den Datenspeichern deines Systems zu schützen. Dadurch kann ein erheblicher Teil des Entwicklungs- und Betriebsaufwands entfallen.
-
Viele Drittanbieter von Zahlungen bieten Gegenmaßnahmen gegen betrügerische Transaktionen und Dienste zur Bewertung von Zahlungsrisiken an. Möglicherweise kannst du diese Funktionen nutzen, um dein Betrugsrisiko im Zahlungsverkehr zu verringern, ohne die zugrunde liegende Infrastruktur selbst aufbauen und pflegen zu müssen.
Auf der anderen Seite bringt die Abhängigkeit von einem externen Dienstleister eigene Kosten und Risiken mit sich.
Kosten und nicht-technische Risiken
Natürlich wird der Anbieter Gebühren verlangen. Das Transaktionsvolumen wird deine Entscheidung wahrscheinlich beeinflussen - ab einem bestimmten Volumen ist es wahrscheinlich kosteneffizienter, die Transaktionen intern zu verarbeiten.
Du musst auch die technischen Kosten bedenken, die entstehen, wenn du dich auf die Abhängigkeit von Dritten verlässt: Dein Team muss lernen, wie man die API des Anbieters nutzt, und du musst möglicherweise Änderungen/Veröffentlichungen der API nach dem Zeitplan des Anbieters verfolgen.
Verlässlichkeitsrisiken
Wenn du die Zahlungsabwicklung auslagerst, fügst du deiner Anwendung eine zusätzliche Abhängigkeit hinzu - in diesem Fall von einem Drittanbieterdienst. Zusätzliche Abhängigkeiten führen oft zu zusätzlichen Fehlermöglichkeiten. Im Falle von Abhängigkeiten von Drittanbietern können sich diese Fehlermöglichkeiten teilweise deiner Kontrolle entziehen. Zum Beispiel kann deine User Story "Benutzer können ihre ausgewählten Widgets kaufen" fehlschlagen, wenn der Dienst des Zahlungsanbieters ausfällt oder über das Netzwerk nicht erreichbar ist. Wie groß dieses Risiko ist, hängt davon ab, ob sich der Zahlungsanbieter an die SLAs hält, die du mit ihm vereinbart hast.
Du könntest diesem Risiko begegnen, indem du Redundanz in das System einbaust (siehe Kapitel 8) - in diesem Fall, indem du einen alternativen Zahlungsanbieter hinzufügst, auf den dein Dienst fehlschlagen kann. Diese Redundanz bringt Kosten und Komplexität mit sich: Die beiden Zahlungsanbieter haben höchstwahrscheinlich unterschiedliche APIs, so dass du dein System so gestalten musst, dass es mit beiden kommunizieren kann, was zusätzliche technische und betriebliche Kosten verursacht und das Risiko von Fehlern oder Sicherheitslücken erhöht.
Du könntest das Zuverlässigkeitsrisiko auch durch Ausweichmechanismen auf deiner Seite mindern. Du könntest zum Beispiel einen Warteschlangenmechanismus in den Kommunikationskanal mit dem Zahlungsanbieter einbauen, um Transaktionsdaten zu puffern, wenn der Zahlungsdienst nicht erreichbar ist. Auf diese Weise könnte der "Kauffluss" auch bei einem Ausfall des Zahlungsdienstes fortgesetzt werden.
Die Hinzufügung des Mechanismus der Nachrichtenwarteschlange führt jedoch zu zusätzlicher Komplexität und kann eigene Fehlermöglichkeiten mit sich bringen. Wenn die Warteschlange nicht zuverlässig ist (z. B. weil sie Daten nur im flüchtigen Speicher speichert), können Transaktionen verloren gehen - ein neues Risiko. Generell können Subsysteme, die nur unter seltenen und außergewöhnlichen Umständen genutzt werden, versteckte Fehler und Zuverlässigkeitsprobleme bergen.
Du könntest dich für eine zuverlässigere Implementierung der Nachrichtenwarteschlange entscheiden. Dazu gehört wahrscheinlich entweder ein In-Memory-Speichersystem, das über mehrere physische Standorte verteilt ist, was wiederum zu mehr Komplexität führt, oder eine Speicherung auf einer dauerhaften Festplatte. Die Speicherung der Daten auf der Festplatte, auch wenn dies nur in Ausnahmefällen der Fall ist, wirft wieder die Bedenken bezüglich der Speicherung sensibler Daten auf (Risiko der Kompromittierung, Überlegungen zur Einhaltung von Vorschriften usw.), die du von vornherein vermeiden wolltest. Insbesondere dürfen einige Zahlungsdaten gar nicht erst auf die Festplatte gelangen, was die Anwendung einer Warteschlange, die sich auf eine dauerhafte Speicherung stützt, in diesem Szenario schwierig macht.
Vor diesem Hintergrund musst du Angriffe (insbesondere Angriffe von Insidern) in Betracht ziehen, die absichtlich die Verbindung zum Zahlungsanbieter unterbrechen, um die lokale Warteschlange der Transaktionsdaten zu aktivieren, die dann kompromittiert werden können.
Zusammenfassend lässt sich sagen, dass du auf ein Sicherheitsrisiko stößt, das durch deinen Versuch entstanden ist, ein Zuverlässigkeitsrisiko zu mindern, das wiederum entstanden ist, weil du versucht hast, ein Sicherheitsrisiko zu mindern!
Sicherheitsrisiken
Die Entscheidung, sich auf einen Drittanbieterdienst zu verlassen, wirft auch unmittelbare Sicherheitsbedenken auf.
Erstens: Du vertraust sensible Kundendaten einem Drittanbieter an. Du solltest einen Anbieter wählen, dessen Sicherheitsstandards mindestens so hoch sind wie deine eigenen, und du musst die Anbieter bei der Auswahl und fortlaufend bewerten. Das ist keine leichte Aufgabe, und es gibt komplexe vertragliche, gesetzliche und haftungsrechtliche Überlegungen, die den Rahmen dieses Buches sprengen würden und die du mit deinem Anwalt besprechen solltest.
Zweitens kann es sein, dass du bei der Integration mit dem Dienst des Anbieters eine vom Anbieter bereitgestellte Bibliothek in deine Anwendung einbinden musst. Dies birgt das Risiko, dass eine Schwachstelle in dieser Bibliothek oder in einer ihrer transitiven Abhängigkeiten zu einer Schwachstelle in deinen Systemen führen kann. Du kannst dieses Risiko mindern, indem du die Bibliothek in einer Sandbox5 und indem du darauf vorbereitet bist, schnell aktualisierte Versionen der Bibliothek zu verteilen (siehe Kapitel 7). Du kannst dieses Problem weitgehend vermeiden, indem du einen Anbieter wählst, der nicht verlangt, dass du eine proprietäre Bibliothek in deinen Dienst einbindest (siehe Kapitel 6). Proprietäre Bibliotheken können vermieden werden, wenn der Anbieter seine API über ein offenes Protokoll wie REST+JSON, XML, SOAP oder gRPC zur Verfügung stellt.
Möglicherweise musst du eine JavaScript-Bibliothek in deinen Webanwendungs-Client einbinden, um die Integration mit dem Anbieter zu ermöglichen. Auf diese Weise vermeidest du, dass die Zahlungsdaten durch dein System geleitet werden, auch wenn dies nur vorübergehend ist - stattdessen können die Zahlungsdaten vom Browser des Nutzers direkt an den Webservice des Anbieters gesendet werden. Diese Integration wirft jedoch ähnliche Probleme auf wie die Einbindung einer serverseitigen Bibliothek: Der Bibliothekscode des Anbieters läuft mit vollen Rechten im Webursprung deiner Anwendung.6 Eine Schwachstelle in diesem Code oder eine Kompromittierung des Servers, der die Bibliothek bereitstellt, kann dazu führen, dass deine Anwendung kompromittiert wird. Du könntest dieses Risiko mindern, indem du die zahlungsrelevanten Funktionen in einem separaten Web-Ursprung oder in einem sandboxed iframe unterbringst. Diese Taktik bedeutet jedoch, dass du einen sicheren Cross-Origin-Kommunikationsmechanismus brauchst, was wiederum zu Komplexität und zusätzlichen Fehlermöglichkeiten führt. Alternativ kann der Zahlungsanbieter eine Integration auf der Basis von HTTP-Weiterleitungen anbieten, aber das kann zu einem weniger reibungslosen Nutzererlebnis führen.
Design-Entscheidungen, die sich auf nicht-funktionale Anforderungen beziehen, können ziemlich weitreichende Auswirkungen auf Bereiche mit domänenspezifischem technischem Fachwissen haben: Wir begannen mit der Erörterung eines Kompromisses im Zusammenhang mit der Risikominimierung bei der Verarbeitung von Zahlungsdaten und endeten mit Überlegungen, die tief in den Bereich der Webplattform-Sicherheit reichen. Dabei sind wir auch auf vertragliche und regulatorische Bedenken gestoßen.
Spannungen bewältigen und Ziele aufeinander abstimmen
Mit etwas Vorausplanung kannst du oft wichtige nichtfunktionale Anforderungen wie Sicherheit und Zuverlässigkeit erfüllen, ohne auf Funktionen verzichten zu müssen, und das zu vertretbaren Kosten. Wenn du Sicherheit und Zuverlässigkeit im Kontext des gesamten Systems und des Entwicklungs- und Betriebsworkflows betrachtest, wird oft deutlich, dass diese Ziele mit allgemeinen Softwarequalitätsmerkmalen übereinstimmen.
Beispiel: Microservices und das Google Web Application Framework
Betrachte die Entwicklung eines Google-internen Frameworks für Microservices und Webanwendungen. Das Hauptziel des Teams, das das Framework entwickelt hat, war es, die Entwicklung und den Betrieb von Anwendungen und Diensten für große Organisationen zu rationalisieren. Bei der Entwicklung dieses Frameworks ( ) hat das Team die Schlüsselidee der statischen und dynamischen Konformitätsprüfung berücksichtigt, um sicherzustellen, dass der Anwendungscode die verschiedenen Codierungsrichtlinien und bewährten Methoden einhält. Eine Konformitätsprüfung stellt zum Beispiel sicher, dass alle Werte, die zwischen gleichzeitigen Ausführungskontexten übergeben werden, unveränderliche Typen sind - eine Praxis, die die Wahrscheinlichkeit von Gleichzeitigkeitsfehlern drastisch reduziert. Eine andere Reihe von Konformitätsprüfungen erzwingt Isolationsbeschränkungen zwischen Komponenten, wodurch es viel unwahrscheinlicher wird, dass eine Änderung in einer Komponente/einem Modul der Anwendung zu einem Fehler in einer anderen Komponente führt.
Da Anwendungen, die auf diesem Framework aufbauen, eine ziemlich starre und klar definierte Struktur haben, kann das Framework viele gängige Entwicklungs- und Bereitstellungsaufgaben automatisieren - vom Gerüst für neue Komponenten über die automatische Einrichtung von Continuous-Integration-Umgebungen (CI) bis hin zu weitgehend automatisierten Produktionsbereitstellungen. Diese Vorteile haben das Framework bei Google-Entwicklern sehr beliebt gemacht.
Was hat das alles mit Sicherheit und Zuverlässigkeit zu tun? Das Entwicklungsteam des Frameworks hat während der gesamten Entwurfs- und Implementierungsphase mit den SRE- und Sicherheitsteams zusammengearbeitet, um sicherzustellen, dass die bewährten Methoden für Sicherheit und Zuverlässigkeit in das Framework eingewoben wurden - und nicht einfach am Ende aufgeschraubt wurden. Das Framework übernimmt die Verantwortung für viele gängige Sicherheits- und Zuverlässigkeitsprobleme. Außerdem werden automatisch Überwachungsfunktionen für Betriebskennzahlen eingerichtet und Zuverlässigkeitsfunktionen wie Zustandsprüfung und SLA-Einhaltung integriert.
Die Webanwendungsunterstützung des Frameworks deckt zum Beispiel die meisten gängigen Arten von Sicherheitslücken in Webanwendungen ab.7 Durch eine Kombination aus API-Design und Code-Konformitätsprüfungen verhindert es effektiv, dass Entwickler versehentlich viele gängige Arten von Schwachstellen in den Anwendungscode einbringen.8 In Bezug auf diese Arten von Schwachstellen geht das Framework über die "Sicherheit durch Voreinstellung" hinaus - es übernimmt vielmehr die volle Verantwortung für die Sicherheit und stellt aktiv sicher, dass jede darauf basierende Anwendung nicht von diesen Risiken betroffen ist. In den Kapiteln 6 und 12 wird genauer erläutert, wie dies erreicht wird.
Angleichung der Anforderungen an Notfalleigentum
Das Beispiel des Frameworks zeigt, dass sicherheits- und zuverlässigkeitsbezogene Ziele entgegen der landläufigen Meinung oft gut mit anderen Produktzielen abgestimmt sind - insbesondere mit der Gesundheit des Codes und des Projekts, der Wartbarkeit und der langfristigen, nachhaltigen Projektgeschwindigkeit. Im Gegensatz dazu führt der Versuch, Sicherheits- und Zuverlässigkeitsziele nachträglich einzubauen, oft zu erhöhten Risiken und Kosten.
Die Prioritäten für Sicherheit und Zuverlässigkeit können auch mit den Prioritäten in anderen Bereichen übereinstimmen:
-
Wie in Kapitel 6 erläutert, ist ein Systemdesign, das es den Menschen ermöglicht, effektiv und genau über Invarianten und das Verhalten des Systems nachzudenken, entscheidend für die Sicherheit und Zuverlässigkeit. Verständlichkeit ist auch ein wichtiger Faktor für die Gesundheit des Codes und des Projekts sowie für die Geschwindigkeit der Entwicklung: Ein verständliches System ist leichter zu debuggen und zu ändern (ohne Fehler zu machen).
-
Die Planung der Wiederherstellung (siehe Kapitel 9) ermöglicht es uns, das Risiko, das durch Änderungen und Rollouts entsteht, zu quantifizieren und zu kontrollieren. In der Regel unterstützen die hier besprochenen Gestaltungsprinzipien eine höhere Änderungsrate (d.h. Einführungsgeschwindigkeit), als wir sonst erreichen könnten.
-
Sicherheit und Zuverlässigkeit erfordern, dass wir unser System für eine sich verändernde Landschaft entwickeln (siehe Kapitel 7). Das macht unser Systemdesign anpassungsfähiger und versetzt uns nicht nur in die Lage, neu auftretende Schwachstellen und Angriffsszenarien schnell zu beheben, sondern auch veränderte Geschäftsanforderungen schneller zu erfüllen.
Anfangsgeschwindigkeit versus anhaltende Geschwindigkeit
Vor allem kleinere Teams neigen dazu, Sicherheits- und Zuverlässigkeitsaspekte auf einen Zeitpunkt in der Zukunft zu verschieben ("Wir fügen Sicherheit hinzu und kümmern uns um die Skalierung, wenn wir einige Kunden haben"). Teams rechtfertigen die Vernachlässigung von Sicherheit und Zuverlässigkeit als frühe und primäre Designtreiber häufig mit dem Argument der "Schnelligkeit" - sie befürchten, dass die Beschäftigung mit diesen Themen die Entwicklung verlangsamt und zu inakzeptablen Verzögerungen im ersten Release-Zyklus führt.
Es ist wichtig, zwischen anfänglicher Geschwindigkeit und anhaltender Geschwindigkeit zu unterscheiden. Wenn du dich dafür entscheidest, kritische Anforderungen wie Sicherheit, Zuverlässigkeit und Wartbarkeit zu Beginn des Projektzyklus nicht zu berücksichtigen, kann sich die Geschwindigkeit deines Projekts zu Beginn der Projektlaufzeit tatsächlich erhöhen. Die Erfahrung zeigt jedoch, dass du dadurch später in der Regel auch deutlich langsamer wirst.10 Die Kosten für die nachträgliche Anpassung eines Entwurfs an Anforderungen, die sich als neue Eigenschaften herausstellen, können sehr hoch sein. Außerdem können invasive Änderungen in der Spätphase, mit denen Sicherheits- und Zuverlässigkeitsrisiken behoben werden sollen, selbst noch mehr Sicherheits- und Zuverlässigkeitsrisiken mit sich bringen. Deshalb ist es wichtig, Sicherheit und Zuverlässigkeit schon früh in der Teamkultur zu verankern (mehr zu diesem Thema findest du in Kapitel 21).
Die frühe Geschichte des Internets,11 und die Entwicklung der zugrunde liegenden Protokolle wie IP, TCP, DNS und BGP bieten eine interessante Perspektive auf dieses Thema. Zuverlässigkeit - insbesondere die Überlebensfähigkeit des Netzwerks auch bei Ausfällen von Knotenpunkten12 und die Zuverlässigkeit der Kommunikation trotz fehleranfälliger Verbindungen13-waren explizite und vorrangige Ziele bei der Entwicklung der frühen Vorläufer des heutigen Internets wie dem ARPANET.
In den frühen Papieren und Dokumentationen zum Internet wird die Sicherheit jedoch kaum erwähnt. Die frühen Netzwerke waren im Wesentlichen geschlossen, mit Knotenpunkten, die von vertrauenswürdigen Forschungs- und Regierungseinrichtungen betrieben wurden. Im offenen Internet von heute gilt diese Annahme jedoch überhaupt nicht mehr - viele Arten von böswilligen Akteuren nehmen am Netzwerk teil (siehe Kapitel 2).
Die grundlegenden Protokolle des Internets - IP, UDP und TCP - haben keine Möglichkeit, den Absender von Übertragungen zu authentifizieren oder eine absichtliche, böswillige Veränderung von Daten durch einen Zwischenknoten im Netzwerk zu erkennen. Viele Protokolle auf höherer Ebene, wie HTTP oder DNS, sind von Natur aus anfällig für verschiedene Angriffe durch böswillige Teilnehmer im Netzwerk. Im Laufe der Zeit wurden sichere Protokolle oder Protokollerweiterungen entwickelt, um sich gegen solche Angriffe zu schützen. HTTPS zum Beispiel erweitert HTTP, indem es Daten über einen authentifizierten, sicheren Kanal überträgt. Auf der IP-Ebene sorgt IPsec für die kryptografische Authentifizierung von Peers auf Netzwerkebene sowie für Datenintegrität und Vertraulichkeit. IPsec kann verwendet werden, um VPNs über nicht vertrauenswürdige IP-Netzwerke aufzubauen.
Die breite Anwendung dieser sicheren Protokolle hat sich jedoch als ziemlich schwierig erwiesen. Das Internet gibt es jetzt seit etwa 50 Jahren, und die kommerzielle Nutzung des Internets begann vor etwa 25 oder 30 Jahren - und trotzdem wird immer noch ein erheblicher Teil des Internetverkehrs nicht über HTTPS abgewickelt.14
Ein weiteres Beispiel für den Zielkonflikt zwischen anfänglicher und anhaltender Geschwindigkeit (in diesem Fall außerhalb des Bereichs Sicherheit und Zuverlässigkeit) sind die agilen Entwicklungsprozesse. Ein Hauptziel agiler Entwicklungsworkflows ist es, die Entwicklungs- und Bereitstellungsgeschwindigkeit zu erhöhen - insbesondere, um die Latenzzeit zwischen der Spezifikation von Funktionen und der Bereitstellung zu verringern. Agile Arbeitsabläufe setzen jedoch in der Regel ausgereifte Verfahren für Unit- und Integrationstests sowie eine solide Infrastruktur für die kontinuierliche Integration voraus, die im Gegenzug für die langfristigen Vorteile in Bezug auf Geschwindigkeit und Stabilität eine Vorabinvestition erfordern.
Generell kannst du dich dafür entscheiden, die Geschwindigkeit deines Projekts über alles zu stellen - du kannst die erste Iteration deiner Webanwendung ohne Tests und mit einem Release-Prozess entwickeln, der darauf hinausläuft, dass du Tarballs auf Produktionsrechner kopierst. Die erste Demo wird wahrscheinlich relativ schnell veröffentlicht, aber bei der dritten Version ist dein Projekt möglicherweise schon im Verzug und mit technischen Schulden behaftet.
Wir haben bereits über die Abstimmung zwischen Zuverlässigkeit und Geschwindigkeit gesprochen: Die Investition in einen ausgereiften kontinuierlichen Integrations- und Bereitstellungsprozess (CI/CD) und eine entsprechende Infrastruktur unterstützt häufige Produktionsversionen mit einem kontrollierten und akzeptablen Zuverlässigkeitsrisiko (siehe Kapitel 7). Die Einrichtung eines solchen Workflows erfordert jedoch einige Vorab-Investitionen - du brauchst zum Beispiel Folgendes
-
Die Abdeckung der Unit- und Integrationstests ist robust genug, um ein akzeptabel niedriges Fehlerrisiko für Produktionsversionen zu gewährleisten, ohne dass umfangreiche Qualifizierungsarbeiten für die Veröffentlichung erforderlich sind.
-
Eine CI/CD-Pipeline, die selbst zuverlässig ist
-
Eine häufig geübte, zuverlässige Infrastruktur für gestaffelte Produktions-Rollouts und Rollbacks
-
Eine Softwarearchitektur, die entkoppelte Rollouts von Code und Konfigurationen ermöglicht (z. B. "Feature Flags")
Diese Investition ist in der Regel bescheiden, wenn sie zu Beginn des Produktlebenszyklus getätigt wird, und erfordert von den Entwicklern nur zusätzliche Anstrengungen, um eine gute Testabdeckung und "grüne Builds" aufrechtzuerhalten. Ein Entwicklungsworkflow mit unzureichender Testautomatisierung, die Abhängigkeit von manuellen Schritten bei der Bereitstellung und lange Release-Zyklen führen dagegen dazu, dass ein Projekt mit zunehmender Komplexität ins Stocken gerät. An diesem Punkt erfordert die Nachrüstung von Test- und Release-Automatisierung in der Regel eine Menge Arbeit auf einmal und kann dein Projekt noch mehr verlangsamen. Außerdem können Tests, die auf ein ausgereiftes System nachgerüstet werden, manchmal in die Falle tappen, das aktuelle fehlerhafte Verhalten mehr zu testen als das korrekte, beabsichtigte Verhalten.
Diese Investitionen sind für Projekte jeder Größe von Vorteil. Größere Organisationen können jedoch noch mehr von den Größenvorteilen profitieren, da sich die Kosten auf viele Projekte verteilen lassen - die Investition eines einzelnen Projekts läuft dann auf die Verpflichtung hinaus, zentral verwaltete Frameworks und Workflows zu nutzen.
Wenn es darum geht, sicherheitsorientierte Designentscheidungen zu treffen, die zu einer anhaltenden Geschwindigkeit beitragen, empfehlen wir die Wahl eines Frameworks und eines Workflows, die einen konstruktionssicheren Schutz gegen relevante Klassen von Schwachstellen bieten. Diese Entscheidung kann das Risiko, dass solche Schwachstellen während der laufenden Entwicklung und Wartung der Codebasis deiner Anwendung eingeführt werden, drastisch reduzieren oder sogar eliminieren (siehe Kapitel 6 und 12). Diese Verpflichtung ist in der Regel nicht mit erheblichen Vorabinvestitionen verbunden, sondern mit einem schrittweisen und in der Regel bescheidenen kontinuierlichen Aufwand, um die Einschränkungen des Frameworks einzuhalten. Im Gegenzug verringerst du das Risiko drastisch, dass ungeplante Systemausfälle oder Sicherheitsübungen die Einsatzpläne durcheinander bringen. Außerdem ist es viel wahrscheinlicher, dass die Überprüfung der Sicherheit und der Produktionsbereitschaft zum Zeitpunkt der Veröffentlichung reibungslos verläuft.
Fazit
Es ist nicht einfach, sichere und zuverlässige Systeme zu entwerfen und zu bauen, zumal Sicherheit und Zuverlässigkeit in erster Linie Eigenschaften sind, die sich aus dem gesamten Entwicklungsworkflow und dem Betrieb ergeben. Bei diesem Unterfangen muss man über eine Menge ziemlich komplexer Themen nachdenken, von denen viele auf den ersten Blick nicht viel mit den primären Anforderungen deines Dienstes zu tun haben.
Bei deinem Entwurfsprozess musst du zahlreiche Kompromisse zwischen Sicherheits-, Zuverlässigkeits- und Funktionsanforderungen eingehen. In vielen Fällen scheinen diese Kompromisse auf den ersten Blick in direktem Widerspruch zu stehen. Es mag verlockend erscheinen, diese Probleme in der Anfangsphase eines Projekts zu vermeiden und sich "später darum zu kümmern" - aber das ist oft mit erheblichen Kosten und Risiken für dein Projekt verbunden: Wenn dein Dienst erst einmal in Betrieb ist, sind Zuverlässigkeit und Sicherheit keine Option. Wenn dein Dienst ausfällt, kann es sein, dass du dein Geschäft verlierst; und wenn dein Dienst gefährdet ist, müssen alle Hände voll zu tun haben, um zu reagieren. Aber mit guter Planung und sorgfältigem Design ist es oft möglich, alle drei Aspekte zu erfüllen. Außerdem kannst du dies mit geringen zusätzlichen Kosten im Voraus und oft auch mit einem geringeren technischen Aufwand während der gesamten Lebensdauer des Systems erreichen.
1 Für eine formalere Behandlung siehe The MITRE Systems Engineering Guide und ISO/IEC/IEEE 29148-2018(E).
2 Für die Zwecke dieses Beispiels ist es nicht relevant, was genau verkauft wird - ein Medienunternehmen könnte Zahlungen für Artikel verlangen, ein Mobilitätsunternehmen könnte Zahlungen für den Transport verlangen, ein Online-Marktplatz könnte den Kauf physischer Waren ermöglichen, die an die Verbraucher versandt werden, oder ein Essensbestelldienst könnte die Lieferung von Essensbestellungen von lokalen Restaurants erleichtern.
3 Siehe z. B. McCallister, Erika, Tim Grance und Karen Scarfone. 2010. NIST Special Publication 800-122, "Guide to Protecting the Confidentiality of Personally Identifiable Information (PII) ". https://oreil.ly/T9G4D.
4 Beachte, dass es von den rechtlichen Rahmenbedingungen abhängt, denen deine Organisation unterliegt; diese rechtlichen Aspekte sind nicht Gegenstand dieses Buches.
5 Siehe z. B. das Sandboxed API-Projekt.
6 Mehr zu diesem Thema siehe Zalewski, Michał. 2011. The Tangled Web: A Guide to Securing Modern Web Applications. San Francisco, CA: No Starch Press.
7 Siehe z. B. die OWASP Top 10 und die CWE/SANS TOP 25 Most Dangerous Software Errors.
8 Siehe Kern, Christoph. 2014. "Securing the Tangled Web". Communications of the ACM 57(9): 38-47. doi:10.1145/2643134.
9 Bei Google wird die Software in der Regel aus dem HEAD eines gemeinsamen Repositorys erstellt, wodurch alle Abhängigkeiten bei jedem Build automatisch aktualisiert werden. Siehe Potvin, Rachel, und Josh Levenberg. 2016. "Warum Google Milliarden von Codezeilen in einem einzigen Repository speichert". Communications of the ACM 59(7): 78-87. https://oreil.ly/jXTZM.
10 Siehe die Diskussion über taktische und strategische Programmplanung in Ousterhout, John. 2018. A Philosophy of Software Design. Palo Alto, CA: Yaknyam Press. Martin Fowler macht ähnliche Beobachtungen.
11 Siehe RFC 2235 und Leiner, Barry M. et al. 2009. "A Brief History of the Internet". ACM SIGCOMM Computer Communication Review 39(5): 22–31. doi:10.1145/1629607.1629613.
12 Baran, Paul. 1964. "On Distributed Communications Networks". IEEE Transactions on Communications Systems 12(1): 1–9. doi:10.1109/TCOM.1964.1088883.
13 Roberts, Lawrence G., und Barry D. Wessler. 1970. "Computer Network Development to Achieve Resource Sharing". Proceedings of the 1970 Spring Joint Computing Conference: 543–549. doi:10.1145/1476936.1477020.
14 Felt, Adrienne Porter, Richard Barnes, April King, Chris Palmer, Chris Bentzel und Parisa Tabriz. 2017. "Measuring HTTPS Adoption on the Web". Proceedings of the 26th USENIX Conference on Security Symposium: 1323–1338. https://oreil.ly/G1A9q.
Get Aufbau sicherer und zuverlässiger Systeme now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.