DEV Community

Securing an EOL Host Against PHP2Shell

Besworks on November 08, 2026

This guide details how to decouple the PHP runtime from your host operating system. By running an up-to-date PHP version inside a Docker container ...
Collapse
 
benjaminkincses profile image
Benjamin Kincses •

One thing worth flagging: network_mode: "host" means the container shares the host network namespace, so the isolation win here is mostly the UID split and getting PHP patched independently of the EOL OS, not network separation. Still a real win, just a narrower one than "isolated" suggests.

If the app does not need to write into the docroot, mounting /var/www read-only for the FPM container closes most of what is left after a shell lands. The writable uploads dir stays the hole either way.

Collapse
 
besworks profile image
Besworks •

This guide is meant to peovide a drop-in replacement for an outdated php installation. Using network_mode:host is no less secure than the typical mod_php setup that it's intended to replace. I'ts definitely no substitute for a properly isolated, fully containerized app.

Collapse
 
mp3sss31 profile image
mp3sss •

This 'untrusted runtime in a thin container, trusted host on the edge' pattern is the same shape we use in desktop: keep the privileged orchestrator (your Apache) small and patched, and push the untrusted code path (PHP, or in our case a third-party plugin renderer) into a constrained process where a single RCE only owns that sandbox. The one extra knob worth borrowing for a desktop client is least-privilege filesystem exposure — bind-mounting the whole /var/www read-write means a PHP bug still reads/writes every vhost's files; we default-mount only the directory the user actually picked, and watch for writes outside that root.

Collapse
 
besworks profile image
Besworks •

bind-mounting the whole /var/www read-write means a PHP bug still reads/writes every vhost's files

That is a very important point. Especially if your vhosts repesent data belonging to different clients/users. Isolating each vhost into it's own container with access only to it's own files would be a much better approach for that scenario.

Collapse
 
onelifevibe profile image
OneLifeVibe •

This is a pragmatic approach — instead of fighting the EOL host, you shrink what it can do to you by moving execution into the container and leaving Apache to do what it is good at. The localhost socket trap is the detail I would have tripped over: it is exactly the kind of failure that looks like a database outage until you realize the socket file simply is not inside the container. One thing I would add: since the :8.5 tag floats to the latest patch, pinning the image by digest as well would make those future upgrades deliberate rather than something a pull does for you. Thanks for writing up the gotchas, not just the happy path.

Collapse
 
besworks profile image
Besworks •

Personally I think using the minor version tag is the best method for the average setup. This ensures that you always have the latest security updates for your current release. If you encounter issues with a specific patch you can easily pull and pin the last digest that worked for your use-case.

Collapse
 
hauscall profile image
Hauscall •

One correction worth having before anyone follows this, because it is the difference between "isolated" and "patched".

hermsi/alpine-fpm-php:8.5, as it resolves today, is built from php-8.5.12.tar.xz (PHP_VERSION=8.5.12 in the image config, build dated 7 Nov). 8.5.12 is still the newest release, from 21 Oct. The fix for CVE-2026-115560 has not shipped: PHP's 23 Oct note says the full advisory arrives with the next regular bugfix release, and until then the standing mitigation is session.upload_progress.enabled=0.

So the container buys isolation and a maintainable runtime, not a patched one. And that ini line has to be added inside it (/usr/local/etc/php/conf.d/), because the host's php.ini never reaches the container. Carry an old host config across and you carry the exposure with it.

Scope, from the advisory itself, so nobody over-reacts: it needs an app that sets session.serialize_handler to php_binary or php_serialize and then switches it to php at runtime. Most never do.

Last bit: pin the digest. The day the fix lands, pull works either way, and nothing in your compose file will tell you which build you got.

Collapse
 
besworks profile image
Besworks •

Excellent points. The setup described in the guide does not provide full isolation. It would definitely be wise for those new to Docker to explore how this could be hardened for greater security. As written though, it does help significantly reduce your exposure to this vulnerability and makes it possible for an EOL host to benefit from current and future security fixes.

Collapse
 
alexshev profile image
Alex Shev •

Hardening an EOL host is a race against the next unknown exposure, so compensating controls should be explicit about their limits. I’d prioritize network segmentation, immutable backups tested for restore, minimal service accounts, and high-signal process or egress telemetry. Those measures buy time, but the migration date still needs an owner and a measurable exit plan.

Collapse
 
besworks profile image
Besworks •

