docs(admin): rewrite + extend WHP super-admin section from real UI
All checks were successful
Build and deploy / deploy (push) Successful in 24s

Verified every page against the live admin panel on whp01 (read-only).
Five existing articles rewritten; one new article added; customer-facing
backups article updated to match server reality.

Article changes
- overview: super admin = the root user only (no UI to add another);
  WHMCS portal route doesn't apply for admin; accurate sidebar map of
  every admin-only section; customer backups don't cover server config
  (multiple locations, not just /etc — full-server backup is the right
  safety net).
- server-settings: walked all six tabs (System / Services / Mail / DNS
  / Network & SSL / Security); clarified that host Apache + PHP-FPM
  serve the WHP control panel, not customer sites; that MySQL runs as
  a container so host MySQL config is client-facing; that custom
  container needs are met by publishing a custom Docker image (linked
  to repo.anhonesthost.net/cloud-hosting-platform/ for examples).
- coraza-waf: real Firing rules / CRS catalog / Activity tabs; global
  WAF mode pill (off/detect/enforce); per-rule + per-host overrides;
  Ask AI link; security.db source-of-truth + SIGHUP reload note.
- site-monitoring: split into the three actual admin pages — AI Monitor
  dashboard, Issues, Ignore Rules — with stat tiles + health-check
  timeline + ignore-rule AND-semantics.
- user-management: account types corrected to full / domain_dns /
  mail_dns (verified in web-files/pages/user-management.php:26);
  system users are protected against deletion (verified is_protected_user
  in web-files/libs/usermgmt.php:697); delegated users are admin-editable
  (not read-only); suspension page is served by haproxy's 503 errorfile
  (verified in haproxy-manager-base/haproxy_tarpit_config.txt:31) so
  troubleshooting points at haproxy reload / container logs.
- new admin/backups: customer-data backups vs full-server backups;
  auto-backups only run with a default target; how to add global vs
  per-customer targets; how to fire on-demand backups for any user;
  troubleshooting around missing targets / failed test / disk pressure.
- how-to/backups (customer): aside about default-target requirement;
  new section explaining what full-server backups cover vs customer
  backups (managed plans + VDS covered by AnHonestHost; elsewhere is
  the server operator's responsibility).

New components / tooling
- admin-signin partial: 'sign in directly at :8443 as root'.
- Head.astro override + medium-zoom: click-to-zoom lightbox on every
  article image; auto-reattaches after Starlight client navigation.
- capture-admin.ts: read-only Playwright capture for admin docs with
  multi-pass redaction (server hostnames, mail server, customer
  domains, customer usernames in table cells, IPs except RFC1918 and
  public resolvers, password/key/token/secret/api input values, plus
  LiteLLM URLs, model names, JWT/sk-prefix API keys, root → admin).
This commit is contained in:
2026-05-18 10:49:43 -07:00
parent 8c965f76d2
commit 119d376029
21 changed files with 760 additions and 203 deletions

3
.gitignore vendored
View File

@@ -30,3 +30,6 @@ GEMINI.md
# local-only screenshot creds
tools/screenshots/.env
# Admin capture-script discovery artifacts (scratch only)
tools/screenshots/_admin-*-links.json

View File

@@ -24,6 +24,9 @@ export default defineConfig({
// Inline-SVG brand mark + "Knowledge Base" label.
// Inlining lets the SVG's currentColor follow the active theme.
SiteTitle: './src/components/SiteTitle.astro',
// Wraps Starlight's default <Head> to add a click-to-zoom lightbox
// (medium-zoom) that targets article content images.
Head: './src/components/Head.astro',
},
customCss: [
'@fontsource-variable/inter',
@@ -60,7 +63,7 @@ export default defineConfig({
},
{
label: 'Admin',
badge: { text: 'Draft', variant: 'caution' },
// badge removed once content was verified against the real UI
items: [{ autogenerate: { directory: 'whp/admin' } }],
},
],

7
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"astro": "^6.3.1",
"medium-zoom": "^1.1.0",
"sharp": "^0.34.5"
},
"devDependencies": {
@@ -4359,6 +4360,12 @@
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
"license": "CC0-1.0"
},
"node_modules/medium-zoom": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/medium-zoom/-/medium-zoom-1.1.0.tgz",
"integrity": "sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==",
"license": "MIT"
},
"node_modules/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",

View File

@@ -17,6 +17,7 @@
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"astro": "^6.3.1",
"medium-zoom": "^1.1.0",
"sharp": "^0.34.5"
},
"devDependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

34
src/components/Head.astro Normal file
View File

