# Hosting Pages

These pages are the same shape: a top-level cross-account view (search + filter) plus a per-account drill-down. The File Manager and the Logs page's **Account Logs** view have no cross-account list: pick an account first — the File Manager then opens that account's home directory, and Account Logs shows its access and error logs per domain and, when there is one, its PHP error log (the System Logs and Admin Logs views need no account). Operations available match the admin user's full capabilities:

| Page | Actions |
|---|---|
| Domains | Add subdomain / parked domain / addon domain / redirect; edit a subdomain or a redirect. Custom vhost directives: API only (see *Custom vhost config* below) |
| Databases | Create DB (optionally with its first user), create DB user, assign an existing user to another DB, revoke a user's access to one DB (behind a confirmation), reset DB user password, remote hosts (removal behind a confirmation), phpMyAdmin single sign-on in a new tab, drop DB; orphan schemas from the server-wide list (every schema no account prefix claims, the system and service schemas aside; a drop takes a name made of letters, digits, `_`, `$` and `-`) |
| Email | Create / delete mailbox, change password, set quota (or unlimited), activate / deactivate, configure forwarders, open webmail; server-wide Mail Queue and Sent Summary tabs. DKIM keys are managed with the account's mail DNS (see below); anti-spam settings live on the Spam Filter page; there is no autoresponder |
| FTP | Create / delete FTP user, set chroot dir (created below home and owned by the account level by level), change password, quota; orphan rows (single or bulk) from the server-wide list |
| DNS | Manage zone (A / AAAA / CNAME / MX / TXT / NS / SRV / CAA), create a missing zone, DNSSEC on / off (behind a confirmation) with the DS records to copy; there is no zone-file import or export |
| SSL | Issue Let's Encrypt cert (auto-renewed by the certbot timer), upload custom cert (with an optional CA bundle), renew cert, delete cert |
| PHP | Switch version per domain (behind a confirmation), edit php.ini per account and per domain, worker count, extensions and the ionCube loader (version-wide, behind a confirmation), OPcache configure / clear, server defaults and the version list |
| File Manager | Browse, upload, download, edit (Monaco), chmod, rename, archive (tar / zip), extract |
| Backups | Create on-demand (files / databases / mailboxes in any combination, optionally copied to a remote destination), schedule (daily / weekly / monthly at a time — the server's local clock; the Last Run and Next Run columns show the stamps in the viewer's own time zone — weekday 0 = Sunday, a retention count per schedule that prunes only that schedule's runs), restore (the parts listed under *Backups — scope and settings* — there is no selective restore), download the archive (resumable), delete; server-wide Remote Storage and Settings tabs |
| Cron | List / create / edit / delete cron entries (schedule fields validated; the command runs as the account user); Run Now executes the command with `/bin/sh` in a login environment and stops it after 85 seconds (the agent’s own limit is 90); a suspended account's crontab is parked until it is resumed |
| Logs | Access / error / PHP logs per domain, line tail (last N), download |

Multi-select exists only in the File Manager: the selected items are deleted, copied, moved or archived together, and a delete asks a plain confirmation that names the number of items — nothing has to be typed. The other pages act on one row at a time (the check boxes in the Databases, Email and FTP dialogs set privileges and unlimited quota); whole-list actions such as **Secure All Domains** on the SSL page, the mail queue's **Flush Queue** and **Delete All**, and **Clean up orphans** on the FTP page are single buttons behind an ordinary confirmation.

**Backups — scope and settings.** A backup holds the account's home files (setuid and setgid bits are not kept: a file or folder that carries one is backed up without it; the PHP session files in the account's `tmp` folder are left out), one SQL dump per database and the mailboxes (addresses, password hashes, quotas and the mail itself). A restore puts back `public_html`, `tmp`, `ssl`, `mail` and the folders of the account's addon domains and subdomains that live outside `public_html` (the sites the account has at the time of the restore; log files, the account's `backups` folder and hidden files are not restored), drops and re-creates each database from its dump while its users keep exactly the grants they held (a database no user held gets all privileges for the account's users), and re-creates the mailboxes with their mail; a backup whose archive has gone from disk is reported as such (`410`) and can only be deleted. The restored folders are written through directory handles as the account's user: a link the account placed at one of those names is removed and replaced by a plain folder, and a name that turns into a link while the restore runs makes the restore fail (`500`, "Failed to restore …") rather than being written through. Each dump is imported by the account's own import user (`whost_imp_<account>`, a login-locked database user that holds privileges on the account's restored databases only; it is not listed among the account's users and is dropped with the account): a statement in the dump that names another database fails and the restore reports it (`500`, "Database restore failed for … ERROR 1142 … denied to user 'whost_imp_<account>'@'localhost' …"; the databases named before that statement are already re-created, so the restore is partial), and the views, triggers, routines and events the dump carries are re-created under that user, so they run with the account's privileges. The Settings tab switches the backup system (with it off, creating, restoring and scheduling are refused with `403 BACKUP_DISABLED` and the panel locks the actions), sets the retention window that prunes every backup, and caps the schedules per account (`403 BACKUP_SCHEDULE_LIMIT` when the cap is reached — the panel shows the reason). A remote copy that fails leaves the local archive intact; the row's badge names the failure — also for a copy the agent could not finish because it stopped during the upload ("The remote copy did not finish: the agent stopped during the upload."). A scheduled backup tries its copy up to three times when the failure may not repeat (a dropped, refused or timed-out connection, a cut TLS session, a `5xx` / `408` / `429` answer from the provider; 15 and then 45 seconds apart), and stops at once on one that will (credentials, permission, quota, a changed server identity); the **Remote Backup Copy Failed** notification goes out only after the last attempt. A backup started by hand tries once. While a WHost update is being installed, creating a backup or restoring one is refused with `409 UPDATE_IN_PROGRESS` (the update ends in a restart of the agent that would cut it) and a scheduled backup that falls due runs right after the update; working folders an interrupted backup left behind are removed in the background after the agent starts. A backup runs inside the request that starts it, so a large account can keep the request open for many minutes; when no answer comes back at all (a gateway timeout, a dropped connection) the panel says the backup may still be running and asks you to refresh the list and check the notifications, rather than reporting a failure — starting it again would make a second copy. A restore answers the same way: when nothing comes back the panel says it may still be running rather than reporting a failure. A refusal the agent names (a disabled system, a limiter) keeps its own message. The long steps — the copy of the home, each database dump, each mailbox copy (also in a restore), the archive and the removal of the working folder — run as long as they keep working, whatever the account's size: a step that reads, writes and computes nothing for 10 minutes is stopped and the backup fails with that step named ("Command did no work for 600s: …"), and no step runs longer than 6 hours.

**Remote backup credentials.** A destination's credentials are encrypted on disk, and every field that is encrypted comes back masked in the API and the panel — passwords, API keys, OAuth client secrets and refresh tokens alike, in list answers and in the answers to a save. The exception is the Google Drive service-account JSON the admin API still accepts (`credentials_json`; the panel does not offer it): it is written as plain text to `/etc/whost/credentials/gdrive-<id>.json` (root, `0600`), the destination keeps only that path (`credentials_file`, returned as it is), and deleting the destination removes the file. An account's own destination cannot name such a file: the client routes answer `422` to `credentials_file`. Leaving a masked field untouched when editing a destination keeps the stored value; typing over it replaces it. The FTP provider tries TLS first and falls back to plain FTP when the server has none — only for a destination that has never signed in over TLS: once one has, a server that refuses TLS is refused before the password is sent (if the server really dropped TLS, remove the destination and add it again); SFTP goes through the same timeout. The agent checks the server before the password leaves. An FTP server's TLS certificate must verify against the system CA store for the destination's host; a self-signed certificate, or one issued for another name (a shared-hosting server whose certificate carries its own host name), is refused with a message saying which — use the certificate's name as the host, or turn on **Accept an unverified certificate** for that destination. With the switch on, the certificate of the first successful connection is remembered (its SHA-256 is shown in the edit form) and a different one, on the control or on a data connection, is refused. An SFTP destination remembers the server's host key at its first successful connection (the test before saving, a stored test, or the first copy) and refuses another key before the password is sent; the edit form shows it as `ssh-keygen -l` prints it. When a server's key or certificate was replaced on purpose, tick **Forget it on save** in the edit form; a new host or port starts over by itself. A destination on a private, loopback or metadata address is refused (`422`; the refused ranges are listed in the developer webhooks guide). Every FTP / SFTP connection is opened to the addresses that check approved, so the host name is not looked up again between the check and the connection. The *Validated* badge is set only by the server's own connection test (Validate before saving, then save within 15 minutes; a changed credential clears it). A disabled destination is refused as a copy target: a manual backup naming it is refused (`409`), a scheduled one keeps its local archive and records why the copy was not made. A destination a schedule still names cannot be deleted (`409`, the answer lists the schedules). The Google Drive / Yandex Disk / OneDrive sign-in state is issued by the server and accepted once, on the surface that issued it.

**Database access boundaries.** Account database users are granted on their own schema only and never hold a global privilege; the API refuses `FILE`. A user created directly is bound to `localhost` or one IPv4 address; a wildcard host (`%`, or a pattern such as `10.0.0.%`) is added only through remote database access (below). MariaDB's `secure_file_priv` is written into the WHost tuning drop-in and confines server-side file reads and writes to one directory (it applies on the next MariaDB restart). Remote database access copies an account's local users to the given host, but MariaDB ships bound to loopback: until you open it to the network deliberately, the panel returns the remote host with a warning that it cannot connect.

**Email.** A mailbox address is stored in lower case whatever spelling the request carries. A mailbox, forwarder or queue message that does not exist answers `404` (`EMAIL_NOT_FOUND`, `EMAIL_FORWARDER_NOT_FOUND`, `MAIL_QUEUE_MESSAGE_NOT_FOUND`), the plan's mailbox cap `403 EMAIL_LIMIT`, a second forwarder to the same destination `409 EMAIL_FORWARDER_EXISTS`; two creates that arrive together for one account are handled one after the other. "Unlimited storage" stores quota 0 (the administrator's dialog only: a client may set a mailbox quota between 1 MB and the plan's disk, and the quotas of the account's mailboxes together may not exceed that disk — `422 VALIDATION_ERROR` for the range, `403 EMAIL_LIMIT` with the allocated / available figures for the sum; a quota the client leaves as it is, is not measured). A save that changes nothing writes no audit row and reloads no service; deactivating a mailbox or changing its password closes its open IMAP/POP3 sessions, and the webmail button refuses a deactivated mailbox (`409 EMAIL_INACTIVE`). Deleting a mailbox removes its forwarders; the last mailbox of a domain takes the domain row and the domain's remaining aliases with it; removing an addon or parked domain removes the mailboxes and forwarders on it; terminating an account removes them all. Each of these also removes the webmail's own record of the address (the Roundcube user with its identities, address book, collected addresses and settings), so a mailbox created later under the same address — by another account too — starts empty. Suspending an account switches its mailboxes and forwarders off (IMAP/POP3 login, SMTP submission and inbound delivery stop) and unsuspending puts back exactly the rows that were on. The webmail button mints a single-use token; the Roundcube session runs as the Dovecot master user for reading and sending, the system-scope mint (`POST /system/webmail/sso`) is audited and refuses a mailbox that does not exist or is inactive, and where nginx serves `/webmail` (nginx and nginx + Apache hosts) the location does not serve Roundcube's schema, scripts, installer or configuration files; on an Apache-only host it refuses only Roundcube's `config`, `temp` and `logs` folders (the installer folder is removed from disk at install). Webmail has no Filters screen: the mail stack carries no ManageSieve server, so the installer leaves Roundcube's `managesieve` plugin out and the agent switches it off at start on a host without such a server (a host that runs one keeps the plugin). The queue's Delete All sends the required `confirm=ALL`; a limiter answer on any mail action is shown as the refusal it is; a list whose read failed shows the failure with a Retry button. The Sent Summary lists senders of hosted domains only, counts a message once whatever its recipient count, opens the bounce sender's records from the "(system)" row, and shows log stamps in their real time zone. A mailbox password may be rotated over an HMAC API key (integrations manage mailboxes); the hosting account's own password may not. The mailbox quota is enforced by Dovecot: IMAP reports it and delivery over the limit is refused (quota 0 = unlimited). A forwarder belongs to its mailbox: while the mailbox is inactive the forwarder is off, and the forwarder dialog offers the account's existing mailboxes as the source (a source without a mailbox is refused, `404`). A mailbox or forwarder change reloads no service — the maps are live database lookups. Mail submitted on the loopback without SMTP authentication that claims an address of a hosted domain is refused by the hourly-limit policy (`554 5.7.1`): web applications authenticate as a mailbox or use the `sendmail` path (PHP `mail()`), which is not affected. A webmail submission authenticates as the mailbox's master login and spends that mailbox's account allowance. **Per-mailbox spam lists** (client panel Email → Spam Filter; `GET/PUT/DELETE /accounts/{username}/spam/{email}` for the administrator) are applied by an Rspamd rule (`lua.local.d/whost_per_user.lua`) over three recipient-keyed maps the agent rebuilds from the settings files on every save: entries are sender addresses or domains (a domain covers its subdomains), lower-cased, at most 100 per list (`422 VALIDATION_ERROR` otherwise); a recipient's blacklist rejects the message at delivery (over a server-wide whitelist), its whitelist or filtering switched off accepts the message without scoring unless the server-wide blacklist names the sender; a message to several mailboxes follows the lists only when every recipient's lists agree; the save reports when Rspamd did not reload; a host that stored lists before this rule existed gets the rule and the maps at the agent's next start; a deleted mailbox takes its settings with it. The server-wide whitelist and blacklist (Spam Filter page) keep their entries in lower case whatever spelling is typed, and removing an entry matches it case-insensitively.

