Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f8d05eec8 | ||
|
|
f1dac00138 | ||
|
|
0e3ae941eb | ||
|
|
1e3e0bd546 | ||
|
|
a28e20f881 | ||
|
|
063f992516 | ||
|
|
e4ea0484fd |
@@ -0,0 +1,59 @@
|
|||||||
|
# Deploy artefacts
|
||||||
|
|
||||||
|
Config that lives on other servers but belongs under version control here,
|
||||||
|
because it exists to serve this KB.
|
||||||
|
|
||||||
|
## `whmcs-kb-redirects.conf`
|
||||||
|
|
||||||
|
301 redirects that send the retired WHMCS knowledgebase
|
||||||
|
(`secure.anhonesthost.com/knowledgebase/*`) to its replacement pages on
|
||||||
|
kb.anhonesthost.com. Installed 2026-08-01.
|
||||||
|
|
||||||
|
**Where it lives in production:** `/home/whmcs/public_html/.htaccess` on
|
||||||
|
`secure.anhonesthost.com`, prepended **before** the
|
||||||
|
`### BEGIN - WHMCS managed rules ###` marker. It has to come first — WHMCS's
|
||||||
|
own block ends with a catch-all that routes everything to `index.php`, so
|
||||||
|
rules placed after it never run.
|
||||||
|
|
||||||
|
The canonical copy on that server is `/root/kb-redirects.conf`, and the
|
||||||
|
pre-migration `.htaccess` is backed up at
|
||||||
|
`/root/htaccess-backup-20260801.bak`.
|
||||||
|
|
||||||
|
### Reinstalling
|
||||||
|
|
||||||
|
WHMCS rewrites `.htaccess` during some updates, which will silently drop
|
||||||
|
these rules. To restore:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/whmcs/public_html
|
||||||
|
cp -a .htaccess /root/htaccess-backup-$(date +%Y%m%d).bak # keep whatever WHMCS wrote
|
||||||
|
cat /root/kb-redirects.conf /root/htaccess-backup-$(date +%Y%m%d).bak > /tmp/htaccess.new
|
||||||
|
install -o whmcs -g whmcs -m 644 /tmp/htaccess.new .htaccess
|
||||||
|
apachectl -t
|
||||||
|
```
|
||||||
|
|
||||||
|
Then spot-check a redirect:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sI https://secure.anhonesthost.com/knowledgebase/13/x.html | grep -i location
|
||||||
|
# expect: https://kb.anhonesthost.com/support/remote-support/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Two things that will bite you when editing it
|
||||||
|
|
||||||
|
**Article and category IDs collide.** Both use the shape
|
||||||
|
`/knowledgebase/<id>/<slug>`, and the same number means different things —
|
||||||
|
article 5 is "What are my Name Servers?", category 5 is "WordPress Specific".
|
||||||
|
The *only* discriminator is the trailing `.html` on articles. That's why every
|
||||||
|
article rule appears before every category rule and terminates with `[L]`: by
|
||||||
|
the time the broad category patterns run, anything ending `.html` is already
|
||||||
|
gone. Reordering the file breaks this silently, and the wrong page still
|
||||||
|
returns 200.
|
||||||
|
|
||||||
|
**The slug is ignored.** WHMCS reads only the id, so any slug with the right
|
||||||
|
id resolves. The rules match `[^/]*` for the slug for the same reason.
|
||||||
|
|
||||||
|
Legacy pre-SEO URLs (`knowledgebase.php?action=displayarticle&id=N` and
|
||||||
|
`?action=displaycat&catid=N`) are mapped explicitly. `(^|&)id=` cannot match
|
||||||
|
`catid=` — the preceding character is `t`, not `&` or start-of-string — so the
|
||||||
|
two sets can't cross-fire.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
### BEGIN - KB migration redirects to kb.anhonesthost.com ###
|
||||||
|
# Added 2026-08-01. The WHMCS knowledgebase is retired; these 301s send its
|
||||||
|
# URLs to the equivalent page on the dedicated KB.
|
||||||
|
#
|
||||||
|
# ORDER MATTERS. Article and category URLs share the shape
|
||||||
|
# /knowledgebase/<id>/<slug>
|
||||||
|
# and their IDs COLLIDE (article 5 is "What are my Name Servers?", category 5
|
||||||
|
# is "WordPress Specific"). The only discriminator is the trailing ".html" on
|
||||||
|
# articles. Article rules therefore come first and terminate with [L]; the
|
||||||
|
# category rules below can then match broadly because anything ending .html
|
||||||
|
# has already been redirected and will never reach them.
|
||||||
|
#
|
||||||
|
# The slug is ignored by WHMCS (only the id is read), so it is ignored here
|
||||||
|
# too — any slug with the right id resolves to the right destination.
|
||||||
|
#
|
||||||
|
# The trailing "?" on each target discards the inbound query string.
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine on
|
||||||
|
RewriteBase /
|
||||||
|
|
||||||
|
# --- Articles (.html) -------------------------------------------------
|
||||||
|
RewriteRule ^knowledgebase/2/[^/]*\.html$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/3/[^/]*\.html$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/4/[^/]*\.html$ https://kb.anhonesthost.com/email/spam-filtering-changes/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/5/[^/]*\.html$ https://kb.anhonesthost.com/cpanel/nameservers/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/6/[^/]*\.html$ https://kb.anhonesthost.com/cpanel/fix-a-403-error/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/7/[^/]*\.html$ https://kb.anhonesthost.com/cpanel/wordpress-email-from-your-domain/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/8/[^/]*\.html$ https://kb.anhonesthost.com/cpanel/free-ssl-certificate/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/9/[^/]*\.html$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/10/[^/]*\.html$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/11/[^/]*\.html$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/12/[^/]*\.html$ https://kb.anhonesthost.com/domains/flush-your-dns-cache/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/13/[^/]*\.html$ https://kb.anhonesthost.com/support/remote-support/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/14/[^/]*\.html$ https://kb.anhonesthost.com/whp/getting-started/welcome/? [R=301,L]
|
||||||
|
|
||||||
|
# --- Categories (no .html) --------------------------------------------
|
||||||
|
RewriteRule ^knowledgebase/2/[^/]*/?$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/3/[^/]*/?$ https://kb.anhonesthost.com/email/spam-filtering-changes/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/4/[^/]*/?$ https://kb.anhonesthost.com/cpanel/nameservers/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/5/[^/]*/?$ https://kb.anhonesthost.com/cpanel/wordpress-email-from-your-domain/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/6/[^/]*/?$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/7/[^/]*/?$ https://kb.anhonesthost.com/cpanel/fix-a-403-error/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/8/[^/]*/?$ https://kb.anhonesthost.com/support/remote-support/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase/9/[^/]*/?$ https://kb.anhonesthost.com/whp/getting-started/welcome/? [R=301,L]
|
||||||
|
|
||||||
|
# --- Legacy query-string URLs -----------------------------------------
|
||||||
|
# Pre-SEO-URL links still in old tickets and emails:
|
||||||
|
# knowledgebase.php?action=displayarticle&id=<id>
|
||||||
|
# knowledgebase.php?action=displaycat&catid=<id>
|
||||||
|
# WHMCS used to 301 these to the friendly URL itself, but our rules above
|
||||||
|
# now intercept first — so map them explicitly or they all land on the KB
|
||||||
|
# home page and lose their destination.
|
||||||
|
#
|
||||||
|
# "(^|&)id=" cannot match "catid=" (the preceding char is "t", not & or
|
||||||
|
# start-of-string), so the two sets can't cross-fire.
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=2(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=3(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=4(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/spam-filtering-changes/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=5(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/nameservers/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=6(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/fix-a-403-error/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=7(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/wordpress-email-from-your-domain/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=8(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/free-ssl-certificate/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=9(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=10(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=11(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=12(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/domains/flush-your-dns-cache/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=13(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/support/remote-support/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)id=14(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/whp/getting-started/welcome/? [R=301,L]
|
||||||
|
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)catid=2(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/set-up-your-email-client/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)catid=3(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/email/spam-filtering-changes/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)catid=4(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/nameservers/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)catid=5(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/wordpress-email-from-your-domain/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)catid=6(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)catid=7(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/cpanel/fix-a-403-error/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)catid=8(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/support/remote-support/? [R=301,L]
|
||||||
|
RewriteCond %{QUERY_STRING} (^|&)catid=9(&|$)
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/whp/getting-started/welcome/? [R=301,L]
|
||||||
|
|
||||||
|
# --- Tags, search, index, and anything else under /knowledgebase ------
|
||||||
|
# Catch-all last: any KB URL not matched above lands on the KB home page
|
||||||
|
# rather than a WHMCS 404.
|
||||||
|
RewriteRule ^knowledgebase\.php$ https://kb.anhonesthost.com/? [R=301,L]
|
||||||
|
RewriteRule ^knowledgebase(/.*)?$ https://kb.anhonesthost.com/? [R=301,L]
|
||||||
|
</IfModule>
|
||||||
|
### END - KB migration redirects to kb.anhonesthost.com ###
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# AnHonestHost Knowledge Base
|
||||||
|
|
||||||
|
> Customer documentation for WHP (AnHonestHost's containerized Web Hosting Platform), plus cPanel hosting, domains, email, and remote support.
|
||||||
|
|
||||||
|
WHP is AnHonestHost's containerized hosting platform, and most of this knowledge base documents it: getting started, how-to guides, the Site Builder, local development with the same container images used in production, reference material, and optional add-ons. Separate sections cover legacy cPanel hosting, domain management, email setup, and remote support. Pages in the WHP Admin section document server-wide super-admin controls, which most customers won't have access to.
|
||||||
|
|
||||||
|
## WHP — Getting started
|
||||||
|
|
||||||
|
- [Welcome to WHP](https://kb.anhonesthost.com/whp/getting-started/welcome/): Quick orientation for new customers — what WHP is and where to start.
|
||||||
|
- [What is containerized hosting?](https://kb.anhonesthost.com/whp/getting-started/what-is-containerized-hosting/): Plain-language explainer of containerized hosting and how it compares to shared hosting and a VPS.
|
||||||
|
|
||||||
|
## WHP — How-to guides
|
||||||
|
|
||||||
|
- [Add a domain](https://kb.anhonesthost.com/whp/how-to/add-a-domain/): Point a domain at your WHP hosting — whether you registered it with us or elsewhere.
|
||||||
|
- [Manage DNS records](https://kb.anhonesthost.com/whp/how-to/manage-dns-records/): View, add, edit, and delete DNS records for your domains using the Domains & DNS records editor in WHP.
|
||||||
|
- [Create a site](https://kb.anhonesthost.com/whp/how-to/create-a-site/): Spin up a containerized site on a domain you've added to WHP.
|
||||||
|
- [Create an email account](https://kb.anhonesthost.com/whp/how-to/create-an-email-account/): Add a mailbox on one of your domains and connect your email client.
|
||||||
|
- [Backups](https://kb.anhonesthost.com/whp/how-to/backups/): Run on-demand and scheduled backups of your sites and databases, and confirm they're succeeding.
|
||||||
|
- [Switching your site's backend](https://kb.anhonesthost.com/whp/how-to/switching-site-backend/): Change the web engine (container type) running a site — standard PHP/FPM or the premium LiteSpeed/OpenLiteSpeed tier.
|
||||||
|
- [Clear your site's cache](https://kb.anhonesthost.com/whp/how-to/clear-your-cache/): Seeing an old version of a page after making a change? Here's how to clear cached content so your updates show up.
|
||||||
|
|
||||||
|
## WHP — Site Builder
|
||||||
|
|
||||||
|
- [Site Builder overview](https://kb.anhonesthost.com/whp/site-builder/overview/): Build a website visually inside WHP — drag-and-drop blocks, ready-made templates, draft / publish workflow.
|
||||||
|
- [Getting started](https://kb.anhonesthost.com/whp/site-builder/getting-started/): Open Site Builder, pick a template or start from scratch, make your first edits, and publish.
|
||||||
|
- [Blocks & pages](https://kb.anhonesthost.com/whp/site-builder/blocks-and-pages/): The Site Builder block library, plus how to add pages and edit the shared header and footer.
|
||||||
|
- [Styling your site](https://kb.anhonesthost.com/whp/site-builder/styling/): Set colours, fonts, and link styles once with Site Design Tokens; reach for Advanced when you need finer control.
|
||||||
|
- [Publishing & code injection](https://kb.anhonesthost.com/whp/site-builder/publishing/): Drafts vs. publish, device previews, and adding analytics, custom fonts, or global CSS via the custom head code panel.
|
||||||
|
|
||||||
|
## WHP — Local development
|
||||||
|
|
||||||
|
- [Develop locally with our containers](https://kb.anhonesthost.com/whp/local-dev/overview/): Run the same Apache/PHP and Node containers locally that we use to host your site in production.
|
||||||
|
- [PHP + Apache locally](https://kb.anhonesthost.com/whp/local-dev/php-apache/): Run the cloud-apache-container image on your laptop for WordPress and other PHP apps.
|
||||||
|
- [Node + Nginx locally](https://kb.anhonesthost.com/whp/local-dev/node/): Run the cloud-node-container image on your laptop for Express, custom Node apps, and PM2-managed processes.
|
||||||
|
|
||||||
|
## WHP — Reference
|
||||||
|
|
||||||
|
- [Service hostnames](https://kb.anhonesthost.com/whp/reference/service-hostnames/): Quick reference for connecting to MySQL, PostgreSQL, and Valkey from inside your container.
|
||||||
|
|
||||||
|
## WHP — Add-ons
|
||||||
|
|
||||||
|
- [Add-ons overview](https://kb.anhonesthost.com/whp/add-ons/overview/): Optional features you can layer on your hosting plan — monitoring, archival email, resource upgrades, and more.
|
||||||
|
- [Site Monitoring](https://kb.anhonesthost.com/whp/add-ons/monitoring/): Proactive alerts for site errors, brute-force attempts, and exploit signatures.
|
||||||
|
- [Archival email](https://kb.anhonesthost.com/whp/add-ons/archival-email/): Long-term, searchable archive of your mailbox content, separate from the live mailbox.
|
||||||
|
- [Resource upgrades](https://kb.anhonesthost.com/whp/add-ons/resource-upgrades/): Add CPU, RAM, or disk to your container without migrating to a different plan.
|
||||||
|
- [Email upgrades](https://kb.anhonesthost.com/whp/add-ons/email-upgrades/): Add mailboxes or bump per-mailbox storage on your plan.
|
||||||
|
- [Optimized Webserver (OpenLiteSpeed + LSCache)](https://kb.anhonesthost.com/whp/add-ons/optimized-webserver/): Run your sites on OpenLiteSpeed with server-level full-page caching for dramatically faster page delivery — ideal for WordPress and other dynamic CMS sites.
|
||||||
|
|
||||||
|
## WHP — Admin
|
||||||
|
|
||||||
|
- [Admin overview](https://kb.anhonesthost.com/whp/admin/overview/): What WHP super admin unlocks — server-wide controls for services, mail, DNS, security, monitoring, and users.
|
||||||
|
- [Server settings & services](https://kb.anhonesthost.com/whp/admin/server-settings/): Restart services, configure mail server, manage DNS / nameservers, HAProxy + system SSL certificates, and integration API keys.
|
||||||
|
- [Coraza WAF rules](https://kb.anhonesthost.com/whp/admin/coraza-waf/): Set the global WAF mode, tune individual rules, audit blocked requests, and add per-host overrides.
|
||||||
|
- [AI Monitor, Issues & Ignore Rules](https://kb.anhonesthost.com/whp/admin/site-monitoring/): The three admin pages that drive the Site Monitoring add-on — AI Monitor dashboard, Issues, and Ignore Rules.
|
||||||
|
- [Users & delegated access](https://kb.anhonesthost.com/whp/admin/user-management/): Create WHP users, set account types, change passwords, plus delegated user access and account suspensions.
|
||||||
|
- [Backups](https://kb.anhonesthost.com/whp/admin/backups/): How WHP's automatic backups work, the default-target requirement, full-server backups vs customer data backups, and managing backup targets.
|
||||||
|
- [Data-drive encryption (LUKS)](https://kb.anhonesthost.com/whp/admin/data-drive-encryption/): Optional LUKS2 encryption of the /docker data volume on new server installs. Encrypts customer data, databases, and container state at rest; adds a manual unlock step after every reboot.
|
||||||
|
|
||||||
|
## cPanel hosting
|
||||||
|
|
||||||
|
- [What are my nameservers?](https://kb.anhonesthost.com/cpanel/nameservers/): The nameservers to set at your registrar so your domain points at your cPanel hosting with us.
|
||||||
|
- [Get your free SSL certificate](https://kb.anhonesthost.com/cpanel/free-ssl-certificate/): Every cPanel account includes free Let's Encrypt certificates through AutoSSL. Here's what has to be in place and what to do if one doesn't issue.
|
||||||
|
- [Why is my site getting a 403 error?](https://kb.anhonesthost.com/cpanel/fix-a-403-error/): A 403 Forbidden on cPanel hosting is almost always file permissions or file ownership. Here's how to check and fix both.
|
||||||
|
- [Send WordPress email from your own domain](https://kb.anhonesthost.com/cpanel/wordpress-email-from-your-domain/): Stop WordPress sending as user@server and send from an address on your domain instead, using an SMTP plugin and a real mailbox.
|
||||||
|
|
||||||
|
## Domains
|
||||||
|
|
||||||
|
- [Transferring a domain to us](https://kb.anhonesthost.com/domains/transferring-a-domain-to-us/): What happens after you start a domain transfer — the confirmation emails, how long it takes, and the nameserver check to do afterwards.
|
||||||
|
- [Flush your DNS cache](https://kb.anhonesthost.com/domains/flush-your-dns-cache/): Your site moved or its DNS changed, but your computer still loads the old version? Clear the DNS cache your device has saved locally.
|
||||||
|
|
||||||
|
## Email
|
||||||
|
|
||||||
|
- [Set up your email](https://kb.anhonesthost.com/email/set-up-your-email-client/): Connect Outlook, Apple Mail, Thunderbird, or your phone to your mailbox — with the server settings for both WHP and cPanel hosting.
|
||||||
|
- [Changes to spam filtering](https://kb.anhonesthost.com/email/spam-filtering-changes/): We're moving spam filtering from Baruwa to Proxmox Mail Gateway. What's changing, when, and what you need to do.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- [Remote support with RustDesk](https://kb.anhonesthost.com/support/remote-support/): Install and configure the RustDesk client so our support team can connect to your device and help directly.
|
||||||
|
|
||||||
|
## Optional
|
||||||
|
|
||||||
|
- [AnHonestHost KB homepage](https://kb.anhonesthost.com/): Start page for the knowledge base.
|
||||||
|
- [Sitemap](https://kb.anhonesthost.com/sitemap-index.xml): XML sitemap index for all pages on this site.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 307 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 288 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 332 KiB |
@@ -56,6 +56,8 @@ When you connect via SFTP, your account home (`/docker/users/<your-user>/`) show
|
|||||||
|
|
||||||
Upload your files to the right place and the site picks them up immediately.
|
Upload your files to the right place and the site picks them up immediately.
|
||||||
|
|
||||||
|
Logging in with your account password works out of the box. If you'd rather use an SSH key instead — or need to let a teammate connect without sharing your password — see [Manage SFTP SSH keys](/whp/how-to/manage-sftp-ssh-keys/).
|
||||||
|
|
||||||
## Connect to databases and caches
|
## Connect to databases and caches
|
||||||
|
|
||||||
<Hostnames />
|
<Hostnames />
|
||||||
@@ -82,6 +84,7 @@ Open your domain in a browser. You should see your site, or the default "no cont
|
|||||||
|
|
||||||
- [Add a domain](/whp/how-to/add-a-domain/)
|
- [Add a domain](/whp/how-to/add-a-domain/)
|
||||||
- [Service hostnames](/whp/reference/service-hostnames/)
|
- [Service hostnames](/whp/reference/service-hostnames/)
|
||||||
|
- [Manage SFTP SSH keys](/whp/how-to/manage-sftp-ssh-keys/)
|
||||||
|
|
||||||
## Still stuck?
|
## Still stuck?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
title: Manage SFTP SSH keys
|
||||||
|
description: Add SSH public keys to your account so you — and anyone else on your team — can log in over SFTP without sharing a password.
|
||||||
|
sidebar:
|
||||||
|
order: 8
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||||
|
import SignIn from '~/content/partials/signing-in.mdx';
|
||||||
|
import Support from '~/content/partials/support-link.mdx';
|
||||||
|
|
||||||
|
SFTP normally logs in with your account password. SSH keys give you (or a teammate) a second way in — no password typing, and no need to share your password with anyone. Add one key per person, and remove any key on its own without touching anyone else's.
|
||||||
|
|
||||||
|
## Before you start
|
||||||
|
|
||||||
|
- An SSH key pair. If you don't have one yet, the steps below cover generating one — it takes about a minute.
|
||||||
|
- About 5 minutes.
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
The main reason to use this: if more than one person needs SFTP access to the same hosting account, everyone can add their own key instead of all sharing one password. Removing someone's access later is just removing their key.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Sign in to WHP
|
||||||
|
|
||||||
|
<SignIn />
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. In the sidebar, click **Security → SFTP SSH Keys**.
|
||||||
|

|
||||||
|
|
||||||
|
2. If you don't already have an SSH key, generate one on your own machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh-keygen -t ed25519 -C "your-email-or-a-note-to-yourself"
|
||||||
|
```
|
||||||
|
|
||||||
|
Accept the default file location (`~/.ssh/id_ed25519`) and set a passphrase if you'd like one. If you already have a key, you'll find it at `~/.ssh/id_ed25519.pub` (or `~/.ssh/id_rsa.pub` for an older RSA key).
|
||||||
|
|
||||||
|
<Aside type="caution">
|
||||||
|
Only ever paste the **`.pub`** file's contents — that's the *public* key, safe to share. Never paste the file without `.pub` (or one that starts with `-----BEGIN OPENSSH PRIVATE KEY-----`) anywhere. The panel will refuse anything that looks like a private key, but it's worth knowing the difference.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
3. Copy the full contents of the `.pub` file — one line, starting with the key type (`ssh-ed25519`, `ssh-rsa`, etc.).
|
||||||
|
|
||||||
|
4. Paste it into the **Public key** box in the **Add Key** card. The **Note** field is optional — whatever you type there is recorded in the account's audit log, but it isn't what shows in the key list below (see the tip after this list for how to label a key so you can actually tell it apart later).
|
||||||
|

