# Installation Guide

## System Requirements

### Supported Operating Systems

WHost supports a narrow, deliberately modern OS matrix. Any other
release is refused by the installer when it starts, before it installs
anything — see [Supported OS](supported-os.md) for the full rationale.

| OS | Version | Status |
|----|---------|--------|
| Ubuntu | 24.04 LTS (Noble) | Fully Supported |
| Ubuntu | 22.04 LTS (Jammy) | Fully Supported — Python 3.12 from the deadsnakes PPA; the nginx ModSecurity connector is compiled from verified source |
| Debian | 12 (Bookworm) | **Not installable in the 1.0 line** — no Python 3.12 package; the installer refuses it. Returns with the multi-ABI build |
| AlmaLinux | 9 | Fully Supported |
| Rocky Linux | 9 | Fully Supported |
| CentOS Stream | 9 | Fully Supported |

> **Validation:** The installer has been run end to end on clean servers
> with **Ubuntu 24.04**, **Ubuntu 22.04** and **AlmaLinux 9.8**. The fixes
> those runs led to (`agent/requirements.txt`, Python 3.12 provisioning, the
> fail2ban log file, pinned digests and bundled signing keys for the
> downloads that are compiled into the web server, services restarted with
> the configuration the installer writes) are part of the installer the
> published repository carries. Rocky Linux 9 and CentOS Stream 9 take the
> same installer path as AlmaLinux 9; on all three the installer switches
> SELinux from Enforcing to Permissive — see
> [SELinux on the RHEL family](#selinux-on-the-rhel-family). **Debian 12**
> is not installable in this release — see *Agent Python runtime* in the
> supported-OS page.

> **Not supported:** every other release — among them Debian 11 and 13,
> the version 8 and 10 releases of AlmaLinux, Rocky Linux and CentOS
> Stream, Red Hat Enterprise Linux itself, and every Ubuntu release other
> than 22.04 and 24.04. The installer stops with an error that names the
> supported releases.

### Minimum Hardware

During the beta every supported system was installed and tested on servers of this size; smaller servers have not been tested.

- **CPU:** 2 vCPU
- **RAM:** 4 GB
- **Disk:** 40 GB (the operating system plus WHost took 8.4–11 GB right after installation)
- **Network:** Static IP address

For production, plan for more: 4+ vCPU, 8+ GB RAM and SSD storage sized to the accounts you host.

### Prerequisites

- Fresh OS installation (no existing web server, database, or mail server)
- Root access (sudo or root user)
- Internet connectivity for package downloads
- `git`, to fetch the installation repository (the installer installs the rest of its tools itself)
- A fully qualified host name for the server (for example `srv1.example.com`), either already set on the system or passed with `--hostname`. Cloud images usually boot with a single-label name such as `ubuntu-4gb-hel1-3`; the installer stops on such a name before it installs anything (see [Host name](#host-name))

---

## Quick Installation

```bash
# Download and run the installer
cd /tmp
git clone https://wisecp.com/files/whost/whost.git
cd whost/installer
bash install.sh --hostname=srv1.example.com
```

`--hostname` can be left out when `hostname -f` already prints a fully qualified name.

The installer will automatically:
- Detect your operating system
- Set the server's host name (see [Host name](#host-name))
- Install and configure all required services
- Generate API credentials and admin password
- Download the web panel for this release and verify its signature
- Start the WHost Agent

---

## Verified Downloads

Everything the installer compiles into the web server or loads as a rule set is fetched over TLS and checked against a detached signature before use, with the upstream keys shipped in `installer/lib/` (`nginx-release-keys.pub`, `owasp-modsecurity-signing-key.pub`, `owasp-crs-signing-key.pub`). The ModSecurity-nginx connector and the OWASP Core Rule Set also carry a pinned SHA-256. The nginx source archive — needed only where the distribution ships no ModSecurity module package, such as Ubuntu 22.04 — carries a pinned SHA-256 for the nginx releases the installer lists (1.18.0, 1.22.1, 1.24.0) and is checked by its signature alone for any other release. A connector or nginx source that fails a check is not built: the module is left out and the installer says so in its output. A rule set that fails its check stops the installation.

The ionCube Loader, which every PHP process loads, is taken as one named release (15.5.1) from ionCube's download server and unpacked only when its archive matches the SHA-256 the installer pins for that release and the machine's architecture (x86_64, aarch64); an archive that fails the check, or a download that fails, stops the installation.

## Panel Archive

The web panel is published as a separate signed archive, `whost-panel-<version>.tar.gz`, beside the repository. During the frontend step the installer downloads it together with its `.asc` signature and `.sha256` file, checks the release signature against the public key shipped in `installer/lib/whost-release.pub` and the SHA-256 when the `.sha256` file is present, and refuses to unpack an archive that has no signature or fails a check.

Two variables change where the archive comes from:

| Variable | Effect |
|---|---|
| `WHOST_PANEL_URL` | Download from another location, such as a mirror. The `.asc` file must be published beside the archive there as well; a `.sha256` file is checked when it is there. |
| `WHOST_PANEL_ARCHIVE` | Use a local copy instead of downloading, for servers without outbound access. Keep the `.asc` file (and optionally the `.sha256`) next to it; the same checks apply. When both variables are set, the local copy is used. |

```bash
WHOST_PANEL_ARCHIVE=/root/whost-panel-VERSION.tar.gz bash install.sh
```

Replace `VERSION` with the release you are installing; `bash install.sh --help` prints it on its first line.

If the archive cannot be fetched or does not verify, the installer leaves the panel out and says so in its output (`Panel archive could not be fetched or verified - the panel is NOT installed`, followed by the file name it expects). The closing check then reports the missing panel page, and the run ends with an error instead of the completion screen. Provide the archive and run `bash install.sh` again; the download is skipped once the panel is in place.

---

## Custom Installation

### Command-Line Options

```bash
bash install.sh [OPTIONS]
```

| Option | Description | Default |
|--------|-------------|---------|
| `--webserver=TYPE` | Web server: `nginx_apache` or `nginx` (see below) | `nginx_apache` |
| `--php-versions=LIST` | Comma-separated PHP versions | `5.6,8.2,8.3,8.4` |
| `--php-default=VER` | Default PHP version | `8.4` |
| `--hostname=FQDN` | Server hostname, fully qualified; set as the system host name, and also the subject of the self-signed certificate and the base of the defaults below. See [Host name](#host-name) | the system's name (`hostname -f`); required when that is not fully qualified |
| `--nameservers=NS1,NS2` | Nameservers written into new DNS zones | `ns1.<hostname>,ns2.<hostname>` |
| `--mail-hostname=HOST` | Mail server hostname | `mail.<hostname>` |
| `--ssl-email=EMAIL` | Email address for Let's Encrypt notifications | not set |
| `--timezone=TZ` | Server timezone, e.g. `Europe/Istanbul` | auto-detected, `UTC` if unknown |
| `--no-dns` | Skip PowerDNS installation | DNS enabled |
| `--no-mail` | Skip Postfix/Dovecot installation (Roundcube and Rspamd are skipped with it) | Mail enabled |
| `--no-ftp` | Skip Pure-FTPd installation | FTP enabled |
| `--no-firewall` | Skip firewall configuration | Firewall enabled |
| `--no-fail2ban` | Skip Fail2Ban installation | Fail2Ban enabled |
| `--no-modsecurity` | Skip ModSecurity (OWASP WAF) installation | ModSecurity installed in `detection_only` (logs, never blocks) |
| `--help` | Show help message | - |

**Web server and the panel.** With `nginx_apache` (the default) or `nginx`, nginx serves the panel at `https://<hostname>/admin/`. The installer refuses `apache`, `openlitespeed` and `litespeed` before it changes anything and names the two supported modes. To host sites on OpenLiteSpeed or LiteSpeed Enterprise, install with the default and switch under **Plugins** in the admin panel; the LiteSpeed Enterprise server itself is downloaded when it is activated there.

### Examples

```bash
# Nginx only, with PHP 8.3 and 8.4
bash install.sh --webserver=nginx --php-versions=8.3,8.4 --php-default=8.4

# All services, PHP 7.4 to 8.5
bash install.sh --php-versions=7.4,8.0,8.1,8.2,8.3,8.4,8.5

# Minimal install (no DNS, no mail, no FTP)
bash install.sh --no-dns --no-mail --no-ftp

# Own nameservers, hostname and Let's Encrypt contact
bash install.sh --hostname=srv1.example.com --nameservers=ns1.example.com,ns2.example.com --ssl-email=admin@example.com
```

### Host name

The name the panel is installed under becomes the subject of the self-signed certificate, the base of the default nameservers (`ns1.<hostname>`) and mail host (`mail.<hostname>`), the DKIM domain, and the domain of the address the panel mails from (`whost@<hostname>` while no SMTP relay is configured). It therefore has to be fully qualified: at least two labels of letters, digits and hyphens, the last one not purely numeric. A single-label name (`ubuntu-4gb-hel1-3`, `localhost`) or an IP address is refused, and the installer stops before it installs anything:

```
[ERROR] 2026-09-26 09:14:03 Server hostname "ubuntu-4gb-hel1-3" is not a fully qualified domain name.
[ERROR] 2026-09-26 09:14:03 Run the installer with --hostname=<fqdn>, for example: --hostname=srv1.example.com
```

The installer also makes the system answer to that name: it runs `hostnamectl set-hostname <fqdn>` and points the `127.0.1.1` line of `/etc/hosts` at it (exactly one such line is kept; it is added when the file has none). `hostname -f` then prints the panel's name, which is what the mail server and the agent read. On cloud images whose `/etc/hosts` is managed by cloud-init the change stays in place across reboots. A run on a server that already carries the name changes nothing.

Give the name an A record in public DNS. The installation finishes without one, but the local mail server checks the domain of every sender address: while the name does not resolve, mail the panel sends through it (notifications, password resets, e-mailed sign-in codes) is turned away with `450 4.1.8 Sender address rejected: Domain not found`, and the agent log records the notification with `email=failed`.

### SELinux on the RHEL family

WHost does not ship an SELinux policy yet. With SELinux **Enforcing** — the default of AlmaLinux, Rocky Linux and CentOS Stream installed from their own media — nginx may not connect to the agent (the panel's API answers `502`) or hand PHP sites to Apache on port 8080, fail2ban may not read the account logs, and `quotaon` is refused at boot. The installer therefore switches a host that enforces SELinux to **Permissive** before it installs anything, in the running system (`setenforce 0`) and in `/etc/selinux/config` (`SELINUX=permissive`), so the mode holds after a reboot. It says so on the screen, in `/var/log/whost/install.log` and in the closing summary:

```
[WARN] 2026-10-01 10:02:11 SELinux switched from Enforcing to Permissive: the running system and /etc/selinux/config
```

In Permissive mode SELinux still labels files and logs what it would have refused (the AVC entries in `/var/log/audit/audit.log`), but refuses nothing. A host that is already Permissive — some cloud images ship that way — or that has SELinux disabled is left as it is. If the installation fails and rolls back, the previous mode is put back. Running WHost with SELinux enforcing is planned after the beta; until then keep the host Permissive (`getenforce` prints the current mode).

> **PHP 5.6 note:** PHP 5.6 is included in the default version list to
> support legacy customer scripts. It installs cleanly on Ubuntu (the
> `ondrej/php` PPA); on RHEL family 9 the Remi repository no longer
> ships PHP 5.6 packages and the installer will skip it with a warning
> (`PHP 5.6 unavailable on RHEL 9+ (Remi dropped EOL series) — skipping`).
> Modern accounts use PHP 8.4 (the default) regardless of OS.

---

## Post-Installation

### Verify Installation

```bash
# Check service status
systemctl status whost-agent

# Verify the agent answers (it listens on 127.0.0.1 only)
curl -k https://127.0.0.1:2000/health
```

The answer is `{"status":"ok","version":"<version>"}`.

The agent tree `/opt/whost/agent` is owned by root with no access for other users (`0750` directories, `0640` files); tenant shells cannot read the agent's source or compiled modules. `stat -c '%a' /opt/whost/agent` should print `750`.

### Access Admin Panel

The built-in admin panel is accessible at:

```
https://<SERVER_HOSTNAME>/admin/
```

It also answers on the server's IP address. Until a trusted certificate is installed, the browser warns about the self-signed one (see *SSL Certificate Issues* under Troubleshooting).

Sign in with the user name `admin` and the admin password printed at the end of the installer; the installer also stores the printed value in `/etc/whost/admin_password` (readable by root only). API keys are managed from **Settings → API Access** in the admin panel.

The installer writes no `license:` block into `/etc/whost/agent.conf` and takes no license option, so the agent starts without a key, in the `NOT_ACTIVATED` state. The first screen is the ordinary sign-in form: once the admin password printed by the installer is accepted, the panel opens on the license page, which says that no license is active and holds the key form. That sign-in's session is what submits the key, and until a key is accepted it opens nothing else in the panel. Activation contacts the license service over outbound HTTPS, binds the license to this server's public IP address and hardware, and stores the issued credentials in `agent.conf`; the license page then shows the active license and the rest of the panel opens. Afterwards the license is managed from **Settings → License**. See the [admin user guide](admin-user-guide.md) for the first-login steps.

**Reinstalling a server under the same key.** The license service keeps a key registered to the installation that activated it, together with credentials that lived only in that installation's `agent.conf`. After a reinstall those credentials are gone, and the activation form answers "This license key is already registered to an installation". Reissue the license with your license provider (for licenses bought from WISECP: the service's management screen on wisecp.com), then enter the key in the form again. A reissue is handed to the first request that reaches the license service afterwards: if the earlier installation is still running, stop its agent (`systemctl stop whost-agent`) before you reissue, or it picks the new credentials up on its next check and the new server is refused again. An installation that keeps running through a reissue needs nothing — its next check applies the new credentials by itself.

A fresh install accepts admin sign-ins from any address: no CAPTCHA is configured and the admin IP whitelist is empty, so the sign-in form is guarded only by the web server's request limit, the agent's lockout and the `whost-agent` fail2ban jail. After the first sign-in open **Settings → Security** and set the **Admin Panel IP Whitelist** and **CAPTCHA Protection**; the installer's closing output says the same.

---

### Disk quota

Per-account disk limits are enforced by filesystem quota on `/`. On an ext4 root filesystem the installer adds `usrquota,grpquota` to the root filesystem's mount options, creates the `aquota.*` files and turns quota on, then **checks whether it is actually active** and says so in its output. On an XFS root it changes nothing and only reminds you in its output that the `uquota` mount option is needed (a reboot may be required), then runs the same check; on any other filesystem it skips quota with a warning.

Quota needs the `quota_v2` kernel module. Ubuntu cloud images boot a kernel whose base module package does not include it. When the module is missing, the installer installs `linux-modules-extra-<running kernel>` and, on images that use the `linux-image-virtual` meta package, `linux-image-extra-virtual`, so that later kernel upgrades keep the module. The second package depends on the generic kernel image, so it also installs the kernel firmware packages (about 0.7 GB on disk) and a newer kernel when one is available; that kernel becomes the running one at the next reboot. Installing only the modules of the running kernel would be smaller, but the next kernel upgrade would then silently turn quota off.

If the installer reports `Disk quota is NOT active on /`, the panel still records and shows each plan's disk limit, but nothing enforces it, and the agent logs an error every time it tries to apply one. On ext4 the installer then takes the quota mount options out of `/etc/fstab` again and removes the `aquota.*` files, so that the server does not come up with a failed `quotaon.service` on every boot. To enable quota afterwards on ext4:

1. Install the kernel module package of your distribution that provides `fs/quota/quota_v2` (Ubuntu: `apt-get install linux-modules-extra-$(uname -r) linux-image-extra-virtual`), then run `modprobe quota_v2`.
2. Add `usrquota,grpquota` to the options of the `/` line in `/etc/fstab`, then run `mount -o remount /`, `quotacheck -cugm /` and `quotaon -v /`.
3. Confirm with `quotaon -p /` — both user and group quota should read `on`.

Limits are applied on `/` only: the agent sets each account's limit on the root filesystem, so a server whose `/home` is a separate filesystem does not enforce them.

## Service Management

```bash
# WHost Agent
systemctl start whost-agent
systemctl stop whost-agent
systemctl restart whost-agent
systemctl status whost-agent

# View logs (the agent writes to agent.log; the journal holds only unit start/stop events)
tail -f /var/log/whost/agent.log
```

On Ubuntu, `unattended-upgrades` installs security updates every day and `needrestart` then restarts the services that use an upgraded library, the WHost Agent among them. The panel and the API are away for the length of that restart (about 15–20 seconds on a small server); the agent checks its files and verifies its license again when it comes back. The installer leaves this distribution default in place: a service that keeps running on the old library does not receive the fix.

---

## Firewall

The installer opens the following ports and refuses the rest: on Ubuntu it sets UFW's incoming policy to deny; on the RHEL family it adds the services below to firewalld's default zone, which refuses what it does not list, and removes the `cockpit` service that zone opens by default. The list is the same whichever `--no-dns`, `--no-mail` or `--no-ftp` options are given; with `--no-firewall` the firewall is left untouched.

| Port | Protocol | Service |
|------|----------|---------|
| 22 | TCP | SSH |
| 80 | TCP | HTTP |
| 443 | TCP | HTTPS |
| 21 | TCP | FTP |
| 30000–30100 | TCP | FTP passive data range |
| 25 | TCP | SMTP |
| 465, 587 | TCP | SMTP submission (implicit TLS / STARTTLS) |
| 110, 995 | TCP | POP3 / POP3S |
| 143, 993 | TCP | IMAP / IMAPS |
| 53 | TCP + UDP | DNS |
| 2000 | TCP, loopback only | WHost Agent — bound to `127.0.0.1`, **not** opened in the firewall; the panel and `/api/v1/` are served through the web server on 443 |

### Manual Port Management

```bash
# Ubuntu (UFW) — example: an extra service port
ufw allow 8443/tcp

# AlmaLinux / Rocky Linux / CentOS Stream (firewalld)
firewall-cmd --permanent --add-port=8443/tcp
firewall-cmd --reload

# Do not open 2000/tcp: the agent only listens on loopback and every
# client, the panel included, reaches it through https://<host>/api/v1/.
```

---

## Troubleshooting

### Agent won't start

```bash
# Check that the configuration file loads (PYTHONPATH as in the service unit)
PYTHONPATH=/opt/whost/agent /opt/whost/venv/bin/python -c "from whost_agent.config import load_config; load_config()"

# Check port availability
ss -tlnp | grep 2000

# Unit start/stop events; the agent's own output goes to agent.log
journalctl -u whost-agent --no-pager -n 50
tail -50 /var/log/whost/agent.log
```

If the agent log shows `ModuleNotFoundError: No module named 'paramiko'`,
the venv is missing dependencies — re-run `pip install`:

```bash
/opt/whost/venv/bin/python -m pip install --require-hashes -r /opt/whost/agent/requirements.lock
systemctl restart whost-agent
```

### SSL Certificate Issues

The installer creates a self-signed certificate for the host name at
`/etc/whost/ssl/cert.pem`, with its key at `/etc/whost/ssl/key.pem`,
**before** any step that needs it. The panel's nginx site, Postfix,
Dovecot and the agent's loopback listener all use this pair. The paths
are fixed in the panel's nginx configuration and in the agent's service
unit, so the `server.ssl_cert` and `server.ssl_key` keys of
`/etc/whost/agent.conf` do not change the certificate that is served. To
use a trusted certificate, replace the two files in place (keep the key
readable by root only), then reload the services that read them:

```bash
systemctl reload nginx postfix dovecot
systemctl restart whost-agent
```

Pure-FTPd uses a self-signed certificate of its own
(`/etc/ssl/private/pure-ftpd.pem` on Ubuntu,
`/etc/pki/pure-ftpd/pure-ftpd.pem` on the RHEL family).

### Multi-OS Specific Issues

#### `Unable to locate package php<version>-fpm` on Ubuntu

The PHP packages come from the `ondrej/php` PPA, which the installer
adds when a requested version is not in the configured sources. A
failure to add the PPA does not stop the installer at that point; it
stops at the package install with this message. Check that the PPA is
configured (`grep -rl ondrej /etc/apt/sources.list.d/`), then run the
installer again.

#### The panel's API answers `502` on AlmaLinux, Rocky Linux or CentOS Stream

The login page opens but every sign-in fails, and the nginx error log
shows `connect() to 127.0.0.1:2000 failed (13: Permission denied)`:
SELinux is enforcing (a server switched back after the installation).
Switch it to Permissive now and for the next boot (see
[SELinux on the RHEL family](#selinux-on-the-rhel-family)):

```bash
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
```

#### `git: command not found` before the installer starts

The quick installation fetches the repository with `git`; the installer
then installs the rest of its base tools (`curl`, `wget`, `tar`, `unzip`
and others) itself. On an image without git, install it first:

```bash
dnf install -y git        # AlmaLinux, Rocky Linux, CentOS Stream
apt-get install -y git    # Ubuntu
```

#### Nginx returns the Apache 404 page on RHEL family

`mod_ssl` on RHEL drops `/etc/httpd/conf.d/ssl.conf` with `Listen 443
https`, which steals the port from Nginx. The installer disables this
listen directive in `nginx_apache` mode. If you see the wrong page,
verify:

```bash
grep -i '^Listen 443\|^#Listen 443' /etc/httpd/conf.d/ssl.conf
ss -tlnp | grep ':443 '
```

The `^Listen 443` line should be commented; `:443` should belong to
`nginx`, not `httpd`. Restart both services if needed:

```bash
systemctl restart httpd nginx
```

#### `nginx: [emerg] Invalid input: IncludeOptional` on account create

ModSecurity v3 (the nginx connector) accepts `Include` only, not
`IncludeOptional`, and an `Include` pattern must match at least one
file. The installer writes `Include /etc/modsecurity/custom/*.conf` into
`/etc/modsecurity/modsecurity.conf` together with a placeholder file,
`/etc/modsecurity/custom/00-placeholder.conf`, so the pattern always
matches. If the error appears after a manual edit, replace
`IncludeOptional` with `Include` and keep at least one `.conf` file in
`/etc/modsecurity/custom/`.

### Rollback

When a step fails, the installer stops and names the command that stopped it, in its output and in the transcript:

```
[ERROR] <date> <time> Installer stopped: "<command>" exited with <code> (transcript: /var/log/whost/install.log)
```

Fix the cause and run `bash install.sh` with the same options again. The second run goes through the steps again and completes the installation: package installs and configuration files come out the same, phpMyAdmin, Roundcube and a panel that is already in place are left as they are, and the admin password, the API key and secret and the service database passwords are generated anew — the closing screen prints the new values. Do not run the installer again on a server that is already in use: it writes `agent.conf` anew, which drops the license credentials and the settings saved from the panel, and on Ubuntu it removes the hosting accounts' PHP-FPM pools.

Steps that already ran are not undone by a failure inside one of the installer's own step functions, which is where almost every failure occurs; only a failure of a top-level command in `install.sh` runs the registered undo steps (stop the services installed so far, drop the databases the installer created, remove `/opt/whost`, the panel tree and the ModSecurity, phpMyAdmin and Roundcube trees). To remove a partial installation by hand (a run that stopped late can also have written the files listed under Uninstallation below):

```bash
systemctl stop whost-agent
rm -rf /opt/whost
rm -rf /etc/whost
rm -f /etc/systemd/system/whost-agent.service
systemctl daemon-reload
```

---

## Uninstallation

A default run (`--webserver=nginx_apache`, Ubuntu 24.04) leaves the files below, the ones the agent writes when it first starts included. Remove what WHost owns and keep the service packages (nginx, Apache, MariaDB, PowerDNS, Postfix, Dovecot, Pure-FTPd, Rspamd, PHP) with their data unless you also mean to remove the services.

```bash
# Stop and disable the WHost units
systemctl disable --now whost-agent whost-policyd whost-http-ban.path whost-http-ban-apply
rm -f /etc/systemd/system/whost-agent.service /etc/systemd/system/whost-policyd.service \
      /etc/systemd/system/whost-http-ban.path /etc/systemd/system/whost-http-ban-apply.service
rm -f /etc/systemd/system/php*-fpm.service.d/whost-limits.conf
systemctl daemon-reload

# WHost trees: agent + venv + bundled Node.js runtimes, config + accounts, panel, state, backups + metrics, logs
rm -rf /opt/whost /etc/whost /var/www/whost /var/lib/whost /var/whost /var/log/whost

# Web server pieces WHost added (reload nginx and Apache afterwards, or remove the packages)
rm -f /etc/nginx/sites-enabled/whost-panel.conf /etc/nginx/sites-available/whost-panel.conf \
      /etc/nginx/snippets/whost-panel-locations.conf /etc/nginx/snippets/whost-security-headers.conf \
      /etc/nginx/conf.d/whost-panel-ratelimit.conf /etc/nginx/conf.d/whost-banlist.conf \
      /etc/nginx/conf.d/whost-cloudflare-realip.conf /etc/nginx/conf.d/whost-modsecurity.conf
rm -f /etc/apache2/conf-enabled/whost-status.conf /etc/apache2/conf-available/whost-status.conf \
      /etc/apache2/conf-enabled/whost-banlist.conf
rm -rf /etc/modsecurity            # ModSecurity configuration, the OWASP CRS copy, per-account rules

# phpMyAdmin and Roundcube, unpacked from their upstream archives and served through the panel site
rm -rf /usr/share/phpmyadmin /etc/phpmyadmin /var/lib/phpmyadmin /usr/share/roundcube /var/lib/roundcube
rm -f /etc/nginx/snippets/phpmyadmin.conf /etc/nginx/snippets/roundcube.conf

# PHP drop-ins, log rotation, fail2ban, SSH and Dovecot drop-ins
rm -f /etc/php/*/fpm/conf.d/99-whost-opcache.ini
rm -f /etc/logrotate.d/whost /etc/logrotate.d/whost-accounts /etc/logrotate.d/modsecurity
rm -f /etc/fail2ban/filter.d/whost-agent.conf /etc/fail2ban/filter.d/whost-web-scan.conf \
      /etc/fail2ban/action.d/whost-http-deny.conf /etc/fail2ban/action.d/whost-nginx-deny.conf
rm -f /usr/local/sbin/whost-http-ban
rm -rf /var/lib/fail2ban/whost-http-ban
rm -f /etc/ssh/sshd_config.d/10-whost-accounts.conf /etc/ssh/sshd_config.d/60-whost-hardening.conf
rm -f /etc/dovecot/conf.d/95-whost-quota.conf

# Kernel settings (ptrace restriction, swap tuning); the running values change back at the next reboot
rm -f /etc/sysctl.d/99-whost-ptrace.conf /etc/sysctl.d/99-whost-perf.conf
```

Kept unless you remove the services too: `/etc/fail2ban/jail.local` (the installer's jails, `[whost-agent]` included) and `/etc/fail2ban/fail2ban.local`, the ionCube loader (`/usr/local/ioncube`, `/etc/php/*/mods-available/00-ioncube.ini` and its `conf.d` links), the MariaDB drop-ins `99-whost.cnf` (loopback only) and `99-whost-tuning.cnf` (sized to the RAM) in `/etc/mysql/mariadb.conf.d/` with `/var/lib/mysql-files`, the MariaDB root login in `/root/.my.cnf`, the MariaDB databases `powerdns`, `pureftpd`, `roundcubemail` and `vmail` with their users (`powerdns`, `pureftpd`, `roundcube`, `vmail`), the `vmail` user and group with the `whost-noshell` and `whost-sso` groups, the UFW rules (`ufw status numbered`), the `usrquota,grpquota` mount options in `/etc/fstab` with `/aquota.user` and `/aquota.group`, the kernel module packages the quota step installed where the image lacked them (`linux-modules-extra-*`, `linux-image-extra-virtual`), and the APT sources the installer added (`ondrej/php`, `deadsnakes`, `rspamd`).

---

**Developed by [WISECP LLC.](https://wisecp.com)**
**Contact:** hello@wisecp.com