**Outbound mail limits and DKIM.** A plan's hourly email limit is applied by a Postfix policy daemon that the agent installs and keeps running; the check leads the recipient restriction list on port 25 and on the submission/smtps ports alike, so authenticated and webmail submissions are both counted, and it fails open (`default_action=DUNNO`) if the daemon is ever down. An authenticated submission is counted on the authenticated mailbox's account whatever its envelope sender says, a message is counted once whatever its recipient count, and mail arriving from outside the host is not counted. The allowance is per account, shared across every mailbox, domain and subdomain it owns, and exceeding it defers rather than rejects; the first message deferred in an hour raises the administrator's *Email Limit Exceeded* notification at the agent's next check (every 15 minutes), at most once per account a day. The daemon is listed on the Services page as *WHost Mail Policy*. DKIM keys generated for a domain are registered with OpenDKIM at generation time, and existing keys for domains the host still serves are registered on agent start, so a published DKIM record actually matches the signature on outgoing mail. Switching an account's mail DNS on generates the domain's DKIM key and publishes its record with the rest of the mail set; switching it off removes both. OpenDKIM is the only signer: Rspamd's own DKIM/ARC signing is off.

**Domain name availability.** A hostname is taken as soon as any account serves it — as its primary, addon, parked or subdomain name — and account creation, addon and parked domains all refuse it; a parked alias cannot take a name the same account already serves either. The server's own hostname (and `localhost`) is reserved: no account may take the name the panel itself answers on. A domain's document root is honoured per domain on every web server, so an addon or subdomain serves its own directory rather than the account's primary docroot; the directory is created with the addon (with a placeholder page) and is kept when the addon is removed. The directory has to resolve inside the account home: a folder name that is a link to a place outside it is refused with `422 VALIDATION_ERROR` (a link that stays inside the home, such as `www` → `public_html`, still works), and the levels the agent creates on the way belong to the account.

