> For the complete documentation index, see [llms.txt](https://docs.layeronecloud.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.layeronecloud.com/platform/web-hosting/email-and-dns.md).

# Hosted email, customer DNS and certificates

Web-hosting plans carry typed mailbox counts and per-mailbox quota.

## 6.7 Hosted email <a href="#id-67-hosted-email" id="id-67-hosted-email"></a>

Web-hosting plans carry typed mailbox counts and per-mailbox quota. The portal has owner-scoped mail-domain, mailbox and alias resources, durable exact management jobs, one-time sealed mailbox credential delivery, public DNS guidance/status, and an operator-configured Cloudflare publication adapter. **Provider credentials are Fernet-encrypted, write-only, and never sent to the hosting node.**

Node-side contract: Postfix (SMTP + submission), Dovecot (authenticated IMAP and LMTP delivery), Rspamd (filtering/DKIM/rate policy), Redis (rate state), and a maintained distribution Roundcube package for webmail. Root-only materializers write fixed reviewed configuration and exact per-domain state. **The unprivileged HTTPS API accepts only typed immutable resource intents and never receives a daemon fragment, DNS credential, DKIM private key, mailbox plaintext, executable, path or command.**

Customer naming, for a verified `example.com`: mailbox addresses are `user@example.com`; SMTP/IMAP clients use `mail.example.com`; Roundcube webmail uses `email.example.com`. **The node derives these names from the domain in portal intent** — customers do not submit Postfix, Dovecot, Rspamd or DNS fragments.

**Client setup sequence** is three stages: update DNS → automatic setup → create an email account. Internal production checks are not customer tasks. DNS instructions identify the purpose, type and host of each record, label whether to add or replace it, and **separate the observed value from the required value**. SPF/DKIM/DMARC instructions target only that policy's TXT record, leaving unrelated TXT records in place. MX priority and the copyable mail server value are separate fields. **Resolver failures do not become evidence that records are missing.**

The scheduled sweep checks pending DNS and advances setup after it matches, with a 30-minute per-domain cooldown. A successful observation persists the certificate job **and its dependent final verification job** immediately, both on the shared provisioning queue. Certificate failure fails the dependent setup attempt **without prematurely submitting its IMAP TLS check**; explicit retry repairs the failed prerequisite before resuming verification, and existing certificate renewal also covers failed first issuance.

**The first-mailbox boundary.** Customers choose their own address and password. To avoid a circular dependency, the first mailbox can be created from a fresh observation of the current domain generation with every infrastructure gate passing and only mailbox sign-in/quota checks pending — requiring current DNS intent and publication, verified IMAP TLS/outbound/filtering/webmail, an unexpired certificate, and no existing mailbox. **It does not mark the domain production-ready.** Mailbox completion automatically rechecks the domain; webmail access still requires the complete production proof.

Job labels are **Queued, Running, Failed, Complete**. A domain with no current job is **Not queued** with an authorized start action — neither verified DNS nor the existence of a periodic sweep claims active work. An accepted/queued node request stays **Queued** until running is observed. Post-DNS transport success with failed production checks is a **failed setup attempt** with a bounded diagnostic and a POST retry. `ProvisioningJobType.MANAGE_MAIL` runs through the same durable dispatcher, claiming, backoff, retry and Jobs controls as VPS provisioning; `apps/webhosting/mail_jobs.py` retains immutable `WebHostingManagementJob` actions as execution/attempt records via their `provisioning_job` FK. Mail resources are excluded from the old management dispatcher and direct task execution. Bounded recovery adopts legacy mail actions, unfinished work first, and treats a historical successful node observation with incomplete current infrastructure as **failed business setup — no readiness proof is fabricated during adoption.**

Explicit retry retains prior actions and checks current ownership, lifecycle, generation and removal fences. Terminal actions receive a fresh generation and request identity; an unconfirmed transport attempt keeps its request ID and resumes observation. A bounded observation deadline ends stalled work as Failed with a retry action rather than promising progress forever. **Generic manual resolution cannot mark mail ready.** Customer GET/refresh never queues or retries work, and credentials do not enter shared job summaries.

`python manage.py inspect_mail_setup example.com` gives a read-only bounded JSON diagnosis: current/latest job identities, setup flags and scheduler observations, **without credentials, mailbox addresses, raw errors or provider payloads**. Scheduler run history is global and does not prove a particular domain was selected.

**Security and delivery gates.** No mail domain activates until its owned web-domain mapping exists and the exact public A/AAAA, MX, SPF, per-domain DKIM and DMARC intent is published and observed. Optional autoconfig/autodiscover records target only the derived webmail name. Production proof also requires forward/reverse node identity and a per-domain ACME lineage covering both client hostnames with trusted IMAP and webmail HTTPS connections. **SMTP TLS is user/operator-managed and is not probed or included in setup readiness.** A changed address or DKIM intent **atomically invalidates the provider receipt and every production proof**; identical retries are idempotent.

Port 25 is inbound server-to-server SMTP only; authenticated client submission uses fixed ports 587 and 465; unknown recipients and unauthenticated relay are rejected. The default reviewed outbound policy is an **authenticated TLS smarthost**; direct outbound is a separate explicit deliverability and reputation qualification requiring a current independent reputation proof.

Mailbox passwords are write-only at creation and rotation. **Plaintext, password hashes, DKIM private keys, message content, headers and queue contents must not enter API read models, logs or audit metadata.** A mailbox address is visible only in an owning customer's authorized read model. Catch-all aliases are not supported in the first release. The shared mail services do **not** execute inside an individual customer's LVE — the plan's PMEM value is the customer's web-process limit, and mail has a separate mailbox storage quota and system-level capacity budget.

The root verifier checks public DNS, IMAP TLS/SNI, Postfix relay and outbound policy, Dovecot passwd/userdb ownership and quota readback, Rspamd DKIM/milter/rate policy, Redis, sealed credential mappings and Roundcube. **A successful portal observation is applied under a row lock only when it matches the exact current DNS intent, current provider receipt, current resource generation and a newer verification timestamp** — a stale verifier failure cannot erase a newer proof.

`email.example.com` is never ready from DNS alone. The fixed Roundcube workflow uses one shared SQLite database, fixed localhost SMTP/IMAP, `root:apache` secret files, a disabled installer, an allowlisted plugin/skin set, exact per-domain TLS vhosts and package/readback checks. The webmail gate makes an anonymous request to the node's **loopback** HTTPS listener with the domain's Host header, SNI and certificate verification, and requires **HTTP 200 with a complete Roundcube login page** — redirects, internal errors and unrelated pages do not pass. Per-domain jobs do not repair a broken shared Roundcube installation.

The installer also owns webmail's PHP runtime configuration: after CloudLinux supplies `/usr/local/bin/lsphp` it identifies that binary's PHP package family and installs the matching extension RPMs (including mbstring and PDO SQLite), because **Roundcube's distribution PHP dependencies alone are not proof that the copied LSAPI runtime can run it.** It writes a dedicated `/etc/roundcubemail/layerone-php/php.ini` and points every generated webmail vhost at it with `lsapi_phprc` and a separate LSAPI backend. **The shared runtime directives are emitted alongside the verified LSAPI handler, not inside `<IfModule>`** — Apache evaluates that container during its initial configuration read, when `layerone-vhosts.conf` can precede the vendor `lsapi.conf` that loads the module, and the old guard silently discarded PHPRC and backend isolation while leaving the ordinary PHP handler active. The exact LSAPI binary must then pass a read-only capability probe; **RPM registration alone is insufficient** — the installer checks the required shared-object files, triggers one bounded reinstall of only their owning packages on a missing or empty payload, and refuses linked or unsafe module paths rather than following them. PHP warnings remain failures even when the process exits zero. Tenant PHP versions, Selector defaults and CageFS extension links are not changed.

`tls_verified` is retained for wire/database compatibility but now means **a trusted certificate and a successful IMAP TLS handshake on port 993**, not SMTP verification; webmail is measured separately over HTTPS. Setup jobs do not connect to SMTP ports 25/465/587 and run no SMTP STARTTLS checks. These probes run on the hosting node's loopback interface, not from the portal. **Removing a readiness probe does not disable client encryption or prove delivery works** — operators remain responsible for their SMTP TLS setup.

**Operational work still required outside this code:** provider approval for port 25 and reverse DNS, bounce/complaint processing, queue and reputation monitoring, malware policy, an outbound kill switch, tested mail suspension, capacity alerting, and mail backup/restore drills.

## 6.8 Customer DNS zones <a href="#id-68-customer-dns-zones" id="id-68-customer-dns-zones"></a>

An owner of an active hosting service can manage bounded DNS records for an active owned domain **when an operator has already mapped that domain into an approved Cloudflare zone**. Customer DNS is a portal/provider workflow: **it does not call the hosting node and is not a node capability.**

Supported types are exactly **A, AAAA, CNAME, TXT, MX, CAA**. Names are normalized into the approved zone, values are parsed by type, TTL comes from a fixed list (60 s to one day). **NS, SOA, wildcards, delegated names, arbitrary provider fields, proxy settings, imports, zone scans and raw API operations are not exposed.**

**Provider boundary.** Reuses the operator-owned `WebHostingDNSProviderConfig` and `WebHostingDNSZone` mapping. The scoped Cloudflare token stays Fernet-encrypted and write-only and is **never sent to the node or the customer**. The Cloudflare zone ID and record ID are **never rendered to a customer, stored in a job payload, included in an audit event or copied into an error**; a verified record ID needed for a later update/delete is Fernet-encrypted on the record row. **The customer-DNS worker rejects legacy plaintext or unreadable token storage** — an operator must re-save through the write-only form, so the compatibility fallback used by older rows cannot weaken this boundary.

Every approved zone has a separate operator-only **`customer_dns_enabled`** switch that **defaults off**. Enabling hosted-mail DNS does **not** implicitly authorize customer record publication. Selection, worker claim **and completion** all re-check that switch, so disabling it fails closed and invalidates any in-flight result. The customer **DNS Settings** tool (control-center label; the URL remains the customer-DNS workspace) always shows the hosting node's public IPv4 and asks the customer to set an A record at their DNS provider. Record create/edit stays available only when that switch and the other gates above all pass.

The adapter calls only the fixed Cloudflare v4 HTTPS origin and exactly: `GET /zones/{id}` (active zone identity); `GET /zones/{id}/dns_records` with exact-name, exact-comment marker or delegation filters; `GET /zones/{id}/dns_records/{verified-record-id}` (exact readback); `POST /zones/{id}/dns_records`; `PUT .../{verified-record-id}`; `DELETE .../{verified-record-id}` followed by absence readback; and `GET /zones/{id}/dnssec` (operator-observed status only — there is no customer or operator DNSSEC toggle). Query keys are pinned to Cloudflare's documented List DNS Records contract; **the adapter does not use the provider's intentionally unspecified free-text `search` parameter.** Redirects and ambient proxies disabled, platform trust store, 2–60 s configured timeout, 1 MiB response cap, duplicate JSON keys rejected, and provider bodies/errors reduced to **local typed codes before durable state is touched**.

**Ownership and protected records.** Every mutation is bound to the authenticated user's selected tenant, active hosting service, active lifecycle state, active owned `WebHostingDomain`, approved zone, record UUID and expected generation. Each immutable job also stores protected FKs to the billing account and user that owned the domain when it was queued, and **claim and completion both reject an ownership reassignment.** Organization members can manage the selected tenant's records with their own identity as the audit actor, while durable node/provider ownership continues to use the tenant's billing account and its original owner.

Create, edit, worker claim and completion refresh the billing account and hosting service **under the same row locks used by billing suspension**, so a suspension that wins those locks prevents a new apply lease and a DNS mutation that wins first is linearized before suspension.

Because the hosting schema historically permits the same custom name on more than one service, customer DNS additionally refuses apply/update whenever a non-deleted domain in **another** billing account is the same name, an ancestor or a descendant. The comparison is **DNS-label-bound**, so `example.com` versus `ample.com` do not collide. Exact-owned deletion stays available.

Cloudflare comments mark records `layerone-customer-dns:<record-uuid>`; **the marker is ownership metadata rather than a secret.** Once a provider record ID is persisted, both the exact encrypted ID **and** marker must match. **Unmanaged records are never adopted.** A delete additionally requires exact name, type, value, priority, TTL, proxy policy and marker readback **before** the provider call, followed by verified absence.

The record row keeps a typed public `provider_intent` observation bound to its `observed_hash`, encrypted provider ID and immutable succeeded apply job. **A later failed edit does not replace that observation**, so deletion uses the last exact applied content — not the newer failed desired fields — while still advancing the generation. If no apply attempt ever reached a provider the portal closes a durable delete job locally; an attempted-but-unconfirmed apply still goes through exact marker/content absence reconciliation. Exact-comment lookup is zone-wide and bounded, so a create whose response was lost can be found even after the customer changes its owner name; **more than one matching marker is a conflict**, and a marker found outside the exact owned domain is never adopted.

LayerOne-owned names a customer can never create or update: `mail.<domain>`, `email.<domain>`, `autoconfig.<domain>`, `autodiscover.<domain>`; `_acme-challenge` at any managed depth; DKIM `_domainkey` and DMARC `_dmarc`; `_layerone*`; and **every exact record in the current validated hosted-mail DNS intent**. When hosted mail owns a domain, a second apex MX or SPF policy is rejected while unrelated apex TXT verification records remain allowed. Provider reconciliation also rejects a second SPF record, CNAME coexistence, an exact unmanaged duplicate, or any record shadowed by an NS delegation. A formerly customer-owned record can still be deleted after mail protection changes, because deletion is bound to its exact encrypted provider ID, marker and full prior contents. The portal repeats the CNAME-coexistence and SPF-singleton checks **under the owned domain lock before queueing**, so two otherwise valid customer requests cannot race while provider inventory has observed neither. **The hosted-mail intent is reparsed from its canonical stored contract each time protection is evaluated, and a malformed nonempty intent fails closed** — never treated as an empty protected set.

**Durable reconciliation.** Customer requests write desired state plus an immutable deterministic apply/delete job and return **without a provider call**. Jobs contain the public zone name and canonical record intent, never a credential, zone ID, record ID or response. Workers lease one row under `select_for_update`, validate current ownership and protection, **commit the lease**, call Cloudflare **outside the transaction**, then lock the job, record, approved zone and provider configuration again. Success publishes only if all of these still match: job ID, action, record/domain UUID and generation; the immutable payload, intent and canonical hash re-read after provider I/O; record desired state; approved zone/configuration update versions plus exact in-memory provider, zone-ID, timeout and decrypted-token equality (none of which are copied into the job); and the exact typed provider receipt and post-mutation readback.

The provider call is deliberately outside the ordinary transaction so suspension can still commit while Cloudflare is processing — and **such a completion never becomes active**: while holding the now-suspended rows the worker deletes only the exact marker-owned intent it just published and requires verified absence before recording the conflict. The same compensation runs when the apply response was lost. A crash or temporary cleanup failure leaves a durable `inactive_cleanup` retry; an expired apply lease on an inactive service resumes cleanup rather than re-applying; unsuspension is serialized behind that cleanup. **Delete jobs safely queued before suspension remain eligible**, so exact owned DNS removal is never blocked by the inactive lifecycle gate.

A generation that changed during provider I/O supersedes the older completion. A customer cannot start a second mutation while a provider lease is running. Pending/retry jobs can be superseded safely before provider I/O, and an expired running lease is recovered by the periodic worker before another mutation is accepted. A protection change is re-read under the completion lock; an apply that became protected during provider I/O is recorded as a conflict and never promoted to active. Both successful and failed completions compare the claimed provider/zone snapshot, so a stale failure from a replaced token cannot overwrite a later corrected configuration. **The operator forms lock the provider/zone row and reject every provider or zone edit while a customer DNS job is pending, running or retrying**, and refuse to change a zone's canonical name or Cloudflare ID while any non-deleted customer record still references it. Direct database edits during reconciliation are outside the supported contract.

Temporary failures use bounded exponential retry for at most **eight** attempts; conflicts are terminal until resolved. Status always shows pending, active, delete pending, conflict, failed or deleted **truthfully**, and a row is not hidden merely because deletion was requested. Audit events contain only job/record/domain identifiers, generation, action, outcome and local error code — **no record name/value, provider identifier, response, credential or token.**

Operator qualification order is in `README.md`; the short version is: scoped token (Zone Read, DNS Read, DNS Write; DNSSEC Read only if needed — **never the global API key**), map the exact approved parent zones with customer management **off**, qualify create/update/delete/retry, exact readback, stale-generation refusal, lost-response idempotency, provider/configuration races, delegation and CNAME conflicts, hosted-mail/ACME protection and cross-account denial **on a disposable staging zone**, then enable the switch on that zone only. **Unit tests and a configured token do not qualify production DNS.**

## 6.9 Certificates <a href="#id-69-certificates" id="id-69-certificates"></a>

Customer-domain AutoSSL state and renewal jobs are **separate from HTTP routing**, so routing, certificate expiry and HTTPS readiness cannot be conflated in the portal. `webhosting.renew_domain_certificates` gives qualified certificates a fresh immutable renewal generation before expiry, independently of domain routing state, and existing renewal also covers a failed first issuance. Certificate issuance is a **durable predecessor** of final mail setup verification ([6.7](#id-67-hosted-email)).

## 6.10 Hosted-mail DNS publication <a href="#id-610-hosted-mail-dns-publication" id="id-610-hosted-mail-dns-publication"></a>

Hosted-mail DNS uses **its own** Cloudflare token — not the node API key, not the website email-delivery token, not a global key — with only **Zone Read** and **DNS Write** for the exact authoritative zones. Operators paste it once into **Web hosting → Hosted-mail DNS** and add each approved zone name with its exact 32-character zone ID.

The **node**, not the browser, generates the DKIM key and returns a closed public DNS intent for `mail.<domain>`, `email.<domain>`, MX, SPF, DKIM, DMARC and optional client-discovery records. **Publish approved records** applies only that validated intent, marks every record with the domain's immutable LayerOne ownership ID, rejects unmanaged conflicts, removes only stale LayerOne-owned records, and performs an exact Cloudflare readback.

**DNS publication alone never marks mail ready.** After public DNS propagates, the node's hosted-mail qualification must pass again, and only then does **Recheck production readiness** advance the immutable generation and submit a fresh `ensure_mail_domain` job — where only the node's current, owner- and DNS-intent-bound `active` observation activates the domain and exposes mailbox or alias creation. **Replaying the original `pending_dns` job cannot activate it.** Changing the node-generated intent clears the previous receipt and every old proof; an exact identical retry is idempotent. **The customer never supplies arbitrary DNS record types, values, provider zone IDs or provider credentials.**


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.layeronecloud.com/platform/web-hosting/email-and-dns.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
