Firewall

Updated Oct 5, 2026 Markdown

IP Blocks Are Server-wide

Every block — from the Firewall page or from the account-level endpoints — is one rule on the server firewall (UFW on Debian, firewalld on RHEL) and affects every site on the server. There is no per-account scoping; the account-level endpoints say so in their warnings and record scope: server in the audit log. Tenants cannot add, remove or list blocks.

The API refuses a deny that covers the address the request came from (SELF_LOCKOUT), so an operator cannot close the panel on themselves from the panel.

A block is written twice: as a source deny in the packet filter and as an entry on the HTTP-layer list fail2ban uses (/etc/whost/http-banlist, rendered into every installed webserver), so it also holds for traffic that arrives through a CDN. On UFW the comment given with the block is kept and shown in the list; firewalld (the RHEL family) does not store it, so there the list shows no comment. Blocking a whitelisted address is refused with 409 IP_WHITELISTED — the allow rule would sit above the deny — so remove the address from the whitelist first. Lifting a block removes only the deny and the HTTP-layer entry, never the address's whitelist rule; lifting a block that does not exist answers 404 NOT_FOUND, and the account-level endpoints answer 404 ACCOUNT_NOT_FOUND for an account that does not exist.

Clear All Bans lifts every block the panel can produce — plain and port-scoped source denies, the HTTP-layer list, the rate-limit ban sets and fail2ban's bans — and answers the per-layer counts (ufw_rules, http_bans, rate_limit_bans, fail2ban_jails, removed). It leaves the security log alone; Clear Log on the Security Logs tab (DELETE /firewall/logs, audit firewall_logs_cleared) empties the event tables.

System Rules

