Grundschutz++ als Code, Teil 4: Messung als Nachweis
Ein nächtlicher Job prüft, ob jeder Hauptzweig geschützt ist. Ein anderer vergleicht die Infrastruktur mit ihrem Code. In vielen Betrieben laufen solche Jobs seit Jahren, und für ein Audit taugen sie trotzdem wenig: Ihr Ergebnis steht im Job-Log, und das ist nach 30 Tagen gelöscht. Vorgelegt wird dann eine Tabelle, die jemand von Hand gepflegt hat.
In diesem Teil geht es darum, wie die Prüfungen ihr Ergebnis selbst als Nachweis ablegen.
Teil 4 der Serie „Grundschutz++ als Code“. Die Übersicht aller Teile steht weiter unten.
01 — Was ein Prüfergebnis in OSCAL ist
Für Prüfungen hat OSCAL ein eigenes Modell, die Assessment Results. Es hält auseinander, was in einem Log ineinanderläuft:
- Beobachtung (observation): was eine Prüfung an einem Prüfgegenstand gesehen hat, zum Beispiel an einem Projekt. Dazu gehören Zeitpunkt und Methode.
- Befund (finding): was daraus für eine Anforderung folgt, also erfüllt oder nicht erfüllt, und welche Beobachtungen das begründen.
- Prüfgegenstand (subject): worauf sich eine Beobachtung bezieht, als eigene Ressource mit Link.
In meinen Ergebnissen gibt es deshalb keinen Befund ohne Beobachtung und keine Beobachtung ohne Prüfgegenstand. Erst das macht aus einer Aussage einen Nachweis.
02 — Prüfungen, die schon laufen
Neue Prüfungen habe ich dafür nicht gebaut. Mich hat interessiert, welche der vorhandenen eine Anforderung aus Grundschutz++ belegen, und zwar über Wochen hinweg und nicht nur an einem Stichtag.
- Geschützte Hauptzweige belegen TEST.1.3 und TEST.2.1: Änderungen erreichen den Hauptzweig nur auf dem vorgesehenen Weg, und seine Geschichte lässt sich nicht umschreiben.
- Geschützte Release-Tags belegen TEST.4.1: Nur wer die Rolle dafür hat, macht aus einem Commit ein Release.
- Mergen nur mit grüner Pipeline belegt TEST.3.1.6: Die Tests laufen bei jeder Änderung, bevor sie landet.
- Drift der Infrastruktur belegt KONF.2.5: Ein geplantes
tofu planvergleicht das Konto mit dem Code. - Frische der Backups belegt NOT.4.4 und NOT.4.10: Die Kopien in der zweiten Region sind jünger als eine festgelegte Grenze.
Welche Prüfung welche Anforderung belegt, ist eine Einschätzung. Ich habe sie deshalb als Daten neben die Prüfungen gelegt statt in den Code. Wer sie anders sieht, ändert eine Zeile und sieht im nächsten Lauf, was sich daraus ergibt.
03 — Vom Prüfbericht zum Ergebnis
Jede Prüfung schreibt einen kleinen Lauf-Datensatz: welche Anforderungen, welche Gegenstände, was sie an jedem Prüfgegenstand gesehen hat. Daraus macht results.mjs aus grundschutz-worksheets ein gültiges OSCAL-Dokument.
{
"check": "protected-tags",
"title": "Protected release tags",
"method": "TEST",
"start": "2026-10-01T03:00:00Z", "end": "2026-10-01T03:02:41Z",
"catalog": { "path": "…/Grundschutz++-resolved_catalog.json", "commit": "367d775…" },
"requirements": ["TEST.4.1"],
"subjects": [
{ "title": "group/app", "href": "https://git.example.org/group/app",
"satisfied": true, "detail": "v* protected, maintainers only" }
]
}
node results.mjs run.json > results.json
node validate.mjs results.json
Der Generator folgt drei Regeln:
- Ein Ausreißer genügt: Ist auch nur ein Prüfgegenstand nicht in Ordnung, gilt die Anforderung in diesem Lauf als nicht erfüllt, und der Befund nennt genau diese Beobachtungen.
- Ohne Prüfgegenstand kein Ergebnis: Ein Lauf, der nichts angesehen hat, bricht mit einem Fehler ab. Er kann nichts belegen.
- Nur Kennungen aus dem Katalog: Jede Anforderung muss im festgelegten Stand des Katalogs stehen, sonst bricht die Erzeugung ab.
04 — Was nicht geprüft wurde, muss dastehen
Heikel wird es bei dem, was eine Prüfung auslässt. Archivierte Projekte oder Repositories ohne CI haben zur Frage „Mergen nur mit grüner Pipeline“ nichts beizutragen. Ließe man sie einfach weg, sähe das Ergebnis besser aus, als es ist.
Die Zuordnung kennt deshalb drei Urteile: erfüllt, ausgenommen und nicht erfüllt. Ausgenommene Projekte tauchen nicht als Beobachtung auf. Die Beschreibung des Ergebnisses nennt sie aber mit Anzahl und Grund. Alles Übrige gilt als nicht erfüllt, auch ein Urteil, mit dem niemand gerechnet hat.
Das ist gleich am ersten Tag passiert. Eine Prüfung gab für archivierte Projekte ein eigenes Urteil aus, das in der Zuordnung fehlte, und der erste Lauf meldete prompt 45 Verstöße, die keine waren. Seitdem prüft ein Test bei jeder Änderung, ob jedes mögliche Urteil zugeordnet ist. Eine solche Lücke fällt jetzt beim Bauen auf und nicht erst im Audit.
Für Quellen außerhalb von GitLab gilt dasselbe. Ist eine Quelle noch nicht eingerichtet, fehlt sie im Ergebnis, und das Log sagt warum. Ist sie eingerichtet, lässt sich aber nicht lesen, zählt sie als nicht erfüllt. Eine Messung, die nicht stattgefunden hat, belegt nichts.
Im Kleinen genauso: Antwortet die API für ein einzelnes Projekt nicht, bekommt es das Urteil „nicht lesbar“ und gilt als nicht erfüllt. Der Bericht über alle anderen Projekte bleibt davon unberührt. Anfangs hat ein einziger Lese-Timeout noch die Nachweise sämtlicher Projekte verworfen.
05 — Das Datum gehört zum Nachweis
„Die Hauptzweige sind geschützt“ beschreibt den heutigen Tag. Ein Audit fragt nach mehr. Es will wissen, ob das in den vergangenen Monaten durchgehend so war, und darauf hat ein einzelner Bericht keine Antwort. Deshalb laufen die Prüfungen alle sechs Stunden, und jeder Lauf erzeugt ein eigenes Dokument mit Beginn und Ende.
Die Kennungen darin leiten sich aus Prüfung, Startzeit und Prüfgegenstand ab (UUID Version 5). Derselbe Lauf ergibt byte-genau dasselbe Dokument. Zwei Läufe lassen sich direkt nebeneinanderlegen, und eine Änderung zwischen ihnen springt sofort ins Auge. Nach den ersten sieben Tagen lagen je Prüfung 34 bis 38 Ergebnisse vor. Der Takt allein ergibt 28, dazu kamen Läufe von Hand und nach Änderungen an den Prüfungen.
06 — Signiert und nicht löschbar
Einen Nachweis, den der Betreiber nachträglich ändern kann, kann man sich sparen. Jedes Ergebnis landet deshalb an zwei Stellen:
- Als OCI-Artefakt in der Registry, signiert mit demselben Schlüssel wie die Images. Zum Prüfen braucht man nur den öffentlichen Schlüssel.
- Als gesperrte Kopie in einem Bucket mit Object Lock im Modus COMPLIANCE. 400 Tage lang kann niemand die Kopie löschen oder überschreiben, auch nicht das Konto, dem der Bucket gehört.
oras pull registry.example.org/evidence/results:protected-tags-2026-10-01T0300Z
cosign verify --key cosign.pub registry.example.org/evidence/results@sha256:<digest>
Warum beides? Die Signatur verhindert, dass jemand ein Ergebnis unbemerkt verändert. Löschen kann man es trotzdem. Bei Images ist das kein Problem, die lassen sich aus dem Code neu bauen. Die Messung von gestern lässt sich nicht wiederholen.
Drei Details, bei denen man leicht danebenliegt:
- 400 Tage statt 365: ein Auditjahr plus etwas Luft, falls sich der Termin verschiebt. Danach entfernt eine Lifecycle-Regel die Kopie.
- Kein eigener Schlüssel für den Bucket: Mit einem selbst verwalteten Schlüssel ließen sich die gesperrten Kopien unlesbar machen, indem man einfach den Schlüssel löscht. Die Sperre wäre dann nur noch Formsache.
- Aufräumen selbst übernehmen: Die Aufräumregel der Registry reicht nur 90 Tage zurück. Ältere Tags löscht der Job deshalb selbst, sobald sie 400 Tage alt sind.
07 — Die absichtlich kaputte Kontrolle
Eine Prüfung, die bisher nur grüne Läufe hatte, hat noch nicht gezeigt, dass sie Rot überhaupt erkennt. Dafür braucht es einen Gegenbeweis, und den habe ich absichtlich herbeigeführt.
Grundlage ist ein leeres Testprojekt, eingestellt wie alle anderen: Release-Tags darf nur anlegen, wer Maintainer ist. Am 30. September lief der Test so ab:
- 20:35 UTC: Der Lauf bewertet das Testprojekt mit „as required“. Das Ergebnis wird signiert und gesperrt abgelegt.
- 20:37: Ich entferne den Tag-Schutz des Testprojekts, sonst nichts.
- 20:57: Der nächste Lauf meldet genau dieses Projekt mit „NOT as required – missing“, als Beobachtung mit Link auf das Projekt, und wertet TEST.4.1 als nicht erfüllt. Auch dieses Ergebnis ist signiert und für 400 Tage gesperrt.
- 20:59: Ich stelle den Schutz wieder her. Um 21:25 bewertet der Lauf das Projekt wieder mit „as required“.
Am Ende liegen drei Dokumente vor, die sich nachträglich nicht mehr ändern lassen. Dass zwischen 20:37 und 20:59 eine Lücke bestand, steht damit fest, und die Prüfung hat sie gesehen. Darauf kommt es an.
Ganz glatt lief es übrigens nicht. Beim ersten Versuch am Vormittag hatte die Prüfung den Bruch zwar erkannt, ein Lese-Timeout bei einem anderen Projekt verwarf aber den ganzen Bericht. Daraus ist die Regel „nicht lesbar“ aus Abschnitt 04 entstanden.
08 — Drei Fehler, die lokal nicht auffielen
Alle drei steckten in den Schritten, die messen, signieren und ablegen. Keiner davon trat auf meinem Rechner auf.
- cosign 3 und das Transparenz-Log: Wer ohne öffentliches Transparenz-Log signiert, kommt in cosign 3 mit
--tlog-upload=falseallein nicht aus. Ohne die übrigen Schalter lehnt cosign den Aufruf ab. Die Image-Signatur hatte das längst richtig, der neue Schritt hatte die Zeile nur zur Hälfte übernommen. - pipefail und ein leeres grep: Der Runner führt Skripte mit
set -o pipefailaus. Beim Aufräumen filtert grep die Tags mit Datum heraus. Gibt es keine, etwa weil nur Signatur-Tags vorhanden sind, scheitert die ganze Zeile, und der Job bricht ohne Meldung ab. - Nur IPv6 im Runner: Die Backup-Prüfung wartete eine Stunde lang auf die reinen IPv4-Endpunkte des AWS-SDK, bis der Job abgebrochen wurde. Lokal war dieselbe Prüfung nach sieben Sekunden fertig. Inzwischen nutzt sie die Dual-Stack-Endpunkte und kurze Zeitgrenzen.
Für einen Nachweis-Job heißt das: Er muss genauso laut scheitern wie eine Prüfung, die etwas findet. Ein stiller Abbruch sieht im Dashboard aus wie ein ruhiger Tag.
09 — Was ein Ergebnis nicht sagt
- Nicht die ganze Anforderung: Die Drift-Prüfung belegt, dass die Infrastruktur ihrem Code entspricht. KONF.2.5 verlangt mehr, zum Beispiel einen geregelten Umgang mit Abweichungen. Gemessen ist nur dieser Teil.
- Nicht den Betrieb im Cluster: Was dort läuft, ohne in einem Repository zu stehen, bleibt diesen Prüfungen verborgen. Darum geht es in der zweiten Ebene, die einen eigenen Teil bekommt.
- Keine Konformität: Ein Ergebnis hält fest, was ein Lauf gesehen hat. Mehr nicht. Ob ein System die Anforderungen erfüllt, ist eine Bewertung, und die trifft am Ende ein Mensch, der auch das kennt, was keine Prüfung misst.
Wie aus Messungen, Komponentendefinitionen und dieser Bewertung ein Sicherheitsplan wird, zeigt Teil 5.
10 — 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. Dieser Beitrag.
- 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.
- 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
Warum OSCAL und nicht einfach JSON-Berichte?+
Weil ein Werkzeug die Ergebnisse lesen können muss, ohne meine Skripte zu kennen. Assessment Results verknüpfen Befund, Beobachtung und Anforderung in einer Form, die jedes OSCAL-Werkzeug versteht. Der Sicherheitsplan aus Teil 5 baut genau darauf auf.
Reicht nicht der Audit-Log von GitLab?+
Er sagt, wer wann eine Einstellung geändert hat. Er sagt nicht, ob alle Projekte zu einem Zeitpunkt wie verlangt eingestellt waren. Das beantwortet erst eine Prüfung über alle Projekte.
Warum alle sechs Stunden?+
Oft genug, damit eine Lücke nicht einen ganzen Tag unbemerkt bleibt. Selten genug, dass die Ergebnisse überschaubar bleiben. Der Takt ist eine Einstellung, keine Eigenschaft des Verfahrens.
Geht das ohne Registry und Object Lock?+
Ja. results.mjs schreibt eine normale Datei. Signatur und Sperre machen sie zum belastbaren Nachweis. Ohne sie muss man der Ablage vertrauen.
Fazit
Die Prüfungen gab es schon vorher. Neu ist, dass ihr Ergebnis bleibt: als datiertes OSCAL-Dokument, signiert und in einer Kopie, die niemand löschen kann. Die eigentliche Arbeit lag weniger im Format als in zwei Entscheidungen, nämlich was als erfüllt zählt und was nicht geprüft wurde. Beide liegen als Daten neben dem Code, und ein Test sorgt dafür, dass keine Lücke unbemerkt bleibt.
Weiter mit Teil 5: Das ISMS als Build-Produkt.
Ich mache aus deinen vorhandenen Prüfungen Nachweise. Datiert, signiert, 400 Tage gesperrt.
Zuordnung zu Grundschutz++ als Daten, Ergebnisse als OSCAL, Ablage als signiertes Artefakt mit gesperrter Kopie und ein Gegenbeweis, der zeigt, dass Rot auch rot wird.
Plattform-Betrieb statt Beratung auf Papier: Auf Wunsch baue ich die nächsten Schritte gleich mit, bis zum Sicherheitsplan.
Ü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.