# Configuration Reference

## Configuration File

Location: `/etc/whost/agent.conf` (YAML format)

File permissions: `600`, owner `root` (readable only by root). The agent resets the mode to `600` at every start.

---

The installer writes a working configuration with sane defaults — most operators never need to edit it directly: most keys below are changed from the panel, which writes this file and applies the change to the service concerned. The sections below describe what each block controls so you know where to look when something does need adjustment.

**How the file is written.** The installer writes the first version with the blocks it needs. From then on the agent rewrites the whole file from the settings it holds in memory: at its first start, which adds every block below with its default values, and on every change saved from the panel. Comments, key order and formatting added by hand are not kept, and a key the agent does not know (a misspelled one included) is ignored without a warning and dropped at the next write. Broken YAML, or a value of the wrong type (text where a number or `true`/`false` is expected), stops the agent from starting; the reason is appended to `/var/log/whost/agent.log`.

**Secrets.** Passwords and secrets are stored in this file in plain text (the administrator password as a bcrypt hash). The agent moves them into an in-memory vault while it runs; on disk the file's `600 root` mode is their only protection, so keep every copy and backup of the file equally restricted.

**Settings kept elsewhere.** Update preferences (`/etc/whost/update_settings.json`, see the last section), the notification channels and relay of Settings › Notifications (`/etc/whost/notification_prefs.json`), remote backup destinations (`/etc/whost/remote_backup.json`), API keys (`/var/lib/whost/api_keys.json`), plugin state (`/etc/whost/plugins.json`) and the server time zone (a system setting) are not in this file.

**Reading the tables.** *Default* is the value the agent uses when the key is missing; where the installer writes a different value, it is named. *Changed from* names the panel page. **file** — only by editing this file (see Applying Changes at the end). **agent** — written by the installer or the agent: do not edit it, a hand-written value breaks the service that uses it. **—** — the key has no effect in this release.

---

## Section Details

### server

