Einige Technik-Kollektive bieten Wordpress-Hosting auf Basis von Wordpress-Multisite an, bspw. blackblogs.org und noblogs.org. In der Regel ist die Funktionalität von Wordpress beschränkt, so dass sich nur eine Auswahl von Themes und Plugins installieren lassen. Ein anderer Ansatz ist der Betrieb einzenen Wordpress-Seiten in einem abgesichertem Hosting.
Diese Seite beschreibt die Einrichtung eines solchen Hostings. Entgegen anderen Anleitungen verwenden wir Apache, um mittels Macros die Seiteneinrichtung zu vereinfachen. Zur Absicherung verwenden wir ein chroot, basierend auf PHP-FPM.
Inhaltsverzeichnis
Einführung
Dieser Anleitung basiert auf anderen sehr gut Howtos. Für ein grundlegendes Verständnis solltest du diese auch lesen:
nickyreinert.de für einen allgemeinen Überblick
blog.kthx zum Setup-Skript
vennedey.net zu den Sicherheitimplikation von NSCD und opcache
binary-butterfly.de zur Verwendung von mknod statt den mount-binds
Variablen, Versionen und Pfade
Zur Veranschaulichung werden die nachfolgenden Variablen und Werte in der Anleitung genutzt:
Beispiel-Domain |
example.org |
Benutzerkonto |
katja |
PHP-Version |
7.3 |
Quota-Partition (ext4) |
/data |
Skript-Verzeichnis |
/usr/local/bin |
Wordpress-Speicherort |
/data/wordpress |
Bitte beachte, dass sich einzelnen Pfadangaben teilweise in mehreren Konfigurationsdateien benutzt werden. Bei Änderungen eines Pfades musst du also alle Vorkommen ändern.
Benutzerkonto zur PHP-Ausführung
Ein Systemnutzer mit eingeschränkten Rechten ist für die Auführung des PHP-Prozesses zuständig. Die Anmeldung per Passwort1 und das Anlegen eines Home-Verzeichnissen werden deaktiviert:
adduser --disabled-login --disabled-password --no-create-home --gecos katja
- Im späteren Verlauf wird das Wordpress-Verzeichnis diesem neuen Benutzerkonto übergeben.
Quotas einrichten
Disk-Quotas sorgen dafür, dass eine einzelne Wordpress-Instanz nicht den gesamten Speicherplatz auf einer Partition aufbrauchen kann. Läuft Wordpress in einem KVM-Gast muss ein Kernel installiert werden2:
apt install linux-image-amd64 quota
Anschließend werden der ext4-Partition in /etc/fstab die notwendigen Parameter übergeben:
/dev/vdb /data auto noatime,nodiratime,usrjquota=aquota.user,jqfmt=vfsv1 0 0
Die Partition mit den neuen Werten remounten:
mount -vo remount /data
Den Quota-Index erstellen und Quotas aktivieren3:
quotacheck -cum quotaon -v /data
Nun können für den neuen Nutzer die Quota-Regeln erstellt werden:
edquota katja
Weitere Hinweise dazu im Arch-Wiki
Apache: Seitenkonfiguration mit Macros
- Durch Apache-Macros ist es möglich, nur eine Seitenkonfiguration für alle Wordpress-Instanzen zu erstellen.
Die Seitenkonfiguration /etc/apache/sites-available/wordpress.conf basiert auf dem später näher erläuterten Verzeichnisaufbau. Die Konfiguration verhindert durch Rewriting die Installation von eigenen Themes und Modulen über das Dashboard4. Zudem wird eine optionale htaccess-Datei eingebunden. Damit lassen sich einzelnen Wordpress-Seiten per htacess zusätzlich schützen:
<Macro WPSite $domain $pool> <VirtualHost *> ServerName $domain SetEnv HTTPS on DocumentRoot /data/wordpress/$domain/htdocs ErrorLog /var/log/apache2/$domain.error IncludeOptional /data/wordpress/_htaccess.d/$domain.conf Include /etc/apache2/conf-available/wordpress-cache.conf # Installation von eigenen Themes und Plugins verhindern <IfModule mod_rewrite.c> RewriteEngine On RewriteCond %{REQUEST_URI} ^/wp-admin/update.php RewriteCond %{QUERY_STRING} action=upload-(plugin|theme) RewriteRule (.*) /wp-admin/plugin-install.php [QSD,R=302,L] </IfModule> <Directory /data/wordpress/$domain/htdocs> Require all granted Options SymLinksIfOwnerMatch MultiViews IncludesNoExec AllowOverride AuthConfig FileInfo Indexes Limit Options # https://wordpress.org/support/article/hardening-wordpress/#securing-wp-includes <IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^wp-admin/includes/ - [F,L] RewriteRule !^wp-includes/ - [S=3] RewriteRule ^wp-includes/[^/]+\.php$ - [F,L] RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L] RewriteRule ^wp-includes/theme-compat/ - [F,L] </IfModule> </Directory> <IfModule proxy_fcgi_module> <FilesMatch ".+\.ph(ar|p|tml)$"> SetHandler "proxy:unix:/run/php/php-fpm-$pool.sock|fcgi://$domain" </FilesMatch> <FilesMatch ".+\.phps$"> Require all denied </FilesMatch> <FilesMatch "^\.ph(ar|p|ps|tml)$"> Require all denied </FilesMatch> <Files xmlrpc.php> Require all denied </Files> <Files wp-config.php> Require all denied </Files> </IfModule> </VirtualHost> </Macro> # Includieren der Variablen Include /etc/apache2/conf-available/wordpress-sites.conf UndefMacro WPSite
Alle Variablen werden in /etc/apache2/conf-available/wordpress-sites.conf definiert. Die pool-Variable bezeichnet den Namen des PHP-FPM-Pools:
# Muster: Use WPSite $domain $pool Use WPSite example.org katja
Für mehr Performance wird über /etc/apache2/conf-available/wordpress-cache.conf ein globaler Cache definiert:
<IfModule mod_expires.c> ExpiresActive on # whitelist expires rules ExpiresDefault "access 1 month" # Favicon (cannot be renamed) ExpiresByType image/x-icon "access plus 1 week" # Media: images, video, audio ExpiresByType image/gif "access plus 1 month" ExpiresByType image/png "access plus 1 month" ExpiresByType image/jpg "access plus 1 month" ExpiresByType image/jpeg "access plus 1 month" ExpiresByType video/ogg "access plus 1 month" ExpiresByType audio/ogg "access plus 1 month" ExpiresByType video/mp4 "access plus 1 month" ExpiresByType video/webm "access plus 1 month" # Webfonts ExpiresByType application/x-font-ttf "access plus 1 year" ExpiresByType font/opentype "access plus 1 year" ExpiresByType application/x-font-woff "access plus 1 year" ExpiresByType image/svg+xml "access plus 1 year" # CSS and JavaScript ExpiresByType text/css "access plus 1 month" ExpiresByType text/javascript "access plus 1 month" ExpiresByType application/javascript "access plus 1 month" <IfModule mod_headers.c> Header append Cache-Control "public" </IfModule> </IfModule>
Anschließend werden alle benötigten Module und die Seite aktiviert:
a2enmod macros expires a2ensite wordpress.conf systecmtl reload apache2
Chroot-Verzeichnis-Struktur
Oberste Ebene eines jeden chroots ist das jeweilige Domain-Verzeichnis. Für die Beispieldomain ist es data/wordpress/example.org.
Das tmp-Verzeichnis hat eine doppelte Funktion: Es markiert ein chroot-Verzeichnis. Das ist für das später vorgestellte chroot-setup-Skript wichtig. Zudem werden dort temporäre Daten gespeichert.
Wordpress selbst liegt unter htdocs. Daraus ergibt sich folgende Struktur:
├── data/wordpress/example.org │ ├── htdocs/ │ │ └── index.php │ ├── logs/ | ├── sessions/ | └── tmp/
htdocs gehört dem oben erstellten Benutzerkonto katja:katja. Alle anderen Verzeichnisse root:root.
Chroot-Setup-Skript
Das Skript /usr/local/bin/php-fpm-chroot-setup nimmt die Ersteinrichtung des chroots und der Verzeichnisstruktur vor. Es kopiert zudem die notwendigen Daten in das chroot.
Im Wesentlichen handelt es sich um Daten für die Namensauflösung, das Überprüfen von Zertifikaten und den mysql-Socket:
BASE_DIR="/data/wordpress" CHROOT_DIR="$BASE_DIR/*" COPY_SOURCES="\ /etc/hosts \ /etc/resolv.conf \ /etc/ssl/certs \ /lib/x86_64-linux-gnu/libnss_dns.so.2 \ /usr/share/zoneinfo" MOUNTS="/var/run/mysqld" MKNOD_DIRS="\ dev \ logs \ sessions \ tmp" if [ $# -ge 1 ]; then ACTION=$1 shift else ACTION=-help fi case "$ACTION" in init) [ $# -ne 1 ] && echo "action 'init' requires one parameter: domain - e.g. 'example.org'" && exit 1 DOMAIN="$1" for i in $MKNOD_DIRS; do mkdir -p "$BASE_DIR"/"$DOMAIN"/"$i" done for i in $CHROOT_COPY_SOURCE; do cp -urpL --parents "$i" "$BASE_DIR"/"$DOMAIN" done mknod -m 666 "$BASE_DIR"/"$DOMAIN"/dev/null c 1 3 mknod -m 444 "$BASE_DIR"/"$DOMAIN"/dev/random c 1 8 mknod -m 444 "$BASE_DIR"/"$DOMAIN"/dev/urandom c 1 9 mknod -m 666 "$BASE_DIR"/"$DOMAIN"/dev/zero c 1 5 ;; restart|force-reload|start) $0 stop 2>/dev/null for chrootdir in $CHROOT_DIR; do if [ -d "${chrootdir}/tmp" ]; then chmod 777 "${chrootdir}/tmp" chmod +t "${chrootdir}/tmp" echo "Setting up ${chrootdir}..." for f in $MOUNTS; do if [ -d "$f" ]; then mkdir -p "${chrootdir}${f}" mount --bind -o ro "${f}" "${chrootdir}${f}" else mkdir -p "${chrootdir}$(dirname "${f}")" touch "${chrootdir}${f}" mount --bind -o ro "${f}" "${chrootdir}${f}" fi done cp -urpL --parents $COPY_SOURCES "${chrootdir}" fi done ;; stop) for chrootdir in $CHROOT_DIR; do if [ -d "${chrootdir}/tmp" ]; then echo "Destructing ${chrootdir}..." for f in $MOUNTS; do umount "${chrootdir}${f}" if [ -d "${chrootdir}${f}" ] && [ ! $(ls -A "${chrootdir}${f}") ]; then rmdir "${chrootdir}${f}" elif [ -f "${chrootdir}${f}" ]; then rm "${chrootdir}${f}" fi done fi done ;; help|--help) echo "Usage: $N {start|stop|restart|force-reload|init}" >&2 exit 1 ;; *) "$0" help >&2 exit 1 ;; esac
Nähere Erläuterungen zu dem Skript findest du hier.
Zudem ist hier erklärt, weshalb die Verwendung von NSCD zur Namensauflösung keine gute Idee ist. Als Alternative kopiert das Skript /etc/resolv.conf und libnss_dns.so.2.
Initieren beim Start
Einige Daten (im Beispiel der mysql-Socket) werden durch das Skript per bind-mount in das chroot gemountet. Diese müssen bei jedem Start wieder hergestellt werden. Dies übernimmt der systemd-Service /etc/systemd/system/php-fpm-chroot.service:
[Unit] Description=Set up PHP-FPM chroot mounts After=network.target php7.3-fpm.service mysqld.service [Service] Type=forking ExecStart=/usr/local/bin/php-fpm-chroot-setup start ExecStop=/usr/local/bin/php-fpm-chroot-setup stop RemainAfterExit=yes [Install] WantedBy=multi-user.target
Den Service aktivieren und den Deamon neu laden:
systemctl enable php-fpm-chroot.service systemctl daemon-reload
Konfiguration des PHP-FPM-Pools mit Chroot
Die Konfigurarion des PHP-FPM-Pools basiert auf den Pfadangaben, die in der Apachekonfiguration und in der Verzeichnisstruktur verwenden werden.
Damit das PHP-FPM-Socket gefunden wird:
php_admin_value[doc_root] = /htdocs php_admin_value[cgi.fix_pathinfo] = 0
Sicherheitseinstellugnen für opcache:
php_admin_value[opcache.validate_permission] = 1 php_admin_value[opcache.validate_root] = 1
Angabe des Cert-Pfades, damit die Validierung klappt:
php_admin_value[openssl.capath] = /etc/ssl/certs
Alles in allem die vollständige FPM-Konfiguration:
[katja] prefix = /data/wordpress/example.org user = $pool group = www-data listen = /run/php/php-fpm-$pool.sock listen.owner = $pool listen.group = www-data listen.mode = 0660 listen.allowed_clients = 127.0.0.1 pm = ondemand pm.max_children = 5 pm.start_servers = 2 pm.process_idle_timeout = 10s; pm.max_requests = 100 chroot = $prefix chdir = / security.limit_extensions = .php .php3 .php4 .php5 php_admin_value[doc_root] = /htdocs php_admin_value[cgi.fix_pathinfo] = 0 php_admin_value[opcache.validate_permission] = 1 php_admin_value[opcache.validate_root] = 1 php_admin_value[session.save_path] = /sessions php_admin_value[openssl.capath] = /etc/ssl/certs php_admin_value[disable_functions] = mail,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_exec,passthru,system,proc_get_status,proc_close,proc_nice,proc_terminate,proc_open,curl_ini,parse_ini_file,show_source,dl,symlink,system_exec,exec,shell_exec,phpinfo
Abschließende Hinweise
In der obigen Apachekonfiguration wird mittels rewrite die Installation von eigenen Themes und Plugins unterbunden. Ergänzend dazu kann die Bearbeitung vorhandener Themes und Plugins im Dashboard verhindert werden:
# wp-config.php ... define('DISALLOW_FILE_EDIT', true)
Einige Anleitung beschreiben das Mounten oder Kopieren von sendmail in das chroot für Mailfunktionalität. Das funktioniert so nicht. Sendmail hat weitere Abhängigkeiten und braucht Zugriff auf verschiedene Verzeichnisse. Alt Aternative wird mini_sendmail vorgeschlagen. Mittels der oben dargestellten PHP-FPM-Konfiguration wird mail() eh deaktiviert. Sendmail ist also in diesem Setup nicht notwendig. Der Mailversand aus Wordpress erfolgt mittels eines SMTP-Plugins.
Hinweise und Links
Validierung von Zertifikaten im chroot debuggen
Anpassen der Benutzerrechte für PHP-PFM
Fussnoten