TYPO3 auf Kubernetes unter IT-Grundschutz, Teil 2: Das gehärtete TYPO3-Image
Das Image ist die erste Verteidigungslinie. Was nicht im Image steckt, kann weder angegriffen noch falsch konfiguriert werden. Und was zur Laufzeit nicht schreiben darf, lässt sich auch nicht dauerhaft verändern.
Dieser Teil zeigt, wie ein TYPO3-Image aussieht, das den Container-Baustein SYS.1.6 trägt. Mit minimaler Basis, einem einzigen Dienst und Laufzeitrechten, die kaum noch etwas erlauben.
Teil 2 von 14 der Serie „TYPO3 auf Kubernetes unter IT-Grundschutz“. Die Übersicht aller Teile steht in Teil 1.
01 — Eine minimale Basis statt eines Betriebssystems
Ein klassisches PHP-Image bringt ein halbes Betriebssystem mit. Shell, Paketmanager, Compiler-Reste und Dutzende Bibliotheken, die TYPO3 nie braucht. Jede davon ist eine mögliche Schwachstelle im nächsten Scan.
Die Alternative ist eine minimale Basis nach dem Distroless-Prinzip. Gut geeignet sind Basen wie Wolfi, die Pakete in kleinen, häufig aktualisierten Einheiten liefern. Das Image enthält dann nur PHP, die benötigten Extensions, FrankenPHP und die Laufzeitbibliotheken.
So entsteht das Image
- Eine Build-Stage installiert PHP und die Extensions, die TYPO3 wirklich nutzt.
- Die Laufzeit-Stage kopiert nur die fertigen Dateien. Paketmanager und Build-Werkzeuge bleiben zurück.
- Das Basis-Image wird per Digest gepinnt, nicht per Tag.
Das erfüllt SYS.1.6.A6 Verwendung sicherer Images. Wer die Basis selbst baut und regelmäßig neu erzeugt, deckt auch SYS.1.6.A14 Aktualisierung von Images ab.
Die Frage nach der Shell
Ganz ohne Shell zu arbeiten, ist konsequent, aber unbequem. Ein typischer Stolperstein ist das Debugging im Fehlerfall. Kubernetes löst das mit ephemeren Debug-Containern über kubectl debug. Sie hängen sich temporär an einen Pod, ohne dass das Image selbst Werkzeuge mitbringen muss. Im Profil restricted braucht es dafür kubectl debug --profile=restricted.
02 — FrankenPHP im Worker-Modus: ein Dienst pro Container
Die klassische Kombination aus Webserver und PHP-FPM braucht zwei Prozesse und meist einen Supervisor dazwischen. Das widerspricht SYS.1.6.A11 Nur ein Dienst pro Container.
FrankenPHP löst das elegant. Es ist ein Webserver mit eingebautem PHP. Ein einziger Prozess nimmt HTTP-Anfragen entgegen und führt TYPO3 aus. Im Worker-Modus bleibt TYPO3 zwischen Anfragen im Speicher geladen. Das spart den Bootstrap pro Anfrage und senkt die Antwortzeiten deutlich.
Worauf man im Worker-Modus achten muss
Ein Worker lebt länger als eine Anfrage. Globaler Zustand, statische Caches oder offene Verbindungen können deshalb in die nächste Anfrage durchsickern. In der Praxis zeigt sich das an Stellen, an denen Extensions mit statischen Variablen arbeiten. Eine Obergrenze für die Anfragen pro Worker begrenzt das Risiko, weil der Worker danach neu startet. Details zu den typischen Fallen stehen in TYPO3 auf FrankenPHP: operative Stolperfallen.
Scheduler als eigener Prozess
Der TYPO3-Scheduler gehört nicht in denselben Container. Er läuft als eigener CronJob mit demselben Image, aber einem anderen Startbefehl. So bleibt auch hier ein Dienst pro Container.
03 — Laufzeitrechte: kein Root, nur lesen, keine Capabilities
Das beste Image hilft wenig, wenn der Container mit vollen Rechten läuft. Der entscheidende Teil steht deshalb im securityContext des Pods:
securityContext:
runAsNonRoot: true
runAsUser: 65532
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
Das deckt SYS.1.6.A17 Ausführung von Containern ohne Privilegien ab. FrankenPHP lauscht dabei auf einem Port oberhalb von 1024. So braucht der Container auch keine Capability, um einen Port zu öffnen.
Nur lesen, wo nur gelesen wird
Mit readOnlyRootFilesystem ist das gesamte Image zur Laufzeit schreibgeschützt. TYPO3 braucht aber einige beschreibbare Pfade. Dafür gibt es gezielte Volumes:
/tmpundvar/für Laufzeitdateien alsemptyDir,typo3tempundfileadminauf gemeinsamem Speicher, siehe Teil 8.
Damit ist SYS.1.6.A23 Unveränderlichkeit der Container weitgehend umgesetzt. Image und Code lassen sich im laufenden Container nicht verändern. Die wenigen beschreibbaren Pfade müssen deshalb eng begrenzt und überwacht werden.
Durchsetzen statt hoffen
Einzelne Pods richtig zu konfigurieren, reicht nicht. Der Namespace erzwingt die Regeln über Pod Security Admission mit dem Profil restricted:
metadata:
labels:
pod-security.kubernetes.io/enforce: restricted
Ein Pod, der die Regeln verletzt, wird gar nicht erst angelegt. Das ist ein wichtiger Baustein für SYS.1.6.A17 und SYS.1.6.A21 Erweiterte Sicherheitsrichtlinien.
04 — Ressourcen begrenzen und Code vom Image trennen
SYS.1.6.A15 verlangt die Limitierung der Ressourcen pro Container. Jeder TYPO3-Pod bekommt deshalb Requests für CPU und Speicher und ein Limit für den Speicher. Eine LimitRange im Namespace setzt Vorgaben für Pods, die keine eigenen Werte mitbringen.
resources:
requests:
cpu: 250m
memory: 384Mi
limits:
memory: 768Mi
PHP und Container sprechen dieselbe Sprache
Ein typischer Stolperstein ist das PHP-Speicherlimit. Liegt memory_limit mal Anzahl der Worker über dem Container-Limit, beendet der Kernel den Container ohne Vorwarnung (OOMKilled). Rechne beide Werte zusammen und lass Luft für FrankenPHP selbst.
Warum der Code nicht ins Image gehört
Dieses Image enthält nur die Laufzeit. Der TYPO3-Code kommt getrennt als eigenes Artefakt dazu. So lässt sich ein PHP-Sicherheitsupdate ausrollen, ohne jede Website neu zu bauen. Und ein Release der Website ändert nichts an der geprüften Laufzeit. Den Hintergrund beschreibt Laufzeit und Anwendung trennen. Wie der Code sicher in den Pod kommt, zeigt Teil 3.
Häufige Fragen
Warum nicht einfach das offizielle PHP-Image verwenden?+
Das offizielle Image ist für breite Verwendung gebaut und bringt viel mit, was TYPO3 nicht braucht. Jede zusätzliche Bibliothek erzeugt Befunde im Scan und Aufwand bei Updates. Eine minimale Basis hält die Angriffsfläche und die Zahl der Befunde klein.
Wie debugge ich einen Container ohne Shell?+
Mit einem ephemeren Debug-Container. kubectl debug hängt einen temporären Container mit Werkzeugen an den laufenden Pod. Er teilt dessen Prozesse und Netz, verschwindet aber wieder. Das Image selbst bleibt schlank. Im Profil restricted braucht es dazu kubectl debug --profile=restricted.
Läuft TYPO3 mit readOnlyRootFilesystem überhaupt?+
Ja, wenn die beschreibbaren Pfade gezielt als Volumes eingehängt werden. /tmp und var/ bekommen ein emptyDir, fileadmin und typo3temp liegen auf gemeinsamem Speicher. Alles andere bleibt schreibgeschützt.
Wie viele FrankenPHP-Worker sind sinnvoll?+
Das hängt von CPU und Speicher des Pods ab. Als Faustregel startest du mit zwei Workern pro CPU-Kern. Entscheidend ist, dass Worker mal PHP-Speicherlimit unter dem Container-Limit bleibt. Skaliert wird dann über zusätzliche Pods, nicht über immer mehr Worker.
Fazit
Ein gehärtetes TYPO3-Image ist klein, hat keinen Paketmanager und führt genau einen Dienst aus. Es läuft ohne Root-Rechte und ohne Capabilities, sein Dateisystem ist schreibgeschützt. Pod Security Admission sorgt dafür, dass das nicht nur für einzelne Pods gilt. Damit sind die zentralen Anforderungen aus SYS.1.6 abgedeckt, bis hin zu A21 und A23 für hohen Schutzbedarf.
Weiter mit Teil 3: Code als signiertes Artefakt. Zurück zu Teil 1: Welche Bausteine wirklich greifen.
Ich baue dir ein gehärtetes TYPO3-Image, das Pod Security im Profil restricted besteht.
Minimale Basis, FrankenPHP im Worker-Modus, schreibgeschütztes Dateisystem und ein Build, der die Basis regelmäßig neu erzeugt.
Plattform-Betrieb statt Beratung auf Papier: Auf Wunsch pflege ich das Image dauerhaft und halte es frei von bekannten Schwachstellen.
Ü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.