Identity of the server, and the agent's own listener.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `hostname` | installer: the host name (`--hostname`) | Settings › General › Hostname | The panel's host name (FQDN). A save from the panel also runs `hostnamectl`, rewrites `/etc/hosts`, sets `mail.hostname`, reloads Postfix and Dovecot and rewrites the panel's nginx vhost. Notification e-mails link to `https://<hostname>`, and it is the fallback origin of the password-reset link (see `frontend.public_url`). |
| `server_ip` | installer: the server's primary IPv4 address | Settings › General › Server IP Address | The primary public IPv4 address: an account gets it unless another address is assigned, the account's DNS zone points at it, and the panel vhost answers on it. A save from the panel rewrites the DNS A records that carried the old address. |
| `force_ssl` | `true` | Settings › General › Force HTTPS Redirection | Redirects the panel's HTTP requests to HTTPS; a request that a proxy or CDN forwards with `X-Forwarded-Proto: https` is served without the redirect. The save rewrites the panel vhost and reloads nginx. |
| `host`, `port` | `0.0.0.0`, `2000` (installer: `127.0.0.1`, `2000`) | file | They do not move the listener: the agent listens on `127.0.0.1:2000` as its service unit sets it (`--host`, `--port`; the unit also reserves ports below 2001 to root — `net.ipv4.ip_unprivileged_port_start = 2001`, kept in `/etc/sysctl.d/60-whost-agent-port.conf` — so no process of an account can listen on the agent's port, not even while the agent restarts; processes that do not run as root cannot listen on ports 1024–2000), and the panel's nginx proxy and the update pipeline use that address. `port` is still read by the firewall rule that keeps tenant processes away from the agent and by the port list Under-Attack mode never drops — keep it `2000`. |
| `ssl_cert`, `ssl_key` | `/etc/whost/ssl/cert.pem`, `/etc/whost/ssl/key.pem` | — | Not read; see the certificate note below. |
| `workers` | `1` | — | Always `1`: any other value is logged and replaced with `1`. |

**Panel certificate.** The pair `/etc/whost/ssl/cert.pem` and `/etc/whost/ssl/key.pem` is created self-signed for the host name at install. The same pair serves the panel's nginx vhost on port 443, Postfix and Dovecot TLS, the OpenLiteSpeed listener when LiteSpeed serves the sites, and the agent's loopback listener. To serve a trusted certificate, replace the two files (the certificate with its chain in `cert.pem`), check with `nginx -t`, then reload nginx, Postfix and Dovecot.

### auth

HMAC-SHA256 authentication of API requests (machine-to-machine; the panels use session cookies).

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `api_key`, `api_secret` | generated by the installer | agent | The API key pair the installer generated (also saved in `/etc/whost/api_key` and `/etc/whost/api_secret`). At its first start the agent copies it into the API key list as **Default API Key** (Settings › API Access), where it is managed like any other key; revoke or delete it there to retire it. A deleted installer key stays retired: it does not answer through this copy and is not copied back at the next start. A pair a later installer run writes answers until it is managed the same way. |
| `allowed_ips` | `[]` | file | Copied onto that key's IP allowlist when the agent adds the key to the list; afterwards the allowlist is edited in Settings › API Access, and this list no longer applies to the key. |
| `api_key_encryption_secret` | generated at first start | agent | Encrypts the stored API key secrets and the credentials of remote backup destinations. Never change or remove it: the stored secrets could no longer be decrypted. |

### webserver

Which web server stack serves the sites, and its global connection tuning. `type` takes one of:

- **nginx:** Nginx handles all HTTP traffic directly
- **apache:** Apache handles all HTTP traffic directly
- **nginx_apache:** Nginx as reverse proxy (port 80/443) + Apache backend (`apache_port`, 8080)
- **openlitespeed**, **litespeed:** OpenLiteSpeed or LiteSpeed Enterprise, set when the matching plugin is activated on the Plugins page; PHP then runs through LSAPI

The installer writes `nginx_apache` or `nginx`; it refuses the other values (see [Installation](installation.md#command-line-options)).

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `type` | `nginx_apache` (installer: `--webserver`) | Settings › General › Default Web Server (`nginx`, `nginx_apache`; `apache` is offered and accepted only on a server installed with Apache alone) | The stack the agent writes every site configuration, certificate renewal hook and WAF setup for. The General save changes only this value — it does not install, start or reconfigure a web server — so change it only after the server's stack has been changed to match. With OpenLiteSpeed or LiteSpeed the field is read-only (the plugin owns it). At start the agent logs `WEBSERVER_BINDING_DRIFT` when the process listening on port 443 does not match. |
| `max_clients`, `keepalive_timeout` | `256`, `65` | Settings › General › Max Clients (1–10000), Keep-Alive Timeout (0–300 s) | Written to the web server when saved from the panel: Nginx `worker_connections` and `keepalive_timeout` in `nginx.conf` (only where those lines exist; `nginx -t`, then reload), Apache `KeepAliveTimeout` only (`apachectl configtest`, then reload), OpenLiteSpeed/LiteSpeed `maxConnections` and `keepAliveTimeout` (the server restarts). A failed test restores the previous file. The page shows the configured values, not the running server's. |
| `apache_port` | `8080` | file | The Apache port nginx proxies to in `nginx_apache` site configurations. Apache's own `Listen` port is set by the installer and does not follow this key. |
| `php_handler` | `fpm` | agent | `fpm` (PHP-FPM pools) or `lsapi`; set to `lsapi` whenever `type` is OpenLiteSpeed or LiteSpeed. |
| `ols_admin_port`, `ols_admin_user`, `ols_admin_password` | `7080`, `admin`, empty | agent | The OpenLiteSpeed/LiteSpeed WebAdmin console. The LiteSpeed Enterprise plugin generates the password when it installs the server (a value already here is reused on a reinstall); read it here when you need the console. |
| `lscache_enabled` | `true` | file | Adds LSCache to OpenLiteSpeed/LiteSpeed site configurations. |
| `litespeed_serial` | — | agent | The LiteSpeed Enterprise serial the plugin was activated with. |
| `nginx_port` | `80` | — | Not read. |

### php

Multiple PHP versions can be installed simultaneously, and each hosting account can use a different one.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `default_version` | `8.4` (installer: `--php-default`) | Settings › General › Default PHP Version; PHP › Default PHP Settings | PHP version of new accounts; must be one of `installed_versions`. |
| `installed_versions` | `7.4`, `8.0`–`8.5` (installer: the versions it installed, `--php-versions`) | PHP page, version switches | Versions offered to accounts. A version can be switched on only when its packages are installed; the default version and a version a domain still uses cannot be switched off. |
| `default_upload_max_filesize`, `default_post_max_size` | `64M` | PHP › Default PHP Settings | A size with an optional `K`/`M`/`G`; `-1` is refused. |
| `default_memory_limit` | `256M` | PHP › Default PHP Settings | A size, or `-1` for unlimited. |
| `default_max_execution_time` | `300` | PHP › Default PHP Settings | Seconds, 0–86400. |
| `default_max_input_time` | `300` | PHP › Default PHP Settings | Seconds, -1–86400. |
| `default_max_input_vars` | `1000` | PHP › Default PHP Settings | 1–1000000. |
| `default_display_errors` | `false` | PHP › Default PHP Settings | `true` or `false`. |

The seven `default_*` values are the ini values an account's pool is rendered with until the account saves its own; an account's saved values live in its metadata and survive a version, worker or plan change. Changing a default does not rewrite existing pools: an account without values of its own gets the new default the next time its pool is rendered.

### python

Python application hosting (the **Python Hosting** plugin). The plugin's on/off state is `client_features.python_apps`, not a key of this block.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `installed_versions` | `3.10`, `3.11`, `3.12`, `3.13` | file | Python versions offered to apps; only those whose interpreter is installed on the server are listed. |
| `default_timeout`, `default_max_requests` | `120`, `1000` | file | Gunicorn `timeout` and `max_requests` of an app that has not set its own. |
| `allowed_manage_commands` | `migrate`, `collectstatic`, `createsuperuser`, `showmigrations`, `check`, `shell` | file | The Django `manage.py` commands the panel may run for an app; any other command is refused. |
| `socket_dir`, `venv_base_dir` | `/run/whost`, `venvs` | agent | Where app sockets and (under the account's home) virtual environments are created. |
| `enabled`, `default_version`, `default_app_server`, `default_workers`, `default_worker_class`, `max_workers_per_app` | — | — | Not read. |

### node

Node.js application hosting (the **Node.js Hosting** plugin). The plugin's on/off state is `client_features.node_apps`, not a key of this block.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `installed_versions` | `18`, `20`, `22` | file | Node.js versions offered to apps; only those whose runtime is installed on the server are listed. |
| `allowed_npm_scripts` | `start`, `build`, `test`, `lint`, `migrate`, `seed` | file | The `npm run` scripts the panel may run for an app; any other script is refused. |
| `socket_dir`, `wrappers_dir` | `/run/whost`, `/etc/whost/wrappers` | agent | Where app sockets and the per-app start wrappers are created. |
| `enabled`, `default_version`, `default_app_type`, `default_package_manager`, `default_instances`, `max_instances_per_app`, `default_max_memory_mb`, `runtime_base_dir` | — | — | Not read. |

### mysql

The agent's MariaDB connection and the MariaDB tuning it applies for you.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `host`, `port` | `localhost`, `3306` | agent | The agent's MariaDB connection. |
| `root_password` | set by the installer | agent | MariaDB `root` password (also in `/etc/whost/mysql_root_password`). |
| `max_connections` | `151` | Settings › General (1–10000) | `max_connections`. |
| `buffer_pool_size` | `256` (MB) | Settings › General (16–65536) | `innodb_buffer_pool_size`. |
| `connect_timeout`, `wait_timeout`, `interactive_timeout` | `300` (seconds) | Settings › General (1–86400) | The MariaDB variables of the same name. |
| `sort_buffer_size`, `read_buffer_size`, `max_allowed_packet` | `4M`, `4M`, `64M` | Settings › General | A non-zero `K`/`M`/`G` size (`4M`, `256K`, `1G`). |

A save from Settings › General that changes a database value rewrites the drop-in `/etc/mysql/mariadb.conf.d/99-whost-tuning.cnf` (`/etc/my.cnf.d/` on RHEL) from the agent's template with all eight values and applies each one live with `SET GLOBAL`, so no restart follows. The drop-in also carries `local_infile = 0` and `secure_file_priv = /var/lib/mysql-files`, which MariaDB reads at its next restart. The installer writes its own version of this file, sized to the server's RAM (InnoDB buffer pool 37.5% of RAM, at least 256 MB; `max_connections = 300`; InnoDB log and flush settings); the first database save from the panel replaces it with the eight values the page shows. Editing these keys in this file does not change MariaDB.

### dns

PowerDNS integration via its HTTP API.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `enabled` | `true` (installer: `false` with `--no-dns`) | file | Whether PowerDNS is installed. While `false`, accounts are created without a DNS zone, and an account's address change or a nameserver change is not pushed to zones. |
| `nameservers` | `ns1.example.com`, `ns2.example.com` (installer: `--nameservers`, default `ns1.<hostname>`, `ns2.<hostname>`) | Settings › General › Nameservers (up to four, filled in order) | NS records of every zone the agent creates; the first entry is the SOA primary. A save from the panel also rewrites the NS records and SOA primary of every existing zone; a hand edit reaches only zones created after the restart. Set them to your real nameserver host names, as the delegation at the registrar names them. |
| `api_url`, `api_key` | `http://127.0.0.1:8081`, set by the installer | agent | The PowerDNS HTTP API and its key (also in `/etc/whost/pdns_api_key`). |
| `default_ttl` | `3600` | file | TTL of the records the agent creates. |
| `soa_content` | `ns1.example.com hostmaster.example.com 0 10800 3600 604800 3600` | file | Only its last four numbers — refresh, retry, expire and negative-cache TTL — are used; the SOA primary is the first nameserver and the contact is `hostmaster.<zone>`. |
| `type` | `powerdns` | — | Not read; PowerDNS is the only DNS server. |

### mail

Postfix (SMTP) + Dovecot (IMAP/POP3) + OpenDKIM. The mail system uses a MariaDB database for virtual mailbox management.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `enabled` | `true` (installer: `false` with `--no-mail`) | file | Whether the mail stack is installed. While `false` the agent does not install or refresh the WHost Mail Policy service (`whost-policyd`) that applies the hourly sending limits. |
| `hostname` | `mail.example.com` (installer: `--mail-hostname`, default `mail.<hostname>`) | file | The mail host name recorded at install; a host-name change in Settings › General replaces it with the new server host name. |
| `default_hourly_limit` | `500` | Settings › General › Default Hourly Email Limit (0–100000) | Messages an account may send per hour when its plan sets no limit of its own; `0` = unlimited. A save from the panel re-renders and restarts `whost-policyd`; a hand edit reaches it at the next agent start. |
| `max_email_size` | `25` (MB) | Settings › General › Maximum Email Size (1–100 MB) | Written to Postfix `message_size_limit` when saved from the panel (Postfix reloads); a hand edit does not change Postfix. |
| `dkim_selector`, `dkim_key_dir` | `default`, `/etc/opendkim/keys` | agent | Selector and key directory of the DKIM keys the agent creates (`<selector>._domainkey.<domain>` records). |
| `maildir_base`, `vmail_uid`, `vmail_gid` | `/var/vmail`, `5000`, `5000` | agent | Mailbox storage and its owner, as installed. |
| `mysql_host`, `mysql_port`, `mysql_user`, `mysql_password`, `mysql_database` | `localhost`, `3306`, `vmail`, set by the installer, `vmail` | agent | The virtual-mailbox database (password also in `/etc/whost/mail_db_password`). |
| `dovecot_master_user`, `dovecot_master_password` | `whost_master`, set with webmail | agent | The Dovecot master login used for webmail single sign-on. |

### ssl

Let's Encrypt integration via certbot. Orders validate over HTTP from the domain's own document root, are named after the domain and install a renewal hook that refreshes the files the vhost points at and reloads the web server; renewals run on certbot's own schedule (its systemd timer, or the daily cron line the installer adds where there is no timer). Deleting a certificate removes the lineage.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `email` | empty (installer: `--ssl-email`) | Settings › General › Let's Encrypt Email | Account e-mail of the orders, required: an order is refused with `400 SSL_EMAIL_REQUIRED` while it is empty. |
| `staging` | `false` | file | `true` sends orders to the Let's Encrypt staging environment, whose certificates browsers do not trust — for testing without hitting rate limits. Restart the agent after changing it. |
| `auto_renew` | `true` | Settings › General › Auto-SSL for New Domains | Stored only in this release: the account and addon-domain forms carry their own Auto-SSL choice. |
| `provider`, `webroot_path`, `renew_days_before` | `certbot`, `/var/www/letsencrypt`, `30` | — | Not read. |

### ftp

Pure-FTPd with MariaDB authentication. Each hosting account can have multiple FTP sub-accounts.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `enabled` | `true` (installer: `false` with `--no-ftp`) | file | Whether Pure-FTPd is installed. |
| `passive_ports` | `30000:30100` | Settings › General › Passive Port Range | `START:END`, 1024–65535, START lower than END. Written to Pure-FTPd's `PassivePortRange` when saved from the panel. The installer opens `30000:30100` in the firewall; open a new range on the Firewall page as a `START:END` port rule. |
| `max_clients` | `50` | Settings › General › Max Clients (1–1000) | `MaxClientsNumber`. |
| `max_per_ip` | `10` | Settings › General › Max Connections per IP (1–100) | `MaxClientsPerIP`. |
| `passive_mode`, `bandwidth_limit` | `true`, `0` (KB/s) | API only (the form does not show them) | Stored only: the save keeps and echoes them, but no Pure-FTPd directive is written for them. |
| `mysql_host`, `mysql_port`, `mysql_user`, `mysql_password`, `mysql_database` | `localhost`, `3306`, `pureftpd`, set by the installer, `pureftpd` | agent | The Pure-FTPd authentication database (password also in `/etc/whost/ftp_db_password`). |

A save that changes `passive_ports`, `max_clients` or `max_per_ip` writes the three directives (`/etc/pure-ftpd/conf/` on Debian/Ubuntu, `/etc/pure-ftpd/pure-ftpd.conf` on RHEL) and restarts Pure-FTPd; a hand edit does not change Pure-FTPd.

### backup

Local backup storage and its retention.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `enabled` | `true` | Backups › Settings › Backup System | While `false`, creating, restoring and scheduling backups is refused (`403 BACKUP_DISABLED`) and scheduled runs are skipped. |
| `local_path` | `/var/whost/backups` | file | Where local archives are kept, one directory per account. |
| `retention_days` | `7` (installer: `30`) | Backups › Settings › Retention Days (1–365) | Once a day, on the scheduler's first run at or after 04:00 UTC, every account backup older than this is deleted; `0` in this file turns pruning off. |
| `schedule.enabled` | `true` | Backups › Settings › Schedule System | Starts the built-in scheduler, which runs the backup schedules and the retention pruning. |
| `schedule.max_per_account` | `3` | Backups › Settings › Max Schedules per Account (1–20) | A schedule beyond this is refused (`403 BACKUP_SCHEDULE_LIMIT`). |
| `schedule.check_interval` | `300` (seconds) | file | Time between scheduler runs. |
| `remote` | `enabled: false`, `type: s3` | — | Parsed but not used; see below. |

The scheduler starts with the agent only while both `enabled` and `schedule.enabled` are `true`, and `schedule.check_interval` is read at that moment: a change of `schedule.enabled` — also when saved from the panel — takes effect at the next agent start, and so does switching `enabled` back on when it was off at start. While the scheduler is not running, neither scheduled backups nor retention pruning run.

Remote destinations are not configured in this file: they are managed from the panel (Backups › Remote Storage) and stored encrypted in `/etc/whost/remote_backup.json` (per-account ones under `/etc/whost/accounts/<user>/remote_backup.json`).

### logging

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `level` | `info` | file | `debug`, `info`, `warning`, `error` or `critical` (an unknown name means `info`). In production, `info` is recommended; use `debug` only for troubleshooting. |
| `file` | `/var/log/whost/agent.log` | file | Keep the default: the service unit appends the process output to this path, the fail2ban `whost-agent` jail watches it and the post-update check writes to it. The file is kept at mode `640`. |
| `max_size`, `keep` | `100M`, `10` | file | The agent rotates the file itself when it reaches `max_size` (a number with an optional `K`, `M` or `G`; any other suffix stops the agent from starting) and keeps `keep` old copies. No logrotate rule covers it. |

### license

`license.state_file` selects the persistent license state file; its default is
`/etc/whost/license.state`. The companion HMAC file appends `.hmac` to that path.
State loading, saving, and independent endpoint, service, startup and runtime
checks use the selected file. An old state file at the default location does
not substitute for a missing file at the configured location.

Keep both files owned by root with mode `0600`. Startup normalizes their modes
to `0600`. Restart the agent after changing the setting; avoid repeated restarts,
which consume license verification limits. The installation's boot token stays
at `/etc/whost/.boot_token`; prior activation evidence in either the selected or
default state file prevents silent regeneration of a missing or malformed token.

License grace and cached-token validity use the license server's verified UTC
time advanced by elapsed kernel boot time. Moving the system clock backward
does not add grace time. The state HMAC also protects the clock anchor, allowing
an agent restart within the same operating-system boot to preserve elapsed time.
After an operating-system reboot, or when upgrading a cache without this anchor,
the agent requires online license verification before cached grace can be used.
Keep NTP enabled: TLS certificate validation and other system services still
depend on the system clock.

Cached activation and grace require the original RSA-signed license assertion
and its expiry. A valid local HMAC alone does not authorize cached access.
Missing signature fields, a missing signed expiry, modified assertions, and
expired tokens discard the cache and require online verification. Activation
and periodic verification both persist the signed assertion. Do not edit or
remove token fields when restoring a state file.

The other keys of this block are maintained by the agent — do not edit them. `key` and the license secrets are written when the license is activated on the **License** page; a hand-edited `key` that differs from the activated one is replaced with the activated key at the next start.

### rate_limit

API rate limiting per client address (the visitor's address as the agent resolves it behind the panel's nginx), using slowapi. A request over its limit is answered `429 RATE_LIMIT` with a `Retry-After` header. Values follow the format `{count}/{period}` (e.g., `120/minute`, `5/minute`).

```yaml
rate_limit:
  default: "60/minute"
  read: "120/minute"
  write: "30/minute"
  delete: "20/minute"
  heavy: "5/minute"
  auth: "5/minute"
```

All six values are read from this file at agent start and applied.

| Field | Description |
|-------|-------------|
| `default` | Applied by the limiter middleware to the few routes that carry no class of their own (deploy event streams, the update status stream, the SSO endpoints, the frontend debug beacon). `/health`, the panel's root files (`/config.json`, favicon, logo), `/` and the page fallback are exempt |
| `read` | Limit for GET (read) endpoints, including the public `GET /api/v1/auth/ip-check` allowlist probe used by the login page |
| `write` | Limit for POST, PUT, PATCH (write) endpoints |
| `delete` | Limit for DELETE endpoints |
| `heavy` | Limit for resource-intensive operations: backups and restores, file upload, compress and extract, Let's Encrypt orders, plugin installs, license activation, password and second-factor changes, password-reset requests |
| `auth` | Limit for authentication endpoints (admin and client login, 2FA) |

### fail2ban

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `enabled` | `true` | — | Not read: the panel manages fail2ban whenever `fail2ban-client` answers. |
| `bantime`, `findtime`, `maxretry` | `3600`, `600`, `5` | — | Not read: the installer writes `jail.local` with its own values, and per-jail changes on the Fail2Ban page go to `/etc/fail2ban/jail.d/<jail>.local`. |
| `ignoreip` | `127.0.0.1/8`, `::1` | file | Shown as the whitelist only when no jail answers; the live whitelist is the jails' `ignoreip` set, edited on the Fail2Ban page and persisted in `jail.local`. |

### modsecurity

WHost enforces ModSecurity at the nginx tier (`nginx` and `nginx_apache`); with another web server the settings are saved but not applied to traffic. `/etc/modsecurity/modsecurity.conf` is rendered from this block when the WAF page changes a setting or a rule — after a web-server configuration test — so a hand edit reaches it only at the next such change.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `enabled`, `default_account_waf` | `true`, `true` | file | An account that has not chosen its own WAF state gets `modsecurity on` in its site configuration only while both are `true` (picked up when the configuration is next rendered). `enabled: false` also makes the WAF page report the WAF as inactive. |
| `engine_mode` | `detection_only` | WAF page | `on` (block), `detection_only` (log only) or `off`; rendered as `SecRuleEngine`. A new installation starts in `detection_only`; an existing one keeps the mode it has. |
| `crs_paranoia_level` | `1` | WAF page | OWASP CRS paranoia level, 1–4. |
| `audit_log` | `true` | WAF page | `SecAuditEngine RelevantOnly` (5xx and 4xx responses except 404) or `Off`. |
| `audit_log_path` | `/var/log/modsecurity/audit.log` | file | The serial audit log the WAF page reads; the installer's logrotate rule (daily, 30 kept, `copytruncate`) names the default path. |
| `owasp_crs` | `true` | file | Loads the OWASP Core Rule Set from `<rules_path>/crs`. |
| `rules_path`, `custom_rules_path` | `/etc/modsecurity`, `/etc/modsecurity/custom` | file | The rule set's base directory, and the custom rules directory (included only while it holds a `.conf`). |
| `per_account_overrides` | `true` | file | Per-account WAF state and rule exclusions under `/etc/modsecurity/accounts/`; while `false` the account-scope WAF writes answer `409 FEATURE_DISABLED`. |

### security

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `admin_allowed_ips` | empty | Settings › Security › Admin Panel IP Whitelist | IP/CIDR entries, one per line (a YAML block string); empty allows every address. nginx answers `404` for the `/admin` pages from any other address, and the agent refuses admin sign-in and admin sessions from it (`403 IP_NOT_ALLOWED`). A save from the panel rewrites `/etc/nginx/conf.d/whost-admin-geo.conf` and `/etc/nginx/snippets/whost-admin-ip-whitelist.conf` (after `nginx -t`), and a changed non-empty list signs out every admin and client session; a list that leaves out your own address is refused unless you confirm it. A hand edit changes only the agent's check. |
| `two_factor_enforcement`, `two_factor_enforcement_method` | `false`, `both` | Settings › Security › Two-Factor Authentication Enforcement | The switch and the method it demands (`totp`, `email`, or `both` = either). While it is on, the login and session endpoints answer `requires_2fa_setup: true` (with `required_2fa_method`) for an identity that has no second factor yet, and the panels keep that identity on its enrolment page (admin Settings › Profile, client Settings › Security) until it enrols. |
| `ssh_port`, `ssh_root_login`, `ssh_password_auth`, `ssh_key_auth`, `ssh_allowed_ips` | `22`, `true`, `true`, `true`, empty | Settings › Security › SSH Access Restrictions | Written to `/etc/ssh/sshd_config.d/00-whost.conf` when saved from the panel (after `sshd -t`; a port change restarts sshd and updates the firewall and fail2ban, other changes reload it). At least one of password and key authentication stays on, and a port another service holds is refused (`PORT_IN_USE`). `ssh_allowed_ips` (one IP/CIDR per line) denies SSH logins from every other address. `ssh_port` is also the SSH port the firewall and Under-Attack mode protect. A hand edit does not change sshd. |
| `auto_configure_dkim`, `auto_configure_spf`, `auto_configure_dmarc`, `dmarc_policy` | `true`, `true`, `true`, `none` | Settings › Security › DKIM / SPF / DMARC | Which mail records the agent adds when it creates an account's mail DNS: the DKIM key record, SPF (`v=spf1 a mx ip4:<server_ip> ~all`) and DMARC (`v=DMARC1; p=<policy>; rua=mailto:postmaster@<domain>`, policy `none`, `quarantine` or `reject`). Existing zones are not rewritten. |
| `trusted_origins` | empty | file | Extra origins allowed to send cookie-authenticated writes (POST, PUT, PATCH, DELETE) whose `Origin` differs from the host they are sent to, whitespace separated as `host[:port]`. The panel calling its own API under whatever name it was opened always passes, and HMAC-signed calls are not checked; any other cross-origin write that carries a panel session cookie is refused with `403 CROSS_ORIGIN_DENIED`. |
| `spam_reject_threshold`, `spam_add_header_threshold`, `spam_greylist_threshold` | `15`, `6`, `4` | — | Not read: the spam scores are set on the Spam Filter page. |

### admin

The built-in administrator, the sign-in protection of both panels and the session lifetimes.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `enabled` | `true` | file | While `false`, cookie sign-in is off for both panels: the admin sign-in answers `404`, admin and client session cookies are no longer accepted, and only HMAC-signed API calls are served. |
| `username` | `admin` | file | The administrator signs in with this name or with `email`. |
| `email` | empty | Settings › Profile | Recovery address and second sign-in name of the administrator; admin notification e-mails and e-mail sign-in codes are sent to it. |
| `password` | set by the installer | Settings › Profile (password change) | A bcrypt hash; a plain-text value is never accepted at sign-in. A change signs out every other admin and client session. |
| `language` | `auto` | Settings › Profile | The administrator's panel language (`auto` keeps the language chosen at sign-in). |
| `session_timeout` | `3600` (seconds; installer: `3600`) | Settings › Security › Session Timeout (5–1440 minutes) | Lifetime of an admin or client session; a session in use is renewed once half of it has passed. |
| `remember_me_timeout` | `2592000` (30 days) | file | Lifetime of a "remember me" session, in both panels. |
| `session_max_lifetime` | `604800` (7 days) | file | Hard limit counted from sign-in, whatever the activity, in both panels. |
| `allow_concurrent_sessions` | `false` | file | Off: a sign-in ends the earlier sessions of the same identity (the administrator, or that client account) and a sign-out ends them all. On: earlier sessions keep working until they time out, a sign-out ends only the browser it is made in, and `session_max_lifetime` counts from the identity's latest sign-in or sign-out; a password change still signs everyone out. |
| `session_secret` | set by the installer | agent | Signs admin and client session cookies. The agent replaces it when the admin password changes and when the admin IP whitelist is changed to a non-empty list; any change signs everyone out. |
| `two_factor.enabled`, `two_factor.method` | `false`, `totp` | Settings › Profile | The administrator's own second factor (`totp` or `email`). |
| `two_factor.email` | empty | Settings › Profile | Where the administrator's e-mail sign-in codes go; it follows `email` unless set to another address here. |
| `two_factor.trusted_ip_days` | `0` | file | How long an address that passed the administrator's second factor skips it: `0` = with no expiry while sign-ins come from that address, `-1` = the code is always asked, `N` = N days. |
| `two_factor.smtp_host`, `two_factor.smtp_port`, `two_factor.smtp_user`, `two_factor.smtp_password`, `two_factor.smtp_tls` | `localhost`, `587`, empty, empty, `true` | file | The relay of the sign-in code e-mails of both panels and of the client password-reset e-mail — not the relay of Settings › Notifications. With the defaults the local Postfix is used on port 25. In this release these messages are sent without SMTP authentication even when `smtp_user` and `smtp_password` are set, so `smtp_host` must name a relay that accepts this server without a login. |
| `recaptcha.enabled`, `recaptcha.provider`, `recaptcha.min_score` | `false`, `recaptcha`, `0.5` | Settings › Security › CAPTCHA Protection | Sign-in captcha of both panels; provider `recaptcha`, `hcaptcha` or `turnstile`; `min_score` (0.0–1.0) is the reCAPTCHA v3 threshold. Switching it on requires the selected provider's site key and secret key. The public `GET /api/v1/auth/captcha-config` publishes only the enabled flag, the provider and its site key. |
| `recaptcha.<provider>_site_key`, `recaptcha.<provider>_secret_key` | empty | Settings › Security › CAPTCHA Protection | The key pair of each provider; the page shows secret keys masked. |
| `notification_smtp_password` | empty | Settings › Notifications | Password of the notification relay. The rest of that SMTP block (host, port, user name, encryption and sender) is stored in `/etc/whost/notification_prefs.json`; only the password is kept here. Notification e-mails use that block, then fall back to the `two_factor.smtp_*` keys, then to the local relay. |
| `two_factor.totp_secret`, `backup_codes`, `pending_*`, `totp_last_step`, `avatar_url`, `last_login` | — | agent | Maintained by the Profile page and the sign-in flow (the TOTP secret, hashed backup codes, an enrolment in progress, the avatar). |

### frontend

Static panel bundle served by nginx.

| Key | Default | Changed from | What it does |
|---|---|---|---|
| `public_url` | empty | file | The address the panel is reached at from outside (`https://panel.example.com`): the origin of the password-reset link the panel mails. When it is empty the link uses `https://<server.hostname>`, and only without a host name the bare `https://<server_ip>` — a name matches the certificate the panel serves, an address does not. Notification e-mails link to `https://<server.hostname>` whatever this key says. |
| `default_language` | `auto` | Settings › General › Language Settings | `auto` or one of the panel's locale codes: the language of a browser that has not chosen one. Written to the panel's `config.json` when saved and at every agent start. |
| `debug_log` | `false` | file | Opens the browser debug beacon route (`POST /api/v1/debug-log`); its file `/var/log/whost/frontend-debug.log` is rotated daily by the installer's `/etc/logrotate.d/whost` (seven copies kept). |
| `enabled`, `path` | `true`, `/opt/whost/frontend/out` | agent | The agent's copy of the panel files; nginx serves `/var/www/whost/panel`, which the installer copies from it. |

### client_features

Eleven switches (`file_manager`, `dns`, `backups`, `cron`, `ssl`, `php`, `email`, `ftp`, `databases`, `python_apps`, `node_apps`), all `true` by default, edited from Settings › General. A switch that is off is enforced at the API for every client route of that surface (`409 FEATURE_DISABLED`, keyed on the request path — `/client/emails` and `/client/spam` for `email`, `/client/files` for `file_manager`, the per-domain `/client/domains/{domain}/php/*` routes for `php`); the client profile carries the switches as `features`, and the client panel hides the sidebar entry and the dashboard tiles of a surface that is off. A switch that is off does not stop the backup scheduler: `backups` off refuses the client's backup routes only, and the account's existing schedules keep running.

`python_apps` and `node_apps` are also the on/off state of the **Python Hosting** and **Node.js Hosting** plugins: activating or deactivating the plugin on the Plugins page sets them (deactivation is refused while apps are deployed), and while one is off every Python or Node.js app route — the administrator's included — answers `503 PLUGIN_DISABLED`.

### whitelabel

`enabled`, `company_name`, four logo addresses (`logo_light`, `logo_dark`, `favicon`, `favicon_dark`), the sign-in page video and its cover for each panel (`login_video_admin`, `login_poster_admin`, `login_video_client`, `login_poster_client`; empty = the default video), six HSL colours (`primary_color`, `secondary_color`, `nav_color` and their `_dark` twins, `"H S% L%"` with H 0–360 and S, L 0–100) and `hide_login_branding` (default `true`). Edited on the **Whitelabel** page (account menu at the top right); every change writes a `settings_updated` audit row and rewrites `config.json` in the panel trees. While the `whitelabel` license addon is not active, the page's saves, logo and login video uploads and removals and reset are refused with `403 LICENSE_ADDON_REQUIRED`; the values already in this block stay, and the public branding read answers `active: false` until the addon is active. A licence check that grants or withdraws the addon rewrites `config.json` at once, so the panel follows without a restart. A hand edit is published at the next agent start while the addon is active.

A logo field holds the address of a file the agent stores under `/var/lib/whost/branding/` — `/branding/<field>.<digest>.<png|jpg|webp|svg>`, served by the panel's nginx (`location ^~ /branding/` in `/etc/nginx/snippets/whost-panel-locations.conf`) with a one-year immutable cache and a sandboxing content policy; the name changes with the content, and a replaced or removed logo's file is deleted. Only such an address (or an inline `data:image/` URL, see below) is published; any other hand-written value stays private. Uploads (2 MB per file) are shaped before they are stored: a raster logo to fit 512 × 160 px (width × height), a favicon to a square PNG of at most 256 px with transparent padding; an image above 25 megapixels is refused (`400 IMAGE_DIMENSIONS_TOO_LARGE`), a file that does not decode as its type too (`400 INVALID_IMAGE`); SVG is sanitised and kept. A sign-in video field holds `/branding/login_video_<admin|client>.<digest>.mp4` — an MP4 whose video track is H.264, at most 5 MB, stored as uploaded (the panel compresses a larger video in the browser before it uploads it) — and its cover `/branding/login_poster_<admin|client>.<digest>.<jpg|png|webp>`, scaled to fit 1920 × 1080 (1080 × 1920 upright); each publishes only a file written for that field. Logos an earlier release stored inline as `data:` URLs move to files at the first agent start that finds the panel nginx serving `/branding/`: the original values are copied first to `/var/lib/whost/branding-legacy-<timestamp>.yaml` (mode 0600), the logos are shaped as uploads are, and the block is saved once; a value that does not decode stays as it was. Include `/var/lib/whost/branding/` in host backups.

### update preferences (not an `agent.conf` block)

`agent.conf` carries no `update:` block; an `update:` block left by an older install is not read by any code and is dropped the next time the agent writes the file. The update preferences live in `/etc/whost/update_settings.json` (root, `0600`): `channel` (`stable` | `beta`), `auto_update`, `auto_update_type` (`all` | `security` | `patch` | `minor`), `backup_before_update`, `notify_available`, `notify_installed`, `os_packages_check_enabled`, `os_packages_notify_security`, `allow_vendor_critical_override`. `auto_update_type` decides what installs on its own: `security` only releases the vendor flags as critical (and only while `allow_vendor_critical_override` is on), `patch` a patch step, `minor` a minor or patch step, `all` every release; a vendor-flagged critical release also installs under the other types while `allow_vendor_critical_override` is on. The installer writes the file once on a new server with the channel of the release it installs (`beta` for a `-beta.N` release, `stable` otherwise) and leaves an existing file alone; afterwards the file is written by the Updates page (Settings tab, `PUT /system/update/settings`) and read by the install pipeline and the scheduler; an absent file means the defaults (auto-update of vendor-flagged critical releases on), a file that cannot be parsed is logged and treated as `auto_update: false` until it is repaired. The check cadence is fixed: five minutes after every agent start, then every 24 hours (up to 30 minutes of jitter).

---

## Environment Variables

The agent reads its settings from this YAML file only; no key of this file can be set through an environment variable. The service unit (`/etc/systemd/system/whost-agent.service`) sets the environment the agent runs with — among it `WHOST_CONFIG_PATH=/etc/whost/agent.conf`, the path of this file — together with hardening switches. Keep the unit as installed: the installer and the agent's start-up permission check work on `/etc/whost/agent.conf`.

---

## Applying Changes

Settings saved from the panel take effect at once: the agent writes this file itself and applies the change to the service concerned, as the tables describe. To change a key the panel does not offer (**file** in the tables):

1. Keep a copy, for example `cp -p /etc/whost/agent.conf /root/agent.conf.bak` — it holds the same secrets, so keep it at mode `600`.
2. Edit the file, then restart the agent before saving anything in the panel; until the restart, the next panel save writes the agent's in-memory settings back over the edit.
3. Check that the agent came back: `curl -sk https://127.0.0.1:2000/health` answers `{"status":"ok",...}` once the start has finished (a few seconds). If it does not, read the end of `/var/log/whost/agent.log`.

```bash
systemctl restart whost-agent
curl -sk https://127.0.0.1:2000/health
```

A hand edit changes what the agent reads; it does not rewrite the service configurations a panel save writes (web server tuning, MariaDB, Pure-FTPd, the Postfix message size, sshd, the nginx admin whitelist, DNS zones, the panel vhost). Change those keys from the panel. Every agent start verifies the license with the license server; avoid repeated restarts.

---

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