Security model

The threat model assumes one of your sites will eventually be compromised — bad WordPress plugin, vulnerable dependency, dumb code. The goal is for the blast radius to stop at that site. Here's what's between an attacker who got PHP-RCE on site A and the rest of the host.

Layered defenses

layerwhat's enforcedwhere
NetworkOnly SSH (22) + HTTP/S (80/443) open; everything else blocked.ufw (setup.sh step 9)
SSHKey-based auth strongly recommended over passwords./etc/ssh/sshd_config
nginxCatch-all default 444s unknown hostnames. Dotfiles + backup extensions denied. Rate limits applied.conf.d/00-tulixhost-default.conf, snippets/tulixhost-security.conf
nginx → FPMtry_files $uri =404 prevents fall-through; cgi.fix_pathinfo=0 globally.per-site vhost, global php.ini
PHP-FPM poolRuns as per-site unix user; cannot escalate without a kernel exploit.per-site pool user=web_*
PHP runtimeopen_basedir limits file access; disable_functions removes RCE primitives; allow_url_include=off.per-site pool php_admin_*
FilesystemSite dirs mode 0750 owned by per-site user; .envtulix mode 0640 root-owned, readable only via per-site group.create_vhost.sh step 3, 5
MariaDBPer-site user limited to one DB. Listens only on 127.0.0.1. Root is unix_socket only.create_vhost.sh step 7, server config
RedisPer-site ACL user restricted to one DB + one key prefix; dangerous commands denied. Localhost only, password required.create_vhost.sh step 8, server config
OSunattended-upgrades enabled for security pocket on first run.setup.sh step 10

What an attacker with PHP-RCE on site A can do

What they cannot do (without an additional kernel/CVE exploit)

Secrets handling

Each site's conf/.envtulix is mode 0640, owned root with group web_<site>. The site's PHP-FPM pool runs as web_<site> (member of its own group), so it can read but not write. The file contains:

None of these are useful for cross-site escalation. Rotating any of them is a matter of editing the file and the corresponding server-side entry (DB ALTER USER or users.acl edit), then reloading the app.

TLS

HTTPS sites get Mozilla intermediate cipher suite, TLSv1.2 + TLSv1.3 only, OCSP stapling, HSTS for 2 years (no preload by default — set preload in the Strict-Transport-Security header manually when you're sure). Cert renewal for Let's Encrypt is handled by certbot's own systemd timer (already installed); tulixhost doesn't need to do anything.

For wildcards or non-public hostnames, supply your own cert with --ssl=./ssl. Place SITE_NAME.crt and SITE_NAME.key in ./ssl/ before running create_vhost.sh. tulixhost copies them into conf/ssl/ and references them from the vhost.

Auditing

Every privileged invocation of a tulixhost script appends a line to /var/log/tulixhost/audit.log:

2026-05-26T22:14:03Z pid=14392 user=george argv=./create_vhost.sh cmd=example.com --ssl=auto --demo

This is rotated weekly by logrotate. Combined with shell history and journalctl _COMM=sudo, you have a full forensic trail of admin actions on the host.

What's not defended (and what to do about it)