Grundschutz++ als Code, Teil 6: Was kein Repository weiß
Nach dem ersten Teil dieser Serie hat mir ein Auditor eine Frage gestellt, die mich seitdem begleitet: „Ihr prüft, ob euer Code angewendet ist. Aber was ist mit allem, was ohne Code entstanden ist?“
Er hatte recht. Alles, was die Kette aus Teil 5 weiß, steht irgendwo geschrieben, und auch ein Drift-Check vergleicht nur das Geschriebene mit dem, was läuft. Legt jemand an der Pipeline vorbei einen Eintrag an, taucht der in keinem Plan auf und fällt deshalb auch niemandem auf. In diesem Teil geht es darum, die Frage umzudrehen und das laufende System selbst zu fragen, was es enthält, das kein Code beschreibt.
Teil 6 der Serie „Grundschutz++ als Code“. Die Übersicht aller Teile steht weiter unten.
01 — Zwei Ebenen
Die Prüfungen aus Teil 4 beantworten im Kern eine Frage: Ist das, was im Code steht, auch so angewendet? Ein Drift-Check kann das sehr gut, weil er den Plan gegen den Ist-Zustand rechnet und jede Abweichung meldet.
Sein Blick endet allerdings am Rand des States. Ein DNS-Eintrag, eine Security Group oder ein Deployment, das jemand von Hand angelegt hat, kommt im Plan gar nicht vor. Wo der Code schweigt, findet der Vergleich nichts, und grün ist er trotzdem.
Auf der zweiten Ebene läuft die Frage deshalb andersherum. Ich liste auf, was tatsächlich existiert, und frage bei jedem Objekt, welcher Code es beschreibt. Findet sich keiner, ist das ein Befund.
02 — Drei Quellen
Für den Anfang habe ich mir drei Stellen ausgesucht, an denen in einer Umgebung wie meiner am ehesten etwas von Hand entsteht:
- DNS: Jeder Eintrag der verwalteten Zonen wird mit den Einträgen verglichen, die der OpenTofu-Code deklariert. Ein Name, den niemand aufgeschrieben hat, ist ein Einstiegspunkt, von dem niemand weiß.
- Cloud-Ressourcen: Instanzen, öffentliche Adressen, Netze, Security Groups mit ihren Regeln und alle Buckets werden gegen den State gehalten.
- Cluster: Die Objekte benannter Arten im Kubernetes-Cluster werden mit dem verglichen, was GitOps ausrollt.
Jede Quelle schreibt ihr Ergebnis genauso wie die Prüfungen aus Teil 4, als OSCAL Assessment Result mit einer Beobachtung je Objekt. Dadurch landet jeder Befund ganz selbstverständlich in der Kette aus Teil 5, bis hinein in den Maßnahmenplan, ohne dass ich dafür etwas Neues bauen musste.
03 — Die deklarierte Seite
Für einen Abgleich braucht es zwei Listen. Die eine liefert die Cloud-API, die andere muss aus dem Code kommen. Dort lese ich allerdings nicht die Konfigurationsdateien, sondern den State, weil nur er weiß, welche ID ein Objekt beim Anlegen bekommen hat.
Der tägliche Drift-Check schreibt dazu zwei kleine Listen als Artefakt: die DNS-Einträge mit Zone, Name und Typ und die Ressourcen mit Typ und ID. Mehr verlässt den Job bewusst nicht. Im State stehen auch Werte, die außerhalb nichts zu suchen haben, und für den Vergleich genügt der Bezeichner vollkommen.
tofu show -no-color | awk -f declared.awk | sort -u > declared-resources.txt04 — Eine Regel ist eine eigene Ressource
Wie groß der Unterschied zwischen den beiden Ebenen ist, zeigt sich am deutlichsten bei Security Groups. In meinem Code ist jede Regel eine eigene Ressource. Das ist sauber und gut lesbar, hat aber eine Folge, die man leicht übersieht, gerade weil der Drift-Check so zuverlässig grün meldet.
Hängt nämlich jemand von Hand eine zusätzliche Regel an eine deklarierte Gruppe, bleibt der Check still. An der Gruppe selbst hat sich nichts geändert, und die neue Regel kennt der Plan nicht. Ausgerechnet die Freigabe, um die es eigentlich geht, ist damit unsichtbar.
Die Cloud-Quelle vergleicht deshalb nicht nur Gruppen, sondern jede einzelne Regel über ihre ID. Eine Regel ohne Gegenstück im State wird zum Befund, auch wenn die Gruppe, an der sie hängt, ordentlich deklariert ist.
05 — Eigentümer statt Liste
Nicht alles, was existiert, steht selbst im Code. Instanzen startet eine Autoscaling-Gruppe, Pods entstehen aus einem Deployment, und Volumes legt ein Speichertreiber an. Eine feste Liste aller erlaubten Objekte wäre deshalb nach einer Stunde veraltet.
Statt nach einer Liste fragen die Quellen nach dem Eigentümer. Ein Objekt gilt als besessen, wenn es selbst deklariert ist, im State oder in einer GitOps-Anwendung, oder wenn ein deklariertes Objekt es angelegt hat, etwa eine Autoscaling-Gruppe oder im Cluster über eine Owner-Referenz. Dazu kommt, was die Plattform von sich aus mitbringt, wie die Standardobjekte von Kubernetes, und was als benannte Ausnahme mit Begründung in der Konfiguration steht.
Alles andere hat keinen Eigentümer und wird zum Befund. Das typische Beispiel für eine Ausnahme ist übrigens der Bucket mit dem OpenTofu-State: Er hält genau den State, der ihn beschreiben müsste, und kann sich deshalb nicht selbst verwalten.
06 — Was eine Quelle nicht lesen darf
Eine Quelle, die alles sehen darf, wird schnell selbst zum Risiko, und im Cluster gilt das ganz besonders. Wer dort Objekte auflisten darf, bekommt sie nämlich vollständig zurück und nicht nur ihre Namen.
Der Job im Cluster liest deshalb nur benannte Arten, und das ausschließlich mit list. Secrets und ConfigMaps gehören nicht dazu, weil beide Werte tragen können, die ein kompromittierter Pod nicht zu sehen bekommen soll. Für die Frage nach dem Eigentümer reichen die Metadaten der übrigen Arten ohnehin aus.
Aus dem Cluster heraus geht am Ende nur ein Bericht, der die Objekte ohne Eigentümer beim Namen nennt. Schreiben darf der Job ihn an genau eine Stelle im gesperrten Bucket und sonst nirgendwohin. Im Netz erreicht er nur die API und öffentliche HTTPS-Ziele, während der Metadatendienst des Knotens für ihn gesperrt bleibt.
Die Lücke, die dadurch entsteht, gehört dazu und soll hier nicht untergehen: Ein von Hand angelegtes Secret oder eine ConfigMap findet diese Quelle nicht.
07 — Neue Quellen laufen erst auf Probe
Die Ergebnisse aus Teil 4 sind signiert und 400 Tage gegen Löschen gesperrt. Das ist gewollt, hat aber eine Kehrseite, die ich erst im Betrieb richtig zu schätzen gelernt habe: Ein falscher Befund lässt sich danach nicht mehr löschen, sondern nur noch erklären.
Und gerade eine neue Quelle produziert in den ersten Läufen vor allem Befunde über sich selbst. Mal kennt sie einen Eigentümer noch nicht, mal liest sie ein Format falsch. In den ersten Tagen der Messungen sind mir genau solche Fehler unterlaufen, und jeder davon liegt heute als gesperrtes Ergebnis im Bucket.
Seitdem startet jede neue Quelle mit trial: true. Ihr Ergebnis wird gebaut und gegen das Schema geprüft, aber weder signiert noch veröffentlicht, und im Prüfplan taucht sie noch nicht auf. Erst wenn ich die ersten Läufe von Hand gelesen und jeden Befund erklärt habe, darf sie für eine Anforderung sprechen.
08 — Ein Test mit bekanntem Ausgang
Eine Prüfung, die nie etwas findet, kann zweierlei bedeuten: Entweder ist alles in Ordnung, oder sie ist blind. Von außen lässt sich das nicht unterscheiden, solange man ihr nicht einmal etwas zum Finden gibt.
Genau das habe ich getan. Von Hand und am Code vorbei habe ich je ein Objekt angelegt, einen TXT-Eintrag in einer der Zonen und eine leere Security Group. Beide tragen im Namen, was sie sind, und richten keinen Schaden an.
Der nächste Lauf meldete genau diese beiden Objekte und sonst nichts. Die Cloud-Quelle fand sogar etwas mehr, als ich angelegt hatte, nämlich die zwei Egress-Regeln, die AWS jeder neuen Gruppe automatisch mitgibt. Auch sie stehen in keinem Code, und auch sie hat die Prüfung gesehen, obwohl ich selbst nicht an sie gedacht hatte.
Danach habe ich beide Objekte wieder entfernt. Der TXT-Eintrag stand am 1. Oktober im Lauf um 16:12 UTC als Befund im Ergebnis. Im Lauf um 16:46 UTC war er verschwunden.
Die Cluster-Quelle bekam ihren Test am 13. Oktober. Um 17:32 UTC habe ich von Hand einen leeren Namespace angelegt, der ebenfalls im Namen trägt, was er ist. Der nächste Lauf um 22:41 UTC meldete ihn als Objekt ohne Eigentümer. Im signierten Ergebnis standen KONF.2.1.1, KONF.2.5 und KONF.10.1 deshalb auf nicht erfüllt. Am 14. Oktober um 05:22 UTC habe ich den Namespace gelöscht. Im Lauf um 10:43 UTC war er verschwunden, und die drei Anforderungen standen wieder auf erfüllt.
Der DNS-Befund und der Namespace-Befund sind übrigens wie jedes Ergebnis signiert und gesperrt. Sie bleiben also 400 Tage im Bucket, aber ihre Namen erklären sie.
09 — Drei Ausgänge für jeden Befund
Ein Befund ohne Eigentümer ist kein Listeneintrag, den man sich für später merkt. Er hat genau drei mögliche Ausgänge:
- Er wird Code: Das Objekt ist gewollt und bekommt jetzt die Beschreibung, die ihm bisher gefehlt hat.
- Er bekommt einen Grund: Das Objekt muss so bleiben, wie es ist, und steht künftig als benannte Ausnahme mit Begründung in der Konfiguration.
- Er verschwindet: Niemand braucht das Objekt, also wird es entfernt.
Einen vierten Ausgang nach dem Muster „schauen wir uns später an“ sehe ich bewusst nicht vor. Er würde aus der zweiten Ebene genau die Liste machen, die irgendwann niemand mehr liest.
Die DNS-Quelle lief vom 1. bis zum 7. Oktober 30-mal. Ihr erster Lauf meldete fünf Einträge, die keine waren: Die Quelle las den Namen der Zone selbst in maskierter Schreibweise und fand ihn deshalb nicht im Code. Eine Stunde später war das behoben. Am 3. Oktober tauchten drei DKIM-Einträge für einen Mail-Dienst auf, angelegt beim Einrichten des Dienstes und nicht im Code. Sie wurden Code und standen ab dem 6. Oktober nicht mehr in den Befunden.
Cloud- und Cluster-Quelle kamen am 7. Oktober aus der Probe. In 27 Läufen bis zum 13. Oktober meldete die Cloud-Quelle zweimal sich selbst: Einmal sah sie weniger Ressourcen als erwartet, einmal war die deklarierte Liste zu alt. Beides ist kein Objekt ohne Eigentümer, sondern eine Quelle, die nicht für sich bürgen kann. Beides war im nächsten Lauf weg. Die Cluster-Quelle fand nur den Test-Namespace aus Abschnitt 8.
10 — Zwei Sichten fehlen bewusst
Ein Proof of Concept ist nur so ehrlich wie die Liste dessen, was er nicht kann. Zwei Sichten, die zu einer vollständigen zweiten Ebene gehören würden, habe ich deshalb bewusst weggelassen, und beide möchte ich hier benennen.
Kein Portscan von außen. Ein Scan würde zeigen, was aus dem Internet tatsächlich erreichbar ist, auch dort, wo der Code etwas anderes vorsieht. Dazu braucht es aber einen Standort außerhalb der eigenen Umgebung, und den habe ich im Moment nicht. Was erreichbar sein soll, lese ich stattdessen aus dem Code, also Adressen und Security Groups aus OpenTofu sowie Services und Ingresses aus GitOps. Das ist schwächer als ein Scan, weil es die Absicht zeigt und nicht das Ergebnis.
Keine Certificate Transparency. Die öffentlichen Logs verzeichnen jedes Zertifikat, das für eine Domain ausgestellt wurde. Durchsuchen lassen sie sich praktisch aber nur über den Index eines Dritten, und von den beiden, die ich probiert habe, war der eine während des Tests nicht erreichbar, während der andere für den Betrieb ein Konto verlangt. Einen Hostnamen ohne Code findet die DNS-Quelle auch direkt. Unsichtbar bleibt dagegen ein Zertifikat, das jemand für eine meiner Domains ausstellen ließ, ohne dass ein Eintrag in meinen Zonen dazugehört, etwa für einen SaaS-Dienst oder durch eine Fehlausstellung. CAA begrenzt zwar, wer ausstellen darf, meldet aber nicht, wer es getan hat.
Beide Lücken nehme ich für diesen Stand bewusst in Kauf. Sie lassen sich später als weitere Quelle anschließen, ohne dass sich an der Kette etwas ändern müsste.
Eine dritte Sicht steht noch aus: die Flow-Logs, also das tatsächliche Netz gegen die beabsichtigte Architektur. Gemessen wird bisher nur, dass die Flow-Logs laufen.
11 — Was die zweite Ebene nicht ist
- Kein vollständiges Inventar: Jede Quelle kennt nur die Arten, die sie auflistet, und nur die Region, in die sie schaut. Was außerhalb liegt, bleibt unbekannt, weshalb die Liste der Arten selbst ein Ergebnis ist, das man regelmäßig prüfen sollte.
- Keine Absicherung: Die Quellen finden ein Objekt erst beim nächsten Lauf und können nichts verhindern. Dafür sind Berechtigungen und Admission-Regeln zuständig.
- Kein Ersatz für den Drift-Check: Der bleibt für die erste Ebene verantwortlich. Die zweite verkleinert das Unbekannte, abschaffen kann sie es nicht.
12 — Die Serie im Überblick
- Grundschutz++ gegen die eigene Dokumentation: die Methode, der Katalog als Datenmodell, der Abgleich mit IaC und GitOps.
- Die Anwendbarkeit als Datensatz: die Anwendbarkeitserklärung als OSCAL-Profil.
- Container, die sagen, welche Anforderungen sie erfüllen: eine signierte Komponentendefinition an jedem Image, das die Pipeline baut.
- Messung als Nachweis: Prüfungen schreiben ihre Ergebnisse als OSCAL, datiert, signiert, nicht löschbar.
- Das ISMS als Build-Produkt: Konzept, Prüfplan, Ergebnis und Maßnahmenplan als OSCAL-Kette, bei jeder Messung neu gebaut.
- Was kein Repository weiß: die zweite Ebene, das laufende System gegen seinen Code. Dieser Beitrag.
- Ein Nachweis, viele Standards: NIS2, DORA, C5 und SOC 2 als Mappings auf Grundschutz++.
- Die Kette zum Mitnehmen: allgemeine Checks, der Bericht über einen Zeitraum und eine Vorlage, die alles zusammenschaltet.
Die Teile 1 bis 6 beschreiben, was in meinem ISMS läuft. Sie erscheinen erst, wenn der Schritt im Betrieb läuft und gemessen ist. Die Teile 7 und 8 zeigen Werkzeuge, die ich nach dem Feedback beim InfoSec-Stammtisch gebaut habe und selbst nicht einsetze. Der Code liegt in grundschutz-worksheets. Eine Karte der ganzen Serie zeigt OSCAL in der Praxis: acht Modelle, eine Kette.
Häufige Fragen
Reicht ein Drift-Check nicht aus?+
Für die Frage, ob der Code angewendet ist, ja. Er vergleicht aber nur, was im State steht. Ein Objekt, das jemand an der Pipeline vorbei anlegt, kommt in seinem Plan nicht vor und löst deshalb auch keine Meldung aus.
Warum keine Secrets und ConfigMaps im Cluster-Inventar?+
Weil das Auflisten in Kubernetes die ganzen Objekte liefert, samt ihrer Werte. Ein kompromittierter Inventar-Job könnte sie sonst lesen. Die Lücke ist bewusst gewählt: Ein von Hand angelegtes Secret findet diese Quelle nicht.
Wozu ein absichtlich angelegtes Testobjekt?+
Eine Prüfung ohne Befund ist entweder richtig oder blind, und von außen sieht beides gleich aus. Ein Objekt mit bekanntem Ausgang zeigt, dass sie sieht. Danach darf man ihr auch glauben, wenn sie nichts findet.
Funktioniert das auch ohne AWS oder Kubernetes?+
Das Muster schon. Man braucht eine Liste dessen, was der Code deklariert, eine Liste dessen, was existiert, und eine Regel für Eigentümer. Die Quellen in diesem Beitrag sind für AWS, Route 53 und Kubernetes geschrieben, das Ergebnis ist in jedem Fall ein OSCAL Assessment Result.
Fazit
Ein Drift-Check beantwortet die Frage, ob der Code angewendet ist, und er beantwortet sie gut. Die Frage des Auditors nicht. Um sie zu beantworten, muss man das laufende System selbst befragen und jedem Objekt einen Eigentümer abverlangen.
Überzeugt hat mich am Ende nicht der erste grüne Lauf, sondern der Test mit bekanntem Ausgang. Wenn eine Prüfung nachweislich etwas findet, darf man sie auch beim Wort nehmen, wenn sie nichts findet. Was sie trotzdem nicht sehen kann, steht in Abschnitt 10, und das gehört ebenso zum Ergebnis.
Weiter mit Teil 7: Ein Nachweis, viele Standards.
Ich gleiche dein laufendes System mit seinem Code ab, ohne an seine Geheimnisse zu gehen.
DNS, Cloud-Ressourcen und Cluster gegen OpenTofu und GitOps, jeder Fund mit Eigentümer oder Befund, neue Quellen erst auf Probe und mit einem Test, der zeigt, dass sie wirklich sehen. Die Ergebnisse fließen als OSCAL in dieselbe Kette wie deine übrigen Nachweise.
Plattform-Betrieb statt Beratung auf Papier: Auf Wunsch baue ich die Quellen in deiner Umgebung mit auf.
Über den Autor

Kai Ole Hartwig
Programmiert seit 2002 – autodidaktisch gelernt, 2012 mit KO-Web selbständig gemacht. Über 100 Projekte, Fokus auf Security, Performance, Automatisierung und Qualität. Heute freiberuflich: DevSecOps-Beratung, Schulungen und Softwareentwicklung.