|
||||||
|
|
||||||
|
5. Click **Add Key**. It appears in the **Authorized Keys** table immediately and works right away — no separate activation step.
|
||||||
|

|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Aside type="tip">
|
||||||
|
The **Comment** column in the table comes from the key itself, not from the Note field — it's whatever followed the key type when the key was generated (the `-C "..."` value above, e.g. `your-email-or-a-note-to-yourself`). If several people are adding keys to the same account, agree on a naming convention for `-C` (e.g. `-C "dana-laptop"`) so everyone can tell the keys apart at a glance. A key generated without `-C` shows as **(no comment)**.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## Connect over SFTP with your key
|
||||||
|
|
||||||
|
Point your SFTP client at the private key file (the one *without* `.pub`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sftp -i ~/.ssh/id_ed25519 <username>@<your-server>.cloud-hosting.io
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `<username>` with your WHP username and `<your-server>` with your server's hostname (find it on the **Dashboard** page under Server Information). Graphical clients — FileZilla, Cyberduck, WinSCP — all support key-based login too: point the client's "private key" field at `~/.ssh/id_ed25519` instead of filling in a password.
|
||||||
|
|
||||||
|
Your account password still works for SFTP exactly as before. Adding a key doesn't disable it.
|
||||||
|
|
||||||
|
## Supported key types
|
||||||
|
|
||||||
|
- **`ssh-ed25519`** — recommended. Generate with `ssh-keygen -t ed25519`.
|
||||||
|
- `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`
|
||||||
|
- `ssh-rsa`
|
||||||
|
- `sk-ssh-ed25519@openssh.com`, `sk-ecdsa-sha2-nistp256@openssh.com` — for a FIDO/hardware security key (e.g. a YubiKey)
|
||||||
|
|
||||||
|
If a key is malformed, an unsupported type, or technically well-formed but not something the SFTP server can actually use, the panel rejects it with an explanation instead of adding it — so a key that's been accepted is a key that works.
|
||||||
|
|
||||||
|
## Remove a key
|
||||||
|
|
||||||
|
Find the key in the **Authorized Keys** table and click **Remove**. Confirm in the dialog — the fingerprint and comment are shown so you can be sure you're removing the right one. This takes effect immediately: anyone using that key loses SFTP access right away, but password login is unaffected.
|
||||||
|
|
||||||
|
There's no restriction on removing your last key. If you remove every key on the account, SFTP still works with your account password — keys are additive, not a replacement for it.
|
||||||
|
|
||||||
|
Common reasons to remove a key: someone leaves the team, or a laptop with a key on it is lost or retired.
|
||||||
|
|
||||||
|
<Aside type="note">
|
||||||
|
Keys can also be added or removed directly in `~/.ssh/authorized_keys` over SFTP itself, and valid changes made that way show up here too. But changes made outside this page aren't recorded in the audit log, and anything in that file the panel doesn't recognize (an unsupported key type, a malformed line, a restricted `command=`/`from=` entry) is cleaned up automatically the next time this page loads. Comments and blank lines are always left alone.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
## If you manage this account on someone else's behalf
|
||||||
|
|
||||||
|
Root and support staff reach the same page for any customer account — either from the same **Security → SFTP SSH Keys** sidebar entry (which then shows an account selector), or via a **SFTP Keys** button on that customer's row on the **User Management** page. Everything above works the same way once an account is selected; the only difference is picking whose keys you're managing first.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**"This key is already present."** That exact key (by fingerprint) is already on the account — check the table before adding it again.
|
||||||
|
|
||||||
|
**Key added, but SFTP still asks for a password.** Double-check you're pointing your client at the matching **private** key file (no `.pub`), and that the username in your connection matches the account the key was added to.
|
||||||
|
|
||||||
|
**A key I added over raw SFTP disappeared.** If it didn't meet this page's rules (unsupported type, malformed, or carried a restricted option), it was removed the next time the page loaded — add it here instead so it's validated and recorded properly.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Create a site](/whp/how-to/create-a-site/) — including where your files go over SFTP.
|
||||||
|
|
||||||
|
## Still stuck?
|
||||||
|
|
||||||
|
<Support />
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* SFTP SSH Keys capture — the new customer-facing key management page
|
||||||
|
* (WHP release 2026.09.11), as the demo customer.
|
||||||
|
*
|
||||||
|
* Captures:
|
||||||
|
* - whp-sftp-keys-empty.png Authorized Keys list with no keys yet
|
||||||
|
* (the real empty state).
|
||||||
|
* - whp-sftp-keys-add-form.png the Add Key form filled in with a
|
||||||
|
* throwaway demo key, before submitting.
|
||||||
|
* - whp-sftp-keys-list.png Authorized Keys list after the key was
|
||||||
|
* added — fingerprint/type/comment/added
|
||||||
|
* columns populated.
|
||||||
|
*
|
||||||
|
* This is a documentation-owned demo account (demo-user), so unlike the other
|
||||||
|
* capture-*.ts scripts, this one DOES submit the Add Key / Remove actions —
|
||||||
|
* that's the only way to show the populated list state, and the account is
|
||||||
|
* reset back to empty at the end of the run (no key is left behind).
|
||||||
|
*
|
||||||
|
* Viewport-only (1440x900, deviceScaleFactor 2), redacted for our multi-server
|
||||||
|
* fleet: server hostnames/IPs become placeholders, while the brand demo
|
||||||
|
* account (demo-user) is kept visible on purpose.
|
||||||
|
*/
|
||||||
|
import { chromium, type Page } from 'playwright';
|
||||||
|
import { mkdir } from 'node:fs/promises';
|
||||||
|
import { resolve, dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
|
||||||
|
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_USER');
|
||||||
|
const PASS = need('WHP_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(() => {
|
||||||
|
const swaps: [RegExp, string][] = [
|
||||||
|
[/ns[12]\.whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, 'ns<n>.<your-server>.cloud-hosting.io'],
|
||||||
|
[/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io'],
|
||||||
|
[/mail\d+\.cloud-hosting\.io/gi, '<mail-server>.cloud-hosting.io'],
|
||||||
|
[/WHP\d+(-[A-Z0-9]+)?\b/g, '<YOUR-SERVER>'],
|
||||||
|
[/whp\d+(-[a-z0-9]+)?\b/gi, '<your-server>'],
|
||||||
|
[/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<server-IP>'],
|
||||||
|
];
|
||||||
|
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 ?? '';
|
||||||
|
for (const [re, rep] of swaps) v = v.replace(re, rep);
|
||||||
|
if (v !== node.nodeValue) node.nodeValue = v;
|
||||||
|
}
|
||||||
|
document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>('input, textarea').forEach((el) => {
|
||||||
|
if ((el as HTMLInputElement).type === 'password' || !el.value) return;
|
||||||
|
let v = el.value;
|
||||||
|
const swaps2: [RegExp, string][] = [
|
||||||
|
[/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '<your-server>.cloud-hosting.io'],
|
||||||
|
[/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<server-IP>'],
|
||||||
|
];
|
||||||
|
for (const [re, rep] of swaps2) v = v.replace(re, rep);
|
||||||
|
if (v !== el.value) el.value = v;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shot(page: Page, id: string) {
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
await redact(page);
|
||||||
|
const path = resolve(OUT_DIR, `${id}.png`);
|
||||||
|
await page.screenshot({ path, fullPage: false });
|
||||||
|
console.log(`captured ${id} -> ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await mkdir(OUT_DIR, { recursive: true });
|
||||||
|
|
||||||
|
// Throwaway ed25519 keypair, generated fresh for this capture only. Never
|
||||||
|
// used to actually connect; discarded with the OS temp dir.
|
||||||
|
const os = await import('node:os');
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
const tmpDir = await fs.mkdtemp(resolve(os.tmpdir(), 'kb-sftp-demo-key-'));
|
||||||
|
const keyPath = resolve(tmpDir, 'id_ed25519');
|
||||||
|
execFileSync('ssh-keygen', ['-t', 'ed25519', '-N', '', '-C', "dana's-laptop", '-f', keyPath]);
|
||||||
|
const pubKey = (await fs.readFile(`${keyPath}.pub`, 'utf8')).trim();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 1. SFTP SSH Keys page — empty state
|
||||||
|
await page.goto(`${BASE}/index.php?page=sftp-keys`, { waitUntil: 'networkidle' });
|
||||||
|
await page.waitForTimeout(600);
|
||||||
|
await shot(page, 'whp-sftp-keys-empty');
|
||||||
|
|
||||||
|
// 2. Add Key form, filled in but not yet submitted
|
||||||
|
await page.fill('#sftp-key-input', pubKey);
|
||||||
|
await page.fill('#sftp-key-note', "Dana's laptop");
|
||||||
|
await shot(page, 'whp-sftp-keys-add-form');
|
||||||
|
|
||||||
|
// 3. Submit, then capture the populated list
|
||||||
|
await page.click('#sftp-keys-add-btn');
|
||||||
|
await page.waitForTimeout(1200);
|
||||||
|
await shot(page, 'whp-sftp-keys-list');
|
||||||
|
|
||||||
|
// Clean up: remove the demo key so the account is left as it was found.
|
||||||
|
page.once('dialog', (d) => d.accept());
|
||||||
|
await page.locator('button:has-text("Remove")').first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
console.log('cleanup: demo key removed');
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => { console.error(err); process.exit(1); });
|
||||||
Reference in New Issue
Block a user