TYPO3 on Kubernetes under IT-Grundschutz, part 2: The hardened TYPO3 image
The image is the first line of defence. Whatever is not in it can be neither attacked nor misconfigured, and whatever may not write at runtime cannot be changed permanently either.
This part shows what a TYPO3 image looks like when it carries the container module SYS.1.6: a minimal base, a single service and runtime privileges that allow almost nothing.
Part 2 of 14 of the series "TYPO3 on Kubernetes under IT-Grundschutz". The overview of all parts is in part 1.
01 — A minimal base instead of an operating system
A classic PHP image ships half an operating system: a shell, a package manager, compiler leftovers and dozens of libraries TYPO3 never needs. Each of them can turn up as a finding in the next scan.
The alternative is a minimal base following the distroless idea. Bases like Wolfi work well because they deliver packages in small, frequently updated units. The image then contains only PHP, the extensions required, FrankenPHP and the runtime libraries.
How the image is built
- A build stage installs PHP and the extensions TYPO3 actually uses.
- The runtime stage copies only the finished files. Package manager and build tools stay behind.
- The base image is pinned by digest, not by tag.
This meets SYS.1.6.A6 use of secure images. If you also build the base yourself and rebuild it regularly, you cover SYS.1.6.A14 updating of images as well.
The question of the shell
Working entirely without a shell is consistent but inconvenient, especially when something goes wrong and needs debugging. Kubernetes solves this with ephemeral debug containers via kubectl debug, which attach temporarily to a pod without the image having to carry tools itself. Under the restricted profile this needs kubectl debug --profile=restricted.
02 — FrankenPHP in worker mode: one service per container
The classic combination of web server and PHP-FPM needs two processes and usually a supervisor in between. That contradicts SYS.1.6.A11 only one service per container.
FrankenPHP solves this elegantly, because it is a web server with PHP built in: a single process accepts the HTTP requests and runs TYPO3. In worker mode TYPO3 stays loaded in memory between requests, so the per-request bootstrap disappears and response times drop noticeably.
What to watch in worker mode
A worker lives longer than a request. Global state, static caches or open connections can therefore leak into the next request, and in practice this shows up exactly where extensions rely on static variables. A cap on requests per worker limits the risk, because the worker restarts afterwards. The typical traps are covered in TYPO3 on FrankenPHP: operational pitfalls.
The scheduler as its own process
The TYPO3 scheduler does not belong in the same container. It runs as a separate CronJob with the same image and a different start command, which keeps one service per container here as well.
03 — Runtime privileges: no root, read-only, no capabilities
The best image helps little if the container runs with full privileges, which is why the pod's securityContext is decisive:
securityContext:
runAsNonRoot: true
runAsUser: 65532
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
This covers SYS.1.6.A17 running containers without privileges. Because FrankenPHP listens on a port above 1024, the container does not even need a capability to open its port.
Read-only where only reading happens
With readOnlyRootFilesystem the whole image is write-protected at runtime. TYPO3 does need a few writable paths, though, and those get dedicated volumes:
/tmpandvar/for runtime files asemptyDir,typo3tempandfileadminon shared storage, see part 8.
This largely implements SYS.1.6.A23 immutability of containers, since image and code can no longer be changed inside a running container. That makes it all the more important to keep the few writable paths narrow and monitored.
Enforce instead of hoping
Configuring individual pods correctly is not enough, though. The namespace therefore enforces the rules via Pod Security Admission with the restricted profile:
metadata:
labels:
pod-security.kubernetes.io/enforce: restricted
A pod that violates the rules is simply never created. That is an important building block for SYS.1.6.A17 and SYS.1.6.A21 extended security policies.
04 — Limiting resources and keeping code out of the image
SYS.1.6.A15 asks for limiting the resources per container. Every TYPO3 pod therefore gets requests for CPU and memory plus a memory limit, while a LimitRange in the namespace sets defaults for pods that bring no values of their own.
resources:
requests:
cpu: 250m
memory: 384Mi
limits:
memory: 768Mi
PHP and the container speak the same language
A typical pitfall is the PHP memory limit. If memory_limit times the number of workers exceeds the container limit, the kernel kills the container without warning (OOMKilled). So add both values up and leave headroom for FrankenPHP itself.
Why the code does not belong in the image
This image contains only the runtime, while the TYPO3 code arrives separately as its own artefact. A PHP security update can then roll out without rebuilding every website, and a website release changes nothing about the verified runtime. The background is in separating runtime and application, and part 3 shows how the code gets into the pod safely.
Frequently asked questions
Why not simply use the official PHP image?+
The official image is built for broad use and ships a lot TYPO3 does not need. Every additional library creates scan findings and update effort. A minimal base keeps both the attack surface and the number of findings small.
How do I debug a container without a shell?+
With an ephemeral debug container: kubectl debug attaches a temporary container with tools to the running pod. It shares the pod's processes and network and disappears again afterwards, so the image itself stays lean. Under the restricted profile this needs kubectl debug --profile=restricted.
Does TYPO3 work with readOnlyRootFilesystem at all?+
Yes, if the writable paths are mounted as dedicated volumes. /tmp and var/ get an emptyDir, fileadmin and typo3temp live on shared storage. Everything else stays write-protected.
How many FrankenPHP workers make sense?+
That depends on the pod's CPU and memory. As a rule of thumb, start with two workers per CPU core. What matters is that workers times the PHP memory limit stays below the container limit. Scaling then happens through additional pods, not ever more workers.
Conclusion
A hardened TYPO3 image is small, has no package manager and runs exactly one service. It runs without root privileges and without capabilities on a write-protected file system, and Pod Security Admission makes sure this applies to more than a few pods. That covers the central requirements of SYS.1.6, up to A21 and A23 for high protection needs.
Continue with part 3: code as a signed artefact. Back to part 1: which modules actually apply.
I build you a hardened TYPO3 image that passes Pod Security at the restricted level.
A minimal base, FrankenPHP in worker mode, a write-protected file system and a build that regenerates the base regularly.
Platform operations rather than advice on paper: if you want, I maintain the image on an ongoing basis and keep it free of known vulnerabilities.
About the author

Kai Ole Hartwig
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.