UFW (Debian) or firewalld (RHEL) abstraction. Lists every rule, allowed/denied/limit/reject, and protected ports (PROTECTED_PORTS, plus the SSH port set under Settings) the API refuses to close or deny globally (so an operator cannot lock themselves out of SSH, HTTP/HTTPS or the agent itself) — a port range that contains a protected port is refused as well, and the rule form asks for the same confirmation for such a range as for the exact port. The list carries source-scoped rules too, with their source, their comment (UFW only — firewalld keeps none) and an id of the form port-protocol--source. Editing replaces the rule and puts the old one back when the new one is refused; editing or deleting a rule the firewall does not carry answers 404 NOT_FOUND (ufw's "non-existent rule" answer is read, not its exit status).

Whitelist: an allowlisted address is let through on every port and is exempt from auto-bans. The list shows applied per entry — whether the firewall really carries the rule — and the agent re-installs missing rules at startup, so a firewall reset no longer leaves entries that exist only on paper. An entry is saved only after the firewall accepted the rule. An entry whose rule is missing is marked Rule missing in the list; adding the address again re-installs the rule (and updates the comment when one is given).

Rate-limit rules: the threshold counts the new connections one address opens within the window — a packet filter cannot see HTTP requests, so the requests a kept-alive connection carries count once. An address over the threshold is banned for the ban duration and loses every port on the server until the ban ends. The rules sit in their own chain (WHOST-RATELIMIT), jumped to from the top of INPUT ahead of ufw / firewalld, so they see the traffic the host firewall lets through; the server's own traffic (loopback) and addresses on the firewall whitelist are never counted or banned. The chain exists only while at least one rule is enabled. Rules apply to IPv4. A rule with several ports (80,443) is applied with one multiport match; up to 15 ports or ranges per rule, and a port filter needs tcp or udp. The rule is recorded only once the kernel carries its objects: a refused rule answers 500 RATE_LIMIT_APPLY_FAILED with the kernel's reason and leaves nothing behind, and an update the kernel refuses puts the previous rule back. The rule store is root-only; the ban set of a rule is WHOST_RL_<RULE ID> — the rule id in upper case. The untargeted unban answers how many ban sets carried the address; an address that is not an IP is refused with 422 and an unknown rule answers 404.

Fail2Ban

Jail list (every jail fail2ban loads plus the ones the panel has disabled), ban list, manual ban/unban, jail-level enable/disable, the jail's ban time / find time / max retry, and the whitelist (ignoreip).

Jails. The list reads every loaded jail from the daemon and the disabled ones from the configuration files (jail.local, jail.d/*) with the filter, log path and tunables they would load with. Switching a jail off asks for confirmation — its current bans are released — and writes the panel's override (jail.d/<jail>.local: enabled, plus the ban time / find time / max retry set through Edit); a later edit keeps the values it does not name, and switching the jail off and on keeps them too. Ban time 60–604800 s, find time 60–86400 s, max retry 1–100. The jail list asks fail2ban-client per jail, so the read takes a few seconds on a host with many jails.

Bans. A ban in an allports jail (sshd, postfix, dovecot, pure-ftpd, recidive) is a packet-filter reject; a ban in an HTTP-facing jail (whost-<user>-webscan, nginx-http-auth, whost-agent) goes through the HTTP-layer list described below, and a network (CIDR) is accepted there too. Banning an address the whitelist ignores is refused with 409 IP_WHITELISTED; unbanning an address that is not banned answers 404 NOT_FOUND. Unban All releases every ban in every jail. The address the request comes from — or a network that covers it — cannot be banned (422 SELF_LOCKOUT): the ban would close the panel on the operator who asked for it.

The agent jail. whost-agent counts failed admin and client sign-ins and refused API signatures from the agent log. The sign-in name is written there as one escaped field, so it cannot add lines of its own and the count goes to the address the attempt came from.

Whitelist. One list for every jail: an added address is ignored by every loaded jail at once, persisted in jail.local's ignoreip, and any ban it currently carries is released. Removing an address that is not on the list answers 404 NOT_FOUND. Entries written by hand into jail.d/*.conf are neither shown nor edited by the panel.

Under-attack mode tightens sshd (bantime 86400, maxretry 3, findtime 300) through a jail.d/whost-attack-mode.local override that survives the reloads the panel's jail writes cause; the file goes away when the mode is disabled.

The agent's own jail (whost-agent) reads /var/log/whost/agent.log with the filter the agent renders from its template at every start (failed admin and client logins, invalid API signatures); a filter written by an older installer is replaced.

Endpoints used: GET /fail2ban/status, GET /fail2ban/jails + GET/PUT /fail2ban/jails/{jail} + POST /fail2ban/jails/{jail}/{enable,disable}, GET /fail2ban/banned, POST /fail2ban/ban, DELETE /fail2ban/ban/{jail}/{ip}, POST /fail2ban/unban-all, GET/POST /fail2ban/whitelist, DELETE /fail2ban/whitelist/{ip}.

Per-account web jail (whost-<user>-webscan). Every hosting account gets a jail over its own access logs (whost-web-scan filter: a burst of 4xx responses). A hit is banned at the HTTP layer rather than in the packet filter: the whost-http-deny action keeps one list (/etc/whost/http-banlist) and renders it into every installed webserver (nginx, Apache, OpenLiteSpeed, LiteSpeed Enterprise), so the ban holds whichever server owns the public port. Behind Cloudflare the banned address is the visitor's own: the installer has nginx restore it from Cloudflare's header (/etc/nginx/conf.d/whost-cloudflare-realip.conf). Behind another CDN, or where Apache, OpenLiteSpeed or LiteSpeed Enterprise serves the public port, the webserver sees the edge address — add that CDN's ranges to ignoreip. The action itself only queues the request (/var/lib/fail2ban/whost-http-ban/queue, the one place a confined fail2ban may write on SELinux hosts); the whost-http-ban.path systemd unit applies the queue as root within a second, so a ban shows up in the webserver after a short delay rather than inside the fail2ban call. whost-http-ban list prints the applied list; whost-http-ban sync re-applies it after a manual webserver config change; journalctl -u whost-http-ban-apply shows a webserver that rejected the list.

WAF (ModSecurity)

The ModSecurity WAF page (Security → WAF) shows the engine (installed, active, mode, OWASP CRS, paranoia level, audit log, rule counts), the configuration form, the CRS rule list and the audit log.

Configuration. Engine mode on (block), detection_only (log, never block) or off; paranoia level 1–4; audit logging on/off. The rendered files are tested with the web server's own configuration test before the values are persisted; a change reloads the web server, a save that changes nothing does not. Test Configuration runs nginx -t (and apachectl configtest on nginx + Apache hosts); on a web server without such a test it answers 409 WAF_TEST_UNAVAILABLE. WHost enforces ModSecurity at the nginx tier only; on Apache-only and OpenLiteSpeed hosts the page says so and the settings are saved but not applied to traffic.

Restricted headers. The CRS setup restricts the Content-Encoding, Proxy, Lock-Token, Content-Range and If request headers but not Accept-Charset (CRS 4 checks it only from paranoia level 2): search engine and AI crawlers send it. An existing install receives the list at the next agent start, through the same nginx -t-checked step as the shared rule set; a list you set yourself (rule 900250 in /etc/modsecurity/crs/crs-setup.conf) is kept.

Argument limit. The CRS setup lets a request carry up to 1,000 arguments (rule 900300, tx.max_num_args — PHP's own max_input_vars default) instead of 255: at a limit of 255, multi-language administration forms, such as a WISECP product or add-on edit form with several hundred fields, are refused with 403 (rule 920380). An existing install that still carries the previous value receives the new one at the next agent start, through the same nginx -t-checked step; a value you set yourself is kept.

Engine mode of a new installation. A new installation starts in detection_only: requests are written to the audit log and none is refused. Switch to on on this page only after the audit log has shown your sites' rarer requests — payment provider webhooks, refunds, 3-D Secure returns, card saving, API and licence calls — without matches that would refuse them. An existing installation keeps the mode it has.

Request bodies. XML and JSON request bodies are parsed by their own processors (rules 200000 and 200001 in /etc/modsecurity/modsecurity.conf), so each JSON value is inspected on its own. An existing installation receives the two rules at the next agent start, through the same nginx -t-checked step; a JSON rule you wrote yourself (id:200001) is kept.

Active means nginx holds the rules. The page shows the WAF as Active only when nginx has loaded the rule set: the connector module and the shared include /etc/nginx/conf.d/whost-modsecurity.conf, which the installer and the agent keep only once nginx -t has accepted the rule set. While nginx has not loaded it, the header reads Inactive and a WAF rule set not loaded note says that requests are not checked although the engine is on; the agent tries again at every start and writes the reason to /var/log/whost/agent.log. On Ubuntu 22.04 one CRS file is left out of the rule set — see Known limitations.

Rules. Every id-bearing rule of the loaded rule set is listed, with its message where it carries one. A rule switched off is written to /etc/modsecurity/disabled-rules.json and as SecRuleRemoveById in modsecurity.conf, and the web server is reloaded; an id that is not in the rule set answers 404 WAF_RULE_NOT_FOUND; a toggle to the state already in force writes nothing.

Audit log. The newest entries of /var/log/modsecurity/audit.log: client address, host, URI, the first rule that matched, severity, and whether the request was blocked, detected (detection-only mode) or only logged (a 4xx/5xx no rule matched). The rotation copies and truncates the file (copytruncate) because libmodsecurity never reopens it; the agent writes that logrotate stanza at every start. Auto Refresh polls every 10 s; Load More widens the window up to 1000 entries.

Per-account WAF (API and the reseller's sub-account page; no admin page): GET /accounts/{username}/waf/status and PUT /accounts/{username}/waf/toggle switch the WAF off or on for one account: the state is written to /etc/modsecurity/accounts/<user>.json, the account's rule file is re-rendered and every vhost of the account is re-rendered so its modsecurity switch follows the state (a vhost rendered while the WAF was off is enforced again after the switch on); POST /accounts/{username}/waf/rules/exclude and DELETE /accounts/{username}/waf/rules/exclude/{rule_id} (?uri= for a URI-scoped one; without it the account-wide entry goes, or the only URI-scoped one when that is all there is — several URI-scoped entries need the URI, 422) exclude one CRS rule for the account, globally or for one URI; GET /accounts/{username}/waf/audit-log lists the entries whose Host header names one of the account's domains. An account that does not exist answers 404 ACCOUNT_NOT_FOUND. While modsecurity.per_account_overrides is false the three writes (toggle, exclude, remove) answer 409 FEATURE_DISABLED and nothing is written; the reads keep answering. The account's rule file (/etc/modsecurity/accounts/<user>.conf) is generated: it is rendered again from the state file on every change and at every agent start, so a rule written into it by hand is dropped. When an agent start rewrites a file that held such lines, the agent log carries one WARNING per account (how many lines were dropped and the folder that keeps the previous file, /var/lib/whost/nginx-modsec-shared-<stamp>/) and the admin panel shows a notification naming the accounts. A server-specific rule that the exclude endpoints cannot express (one that exempts a single argument, for example) goes into /etc/modsecurity/custom/<name>.conf — the main WAF file includes that folder and the agent does not write to it; run nginx -t and reload nginx after adding one.

Endpoints used: GET /waf/status, PUT /waf/config, GET /waf/rules, PUT /waf/rules/{rule_id}, GET /waf/audit-log, POST /waf/test; the account routes above.

Under-Attack Mode

Top-right toggle. With the Auto Enable switch on (it is off by default) the mode arms itself when the threat score reaches 5 (driven by FirewallLogService scoring of recent blocks); with the switch off, such a score only raises a High Threat Level Detected notification. The sysctl hardening is handed to systemd-sysctl through the drop-in /etc/sysctl.d/90-whost-attack-mode.conf (the agent runs with ProtectKernelTunables, so it cannot write /proc/sys itself); the drop-in is removed and the previous values are written back when the mode is disabled, and sysctl_hardening is claimed only when at least one value actually changed. The packet rules — the SYN-flood limit and the connlimit on 80/443 — live in the WHOST-ATTACK chain that the first INPUT rule jumps to, ahead of ufw's chains; every verdict in it is RETURN, DROP or REJECT, so the firewall's own policy still decides what a connection that survived the flood check may reach, and the management ports are never dropped. A reboot empties the packet filter while the mode stays on; the agent puts the chain back when it starts. The sources nginx trusts as real-ip proxies (set_real_ip_from, the CDN's ranges) are exempt from the connection limit: one edge address carries every visitor, so counting it as a single client would cut them all off. Nginx limit_req and tighter Fail2Ban thresholds complete the set; on disable the Fail2Ban values read before the mode was enabled are restored. The card lists the applied protections. Manual mode persists across restarts; auto mode disarms when the threat score drops. Enabling an already-active mode (or disabling an inactive one) answers 409 ATTACK_MODE_ACTIVE / ATTACK_MODE_INACTIVE; every state change answers the new state in data.

Security Logs

The Security Logs tab shows the blocked events, grouped by address and type — count is the number of blocks, taken from the per-minute counters when the rows were sampled under load, and such a group is marked sampled — and the suspicious requests the access-log scanner found. Filters: an exact address, a fragment of one (0.2.1 narrows with a partial match; anything that cannot name a host answers 422), the block type (rate_limit, under_attack, manual, autoban), the category, and since / until as ISO-8601 timestamps. The list is paged on the agent (100 per page). The hour chart covers the last 24 hours in the viewer's local time; every hour is present and an empty one reads zero. The banned badge is true only while the address is banned right now — in a fail2ban jail (every jail is asked), on the HTTP-layer list, or by an auto-ban younger than its hour. Unban & Whitelist lifts the block first and then adds the address to the whitelist; when the second step is refused the toast says so, and the address stays unblocked but untrusted. Refresh, the TXT export, Clear Log (empties both tables; audit firewall_logs_cleared) and the Clear All Bans twin sit in the toolbar. Retention is 24 hours, capped at 50,000 blocked and 10,000 suspicious rows; the cleanup commits its deletes before the WAL checkpoint, so a busy writer only defers the checkpoint. Firewall notifications go through the shared notification service in the firewall category, so the operator's e-mail and panel preferences apply to them.

Endpoints used: GET /firewall/status, GET/POST /firewall/rules + PUT/DELETE /firewall/rules/{rule_id}, GET/POST /firewall/blocked-ips + DELETE /firewall/blocked-ips/{ip} + POST /firewall/blocked-ips/clear-all, GET/POST /firewall/whitelist, GET/POST /firewall/rate-limits, POST /firewall/attack-mode/{enable,disable,auto-enable,auto-disable}, GET /firewall/logs/{stats,blocked,suspicious}, DELETE /firewall/logs; the account-level /accounts/{username}/firewall/blocked-ips endpoints apply the same server-wide rule.

Still Need Help?

Our support team is here around the clock for anything you can't find above.