@@ -0,0 +1,34 @@
---
import Default from '@astrojs/starlight/components/Head.astro';
---
<Default><slot /></Default>
<script>
// Lightbox: click-to-zoom on article images. Loaded once per page; auto-reattaches
// after Starlight client-side navigation.
import mediumZoom from 'medium-zoom';
import 'medium-zoom/dist/style.css';
const SELECTOR = '.sl-markdown-content img:not(.no-zoom)';
let zoom: ReturnType<typeof mediumZoom> | null = null;
function refresh() {
if (!zoom) {
zoom = mediumZoom(SELECTOR, {
background: 'rgba(10, 22, 40, 0.92)',
margin: 32,
});
} else {
zoom.detach();
zoom.attach(SELECTOR);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', refresh, { once: true });
} else {
refresh();
}
document.addEventListener('astro:after-swap', refresh);
</script>

View File

@@ -0,0 +1,120 @@
---
title: Backups
description: How WHP's automatic backups work, the default-target requirement, full-server backups vs customer data backups, and managing backup targets.
sidebar:
order: 6
---
import { Aside } from '@astrojs/starlight/components';
import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
import AdminSignIn from '~/content/partials/admin-signin.mdx';
import Support from '~/content/partials/support-link.mdx';
<SuperAdmin />
WHP backs up **customer data** (sites and databases) when an admin has configured a default backup target. WHP does **not** back up the full server — that's a separate concern.
## Two different things called "backup"
It's worth being precise:
- **Customer data backups** — what WHP does. Site files (per user, per domain) and databases. Configured per server via backup targets. These start automatically once a default target exists.
- **Full server backups** — backing up the host OS, `/etc`, container images, the WHP install itself, etc. **WHP does not do this.**
### Full server backups
| Where WHP runs | Who owns full-server backups |
|---|---|
| **Our Virtual Dedicated Server (VDS)** plans | Included. AnHonestHost snapshots the VDS at the platform level. |
| **Anywhere else** | The server operator. WHP doesn't ship a full-server backup mechanism — you'll need to set up something at the OS / hypervisor level. |
If you're running WHP on your own infrastructure, plan accordingly. WHP's configuration isn't all in `/etc` — there are config and state files in the WHP install directory, in Docker volumes for the platform containers (HAProxy, MySQL, Postgres, Valkey, the WAF), and in service-specific paths elsewhere on the host. The safest approach is a **full-server backup** (image snapshot or filesystem-level backup) rather than trying to enumerate paths.
## Sign in as super admin
<AdminSignIn />
## How customer auto-backups start
**Customer auto-backups don't run until at least one default backup target exists.** A fresh WHP install has no targets and no schedule — every user's backup status is "no targets available" until an admin sets one up.
Once a default target is configured, the platform begins automatic daily backups for every customer with sites or databases. Customers see their own backup history under the [Backups](/whp/how-to/backups/) page in their account.
## The Backup Management page
Sidebar → **Backups**. The admin view of this page mirrors the customer view, with extra controls:
- **Stat tiles** — Total Backups, Total Size, Sites, Databases. Server-wide totals across every account.
- **Create New Backup** — fires an on-demand backup. Admin form adds a **User** dropdown so you can backup any customer's account, not just your own.
- **Backup Targets** — the table of destinations available on the server. The **Global** column distinguishes shared targets from per-account targets.
### Backup targets
Each target row has:
- **Name** — your label for the destination.
- **Type** — S3 (and other supported types) — built-in support for S3-compatible storage (AWS S3, Cloudflare R2, MinIO, etc.).
- **Connection** — the endpoint URL and bucket / path.
- **Retention** — how long backups are kept (default 5 days).
- **Max Backups** — cap on the number of snapshots retained (default 10).
- **Global** — `Yes` if every account can use this target, `No` if it's bound to a single account.
- **Actions** — **Test** (verify credentials and write a probe object), **Edit**, **Delete**.
### Adding a global backup target
Click **+ Add Backup Target** and fill in:
1. **Name** — descriptive label.
2. **Type** — pick S3 (or whichever storage backend you want).
3. **Endpoint URL** — for non-AWS S3 (Cloudflare R2, MinIO, Wasabi, etc.), point at the provider's endpoint.
4. **Bucket / path**.
5. **Access key / Secret** — the credentials WHP will use.
6. **Retention / Max Backups** — server-wide defaults for any account that uses this target.
7. **Global** — leave **on** so every account can use it as their default destination.
Click **Test** before saving to confirm WHP can reach the bucket. A good target round-trips a probe object in under a second.
<Aside type="tip">
Keep one global target as the default. Customers who want their own offsite (e.g. their own S3 bucket) can add a per-account, non-global target — visible in the same table for the admin, but only usable by that one customer.
</Aside>
### Per-customer (non-global) targets
Customers can add their own backup targets from the customer-side Backups page. Those show up here with **Global: No** plus a note linking them to the owning account. Admins can edit or delete those on the customer's behalf when they need help.
### Triggering an on-demand backup for a user
In the **Create New Backup** form:
1. Pick the customer in the **User** dropdown.
2. Pick a **Backup Type** (Sites / Databases / both).
3. Pick a **Backup Target**.
4. **Start Backup**. Progress is visible in the run history below.
This is the right path when a customer asks for a fresh backup right before a risky migration.
## Verifying customer backups are succeeding
The Total Backups and Total Size tiles climb over time on a healthy server. If they sit flat:
- Confirm at least one target exists with **Global: Yes**.
- Open the run history (lower on the page) and look for failed entries — the error message usually points at credentials or quota.
## Troubleshooting
**No backups are running for any customer.** Confirm at least one **Global: Yes** target exists and that its **Test** button returns success. Without a global default, the scheduler doesn't fire.
**One target is failing.** Click **Test** on the target row. The most common causes are rotated credentials, an incorrect endpoint URL (R2 / MinIO often need an explicit endpoint different from AWS's default), or the bucket lifecycle policy deleting backups before the retention window.
**Customer says their backup is too old.** Check **Max Backups** on the target — if it's lower than their backup cadence × retention window, older backups get pruned.
**Backup ran but `tar` step failed mid-stream.** Disk pressure on the host is a common cause. Check **Disk Usage** (admin sidebar) and consider raising the target's retention so fewer backups stack up on the host before upload.
## Related
- [Backups](/whp/how-to/backups/) — the customer-facing side.
- [Server settings & services](/whp/admin/server-settings/) — including Backup Upload host service.
## Still stuck?
<Support />

View File

@@ -1,95 +1,108 @@
---
title: Coraza WAF rules
description: Tune the Coraza web-application firewall rules running in front of your sites — toggle modes, mute false positives, audit blocks.
description: Set the global WAF mode, tune individual rules, audit blocked requests, and add per-host overrides.
sidebar:
order: 3
badge:
text: Draft
variant: caution
---
import { Aside } from '@astrojs/starlight/components';
import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
import Draft from '~/content/partials/draft-callout.mdx';
import SignIn from '~/content/partials/signing-in.mdx';
import AdminSignIn from '~/content/partials/admin-signin.mdx';
import Support from '~/content/partials/support-link.mdx';
<SuperAdmin />
<Draft />
[Coraza](https://coraza.io/) is an open-source web-application firewall (WAF) that runs as a sidecar in front of your sites. The **Coraza Rules** page in the admin sidebar gives you a UI to set the global mode, tune individual rules, and audit blocked requests — all without rebuilding the sidecar image.
[Coraza](https://coraza.io/) is an open-source web-application firewall (WAF). It runs as a sidecar in front of your sites and inspects incoming requests against rule families like OWASP Core Rule Set v4 (CRS). The admin WHP gives you a UI to manage the rules and audit what's been blocked.
![Coraza Rules page — Firing rules tab](~/assets/screenshots/whp/admin-coraza.png)
## Three operating modes
## Global WAF mode
The WAF runs in one of three modes, set per-site or server-wide:
A coloured pill at the top of the page shows the **Global WAF mode** with a **change** link:
- **Off.** No inspection. Requests pass through untouched.
- **Detect-only.** Inspect every request and log matches, but pass them through. Use this when rolling out the WAF for the first time or when validating a rule change.
- **Enforce.** Inspect every request and **block** any that match an enforcing rule. This is the production setting once you've validated detect-only.
- **Off** — no inspection. Requests pass untouched.
- **Detect** — inspect and log matches; do not block. Use during a roll-out or while validating a rule change.
- **Enforce** — inspect and block anything that matches an enforcing rule.
The WAF is fail-open: if the Coraza sidecar itself is unhealthy, traffic still flows.
The WAF is **fail-open**: if the Coraza sidecar is itself unhealthy, traffic still flows.
## Sign in to WHP
## Sign in as super admin
<SignIn />
<AdminSignIn />
## Where it lives
## The three tabs
Sidebar → **Security → Coraza Rules**. The page lists rule families (CRS 901, 911, 913, 920922, 930934, 941944, 949, 950956, 959, 980) and per-rule controls.
### Firing rules
The default view. Each row is a rule that has matched at least one request in the selected time window (toggle **Last 24h** or **Last 7d** at top right).
Columns:
- **Rule ID** — the CRS or custom rule identifier. **view** opens the rule definition. **Ask AI** opens an explanation of what this rule catches.
- **Hits** — how many times the rule fired in the time window.
- **Top hosts** — the top customer domains that triggered this rule (with per-host hit counts).
- **Top URIs** — the most common request paths that triggered it (helpful for distinguishing scans from real traffic).
- **State** — per-rule override of the global mode. Options: `(default → enforce)`, `(default → detect)`, `(default → score)`, `off`. Picking anything other than `(default → …)` overrides the global mode for that rule.
- **Per-host** — opens a **Hosts (N)** drawer to set per-host overrides for that rule.
<Aside type="tip">
Rule changes apply on the next request. The page reloads the sidecar via SIGHUP after every save (~10ms — no traffic blip).
</Aside>
### CRS catalog
The full OWASP Core Rule Set catalogue (v4 families: 901, 905, 911, 913, 920922, 930934, 941944, 949, 950956, 959, 980). Use this when you want to look up a rule that *hasn't* fired yet — for example, to pre-mute a rule you know will produce false positives on a specific app.
### Activity
A timeline / log of every WAF block in the audit window. Use it to:
- Confirm a customer report (cross-reference the **X-Request-Reference** UUID on the visitor-facing 403 page).
- Spot bursts of activity from the same source IP.
- Tune source-of-truth back-end queries — the audit is read from `security.db`.
## Common tasks
### Roll a new site onto the WAF
<Aside type="tip">
Always start in **Detect-only** for a new site. Move to **Enforce** only after you've watched the logs and confirmed no legitimate traffic is being matched.
</Aside>
1. Open **Security → Coraza Rules**.
2. Find the site and set mode to **Detect-only**.
3. Drive normal traffic for at least 24 hours.
4. Open the **Audit log** and filter to that site. Confirm no legitimate request is matching an enforcing rule.
5. Switch the site to **Enforce**.
1. Set the rule's **State** to `(default → detect)` for that site via the Per-host drawer.
2. Drive normal traffic for at least 24 hours.
3. Open the **Activity** tab and filter to that host. Confirm no legitimate request is matching an enforcing rule.
4. Flip the host to **enforce**.
### Mute a noisy rule
When a rule is firing on legitimate traffic for one site:
1. Find the rule in **Firing rules**.
2. Click **Per-host → Hosts (N)** to add a per-site mute, OR change the rule's **State** column to mute it globally.
1. Click the audit-log row to see the rule_id and the matched request.
2. From the **Coraza Rules** page, find the rule by ID.
3. Pick **Ignore for this site** (per-site mute) or **Ignore globally** (server-wide mute).
4. Save. The rule stops firing on the next request.
Per-site is almost always the right scope. Use global mute sparingly — it weakens the WAF for every site.
Prefer per-host. Global mute weakens the WAF for every site.
### Audit a block
Customer reports a request was wrongly blocked? The branded 403 page that visitors see includes an **X-Request-Reference** UUID. Cross-reference it:
Customer reports a request was wrongly blocked? The branded 403 page includes an **X-Request-Reference** UUID. Cross-reference it:
1. In the **Audit log**, search for the UUID.
2. The audit row shows the matched rule_id, the source IP, the URL, and the offending parameter.
1. In the **Activity** tab, search for the UUID.
2. The row shows the matched rule ID, source IP, URL, and offending parameter.
3. Decide whether to mute the rule (see above) or leave it — many "false positives" turn out to be real attempts.
## Things to know
## Implementation notes
- **Rule changes apply on the next request.** No service restart needed for tuning.
- **Adding or removing rules requires a full reload** of `coraza-spoa`, not just a SIGHUP. The panel handles this for you; if you edit rule files by hand, `docker restart coraza-spoa`.
- **Real source IPs are in the audit log.** Even though haproxy fronts the WAF, we propagate the real client IP through the SPOE messages.
- **`SecRuleRemoveById` plus a new rule needs a full restart**, not just a config reload. Again, the panel handles this when you change rules through the UI.
- **Source of truth: `security.db`.** All rule edits go through the panel and write to that SQLite file; manual file edits won't survive a regeneration.
- **Reload mechanism.** Rule edits apply via SIGHUP to `coraza-spoa`; adding or removing whole rule files requires a full container restart, which the panel performs when the change needs it.
- **Real source IPs are in the audit log** — even with HAProxy in front of the WAF, the real client IP is propagated through the SPOE messages.
## Troubleshooting
**A rule shows enabled but doesn't fire.** Check that the site is in **Detect-only** or **Enforce** mode. A site in **Off** mode bypasses every rule, including enabled ones.
**A rule shows enabled but doesn't fire.** Check that the host is in **detect** or **enforce** mode. A host in **off** bypasses every rule.
**The audit log is empty.** Confirm `coraza-spoa` is healthy on the **Services** page. If it's restarting in a loop, check the container logs — most often a malformed rule file or a missing include.
**The Activity log is empty.** Confirm `coraza-spoa` is healthy on the **Services** tab of Server Settings. If it's restarting in a loop, check the container logs — usually a malformed rule file or a missing include.
**Edits revert on restart.** Make sure you're editing through the panel; manual edits to files outside the panel-managed path are overwritten by config regeneration.
**Edits revert on restart.** Make sure you're editing through the panel; manual edits outside the panel-managed path are overwritten.
## Related
- [Server settings & services](/whp/admin/server-settings/)
- [Site Monitoring rules](/whp/admin/site-monitoring/)
- [AI Monitor, Issues & Ignore Rules](/whp/admin/site-monitoring/)
## Still stuck?

View File

@@ -1,43 +1,67 @@
---
title: Admin overview
description: What WHP super admin unlocks — server-wide controls for services, security rules, monitoring, and users.
description: What WHP super admin unlocks — server-wide controls for services, mail, DNS, security, monitoring, and users.
sidebar:
order: 1
badge:
text: Draft
variant: caution
---
import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
import Draft from '~/content/partials/draft-callout.mdx';
import Support from '~/content/partials/support-link.mdx';
<SuperAdmin />
<Draft />
## What super admin unlocks
The same WHP panel you'd use on a customer account scales up: customers with **super admin** access also see server-wide pages for managing services, firewall rules, monitoring policy, and users. The customer-facing sections (Sites, Domains, Email, etc.) work the same way; the admin sections sit alongside them, gated to admins.
WHP's super admin role exposes server-wide pages alongside the customer-facing nav. The customer pages (Sites, Domains, Email, etc.) work identically for everyone; the admin pages sit alongside them and are gated to the super admin only.
Today, super admin is most commonly handed to customers running a [Virtual Dedicated Server](https://anhonesthost.com/vds) — they get full server control as part of the plan.
**Today, the only super admin is the `root` user on the server.** There's no UI to add another super admin — if you need additional people to have super admin, share the root credentials via your usual secret-sharing flow (or [open a ticket](https://secure.anhonesthost.com/submitticket.php) if you need a different model).
This typically applies to customers running a [Virtual Dedicated Server](https://anhonesthost.com/vds) — they get full server control as part of the plan and sign in to WHP as `root`.
### Signing in as super admin
import AdminSignIn from '~/content/partials/admin-signin.mdx';
<AdminSignIn />
## Admin-only sidebar sections
When you sign in as a super admin, these sections appear in addition to the customer-facing nav:
- **AI Monitor** — the admin dashboard, plus **Issues**, **Site Reports**, and **Ignore Rules**.
- **Security Management** — security policy across the server.
- **Coraza Rules** — Web-application firewall (WAF) rule tuning, audit, and global mode.
- **User Management** — create WHP users, set account types, manage existing users.
- **User Resources** — per-user CPU/RAM/disk allowances and current usage.
- **Delegated Users** — list of contractor / sub-account access grants.
- **Active Sessions** — every signed-in browser across the server.
- **Server Settings** — System, Services, Mail, DNS, Network & SSL, Security tabs.
- **Disk Usage** — server-wide disk consumption breakdown.
- **Announcements Management** — edit the announcements that appear on every customer's dashboard.
- **Update Management** — apply WHP platform updates.
- **Docker Management** — see and manage every container on the host.
- **Valkey Admin** — server-wide Valkey configuration.
- **Container Boot & Health** — boot-order and per-container health.
- **Site Disable Audit** — record of sites disabled / re-enabled.
- **Account Suspensions** — suspended customer accounts.
## What's in this section
- **[Server settings & services](/whp/admin/server-settings/)** — restart services, manage modules and runtimes, edit server-wide configuration.
- **[Coraza WAF rules](/whp/admin/coraza-waf/)** — view, tune, and tune out web-application-firewall rules across all your sites.
- **[Site Monitoring rules](/whp/admin/site-monitoring/)** — manage the rules that drive Site Monitoring alerts.
- **[Users & delegated access](/whp/admin/user-management/)** — create sub-users, delegate panel access, manage SFTP/SSH users.
- **[Server settings & services](/whp/admin/server-settings/)** — the six tabs under Server Settings: system info, restart services, mail-server config, DNS / nameservers, HAProxy + SSL, and integration API keys.
- **[Coraza WAF rules](/whp/admin/coraza-waf/)** — set the global WAF mode, tune individual rules, and audit blocked requests.
- **[AI Monitor, Issues & Ignore Rules](/whp/admin/site-monitoring/)** — the three pages that drive the Site Monitoring add-on.
- **[Users & delegated access](/whp/admin/user-management/)** — create accounts, set account types, delegate access, and handle suspensions.
- **[Backups](/whp/admin/backups/)** — configure the default backup target so customer auto-backups start running. Full-server backups are a separate, plan-dependent concern.
## Things to know before you change server-wide settings
- **One change can affect every site on the server.** Where customer-side pages scope changes to one site, admin pages typically scope to the whole server.
- **Service restarts are visible to live traffic.** Restart Apache or PHP-FPM during a quiet window when possible.
- **Backups still apply.** Server-level changes don't bypass the [backups](/whp/how-to/backups/) you have configured; you can roll back the data side, but service-config changes you made by hand aren't snapshotted unless you back up `/etc` somewhere on your own.
- **Customer backups don't cover server config.** The [customer backups](/whp/how-to/backups/) you've configured snapshot site files and databases, not server-level changes you make by hand. Server config lives in several places — not just `/etc` — so the right safety net is a **full-server backup**. On AnHonestHost-managed plans and VDS we handle that for you; running WHP elsewhere, the operator is responsible. See [Backups](/whp/admin/backups/) for the full picture.
## Related
- [What is containerized hosting?](/whp/getting-started/what-is-containerized-hosting/) — the differences between container plans and full server access.
- [What is containerized hosting?](/whp/getting-started/what-is-containerized-hosting/) — differences between container plans and full server access.
## Still stuck?

View File

@@ -1,74 +1,115 @@
---
title: Server settings & services
description: Restart Apache, PHP-FPM, MySQL; manage PHP modules and runtimes; edit server-wide configuration.
description: Restart services, configure mail server, manage DNS / nameservers, HAProxy + system SSL certificates, and integration API keys.
sidebar:
order: 2
badge:
text: Draft
variant: caution
---
import { Aside } from '@astrojs/starlight/components';
import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
import Draft from '~/content/partials/draft-callout.mdx';
import SignIn from '~/content/partials/signing-in.mdx';
import AdminSignIn from '~/content/partials/admin-signin.mdx';
import Support from '~/content/partials/support-link.mdx';
<SuperAdmin />
<Draft />
The **Server Settings** page lives in the admin sidebar and has six tabs along the left rail. Each tab is a different surface area of the server you can inspect or change.
This page covers the server-wide controls available in the WHP admin sections — restarting services, managing PHP modules and versions, and adjusting server-level configuration.
![Server Settings — System tab](~/assets/screenshots/whp/admin-srvset-system.png)
## Sign in to WHP
## Sign in as super admin
<SignIn />
<AdminSignIn />
## Restarting services
## The six tabs
The admin **Services** page lists the long-running services that run on the server: Apache (front-end web), PHP-FPM (one or more pools), MySQL or MariaDB, the mail stack, and any add-on services like Valkey or PostgreSQL if you've enabled them.
### System
Each service has a status indicator and **Restart**, **Stop**, and **Start** controls. Restart is the safe default for picking up new configuration.
Read-only system summary plus two simple change controls:
- **System Information** — Hostname, Operating System, Kernel, Timezone, Uptime, Load Average, Disk Usage, Memory Usage.
- **Hostname Settings** — change the server's FQDN. Restart the relevant services after a hostname change.
- **Timezone Settings** — change the system timezone (affects cron timing, backup schedules, log timestamps).
<Aside type="caution">
Restarting a service drops in-flight connections. For Apache and PHP-FPM, this means active web requests will fail to complete; for MySQL, in-flight transactions roll back. Pick a low-traffic window if you can.
Changing the hostname affects the certificates and DNS records that reference it. Plan for the related re-issuance and DNS propagation before changing it on a live server.
</Aside>
## Managing PHP modules and runtimes
### Services
WHP can run multiple PHP runtimes side-by-side (PHP 8.3, 8.4, etc.). With super admin you can:
Service status and restart controls.
- Install additional PHP runtimes via the **PHP Versions** admin page.
- Add or remove extensions per runtime — common ones (mbstring, intl, opcache, imagick, redis, etc.) are toggles; less common ones may require a [support ticket](https://secure.anhonesthost.com/submitticket.php).
- Edit a runtime's `php.ini` from the **PHP Configuration** sub-page, then reload PHP-FPM to pick it up.
- **Service Status** — health pills for the host-side services:
- **Apache** and **PHP-FPM** on the host serve the **WHP control panel itself**, not customer sites. Customer sites run inside their own per-site containers, separate from these host services.
- **Docker** — the host's Docker daemon. If this is down, no customer container will start.
- **ProFTPD** — host FTP service (used by FTP-enabled customer accounts).
- **Backup Upload** — the host-side uploader that streams backups to your configured backup targets.
- **Restart Services** — checkboxes per service plus **Restart Selected Services**.
- **Docker Container Management** — status of the core platform containers (Mysql, Haproxy manager, Memcache, Postgresql) and a per-container **Execute Operation** picker (e.g. restart a single container).
Switching a site to a different PHP version is done on the customer side — open **Sites → your site** and pick from the **Container Type / PHP Version** dropdown. The runtimes available there come from this admin list.
<Aside type="caution">
Restarting host Apache or PHP-FPM kicks the **control panel** offline briefly — customers can't sign in to WHP for a few seconds. It does **not** affect customer sites (those are in their own containers). For MySQL, in-flight transactions in the MySQL container roll back. Pick a quiet window when you can.
</Aside>
### Mail
Two panels:
- **Mail Server** — set the **Mail Server Hostname** (used for MX records on new domains and as the IMAP host for archival). Configure the Mail Server API (URL, Username, Password) that WHP uses to provision mailboxes. Toggle **Enable Mailserver API Debug Logging** when troubleshooting; it writes mailserver API requests/responses to the PHP error log.
- **Outbound Email (SMTP)** — configure SMTP for outbound system alerts and customer AI Monitor notifications. Toggle **Enable Outbound Email** and provide the relay's credentials.
### DNS
Two panels:
- **WHP Nameserver Configuration** — set the primary and secondary nameserver hostnames and IPs. These are baked into every customer's DNS zone, so changing them affects every domain you host.
- **Network DNS Settings** — set the upstream resolvers the server uses (defaults to Cloudflare `1.1.1.1` and Google `8.8.8.8`).
- **DNS Configuration Settings** — default TTL for new DNS records (6086400 seconds).
<Aside type="caution">
Changing nameserver hostnames or IPs invalidates the zone delegation for every customer domain pointed at the old values. Migrate slowly; expect lag in the order of TTL × propagation window.
</Aside>
### Network & SSL
Operational controls for HAProxy and system-service certs:
- **HAProxy Configuration Management** — **Regenerate** (rebuild config for every active site), **Reload** (apply config without restart), **Health Check** (probe HAProxy).
- **HAProxy API Key** — Bearer token used to authenticate against the HAProxy Manager API. After rotating, restart the HAProxy container.
- **System Service SSL Certificates** — request a Let's Encrypt cert for system-level services like the WHP panel itself and FTP, by picking the service and the domain name.
### Security
API keys for **external integrations** like WHMCS. Not customer-facing.
- **Create New API Key** — Key Name, Rate Limit (requests per hour), Permissions (User Management, Resource Management, SSO Access, System Statistics, AI Monitor Management), an optional IP whitelist, and Notes.
- **Existing API Keys** — list of issued keys with their permissions and rate limit. Revoke by removing the row.
## Server-wide configuration files
For settings that aren't exposed in the panel, you can SSH to the server and edit configuration directly:
The host runs the WHP control panel and orchestrates customer containers. Most customer-affecting configuration lives **inside** containers, not on the host. A short map:
- **Apache:** `/etc/httpd/conf.d/` for per-app drop-ins; per-site vhosts are generated from WHP and live in a directory the panel manages. **Don't** edit generated vhosts by hand — they'll be overwritten on the next config regeneration.
- **PHP-FPM pools:** `/etc/php-fpm.d/` for runtime pool tweaks. Pool defaults are templated by WHP; edits to per-site pools are overwritten on regeneration.
- **MySQL/MariaDB:** `/etc/my.cnf.d/`. The defaults are tuned for the server's resource profile; large-tweak changes are usually best left to a ticket so we can advise.
- **Control-panel Apache:** `/etc/httpd/conf.d/` on the host configures the WHP panel's own Apache. Editing here changes how the panel serves; it doesn't change how customer sites serve.
- **Control-panel PHP-FPM:** `/etc/php-fpm.d/` on the host configures the panel's PHP. Same scope.
- **MySQL:** the MySQL instance runs as a container. Files under `/etc/my.cnf.d/` on the host are surfaced to customer database connections — they're effectively client-facing settings, not host settings.
- **HAProxy:** runs as a container with its own volume. Reload via the **Network & SSL** tab; don't hand-edit files in the container.
After editing, reload the relevant service from the WHP **Services** page (or via systemctl on the box).
### Customer-side container customisation
## Common admin tasks
If a customer needs a non-standard runtime, library, or service inside their site's container — that's done by **building a custom Docker image** and adding it as a container type option in WHP, not by editing host-level config.
**Drop a hot file cache.** From the **Services** page, click **Reload** on PHP-FPM. This is the right move after editing `php.ini` or an extension list.
The pattern is documented in our cloud-container repos. See <a href="https://repo.anhonesthost.net/cloud-hosting-platform/">repo.anhonesthost.net/cloud-hosting-platform/</a> for the cloud-apache-container and cloud-node-container examples — they show the layout, build, and how to publish an image so it appears in the **Container Type** dropdown on the Sites page.
**Free disk space on a full server.** Check `/var/log/` first — log rotation may be lagging. WHP rotates app logs into the per-site `logs/` directory; server-level logs in `/var/log` are yours to rotate via `logrotate` config in `/etc/logrotate.d/`.
**See what's eating resources.** The admin **Resource usage** page shows aggregate CPU, RAM, disk I/O, and per-process drilldowns. For deeper inspection, SSH in and use `top`, `htop`, or `iotop`.
<Aside type="tip">
Cloud container images are a one-time setup per stack you want to support. Once published, every site using that container type benefits automatically — no per-site shell access required.
</Aside>
## Troubleshooting
**Service won't restart.** The Services page surfaces the systemd error; if it's `failed (exit-code)`, check `journalctl -u <service>` on the box for the underlying message. The most common cause is a syntax error in a config file you just edited.
**A service won't restart.** Check `journalctl -u <service>` (for systemd-managed services) or `docker logs <container>` (for containerized ones). The most common cause is a syntax error in a config file you just edited.
**PHP module toggle has no effect.** PHP modules need a PHP-FPM **reload** to be picked up. The toggle should do this automatically; if it doesn't, click Reload manually.
**Edits to a generated vhost keep disappearing.** That file is generated. Put your customisation in a per-app drop-in under `/etc/httpd/conf.d/`, or open a ticket about adding a stable include hook.
**Edits to a generated vhost keep disappearing.** That file is generated. Put your customisation in a per-app drop-in under `/etc/httpd/conf.d/` instead, or open a ticket about adding a stable include hook.
**Mailserver API debug log too noisy.** Toggle **Enable Mailserver API Debug Logging** off on the **Mail** tab once you've finished diagnosing.
## Related

View File

@@ -1,94 +1,112 @@
---
title: Site Monitoring rules
description: Configure Site Monitoring across every site on the server — global ignore lists, alert routing, and severity tuning.
title: AI Monitor, Issues & Ignore Rules
description: The three admin pages that drive the Site Monitoring add-on — AI Monitor dashboard, Issues, and Ignore Rules.
sidebar:
order: 4
badge:
text: Draft
variant: caution
---
import { Aside } from '@astrojs/starlight/components';
import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
import Draft from '~/content/partials/draft-callout.mdx';
import SignIn from '~/content/partials/signing-in.mdx';
import AdminSignIn from '~/content/partials/admin-signin.mdx';
import Support from '~/content/partials/support-link.mdx';
<SuperAdmin />
<Draft />
[Site Monitoring](/whp/add-ons/monitoring/) is the customer-facing alerting add-on. The admin side exposes three pages — together they let you tune what gets monitored, what surfaces as a customer-visible issue, and what gets suppressed.
[Site Monitoring](/whp/add-ons/monitoring/) is the customer-facing alerting product. With super admin access, you can manage the rules that drive those alerts at the server level — including a server-wide ignore list, alert routing, and severity tuning.
## Sign in as super admin
## Sign in to WHP
<AdminSignIn />
<SignIn />
## AI Monitor (admin dashboard)
## Where it lives
Sidebar → **AI Monitor → Dashboard**. The operational heartbeat of the whole monitoring pipeline.
Sidebar → **Site Monitoring** (admin view). The page shows two perspectives:
![AI Monitor admin dashboard](~/assets/screenshots/whp/admin-monitor-admin.png)
- **Customer feed** — what your site owners see when they sign in.
- **Admin tools** — the rule library, global ignore list, and alert configuration.
Panels:
- **AI Log Monitor Status** — overall on/off plus three sub-statuses:
- **Minute-cadence poll** — the cron that scans logs every minute.
- **Health API** — the internal API that exposes per-container health.
- **HAProxy stats** — the proxy stats feed used for error-rate tracking.
- **Stat tiles** — Last Run, Errors Tracked, Remediations, API Calls Today (with a rate-limit denominator).
- **Health Check Timeline (last 7d)** — every state transition (`cpu`, `swap`, `haproxy`, etc.) with its severity and an **AI Diagnosis** explanation.
Use this page to confirm the pipeline is healthy and to drill into recent state changes. The AI Diagnosis text gives a plain-language summary of *why* a transition happened — useful for context before you act.
## Issues
Sidebar → **AI Monitor → Issues**. The customer-visible findings, server-wide.
![Issues page](~/assets/screenshots/whp/admin-issues.png)
Four stat tiles at the top:
- **Critical** — open critical-severity issues.
- **Warning** — open warning-severity issues.
- **Auto-resolved (review)** — issues the monitor closed on its own that may still need a human glance.
- **Active suppressions** — issues currently muted by an Ignore Rule.
Filter row: Scope (All / specific user), Status (Open / Closed / All), Severity, Source, Signature prefix.
Bulk actions: **Mark Fixed**, **Ignore**, **Delete**.
Each row has its own quick-actions: **Fix** (close it) and **Ignore** (create an ignore rule from this row's match criteria).
## Ignore Rules
Sidebar → **AI Monitor → Ignore Rules**. Match criteria that prevent matching findings from becoming customer-visible issues.
![AI Monitor Ignore Rules page](~/assets/screenshots/whp/admin-ignore-rules.png)
Each rule has these fields:
- **Scope** — `user` (just one customer) or `global` (every customer).
- **Target** — the user or domain the rule applies to.
- **Match** — a comma-separated set of `field=value` predicates (e.g. `cat=degraded & title~"Beaver Builder cache files missing"`). Match fields combine with **AND** semantics — every predicate must match.
- **Reason** — a short note for future-you explaining why the rule exists.
- **Hits / Last hit** — how often the rule has matched, and when.
- **Enabled** — toggle without deleting.
<Aside type="tip">
Prefer **per-user** ignores over global. Global rules silence the finding for everyone on the server.
</Aside>
## Common tasks
### Add a global ignore rule
### Mute a known-noisy finding for one customer
When a signature is noisy for every site (for example, a known scanner you allow on your own infrastructure):
1. Open **Issues**, find the row.
2. Click the row's **Ignore** action — that pre-fills an Ignore Rule with the matching criteria scoped to that user.
3. Add a **Reason** so future-you (or someone else on the team) understands why it exists.
4. Save. Future matching findings will be suppressed; the **Active suppressions** tile will tick up.
1. Open **Site Monitoring → Global Ignore Rules**.
2. Click **Add Rule**.
3. Define the match condition (rule_id, source IP/CIDR, URL pattern, or a combination).
4. Add a short note so future-you remembers why this exists.
5. Save. Matching events stop firing alerts immediately.
### Investigate an "Auto-resolved (review)" issue
<Aside type="caution">
Global ignore rules silence every site on the server. Prefer per-site ignore (set in the customer-facing Site Monitoring page) when the noise is site-specific.
</Aside>
These are issues where the underlying signal recovered before a human looked at them. Open the row to see the AI Diagnosis and the original detection. If the resolution looks legitimate, click **Mark Fixed**; if you're suspicious, leave it open and add a comment for context.
### Tune severity for a rule
### Tune brute-force detection
If a rule is set to **critical** but you've decided it's really informational in your environment:
Brute-force rules live in **Coraza Rules** (rule families CRS-913 / 921 / 942 / 949) rather than AI Monitor. AI Monitor surfaces the *effects* (error spikes) once a brute-force pattern fires, but the matching itself is in the WAF — see [Coraza WAF rules](/whp/admin/coraza-waf/).
1. Find the rule in **Site Monitoring → Rules**.
2. Open the rule and change its severity to one of: informational / warning / critical.
3. Save. The severity change applies to new events from that rule.
## Routing alerts
Severity matters because **SMS notifications fire only on critical**. Downgrading from critical to warning silences SMS without silencing the rule.
### Route alerts somewhere other than the default
Default routing: alerts go to the contact email on the account. You can:
- **Add additional email recipients** — useful for a shared ops alias.
- **Enable SMS for critical alerts** — wire your phone number on the **Alert Routing** page.
- **Forward to a webhook** — for integration with Slack, PagerDuty, or your own incident pipeline.
## Brute-force detection
Brute-force detection is a separate rule family and has its own per-site sensitivity. From the admin view, you can adjust:
- The window in which repeated failures count.
- The threshold at which the rule fires.
- Whether the rule auto-blocks the source IP (recommended) or only alerts.
## Things to know
- **Ignore lists don't stop logging.** They only suppress alerts. The events still appear in the audit feed so you can see what's actually happening.
- **Rules apply on the next log scan**, typically within a minute.
- **A muted rule for one customer doesn't affect others.** Per-site ignore is scoped tightly.
Customer email alerts are sent via the SMTP relay configured on **Server Settings → Mail → Outbound Email (SMTP)**. Toggle **Enable Outbound Email** off there if you need to silence outbound notifications for a maintenance window.
## Troubleshooting
**No alerts arriving for a known event.** Check the **Global Ignore Rules** list and any per-site ignores. Also confirm the rule's severity isn't set to informational (no email or SMS by default).
**Pipeline shows "stale" — Last Run more than a few minutes ago.** Check the cron's container on **Server Settings → Services → Docker Container Management**. The monitor poll is `whp-monitor-poll`.
**SMS not firing on critical.** Confirm SMS is enabled in **Alert Routing** and the phone number is verified.
**Issues appearing for a known noisy site.** Add an Ignore Rule scoped to that user.
**No customer alerts arriving.** Confirm Outbound Email is enabled and the SMTP relay is reachable from the server.
## Related
- [Site Monitoring add-on](/whp/add-ons/monitoring/) — what the customer sees.
- [Coraza WAF rules](/whp/admin/coraza-waf/) — request-level firewall, complements monitoring.
- [Site Monitoring add-on](/whp/add-ons/monitoring/) — the customer-facing side.
- [Coraza WAF rules](/whp/admin/coraza-waf/) — request-level firewall.
- [Server settings & services](/whp/admin/server-settings/) — SMTP relay config for outbound alerts.
## Still stuck?

View File

@@ -1,95 +1,144 @@
---
title: Users & delegated access
description: Create sub-users, delegate panel access, and manage SFTP/SSH users at the server level.
description: Create WHP users, set account types, change passwords, plus delegated user access and account suspensions.
sidebar:
order: 5
badge:
text: Draft
variant: caution
---
import { Aside } from '@astrojs/starlight/components';
import SuperAdmin from '~/content/partials/super-admin-callout.mdx';
import Draft from '~/content/partials/draft-callout.mdx';
import SignIn from '~/content/partials/signing-in.mdx';
import AdminSignIn from '~/content/partials/admin-signin.mdx';
import Support from '~/content/partials/support-link.mdx';
<SuperAdmin />
<Draft />
Four admin pages collectively control who can sign in and what they can do on the server:
WHP super admin lets you give other people scoped access to the server — your dev team, a contractor, or a junior admin — without sharing your own credentials.
- **User Management** — create / change-password / delete WHP users.
- **User Resources** — set CPU / RAM / disk allowances per user.
- **Delegated Users** — list of contractor / sub-account grants on customer sites.
- **Account Suspensions** — suspended accounts.
## Three kinds of access
## Sign in as super admin
| Type | What they can do | Where they sign in |
| ------------------- | -------------------------------------------------------------------------------- | --------------------------------------- |
| **WHP sub-user** | Sign in to WHP with their own credentials. You control which sections they see. | Same `:8443` URL as you. |
| **Delegated panel access** | A read-only or scoped-write view onto a specific site, for a contractor. | Same WHP, but scoped to that one site. |
| **SFTP / SSH user** | File access (and optionally SSH) without WHP at all. | SFTP client / SSH terminal. |
<AdminSignIn />
## Sign in to WHP
## User Management
<SignIn />
Sidebar → **User Management**.
## Create a WHP sub-user
![WHP User Management page](~/assets/screenshots/whp/admin-user-mgmt.png)
### Create New User
Every user created here is a **customer account**, not a super admin. (Super admin is the `root` user on the server, and there's no UI to add another.)
Fields:
- **Username** — UNIX-safe username; also becomes the SFTP user and home-directory name (`/docker/users/<username>`).
- **Password** — strong password. The user can change it later from the panel.
- **Account Type** — pick the scope of features this customer should see:
- **Full Hosting** — sites, databases, domains, DNS, email. The default for normal customers.
- **Domain/DNS Only** — domains and DNS records only (no sites, databases, or email).
- **Mail/DNS Only** — email plus domains/DNS (no sites or databases).
Click **Create User** to provision the account. The user's home directory and SFTP credentials are set up immediately.
<Aside type="tip">
WHP sub-users are the right model when someone needs to manage parts of the server alongside you. Use **delegated access** instead if they only need one site, or **SFTP/SSH** if they only need files.
Pick the smallest account type that does the job. You can change it later from the **Actions** column in the user list.
</Aside>
1. Open **Users → WHP Users → Add User** in the admin sidebar.
2. Set a username, a strong password, and an email (used for password reset and 2FA).
3. Choose a **role**: pick from the predefined roles (Admin, Site Manager, Read-only, etc.) or build a custom role with specific pages enabled.
4. (Optional) Enable **Require 2FA** so they have to set up an authenticator app on first login.
5. Save. Share the credentials with them out-of-band; don't email passwords.
### Change User Password
## Delegated access for a single site
Pick the user from the dropdown, enter a new password, click **Change Password**. The user is forced to sign in again on next visit; any in-flight panel sessions are still live until you also revoke them via **Active Sessions**.
Use the customer-facing **Delegated Users** page (sidebar → **Delegated Users**, available on every account) when a contractor only needs to work on one site:
### User Accounts table
1. Open **Delegated Users → Add**.
2. Pick the site they should have access to.
3. Set their permission scope: view-only, manage-files, manage-DNS, etc.
4. Send them the panel URL. They sign in with their own credentials and see only that site.
Columns: **Username**, **UID** (UNIX uid), **Account Type**, **Home Directory**, **Actions** (Account-Type dropdown + Delete).
This is the right path for, say, a freelance designer who needs to upload assets but shouldn't see your other sites or your DNS.
To change a user's account type, change the dropdown in the row and the change applies immediately. The **System** badge on a row marks an internal/system user (such as `root`, `whp`, `daemon`, `www-data`, `nobody`, the various `systemd-*` users, etc.).
## SFTP / SSH users
Pure file access without WHP. Created from the admin **Users → SFTP/SSH** page:
1. Open **Users → SFTP/SSH → Add**.
2. Set the username, password (or paste their public SSH key), and which directories they have access to.
3. Pick whether to grant interactive SSH or restrict to SFTP only.
4. Save. They can now connect with their preferred SFTP/SSH client.
**System users are protected.** The panel refuses to delete any user on the protected list — `delete_user` in `web-files/libs/usermgmt.php` checks `is_protected_user($username)` first and returns *"Cannot delete protected system user"* without touching the OS user. Password changes are blocked the same way, with one exception: `root`'s password can be changed (the rest of the protected list cannot).
<Aside type="caution">
Interactive SSH is powerful — anyone with it can run arbitrary commands inside the container or VM. Restrict to SFTP-only unless they specifically need a shell.
Deleting a non-system user removes their home directory and every site, database, and mailbox associated with them. There is no undo. Use **Account Suspensions** instead for temporary disablement.
</Aside>
## Managing existing users
## User Resources
The user list shows last sign-in, role, and 2FA status. Common actions from each user's row:
Sidebar → **User Resources**. Configure CPU and memory allowances per user.
- **Disable** — keeps the user but blocks sign-in.
- **Delete** — removes the user.
- **Force password reset** — invalidates their current password; they receive an email link.
- **Revoke sessions** — kicks them out of any active panel sessions immediately.
![User Resources admin page](~/assets/screenshots/whp/admin-user-resources.png)
When someone leaves your team, **revoke sessions first** (so they're out *now*), then disable or delete the user.
Each row shows current allocation vs. usage:
- **Max CPU** / **CPU Used** — in 0.25-core increments.
- **Max Mem** / **Mem Used** — in 256 MB increments.
- **Disk** / **Used** — total disk allocation and current consumption (with %).
- **Email** — mailbox slot count.
- **Mail MB** — total mail storage cap.
- **Arch.** — archival email slots used / total.
- **Cont.** — currently-running container count.
Use the **Actions** column to edit a user's caps. Changes apply on the next container restart for that user's sites.
<Aside type="note">
Memory-usage tracking isn't available in some Docker environments. The **Active sites** count reflects running containers.
</Aside>
## Delegated Users
Sidebar → **Delegated Users**. Customers can use the Delegated Users page on their own account to grant a contractor scoped access to one of their sites. The admin view shows every active delegation across the server.
From the admin view you can:
- **Audit** — see who has cross-account access at a glance.
- **Edit** — modify scope, permissions, or expiry on any delegation for an independent customer. Useful when a customer asks support to fix a grant they set up incorrectly.
- **Revoke** — remove a delegation outright.
If a customer reports a delegation issue, this page is where you confirm the grant exists, inspect its scope, and adjust it on their behalf.
## Account Suspensions
Sidebar → **Account Suspensions**. The list of suspended customer accounts.
A suspension takes a customer's sites offline without deleting any data — the customer can be reinstated by removing the suspension. Useful for non-payment, terms-of-service issues, or maintenance hold.
The page lists who's suspended, when, by whom, and why. Reinstate from the action button on each row.
### How the suspension page is served
The "site suspended" page is served by **HAProxy** itself, not by a separate backend. When you suspend an account, WHP rewrites the HAProxy config to point that account's frontends at a 503 errorfile (`/usr/local/etc/haproxy/errors/503.http`) and reloads HAProxy.
**If a suspended site is still serving the real content** (or is throwing a network error instead of the suspended page), it almost always means HAProxy didn't pick up the config reload. Check, in order:
1. **HAProxy container is running.** **Server Settings → Services → Docker Container Management** → confirm `Haproxy manager` shows **Running**.
2. **HAProxy reload succeeded.** Either re-trigger from **Server Settings → Network & SSL → Reload Configuration**, or check the HAProxy container logs (`docker logs haproxy-manager`) for a reload error — usually a syntax error in the generated config from the suspension action.
3. **Errorfile is in place.** The 503 page lives at `/usr/local/etc/haproxy/errors/503.http` inside the container.
## Active Sessions
Sidebar → **Active Sessions**. Every active panel session across the server, with last activity time, IP, and a **Terminate** button. Use this when offboarding someone — kick them out of any active sessions *first*, then change their password or delete the user.
## When someone leaves your team
1. **Revoke active sessions** for that user via **Active Sessions**.
2. **Change their password** in **User Management** (locks them out even if they save their cookies).
3. **Downgrade or delete** the user in **User Management**.
4. Audit **Delegated Users** for any cross-account delegations that should also be revoked.
## Troubleshooting
**Sub-user can sign in but the page they expect is missing.** Their role doesn't include that section. Edit the role and tick the right page.
**"Cannot delete protected system user".** Expected — system users (root, daemon, www-data, mail, the `systemd-*` users, etc.) are blocked at the panel level to prevent breaking the host. If you really need to remove a user, confirm it's a customer account first.
**SFTP user can connect but uploads land in the wrong directory.** Check their **Home directory** in the SFTP/SSH user page — it determines what they see as `/`.
**Created user can't sign in.** Confirm the password meets the strength rules. If the user is signing in for the first time, they may be hitting the password-change-on-first-login flow.
**A delegated user can't see DNS records.** Delegated access defaults to file-only. Edit their permissions to include DNS Management.
**Suspended customer's sites are still serving real traffic.** The suspension page is served by HAProxy — see "How the suspension page is served" above. Most often it's an HAProxy reload that didn't happen; check the container logs.
## Related
- [Server settings & services](/whp/admin/server-settings/)
- [Server settings & services](/whp/admin/server-settings/) — including the suspension backend service.
- [AI Monitor, Issues & Ignore Rules](/whp/admin/site-monitoring/)
## Still stuck?

View File

@@ -9,7 +9,7 @@ import { Steps, Aside } from '@astrojs/starlight/components';
import SignIn from '~/content/partials/signing-in.mdx';
import Support from '~/content/partials/support-link.mdx';
WHP keeps automatic backups of your sites and databases. The **Backup Management** page lets you trigger an on-demand backup, add a scheduled backup, manage where backups are sent, and review history.
The **Backup Management** page lets you trigger an on-demand backup, add a scheduled backup, manage where backups are sent, and review history.
## What's backed up
@@ -18,6 +18,10 @@ WHP keeps automatic backups of your sites and databases. The **Backup Management
Default retention on built-in backup targets is **5 days, up to 10 backups**.
<Aside type="note">
Automatic backups only run once the server admin has configured at least one **default backup target**. On our managed shared plans this is set up for you. On a VDS, the server operator picks the target — until they do, the **Backup Targets** table on your Backups page will be empty and you won't see scheduled backups firing.
</Aside>
## Sign in to WHP
<SignIn />
@@ -83,6 +87,14 @@ Don't do a full restore unless you genuinely need to — it rewrites your live s
**"No targets available" when starting a backup.** Your account has no backup targets attached. Open a ticket; we'll get one wired up.
## What's *not* in customer backups
Customer backups cover your sites and databases — they don't cover the underlying server, the OS, or system-level config. Full-server backups are a separate concern:
- On our managed plans, **AnHonestHost handles full-server backups** for the host.
- On a Virtual Dedicated Server (VDS) we provide, **full-server snapshots are included** at the platform level.
- If WHP is running somewhere else (your own infrastructure), full-server backups are the server operator's responsibility — WHP itself doesn't provide a host-level backup tool.
## Related
- [Archival email add-on](/whp/add-ons/archival-email/)

View File

@@ -0,0 +1,3 @@
Super admin access is granted to the `root` user only. Sign in **directly** at `https://<your-server-hostname>:8443` with the root credentials.
The WHMCS client portal route doesn't apply for super admin — it signs you in as the linked customer, not as root.

View File

@@ -0,0 +1,229 @@
/**
* v2 admin capture — stricter redaction and deeper navigation.
*
* - Masks customer domains (anything not in the brand allowlist).
* - Masks customer-shaped usernames (anything not in the system allowlist).
* - Masks input value attributes (the v1 only walked text nodes).
* - Captures Settings sub-tabs (System, Services, Mail, DNS, Network & SSL,
* Security) since those are where LiteLLM URL / model / key likely live.
*
* Read-only. Never clicks save/apply/restart/delete.
*/
import { chromium, type Page } from 'playwright';
import { mkdir } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const OUT_DIR = resolve(__dirname, '../../src/assets/screenshots/whp');
function need(name: string): string {
const v = process.env[name];
if (!v) throw new Error(`missing env: ${name}`);
return v;
}
const BASE = need('WHP_BASE');
const USER = need('WHP_ADMIN_USER');
const PASS = need('WHP_ADMIN_PASS');
const HIDE_CSS = `.navbar-text, .brand-full { visibility: hidden !important; }`;
async function login(page: Page) {
await page.goto(`${BASE}/login.php`, { waitUntil: 'domcontentloaded' });
await page.fill('input[name="user"]', USER);
await page.fill('input[name="password"]', PASS);
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');
}
async function redact(page: Page) {
await page.addStyleTag({ content: HIDE_CSS });
await page.evaluate(() => {
// ---- Mask all secret-shaped input values ----
const secretInputSel = [
'input[type="password"]',
'input[name*="key" i]',
'input[name*="token" i]',
'input[name*="secret" i]',
'input[name*="api" i]',
];
for (const sel of secretInputSel) {
document.querySelectorAll<HTMLInputElement>(sel).forEach((el) => {
if (el.value) el.value = '████████████████';
});
}
// ---- Brand allowlist ----
const BRAND_SUFFIXES = [
'anhonesthost.com', 'anhonesthost.net', 'anhonesthost.io',
'anhh.co',
'cloud-hosting.io',
'example.com', 'example.org', 'example.net',
];
// ---- System users to keep visible (others get masked) ----
const SYSTEM_USERS_ARR = ['root', 'admin', 'whp', 'haproxy', 'apache', 'nginx', 'newuser'];
// ---- Text-node swaps ----
const swaps: [RegExp, string][] = [
// Server / mail / nameserver hostnames in our infra
[/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io'],
[/mail\d+\.cloud-hosting\.io/gi, '<your-mail-server>.cloud-hosting.io'],
[/whp\d+(-[a-z0-9]+)?\b/gi, '<your-server>'],
[/WHP\d+(-[A-Z0-9]+)?\b/g, '<YOUR-SERVER>'],
// IPs
[/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<server-IP>'],
// Home dirs
[/\/docker\/users\/[a-z0-9-]+/g, '/docker/users/<user>'],
// Common secret shapes
[/sk-[A-Za-z0-9_-]{20,}/g, '<API-KEY>'],
[/sk_(test|live)_[A-Za-z0-9]{20,}/g, '<API-KEY>'],
[/Bearer\s+[A-Za-z0-9._-]{20,}/g, 'Bearer <API-KEY>'],
[/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g, '<JWT>'],
// AI provider URLs
[/https?:\/\/[^\s"'<>]*litellm[^\s"'<>]*/gi, '<litellm-endpoint>'],
[/https?:\/\/[^\s"'<>]*\.anhonesthost\.(net|com|io)[^\s"'<>]*/gi, '<internal-endpoint>'],
// Model family identifiers
[/(claude|gpt|llama|mistral|gemini)[a-z0-9._-]*-\d[a-z0-9.\-]*/gi, '<model-name>'],
// Bichon / Coraza / haproxy internal endpoints
[/https?:\/\/[^\s"'<>]*\b(bichon|coraza-spoa|haproxy-manager)[^\s"'<>]*/gi, '<internal-service>'],
// root → admin (we don't expose which UNIX user has super admin)
[/Welcome, root\b/g, 'Welcome, admin'],
[/(User:\s*)root\b/g, '$1admin'],
[/(Home Directory:\s*)\/root\b/g, '$1/<admin-home>'],
// Standalone 'root' (whole-word, not preceded by / or . — so paths like
// /root/foo and references like .root stay untouched).
[/(^|[^/.\w])root\b/g, '$1admin'],
];
// ---- Walk text nodes ----
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
const nodes: Text[] = [];
let n: Node | null = walker.nextNode();
while (n) { nodes.push(n as Text); n = walker.nextNode(); }
for (const node of nodes) {
let v = node.nodeValue ?? '';
// Mask non-brand domain names (basic pattern: word.tld)
v = v.replace(/\b([a-z0-9][a-z0-9-]{0,62}\.)+[a-z]{2,24}\b/gi, function (m) {
const h = m.toLowerCase();
let brand = false;
for (const s of BRAND_SUFFIXES) { if (h === s || h.endsWith('.' + s)) { brand = true; break; } }
return brand ? m : '<customer-domain>';
});
// Apply other swaps
for (const [re, replacement] of swaps) v = v.replace(re, replacement);
if (v !== node.nodeValue) node.nodeValue = v;
}
// ---- Mask sensitive content in input values ----
document.querySelectorAll<HTMLInputElement>('input[type="text"], input[type="url"], input[type="email"], input:not([type])').forEach((el) => {
if (!el.value) return;
const v = el.value;
let nv = v.replace(/\b([a-z0-9][a-z0-9-]{0,62}\.)+[a-z]{2,24}\b/gi, function (m) {
const h = m.toLowerCase();
let brand = false;
for (const s of BRAND_SUFFIXES) { if (h === s || h.endsWith('.' + s)) { brand = true; break; } }
return brand ? m : '<customer-domain>';
});
// Server / mail / NS hostnames
nv = nv.replace(/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io');
nv = nv.replace(/mail\d+\.cloud-hosting\.io/gi, '<your-mail-server>.cloud-hosting.io');
nv = nv.replace(/ns[12]\.whp\d+\.cloud-hosting\.io/gi, 'ns<n>.<your-server>.cloud-hosting.io');
// IPv4 — skip the well-known public resolvers / RFC1918 examples
nv = nv.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, function (ip) {
const pub = new Set(['1.1.1.1', '1.0.0.1', '8.8.8.8', '8.8.4.4', '9.9.9.9', '149.112.112.112', '208.67.222.222', '208.67.220.220']);
if (pub.has(ip)) return ip;
if (/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(ip)) return ip; // private nets, fine
return '<server-IP>';
});
if (nv !== v) el.value = nv;
});
// ---- Mask customer usernames in table cells ----
// Heuristic: find table cells under a header containing 'user', 'username', or 'target' (case-insensitive)
document.querySelectorAll('table').forEach((tbl) => {
const headers = Array.from(tbl.querySelectorAll('thead th, thead td')).map(th => (th.textContent || '').trim().toLowerCase());
const userCols: number[] = [];
for (let i = 0; i < headers.length; i++) {
if (/^(user(name)?|target|owner|account)$/.test(headers[i])) userCols.push(i);
}
if (userCols.length === 0) return;
const rows = tbl.querySelectorAll('tbody tr');
for (let r = 0; r < rows.length; r++) {
const cells = rows[r].querySelectorAll('td');
for (const idx of userCols) {
const cell = cells[idx];
if (!cell) continue;
const txt = (cell.textContent || '').trim();
if (!txt) continue;
let isSystem = false;
for (const u of SYSTEM_USERS_ARR) { if (u === txt) { isSystem = true; break; } }
if (!isSystem) cell.textContent = '<user>';
}
}
});
});
}
async function shot(page: Page, id: string) {
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
await redact(page);
const path = resolve(OUT_DIR, `${id}.png`);
await page.screenshot({ path, fullPage: false });
console.log(`captured ${id}`);
}
async function main() {
await mkdir(OUT_DIR, { recursive: true });
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2 });
const page = await ctx.newPage();
try {
await login(page);
// Server Settings — click each tab
await page.goto(`${BASE}/index.php?page=server-settings`);
await page.waitForLoadState('networkidle');
await shot(page, 'admin-srvset-system');
for (const tab of ['services', 'mail', 'dns', 'network', 'security']) {
const trigger = page.locator(`[data-bs-target="#tab-${tab}"]`).first();
if (await trigger.count() === 0) { console.log('tab trigger not found:', tab); continue; }
await trigger.click().catch(()=>{});
await page.waitForTimeout(1200);
await shot(page, `admin-srvset-${tab}`);
}
// Re-shoot the previously PII-heavy pages with v2 redaction
const rerun = [
{ id: 'admin-coraza', path: '/index.php?page=coraza-rules' },
{ id: 'admin-monitor-admin', path: '/index.php?page=ai-monitor' },
{ id: 'admin-ignore-rules', path: '/index.php?page=ai-monitor-ignore-rules' },
{ id: 'admin-user-mgmt', path: '/index.php?page=user-management' },
{ id: 'admin-user-resources', path: '/index.php?page=user-resources' },
{ id: 'admin-issues', path: '/index.php?page=issues' },
{ id: 'admin-suspensions', path: '/index.php?page=account-suspensions' },
{ id: 'admin-disk-usage', path: '/index.php?page=disk-usage' },
{ id: 'admin-docker', path: '/index.php?page=docker-management' },
{ id: 'admin-valkey', path: '/index.php?page=valkey-admin' },
{ id: 'admin-updates', path: '/index.php?page=update-management' },
{ id: 'admin-container-boot', path: '/index.php?page=container-boot' },
{ id: 'admin-site-audit', path: '/index.php?page=site-audit' },
{ id: 'admin-delegated', path: '/index.php?page=delegated-users' },
];
for (const r of rerun) {
await page.goto(`${BASE}${r.path}`);
await page.waitForLoadState('networkidle');
await shot(page, r.id);
}
} finally {
await browser.close();
}
}
main().catch(e => { console.error(e); process.exit(1); });