**Addon domains, subdomains and redirects.** Adding an addon domain creates its directory, vhost (on the account's PHP version) and (unless switched off) DNS zone; a parked domain runs the PHP version of the domain it points at; removing it also removes the vhosts of its subdomains, the vhosts and zones of the parked domains pointing at it, and its certificate files. A subdomain that runs a PHP version the account does not use yet gets that version's pool. A redirect rule cannot be moved onto a source another rule of the same domain already uses, and a save that changes nothing is neither rendered nor recorded. Deleting a subdomain removes its vhost and DNS record and renames its own folder directly under the account home to `<folder>-removed-<timestamp>` (nothing is deleted; a folder inside another site's tree, or one another domain also serves, is left as it is). A directory still there under the original name when the name is added again is reused as it is (its files stay the account's). In the addon dialog Auto SSL is off by default — switch it on only for a name that already resolves to this server, since a failed Let's Encrypt order still counts against the quota — and an internationalized name has to be entered in Punycode (`xn--…`); the dialog says so when accented or non-Latin letters are typed. Editing a subdomain sends only the fields that changed, and "Inherit from account" sets the account's own PHP version. On the Domains, DNS and SSL pages a list whose read was refused (a limiter answer among them) shows the failed read with a Retry button instead of an empty list, and Secure All waits for the account list before it judges anything.

**Custom vhost config.** The panel has no control for this: custom directives are read, saved and removed through `GET` / `PUT` / `DELETE /accounts/{username}/domains/{domain}/vhost-config`, and only for the account's primary domain — any other domain answers `404 DOMAIN_NOT_FOUND`. Saving custom directives re-renders the domain from its stored state, so its redirects, per-domain PHP version and SSL listener survive the save. A config the webserver rejects is undone before the error is returned: the previous vhost file is restored, so one account's bad snippet cannot leave the host in a state where every other account's domain operation fails its config test. A field meant for a web server that is not the provider on the host (for example `apache_config` on an nginx host) is refused with `422 VALIDATION_ERROR` rather than dropped; saves and removals are written to the audit log (`vhost_config_updated` / `vhost_config_deleted`).

**Databases.** A database user is created with the privileges the dialog lists; a request without any privilege is refused (`422`) instead of being widened to all of them. The collation has to belong to the chosen character set. In the Manage dialog the X next to a user revokes that user's access to this database only (the user keeps its other databases and is dropped once none remain); the confirmation box says so. A remote host is matched literally, so `10.0.0.%` can be added while `10.0.0.5` exists and removing a pattern that was never added is refused; removal asks for a confirmation. The create dialog refuses a name or user name that does not start with a letter and a password shorter than eight characters before the request is sent; when the database was created but its user step was refused, the result panel says so and the database is kept. Dropping an owned or orphan schema from the server-wide list also drops the users left without a grant; an orphan schema is deleted whatever the case of its name. The phpMyAdmin button mints a single-use token that carries the MariaDB root login, not a schema — the schema only names where phpMyAdmin opens (none is minted for a schema that does not exist) — and opens the signon in a new tab. The system-scope token (`POST /system/phpmyadmin/sso`) is minted only for the panel's own MariaDB host (`localhost`, `127.0.0.1`, `::1` or the configured `mysql.host`); any other host answers `422`. The signon endpoints under `/sso/*` carry no rate limit of their own: they answer only on the loopback with the SSO pin, and a token is single-use with a five-minute lifetime. The `roundcubemail` and `phpmyadmin` schemas are not hidden from the admin phpMyAdmin session, which runs as root and sees every tenant schema; a tenant's session is bound to its own database user. A list whose read failed shows the failure with a Retry button rather than "No databases found".

**FTP.** The directory of an FTP account is stored in one spelling (`/public_html/site`, never `/public_html/./site/`) and every level the agent creates below the home directory belongs to the account, so the chrooted session can enter it. A row whose owner column is empty belongs to the account named by its prefix; only rows whose account no longer exists count as orphans for the single and bulk cleanup. The Edit dialog sends only the fields that changed and closes an untouched form without a request, and the server compares as well: a request that carries no field or only the stored values leaves the row as it is and writes no audit row, a new password alone is recorded as a password change (`ftp_password_changed`), and an update row names only the fields that changed. A limiter answer (`429`) on create, edit, password change or delete is reported as the refusal it is — no credential card, no success toast. The daemon compares SHA-512 crypt hashes (`MYSQLCrypt crypt`), requires TLS (`TLS 2`; a cleartext login is answered `421`) and refuses anonymous logins; a host installed before the hash default has to carry `MYSQLCrypt crypt` in its Pure-FTPd configuration or every panel-created FTP account is refused at login. On AlmaLinux, Rocky Linux and CentOS Stream they live in `/etc/pure-ftpd/pure-ftpd.conf` (`TLS 2`, `CertFile /etc/pki/pure-ftpd/pure-ftpd.pem`, `NoAnonymous yes`); a RHEL-family host set up by an earlier installer has TLS off and anonymous logins on until those three lines are set and `pure-ftpd` is restarted. The create dialog suggests the selected domain's document root as the directory (`public_html` for the primary domain); the FTP user is confined to that directory, and a directory typed by hand is kept. The account's own FTP user, created with the account, is recorded as an `ftp_account_created` audit row with `scope: default` and reaches the webhook bridge like any other. Two create requests that arrive together for one account are handled one after the other, so the plan's FTP cap holds. Creating an FTP account requires an active license, as creating a database, an email account or a DNS zone does.

**PHP.** The account's php.ini values (upload and post size, memory limit, execution and input time, `max_input_vars`, `display_errors`, `error_reporting`) are kept in the account record and rendered into every pool of the account from that one place: a version change, a worker-count change or a plan change carries them along, and a new pool starts from the server defaults (Settings › PHP) with the plan's memory ceiling standing in for an unset memory limit. A per-domain configuration writes only the values that differ from the account's into the domain's `.user.ini`, so an account-level change keeps reaching the domain; a save equal to the account's values writes nothing and Reset (behind a confirmation) removes the file. Changing a domain's version from the row select asks first; the account's version change (on the account page as well as here) keeps the pools that domain overrides still use, rewrites every vhost — parked names included, which follow the domain they point at — and removes pools no domain uses — a subdomain or addon deleted with its own version takes its pool with it. A domain version equal to the account's is stored as inherited, so when the account itself moves onto a domain's override version that override folds into the inheritance and a later account change moves the domain along. `upload_max_filesize` and `post_max_size` do not accept "unlimited" (`-1`); `memory_limit` does. The ionCube loader is per PHP version and shared by every account on it: the status shown is what the version runs, and a disable that another account still needs leaves the loader in place. Extension toggles and the OPcache clear restart PHP-FPM for every account on that version and ask first; the OPcache dialog saves only when a value changed. A version whose packages are not on the host shows "Not installed on this server" with its switch locked. A version request for a domain the account does not hold answers `404 DOMAIN_NOT_FOUND`; the overview, defaults and OPcache reads show a failed read with a Retry button. When a vhost cannot be re-rendered after a version or worker-count change (the webserver configuration test fails), the change is kept and the response carries a `warnings` entry naming the domain; the site keeps running on its previous configuration until the webserver configuration is repaired. The OPcache `save_comments` value is written as `1` on every save. Disabling a version in the version list only stops offering it to accounts: its FPM service keeps running for the pools that still use it. PHP writes each account's sessions to the account's `tmp` folder; every 30 minutes the agent removes the session files there (`sess_*`) that have not been written for a day — or for the longer `session.gc_maxlifetime` a php.ini of the host sets — and the agent log names how many it removed per account. On Ubuntu and Debian neither PHP (`session.gc_probability = 0`) nor the distribution's `phpsessionclean` timer expires these folders, so an earlier release left every session a site opened on disk.

**DNS zones.** A zone the panel creates (account creation, addon or parked domain, the "Create DNS Zone" call to action) carries an SOA naming the configured primary nameserver (Settings › General) and `hostmaster.<zone>`, and NS records for the configured set; changing the nameservers in Settings rewrites the NS records and SOA primary of every existing zone. Record targets (MX, CNAME, NS, PTR, SRV) are accepted with or without the trailing dot and are shown without it. DNSSEC DS records are listed as SHA-256 and SHA-384 digests only; a save that changes nothing writes no audit row. A zone created from the "Create DNS Zone" call to action is signed with DNSSEC the way a zone created with the account is (a signing failure is reported as a warning and leaves the zone usable). The mail records (MX, `mail` A, SPF, DMARC and the DKIM key) are one set that follows the account's *setup mail DNS* switch: a zone bootstrapped while the switch is off — at account creation, for an addon or parked domain, or from the call to action — carries only the A records, and the switch later adds or removes the whole set on the primary zone. On an existing zone the call to action also puts back a missing SOA or apex NS set (a zone without its SOA is answered REFUSED for every name); the SOA and the zone's last NS record are protected — the SOA cannot be edited or deleted and the last NS cannot be deleted until another is added (`409 DNS_RECORD_PROTECTED`).

**Certificates.** A Let's Encrypt order validates over HTTP from the panel's own challenge folder, `/var/www/letsencrypt/.well-known/acme-challenge` — every site's configuration (nginx, Apache and OpenLiteSpeed alike) serves `/.well-known/acme-challenge/` from that folder, and nothing is written into the account's site folders — is named after the domain (`--cert-name`) and installs a renewal hook that refreshes the certificate files the vhost points at and reloads the web server, so the timer's renewals reach the site. When an update changes the site templates, the first start after it renders every hosted name's site configuration once from the current templates (the previous files are copied to `/var/lib/whost/vhost-rerender-<timestamp>/` first; a name whose render is refused keeps its previous files and is tried again at the next start); with many sites that first start takes longer. Deleting a Let's Encrypt certificate also removes its lineage from certbot. A custom certificate is refused when it has expired or is not valid yet and when the CA bundle does not parse; the served file carries the chain. The certificate files live under `/home/<user>/ssl`, which belongs to root so an account cannot remove a file the web server's configuration depends on. That folder has to be a plain folder — when it is a link, installing a certificate answers `422 VALIDATION_ERROR` — and each file is written as a new file (key `0600`, certificate and chain `0640`), so an entry left there beforehand is replaced, not written through. The HTTPS vhost keeps the domain's own document root and redirect rules, and removing a certificate restores the HTTP vhost with them. A forced order for a certificate that is not due yet keeps the current one: the answer says the certificate is current, carries a warning, and no renewal is recorded; `force_renew` renews it at once. Every 15 minutes the agent reads the certificate each hosted name serves and warns the administrator and the account owner (*SSL Expiry Warning*) when 14, 7, 3 and 1 days are left — once at each step for that certificate; a renewed or replaced certificate starts over, and a certificate that has already expired is not reported.

**Endpoints used:** `/accounts/{u}/{resource}/...` family — see the API reference for each.

---
