diff --git a/.gitignore b/.gitignore index 41595cf..1dd8194 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/astro.config.mjs b/astro.config.mjs index 7862646..97090df 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -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 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' } }], }, ], diff --git a/package-lock.json b/package-lock.json index 3eefe66..6dc891d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 8652035..fe6b242 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/assets/screenshots/whp/admin-coraza.png b/src/assets/screenshots/whp/admin-coraza.png new file mode 100644 index 0000000..e10580c Binary files /dev/null and b/src/assets/screenshots/whp/admin-coraza.png differ diff --git a/src/assets/screenshots/whp/admin-ignore-rules.png b/src/assets/screenshots/whp/admin-ignore-rules.png new file mode 100644 index 0000000..1521cf5 Binary files /dev/null and b/src/assets/screenshots/whp/admin-ignore-rules.png differ diff --git a/src/assets/screenshots/whp/admin-issues.png b/src/assets/screenshots/whp/admin-issues.png new file mode 100644 index 0000000..46de95e Binary files /dev/null and b/src/assets/screenshots/whp/admin-issues.png differ diff --git a/src/assets/screenshots/whp/admin-monitor-admin.png b/src/assets/screenshots/whp/admin-monitor-admin.png new file mode 100644 index 0000000..35247e6 Binary files /dev/null and b/src/assets/screenshots/whp/admin-monitor-admin.png differ diff --git a/src/assets/screenshots/whp/admin-srvset-system.png b/src/assets/screenshots/whp/admin-srvset-system.png new file mode 100644 index 0000000..3da7636 Binary files /dev/null and b/src/assets/screenshots/whp/admin-srvset-system.png differ diff --git a/src/assets/screenshots/whp/admin-user-mgmt.png b/src/assets/screenshots/whp/admin-user-mgmt.png new file mode 100644 index 0000000..3bda7d3 Binary files /dev/null and b/src/assets/screenshots/whp/admin-user-mgmt.png differ diff --git a/src/assets/screenshots/whp/admin-user-resources.png b/src/assets/screenshots/whp/admin-user-resources.png new file mode 100644 index 0000000..160af0a Binary files /dev/null and b/src/assets/screenshots/whp/admin-user-resources.png differ diff --git a/src/components/Head.astro b/src/components/Head.astro new file mode 100644 index 0000000..d6d1f94 --- /dev/null +++ b/src/components/Head.astro @@ -0,0 +1,34 @@ +--- +import Default from '@astrojs/starlight/components/Head.astro'; +--- + + + + diff --git a/src/content/docs/whp/admin/backups.mdx b/src/content/docs/whp/admin/backups.mdx new file mode 100644 index 0000000..cb78c91 --- /dev/null +++ b/src/content/docs/whp/admin/backups.mdx @@ -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'; + + + +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 + + + +## 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. + + + +### 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? + + diff --git a/src/content/docs/whp/admin/coraza-waf.mdx b/src/content/docs/whp/admin/coraza-waf.mdx index 85a0016..b5d0d6a 100644 --- a/src/content/docs/whp/admin/coraza-waf.mdx +++ b/src/content/docs/whp/admin/coraza-waf.mdx @@ -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'; - +[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 - + -## Where it lives +## The three tabs -Sidebar → **Security → Coraza Rules**. The page lists rule families (CRS 901, 911, 913, 920–922, 930–934, 941–944, 949, 950–956, 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. + + + +### CRS catalog + +The full OWASP Core Rule Set catalogue (v4 families: 901, 905, 911, 913, 920–922, 930–934, 941–944, 949, 950–956, 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 - - -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? diff --git a/src/content/docs/whp/admin/overview.mdx b/src/content/docs/whp/admin/overview.mdx index 652f847..8231600 100644 --- a/src/content/docs/whp/admin/overview.mdx +++ b/src/content/docs/whp/admin/overview.mdx @@ -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'; - - ## 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'; + + + +## 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? diff --git a/src/content/docs/whp/admin/server-settings.mdx b/src/content/docs/whp/admin/server-settings.mdx index e5df25d..e2cb0bf 100644 --- a/src/content/docs/whp/admin/server-settings.mdx +++ b/src/content/docs/whp/admin/server-settings.mdx @@ -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'; - +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 - + -## 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). -## 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. + + +### 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 (60–86400 seconds). + + + +### 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 repo.anhonesthost.net/cloud-hosting-platform/ 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`. + ## Troubleshooting -**Service won't restart.** The Services page surfaces the systemd error; if it's `failed (exit-code)`, check `journalctl -u ` 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 ` (for systemd-managed services) or `docker logs ` (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 diff --git a/src/content/docs/whp/admin/site-monitoring.mdx b/src/content/docs/whp/admin/site-monitoring.mdx index 544db64..1c061ad 100644 --- a/src/content/docs/whp/admin/site-monitoring.mdx +++ b/src/content/docs/whp/admin/site-monitoring.mdx @@ -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'; - +[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 + - +## 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. + + ## 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 - +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? diff --git a/src/content/docs/whp/admin/user-management.mdx b/src/content/docs/whp/admin/user-management.mdx index acef299..7b1d4b5 100644 --- a/src/content/docs/whp/admin/user-management.mdx +++ b/src/content/docs/whp/admin/user-management.mdx @@ -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'; - +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. | + -## Sign in to WHP +## User Management - +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/`). +- **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. -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). -## 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. + + + +## 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? diff --git a/src/content/docs/whp/how-to/backups.mdx b/src/content/docs/whp/how-to/backups.mdx index 106cd13..de9cec6 100644 --- a/src/content/docs/whp/how-to/backups.mdx +++ b/src/content/docs/whp/how-to/backups.mdx @@ -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**. + + ## Sign in to WHP @@ -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/) diff --git a/src/content/partials/admin-signin.mdx b/src/content/partials/admin-signin.mdx new file mode 100644 index 0000000..5d7868f --- /dev/null +++ b/src/content/partials/admin-signin.mdx @@ -0,0 +1,3 @@ +Super admin access is granted to the `root` user only. Sign in **directly** at `https://: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. diff --git a/tools/screenshots/capture-admin.ts b/tools/screenshots/capture-admin.ts new file mode 100644 index 0000000..c1ee869 --- /dev/null +++ b/tools/screenshots/capture-admin.ts @@ -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(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, '.cloud-hosting.io'], + [/mail\d+\.cloud-hosting\.io/gi, '.cloud-hosting.io'], + [/whp\d+(-[a-z0-9]+)?\b/gi, ''], + [/WHP\d+(-[A-Z0-9]+)?\b/g, ''], + // IPs + [/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, ''], + // Home dirs + [/\/docker\/users\/[a-z0-9-]+/g, '/docker/users/'], + // Common secret shapes + [/sk-[A-Za-z0-9_-]{20,}/g, ''], + [/sk_(test|live)_[A-Za-z0-9]{20,}/g, ''], + [/Bearer\s+[A-Za-z0-9._-]{20,}/g, 'Bearer '], + [/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g, ''], + // AI provider URLs + [/https?:\/\/[^\s"'<>]*litellm[^\s"'<>]*/gi, ''], + [/https?:\/\/[^\s"'<>]*\.anhonesthost\.(net|com|io)[^\s"'<>]*/gi, ''], + // Model family identifiers + [/(claude|gpt|llama|mistral|gemini)[a-z0-9._-]*-\d[a-z0-9.\-]*/gi, ''], + // Bichon / Coraza / haproxy internal endpoints + [/https?:\/\/[^\s"'<>]*\b(bichon|coraza-spoa|haproxy-manager)[^\s"'<>]*/gi, ''], + // 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/'], + // 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 : ''; + }); + + // 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('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 : ''; + }); + // Server / mail / NS hostnames + nv = nv.replace(/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '.cloud-hosting.io'); + nv = nv.replace(/mail\d+\.cloud-hosting\.io/gi, '.cloud-hosting.io'); + nv = nv.replace(/ns[12]\.whp\d+\.cloud-hosting\.io/gi, 'ns..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 ''; + }); + 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 = ''; + } + } + }); + }); +} + +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); });