Absolutely, agree. Maintaining an EOL host is definitely not an ideal situation to be in. Devising a migration plan should be a top priority for anyone dealing with a system like this. Thankfully Docker exists and makes it possible to containerize your legacy architecture.

Collapse
 
alexcodebytes profile image
Oleksandr •

Good write-up on the decoupling pattern, but I have to flag the elephant in the room: network_mode: "host" quietly undoes a big chunk of the isolation you're describing.

The whole argument for moving PHP into a container is that a compromised runtime is bounded by the container. With host networking, that boundary mostly disappears — the container shares the host's network namespace, so a PHP RCE can bind or connect to anything the host can reach: your database, your Redis, SSH on 22, whatever else is listening. That's a meaningfully worse blast radius than a normal container, and it's exactly the scenario you're trying to defend against.

You can keep the setup simple without it. Bind-mount a UNIX socket into the container and have Apache proxy to that instead:
services:
php-fpm:
image: hermsi/alpine-fpm-php:8.5
volumes:
- /var/www:/var/www
- /run/php-fpm:/run/php-fpm
Then in Apache: SetHandler "proxy:unix:/run/php-fpm/sock|fcgi://localhost"

The socket lives in a directory only Apache and the container can reach, so PHP-FPM is still reachable for legitimate requests but not exposed on any interface. If you'd rather stay on TCP, publish it explicitly on loopback only — ports: ["127.0.0.1:9000:9000"] — which at least keeps it off every other interface on the host.

A couple of other things worth tightening while you're in there:

Your Apache block has Options Indexes, which means directory listings for any folder without an index file. On a public-facing host that's a small information leak you probably don't want.

Require all granted on the document root is fine if your app handles auth, but combined with Indexes it means anyone can browse unindexed paths.

The EOL-host reality is real and I don't envy anyone running Debian 11 today. But the container is the one part of this that you can actually get right cheaply, so it's worth not giving away the boundary you're paying for.

Collapse
 
besworks profile image
Besworks •

The purpose of this guide is to replace an outdated PHP runtime with a patched version on a host that cannot easily be upgraded. It is not intended to provide additional isolation beyond the original native PHP deployment. Host networking is a deliberate compatibility choice that preserves access to existing services without requiring additional infrastructure changes. The UNIX socket approach is certainly valid, but it introduces additional configuration requirements that fall outside the scope of this particular solution.

Good catch on Options Indexes, though. I will change that.

Collapse
 
alexcodebytes profile image
Oleksandr •

Fair enough — if the explicit goal is zero infrastructure changes and you just need a patched PHP binary to sit behind the existing Apache setup, host networking is the path of least friction.

The only thing I'd still push back on slightly is the blast radius. Since the specific vulnerability is an RCE (PHP2Shell), the moment someone exploits it, they inherit the host's network namespace. It’s not about expecting the container to provide full isolation; it’s that host networking removes the accidental network boundaries Docker usually provides as a side effect.

A quick note in the README warning about that specific trade-off might be worth it for anyone blindly copy-pasting this into a production environment. Glad you're fixing the Indexes issue, though — that's a real easy one to miss!

Collapse
 
n_c_e4a8149b59e51d8be77 profile image
N. C. •

A practical and well-structured guide that addresses a real-world problem many administrators face when maintaining legacy infrastructure. I particularly appreciate the attention to the less obvious pitfalls, especially the localhost versus 127.0.0.1 database connection issue. One important consideration, though, is that containerization alone doesn't guarantee host isolation. The use of network_mode: host deserves extra scrutiny because it reduces network separation, and shared volumes can still expose sensitive host files. This is a useful mitigation strategy, but it shouldn't become a substitute for planning an eventual OS upgrade. Great job highlighting both the practical solution and its limitations!

Collapse
 
besworks profile image
Besworks • • Edited

This guide definitely stemmed from a real-world problem that I had to resolve. And the result, while not perfect isolation, is much more secure than when I started. I wouldn't use network_mode:host for a self-contained app with all it's services containerized, but for this use-case of globally overriding the system's native php install it's the optimal choice.

As for exposing sensitive host files, it really makes no difference if your DocumentRoot lives inside an isolated volume or directly on the host filesystem. In each of these scenarios the files in question are equally exposed. If you really want to protect these files from rogue modifications, mount the volume read-only.

volumes:
  - /var/www:/var/www:ro
Enter fullscreen mode Exit fullscreen mode