PHP 5.6 und PHP 7.4 weiter sicher betreiben: FreeBSD-Jails mit eigenem Ports-Overlay
Manche Webanwendungen laufen nur mit einer alten PHP-Version. Bei mir ist das der Fall bei einigen gewachsenen Kundenprojekten: ein Teil braucht PHP 7.4, ein kleiner Rest sogar noch PHP 5.6. Beide Versionen sind seit Jahren am Ende ihres Lebenszyklus (PHP 5.6 seit 2019, PHP 7.4 seit November 2022), und FreeBSD hat die Ports dafür entfernt (lang/php56 Anfang 2019, lang/php74 Ende 2022).
Trotzdem muss die Anwendung weiter laufen, bis die Migration erledigt ist. Dieser Artikel zeigt, wie ich das wiederholbar und möglichst sicher löse:
- Jeder PHP-Stand bekommt einen eigenen FreeBSD-Jail (
php74jail,php56jail). Die alten PHP-Versionen sind damit vom Rest des Servers getrennt, und auf dem Host bleibt alles aktuell. - Die Ports kommen aus dem aktuellen FreeBSD-Ports-Tree - plus ein kleines Git-Overlay (php-ports-overlay auf GitHub), das die entfernten PHP-Ports wieder einhängt. Ein Skript (
sync-ports.sh) erzeugt daraus bei jedem Lauf den Ports-Tree für den jeweiligen Jail neu. Von Hand gepflegt wird nichts mehr. - Das Overlay trägt die Anpassungen, die moderne Compiler und Bibliotheken brauchen (aktuelles Clang, OpenSSL 3, libxml2, ICU, Autoconf 2.7x) und die neuesten Sicherheits-Patches:
- PHP 7.4: der offizielle End-of-Life-Backport der PHP-Entwickler für CVE-2026-109618 und CVE-2026-109619 (php2shell).
- PHP 5.6: dafür gibt es keinen offiziellen Fix. Das Overlay enthält Community-Backports, die aus der Debian-Patchserie von Ondej Surý stammen (unter anderem der Bailout-Fix und das Limit
max_multipart_body_parts).
Wichtig: Das ist ein Notbehelf für die Übergangszeit und keine Dauerlösung. Die Backports für 5.6 sind Community-Arbeit ohne Garantie. Mehr dazu im Abschnitt Sicherheit und Grenzen.
Beschrieben ist alles am Beispiel PHP 5.6. Für PHP 7.4 ist es fast identisch - die wenigen Unterschiede stehen am Ende im Abschnitt PHP 7.4: was anders ist.
Überblick
Die Architektur ist einfach:
Host (FreeBSD 14, aktueller Ports-Tree in /usr/ports, nginx)
+-- php74jail 10.1.1.9 PHP 7.4 + php-fpm
+-- php56jail 10.1.2.9 PHP 5.6 + php-fpm
- Der Webserver (nginx) läuft auf dem Host und spricht mit dem php-fpm im jeweiligen Jail.
- Die Webroots liegen auf dem Host und werden per
nullfsin die Jails eingehängt. - Die Jails haben ein eigenes Netz auf Loopback-Interfaces und gehen per NAT (pf) ins Internet, um Quellen nachzuladen.
- Der Ports-Tree im Jail (
/usr/ports.php56bzw./usr/ports.php74) wird vom Host aus erzeugt.
Voraussetzungen: ein FreeBSD-Host mit ZFS oder UFS, Root-Zugang und ein paar Stunden Compile-Zeit. Auf dem Host brauchst du ezjail, git und rsync:
pkg install ezjail git rsync
(Wer seine Ports selbst baut, kann stattdessen z. B. synth install sysutils/ezjail verwenden.)
1. ezjail einrichten und Basejail installieren
ezjail verwaltet Jails auf Basis eines gemeinsamen, schreibgeschützten Basejails. Die Jails selbst sind dadurch klein, und Updates des Basissystems müssen nur einmal gemacht werden.
ezjail-admin install
Das lädt das Basissystem passend zur Version des Hosts und spielt die aktuellen Patches ein. Zum Schluss steht dort ein Hinweis, dass die /etc/make.conf des Hosts als Vorlage in den Template-Jail kopiert wurde, damit die Ports-Sammlung in den Jails läuft.
Tipp: Die Userland-Version im Jail sollte nicht neuer sein als der Kernel des Hosts. Spätere Updates des Basejails macht man mit
ezjail-admin update -u.
2. Netzwerk: Loopback-Interfaces und NAT
Jeder Jail bekommt ein eigenes Loopback-Interface und ein eigenes kleines Netz. Damit die Jails Quellen herunterladen können, übersetzt der Paketfilter pf auf dem Host deren Adressen nach außen (NAT).
In /etc/rc.conf auf dem Host:
# Jails
ezjail_enable="YES"
# php74jail
cloned_interfaces="${cloned_interfaces} lo1"
ifconfig_lo1="inet 10.1.1.9 netmask 0xffffff00"
# php56jail
cloned_interfaces="${cloned_interfaces} lo2"
ifconfig_lo2="inet 10.1.2.9 netmask 0xffffff00"
# pf wird für das NAT der Jails gebraucht
pf_enable="yes"
Weitere Jails (z. B. ein Jail für Mailman oder ein späteres PHP 8.x) werden nach demselben Schema mit lo3, 10.1.3.9 usw. ergänzt.
Die Firewall-Regeln in /etc/pf.conf auf dem Host. ext_if und ext_ip sind dein externes Interface und deine öffentliche Adresse (hier als Beispiel):
ext_if="bge0"
ext_ip="203.0.113.10"
scrub in all
# php74jail
net_jail="10.1.1.0/24"
nat pass on $ext_if from $net_jail to any -> $ext_ip
# php56jail
net_jail2="10.1.2.0/24"
nat pass on $ext_if from $net_jail2 to any -> $ext_ip
3. Jails anlegen und starten
ezjail-admin create php74jail 'lo1|127.0.1.1,bge0|10.1.1.9'
ezjail-admin create php56jail 'lo1|127.0.2.1,bge0|10.1.2.9'
Die Namensauflösung des Hosts wird in die Jails kopiert, damit sie Hostnamen auflösen können:
cp /etc/resolv.conf /usr/jails/php74jail/etc/
cp /etc/resolv.conf /usr/jails/php56jail/etc/
Interfaces anlegen, pf neu laden und die Jails starten:
service netif cloneup
service pf restart
service ezjail start
jls
JID IP Address Hostname Path
25 127.0.1.1 php74jail /usr/jails/php74jail
26 127.0.2.1 php56jail /usr/jails/php56jail
Die Warnung Per-jail configuration via jail_* variables is obsolete ist normal: ezjail arbeitet noch mit den alten jail_*-Variablen. Sie ist harmlos.
Mit den Jails arbeiten
ezjail-admin start php56jail # einen Jail starten
ezjail-admin console php56jail # Konsole im Jail (Login als root)
jexec php56jail # Shell im Jail ohne Login
ezjail-admin stop php56jail # einen Jail stoppen
ezjail-admin stop # alle Jails stoppen
4. Grundkonfiguration im Jail
Ab hier arbeiten wir im Jail (jexec php56jail).
rc.conf
In /etc/rc.conf des Jails: den Mailserver abschalten (der Host kümmert sich um Mail) und php-fpm aktivieren. Das gilt für beide PHP-Jails:
sendmail_enable="NO"
sendmail_submit_enable="NO"
sendmail_outbound_enable="NO"
sendmail_msp_queue_enable="NO"
php_fpm_enable="YES"
make.conf
Die /etc/make.conf im Jail steuert, wie die Ports gebaut werden. Sie ist in beiden PHP-Jails nahezu gleich. Den Wert für DEFAULT_VERSIONS+=php= musst du pro Jail anpassen (im php74jail 7.4).
# Arbeitsverzeichnisse außerhalb des Ports-Trees,
# damit der Tree jederzeit neu erzeugt werden kann
WRKDIRPREFIX= /var/ports
DISTDIR= /var/ports/distfiles
PACKAGES= /var/ports/packages
INDEXDIR= /var/ports
MAKE_JOBS_NUMBER=4
DEFAULT_VERSIONS+=python3=3.12
DEFAULT_VERSIONS+=perl=5.42
DEFAULT_VERSIONS+=golang=1.23
DEFAULT_VERSIONS+=ruby=3.4
# php56jail: 5.6 php74jail: 7.4
DEFAULT_VERSIONS+=php=5.6
# Kein X11, keine GUI-Bibliotheken im Server-Jail
OPTIONS_UNSET+=X11 XPM
OPTIONS_UNSET+=GUI
OPTIONS_UNSET+=CUPS
OPTIONS_UNSET+=GNOME
OPTIONS_UNSET+=GTK
OPTIONS_UNSET+=GTK2
OPTIONS_UNSET+=KDE
OPTIONS_UNSET+=QT
OPTIONS_UNSET+=VULKAN
OPTIONS_UNSET+=WAYLAND
DISABLE_VULNERABILITIES=yes
ALLOW_UNSUPPORTED_SYSTEM=yes
BATCH=yes
# Für PHP 5.6 und PHP 7.4 nötig: moderne Clang-Versionen behandeln viele Konstrukte
# des alten C-Codes als Fehler. Diese Flags schalten das wieder ab.
CFLAGS+= -Wno-incompatible-pointer-types -Wno-deprecated-declarations -Wno-implicit-const-int-float-conversion
CFLAGS+= -Wno-int-conversion -Wno-implicit-function-declaration -Wno-pointer-bool-conversion
CFLAGS+= -Wno-compare-distinct-pointer-types -Wno-pointer-sign
CPPFLAGS+= -I${LOCALBASE}/include/freetype2
Was die Einstellungen bedeuten:
WRKDIRPREFIX,DISTDIR,PACKAGES,INDEXDIR: Alle Build-Artefakte liegen unter/var/portsund nicht im Ports-Tree. Das ist wichtig, weil der Tree bei jedem Sync mitrsync --deleteüberschrieben wird.DISABLE_VULNERABILITIES=yes: Die Ports-Infrastruktur bricht sonst bei Ports mit bekannten Schwachstellen ab. PHP 5.6 und 7.4 stehen dort (zu Recht) drin. Das Flag ist hier notwendig, aber nicht ohne Folgen: Du verlierst die Warnung bei allen Ports in diesem Jail. Installiere im PHP-Jail deshalb nur, was du wirklich brauchst.ALLOW_UNSUPPORTED_SYSTEM=yes: unterdrückt den Abbruch bei Systemversionen, für die die Ports-Sammlung keinen Support mehr garantiert.BATCH=yes: keine interaktiven Rückfragen beim Bauen.- Die Compiler-Flags gehören in
CFLAGSundCPPFLAGS
5. Ports-Tree mit PHP für den Jail erzeugen
Das ist der Kern des Ganzen. Der Ablauf, jetzt wieder auf dem Host:
Aktuellen Ports-Tree holen
Der Host-Tree in /usr/ports ist die Quelle. Beim ersten Mal wird er geklont (das Verzeichnis muss leer sein), später genügt ein git pull:
git clone https://git.FreeBSD.org/ports.git /usr/ports
# später:
git -C /usr/ports pull
(Das für das Jails-Ports-Overlay verwendete sync-ports.sh führt selbst ein fetch und merge --ff-only aus. Das manuelle git pull ist also nur nötig, wenn du den Tree zusätzlich für den Host selbst aktuell halten willst.)
Overlay holen
mkdir -p /usr/local/src
cd /usr/local/src
git clone https://github.com/chris-x-tra/php-ports-overlay
cd php-ports-overlay
Das Overlay ist aufgebaut wie ein Ports-Tree:
lang/php56,lang/php74und alle zugehörigen Erweiterungs-Ports (archivers/php56-zlib,security/php56-openssl, …)- die Patches für moderne Toolchains und die Sicherheits-Patches in den jeweiligen
files/-Verzeichnissen patch-php-mk.sh: fügt PHP 5.6 und 7.4 wieder in die aktuelleMk/Uses/php.mkdes Upstream-Trees ein. Das Skript prüft jede Änderung und bricht ab, wenn Upstream die Datei umgebaut hat.profiles/php56/: Ports, die nur der 5.6-Jail braucht, zum Beispiel ältere PECL-Erweiterungen (pecl-intl,pecl-memcache, APCu 4 ).
Tree direkt in den Jail erzeugen
sync-ports.sh baut aus Upstream + Overlay den Ports-Tree direkt im Dateisystem des Jails. Das Ziel (TARGET) ist Pflicht, damit nie versehentlich in den falschen Jail geschrieben wird:
cd /usr/local/src/php-ports-overlay
PROFILE=php56 TARGET=/usr/jails/php56jail/usr/ports.php56/ ./sync-ports.sh
php.mk: PHP 5.6 and 7.4 inserted
OK: /usr/jails/php56jail/usr/ports.php56/ = 8bc05c180ed7 (main) + overlay d97a478
php74 version: 7.4.33
php56 version: 5.6.40
Das Skript
- aktualisiert den Upstream-Tree und das Overlay (Fast-Forward, keine Merge-Überraschungen),
- kopiert den Upstream-Tree in den Jail (
rsync --delete), - legt das Overlay und - bei gesetztem
PROFILE- die Profil-Ports darüber, - entfernt aus
MOVEDdie Zeilen, die die Entfernung der alten PHP-Ports beschreiben (sonst würden Paketwerkzeuge installierte Pakete “umziehen” oder als entfernt melden), - trägt die Overlay-Ports in die Kategorie-Makefiles ein,
- und passt
Mk/Uses/php.mkan.
Der Ports-Tree im Jail wird nie von Hand editiert. Änderungen gehören ins Overlay-Repository; der Tree wird bei jedem Lauf neu erzeugt.
Die Version
5.6.40ist die letzte Version der 5.6er-Reihe. Wer noch 5.6.39 betreibt, sollte unbedingt auf 5.6.40 wechseln.
6. PHP 5.6 im Jail bauen
Jetzt in den Jail und den Tree benutzen:
jexec php56jail
cd /usr/ports.php56/lang/php56
make config
make
make install
Bei make config sind die wichtigen Optionen: FPM an (wir betreiben PHP über php-fpm hinter nginx); alles, was du nicht brauchst (z. B. Apache-Modul oder CGI), kannst du abschalten.
Danach die Erweiterungen:
cd /usr/ports.php56/lang/php56-extensions
make config
make
make install
Reihenfolge und Jail: Erst
lang/php56, dann die Erweiterungen. Die Erweiterungs-Ports einer PHP-Version baut man im Jail dieser Version - im php74jail lassen sich die 5.6-Erweiterungen nicht bauen, weil dort die Konfiguration der installierten PHP-Version vorgibt, welche PHP-Version die Ports verwenden.
Bei mir ergibt das diese Module (so sieht das für PHP 5.6 aus):
# php -v
PHP 5.6.40 (cli) (built: Nov 1 2026 13:37:00)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2016, by Zend Technologies
# php -m
[PHP Modules]
bcmath bz2 Core ctype curl date
dom ereg exif fileinfo filter
gd gettext hash iconv json libxml
mbstring mcrypt mhash mysql mysqli
mysqlnd openssl pcntl pcre PDO pdo_mysql
pdo_sqlite Phar posix readline recode
Reflection session SimpleXML snmp soap SPL
sqlite3 standard sysvmsg sysvsem sysvshm tidy tokenizer
xml xmlreader xmlwriter xsl Zend OPcache zip zlib
[Zend Modules]
Zend OPcache
Für PHP 7.4 sieht die Liste ähnlich aus, nur ohne die Module, die es dort nicht mehr gibt (ereg, mysql und mcrypt wurden in der 7.x-Reihe entfernt).
Wenn dir ein Port nicht baut, ist das meist ein Problem zwischen altem PHP-Code und neuen Bibliotheken (OpenSSL 3, libxml2, ICU). Die bekannten Fälle sind im Overlay bereits gepatcht. Neue Fälle meldest du am besten als Issue im Overlay-Repository.
7. php-fpm konfigurieren
Ab hier hängt die Konfiguration davon ab, womit du deine Webserver verwaltest. Bei mir erzeugt Froxlor pro Website eine eigene Pool-Konfiguration. Ob du das auch so machst oder alles von Hand schreibst, ist Geschmackssache (YMMV).
/usr/local/etc/php-fpm.conf im Jail (Auszug):
;;;;;;;;;;;;;;;;;;;;;
; FPM Configuration ;
;;;;;;;;;;;;;;;;;;;;;
; Standard-Include auskommentiert, stattdessen die von Froxlor erzeugten Pools
;include=etc/fpm.d/*.conf
include=/usr/local/etc/wtf/*.conf
[www]
user = www
group = www
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
Der Ordner wtf ist nur der Name, den ich für die generierten Pool-Configs gewählt habe - er kann beliebig heißen.
Die Verzeichnisse, die php-fpm im Jail erwartet, legst du an:
mkdir -p /usr/local/etc/wtf
mkdir -p /var/run/nginx
mkdir -p /var/customers/webs
mkdir -p /var/customers/tmp
8. Webroots per nullfs in den Jail einhängen
nginx und php-fpm müssen dieselben Dateien sehen: nginx liefert statische Dateien aus und übergibt PHP-Skripte an php-fpm, das dieselben Pfade öffnet. Deshalb werden die Webroots des Hosts mit nullfs in den Jail “gespiegelt”. Quasi liegt alles übereinander.
Die Mountpoints (auf dem Host, im Dateisystem des Jails):
mkdir -p /usr/jails/php56jail/var/customers/webs
mkdir -p /usr/jails/php56jail/var/customers/tmp
mkdir -p /usr/jails/php56jail/var/customers/logs
Von Hand mounten - zum Ausprobieren:
mount_nullfs /var/customers/webs/ /usr/jails/php56jail/var/customers/webs/
Dauerhaft über die /etc/fstab des Hosts (die Swap- und Boot-Zeilen bleiben, wie sie sind):
# Device Mountpoint FStype Options Dump Pass#
/var/customers/webs/ /usr/jails/php74jail/var/customers/webs/ nullfs rw 0 0
/var/customers/tmp/ /usr/jails/php74jail/var/customers/tmp/ nullfs rw 0 0
/var/customers/webs/ /usr/jails/php56jail/var/customers/webs/ nullfs rw 0 0
/var/customers/tmp/ /usr/jails/php56jail/var/customers/tmp/ nullfs rw 0 0
Dann alles einhängen und prüfen:
mount -a
mount | grep jail
/var/customers/webs on /usr/jails/php56jail/var/customers/webs (nullfs, local)
/var/customers/tmp on /usr/jails/php56jail/var/customers/tmp (nullfs, local)
/usr/jails/basejail on /usr/jails/php56jail/basejail (nullfs, local, read-only)
devfs on /usr/jails/php56jail/dev (devfs)
...
9. Benutzer und Gruppen im Jail
php-fpm läuft pro Website unter dem Benutzer dieser Website. Diese Benutzer und Gruppen müssen im Jail mit denselben UIDs und GIDs wie auf dem Host existieren, sonst passen die Dateirechte auf den gemeinsamen Webroots nicht.
Am einfachsten ist es, die Einträge vom Host in den Jail zu übernehmen (vipw und vigr im Jail):
# vipw
froxlorlocal:*:10000:10000::0:0:Froxlor local:/var/empty:/usr/sbin/nologin
user01:*:10001:10001::0:0:Web user 01:/var/empty:/usr/sbin/nologin
user02:*:10002:10002::0:0:Web user 02:/var/empty:/usr/sbin/nologin
# vigr
user01:*:10001:
user02:*:10002:
Alternativ per Kommando, ebenfalls im Jail:
pw groupadd -n user01 -g 10001
pw useradd -n user01 -u 10001 -g 10001 -c "Web user 01" -d /var/empty -s /usr/sbin/nologin
10. php-fpm starten
service php-fpm start
Performing sanity check on php-fpm configuration:
[04-May-2026 20:38:06] NOTICE: configuration file /usr/local/etc/php-fpm.conf test is successful
Starting php_fpm.
Fertig: Der Jail läuft, php-fpm ist gestartet, und nginx auf dem Host kann PHP-Anfragen an ihn weiterreichen.
Aktualisieren
Wenn sich der Upstream-Ports-Tree oder das Overlay geändert haben (neue Patches, neue Sicherheits-Fixes):
# auf dem Host
cd /usr/local/src/php-ports-overlay
git pull
PROFILE=php56 TARGET=/usr/jails/php56jail/usr/ports.php56/ ./sync-ports.sh
# im Jail
cd /usr/ports.php56/lang/php56
make clean
make
make reinstall
service php-fpm restart
Wurde ein Patch an einer Erweiterung geändert (z. B. an php56-openssl), baust du diesen Port auf dieselbe Weise neu. Handarbeit im Tree ist nie nötig: Alles, was sich ändert, wird im Overlay committet und per sync-ports.sh verteilt.
Einen Jail wieder löschen
Nur wenn es wirklich nötig ist:
ezjail-admin stop php56jail
mount | grep php56
umount /usr/jails/php56jail/var/customers/webs
umount /usr/jails/php56jail/var/customers/tmp
# -w löscht auch die Dateien des Jails, ohne -w bleibt das Verzeichnis liegen
ezjail-admin delete -w php56jail
jls
Vergiss nicht, die zugehörigen nullfs-Zeilen aus der /etc/fstab zu entfernen, sonst scheitert mount -a beim nächsten Boot.
Sicherheit und Grenzen
- Jails sind eine zusätzliche Hürde, keine Garantie. Sie trennen das alte PHP vom Rest des Servers, ersetzen aber keine Updates.
- PHP 7.4 hat mit dem offiziellen EOL-Backport einen Fix für CVE-2026-109618/109619. Er ist bewusst minimal gehalten (bei Speichererschöpfung beendet sich der Prozess hart, der Upload-Fortschritt in Sessions ist abgeschaltet).
- PHP 5.6 hat keinen offiziellen Fix. Die Community-Backports im Overlay sind getestet, soweit ich es nachstellen konnte, aber ohne Garantie.
- Solange du dir nicht sicher bist: Setze in den Pool-Konfigurationen, wo es geht,
file_uploadsundsession.upload_progress.enabledaufOff. - Erreichbar sollte der Jail nur über den Webserver auf dem Host sein. Beschränke Pools auf lokale Adressen und gib keine Dienste im Jail nach außen frei.
DISABLE_VULNERABILITIES=yesschaltet die Schwachstellenprüfung im Jail ab. Installiere dort nichts anderes als PHP und seine Erweiterungen.- Plane die Migration auf eine unterstützte PHP-Version. Der Jail kauft dir Zeit, nicht mehr.
PHP 7.4: was anders ist
Alles oben gilt sinngemäß auch für PHP 7.4. Die Unterschiede auf einen Blick:
| PHP 5.6 | PHP 7.4 | |
|---|---|---|
| Jail | php56jail |
php74jail |
| Loopback / IP | lo2, 10.1.2.9 |
lo1, 10.1.1.9 |
DEFAULT_VERSIONS in der make.conf |
php=5.6 |
php=7.4 |
| Sync-Aufruf | PROFILE=php56 TARGET=.../php56jail/usr/ports.php56/ ./sync-ports.sh |
TARGET=.../php74jail/usr/ports.php74/ ./sync-ports.sh (kein PROFILE nötig) |
| Ports-Tree im Jail | /usr/ports.php56 |
/usr/ports.php74 |
| Module | mit ereg, mysql, mcrypt, recode |
ohne ereg, mysql, mcrypt |
| Zusätzliche Ports | PECL-Erweiterungen aus profiles/php56 |
keine nötig |
Beim php74jail baust du lang/php74 und danach lang/php74-extensions - wieder in dieser Reihenfolge und im php74jail.
Fazit
Mit Jails, einem aktuellen Ports-Tree und einem kleinen Overlay lassen sich auch längst entfernte PHP-Versionen reproduzierbar auf modernem FreeBSD bauen: Der Tree wird bei jedem Lauf neu erzeugt, die Patches sind versioniert, und der Host bleibt unberührt. Das Overlay ist auf GitHub: chris-x-tra/php-ports-overlay. Verbesserungen und Fehlermeldungen sind willkommen.
Artikel erstellt am: 01.11.2026, aktualisiert am 02.11.2026.