About WPHoster

A WordPress hosting control panel for agencies and small hosts, on their own server.

How it started

I've worked in web hosting for more than 12 years. Hosting clients' WordPress sites always meant choosing between two compromises: a commercial panel that bills you for every account, or a cheaper setup where one hacked site can reach the files and databases of the others.

WPHoster began about 8 years ago as a private API I used to manage my own hosting servers. I wrote it first in C++, then rewrote it in Go. This is its third version: rewritten in Rust and turned into a full control panel that anyone can install.

It ships as a single binary that sets up the whole server (web server, PHP, databases, DNS, mail and backups) and isolates every client account from the others.

Why it's free

WPHoster is free, with no limits on accounts, sites or mailboxes.

I build and run it myself on a few servers, so costs stay low, and more servers running WPHoster means more feedback and a better panel. If paid extras ever arrive, such as priority support, they’ll sit on top. What’s free now stays free.

How client accounts are isolated

Every client account is treated as hostile. WPHoster is built so that a hacked WordPress site can damage its own account and nothing else, using standard Linux isolation: systemd sandboxing, control groups and file permissions.

In short: a hacked client site can’t read other clients’ files, databases or email, can’t see their processes, can’t open a backdoor port, and can’t use up the server’s memory or CPU.

What runs on the server

Every service handles traffic as an unprivileged user of its own. The jobs that need root (the agent, systemd, certificates, bans, statistics and backups) can’t be reached from the internet.

Panel

WPHoster panel (with SQLite)
wphoster, unprivileged
Every account, but only through what the panel can ask for. No shell, no root commands.
WPHoster agent
root
The whole server. It isn’t on the network: only the panel can reach its local socket, and it checks every request again.
systemd
root
The whole server. Not reachable from the network; it builds the cages.

Web

nginx
www-data
The files in every site’s web folder, which it serves anyway. Not wp-config.php (owner-only), databases or mail.
PHP-FPM
each account, in its cage
Only that account.
WP-CLI
each account, in its cage
Only that account.
Redis
each account, in its cage, no network
Only that account’s cache.
Certbot
root, from a timer
Nothing to attack from outside: it only connects out to Let’s Encrypt.

Databases

MariaDB
mysql
Every database. Closed to the outside unless you open Remote MySQL to listed hosts.
phpMyAdmin
each account, in its cage
Only that account’s databases. Reachable only through the panel.

DNS and FTP

PowerDNS
pdns
Every hosted DNS zone. Not files, sites or databases.
Pure-FTPd
each account, after sign-in
A signed-in session stays locked in its own home. TLS is required and repeated failures are banned.

Email

Postfix
postfix
Mail passing through the server. Not stored mailboxes.
Dovecot
each mailbox’s account
A signed-in session reaches only its own account’s mailboxes.
Rspamd
_rspamd
Spam scores and DKIM signing. Not stored mailboxes.
ClamAV
clamav
The attachments it scans. Not stored mailboxes.
Roundcube
wphwebmail, its own PHP
Webmail sessions. Mailboxes still need their own sign-in.
Radicale
radicale, local only
Calendars and contacts.
mlmmj
mlmmj
Mailing lists and their subscribers.

Security

CrowdSec
root
Bans and firewall rules. It reads logs and listens only locally.
nftables
the kernel
No service to attack: it is the firewall itself.

Statistics and backups

AWStats
root, sandboxed: read-only system, no home folders, no network
Only the visitor statistics.
restic
root, from a timer
Reads everything to back it up, and only connects out. The backups are encrypted.

Well-known open-source software from your distribution and a few vendor repositories. See the technology stack for what each one does.

What a new account gets

  1. A Linux user with no shell. SSH refuses every hosting account, whatever its password.
  2. A home folder only the account can enter. The web server may read its public_html, other accounts nothing.
  3. Its own PHP-FPM process, started inside the sandbox below, on its own socket.
  4. Its own MariaDB user, which signs in by its Linux user, so there is no database password to steal.
  5. One password, kept in the system’s shadow file, for the panel and FTP.
  6. A DNS zone for the main domain, if the server serves DNS.

If any step fails, everything made so far is removed again. See Create an Account.

Inside each account’s sandbox

Each account’s PHP and Redis run in a systemd sandbox. WordPress, its plugins and anything a visitor manages to upload run in it too.

Files

The operating system is read-only. Other homes, logs, mail and the panel’s data are hidden; the account sees its own home, a private /tmp and a minimal /etc whose user list names only itself.

Processes and kernel

Other processes are invisible. Kernel settings, modules, logs, the clock and control groups are out of reach.

Privileges

No setuid programs, no new privileges, no namespaces, and a system call filter limited to what a service needs.

Network

Sites may connect out (updates, APIs) but may not listen on a port, and the cloud provider’s metadata address, where server credentials can leak, is blocked.

PHP

open_basedir keeps PHP inside the home, and the functions that run shell commands are switched off.

Limits

Each account has its own control group with the package’s memory, CPU, process and disk I/O caps, plus a disk quota on ext4.

The limits come from the account’s package.

Kept apart in every service

Web
nginx follows a symlink only when its owner owns the target, keeps a log per site, and never runs PHP from uploads.
Databases
An account is granted only its own databases, named after it, and MariaDB is closed to the outside unless you open it to listed hosts.
FTP
TLS is required and each account is locked in its home; extra FTP logins are locked in one folder of it.
Email
Mail from PHP goes out under the account’s own login, counted against its hourly cap and DKIM-signed.
Redis
Each account’s cache runs in its own cage, with no network, on a socket in its home.

The panel and its releases

The web panel never runs as root

It runs as an unprivileged user. A separate agent does the root work and checks every request again before acting on it.

Brute-force protection

CrowdSec bans addresses that keep failing to sign in to the panel, FTP, mail, SSH or WordPress.

Signed updates that roll back

Releases are signed and install every night. If one fails, the server restores the previous version on its own.

Checked before every release

Static analysis of the code and its dependencies, a real install scanned from outside, and the end-to-end suite with its tenant-isolation checks.

See the latest release's results, read the install script before you run it, and follow the security best practices.