Skip to main content
One certificate serves every TLS surface of the instance: HTTPS/JMAP (443), submission STARTTLS (587), SMTPS (465), and IMAPS (993). Who obtains it depends on the deployment topology chosen at first boot.

Direct topology: built-in ACME

When OxiMail owns ports 80/443, it provisions and renews its own certificate from Let’s Encrypt:
The first certificate is obtained at first start (the wizard polls up to 90 seconds for it); renewals happen automatically before expiry. Port 80 must stay reachable from the internet for the HTTP-01 challenge. The wizard also publishes a CAA record restricting issuance to Let’s Encrypt — a hardening default that matches this setup. Renewal never restarts anything. The new certificate is swapped in atomically (ArcSwap, ADR-016): in-flight connections finish on the old one, new handshakes get the new one, on every listener at once — including SMTP and IMAP. The HTTPS listener follows too: it used to keep the certificate it started with until a restart, so after a renewal or a key rotation it went on serving the previous certificate while the TLSA record of its port had moved on. A failed renewal keeps the certificate in service. When an order fails, the certificate being served stays in place as long as it has not expired, and nothing is republished: the TLSA records keep the pin of the key actually served. The failure is retried after 1 hour, then 2, 4, 8 and 16 hours, then once a day. Only an expired certificate gives way to a self-signed one, and that placeholder keeps the key being served, so the published pin stays valid (a 3 1 1 pin ignores dates); it used to make a new key and re-pin the TLSA records to it. The same holds at start-up: if the boot order fails, a persisted certificate that has not expired is served (even inside its renewal window, or missing a newly configured name) and the order is retried on that schedule. A box that has nothing persisted serves a self-signed placeholder, and the placeholder itself counts as a reason to renew, so it is retried on the same schedule. A failed renewal used to swap a still-valid certificate for a self-signed one and republish the DANE pin of that self-signed key, and a failed first order left the box on its placeholder for about sixty days. Adding a name renews. The renewal decision compares the names the served certificate actually carries with [tls] acme_domains, not only its expiry: a name added to the list is obtained at the next start, and the journal says which of the two reasons applies, “the date is near” or “a name you configured is missing”, since they call for different reactions. The check runs at start-up, not twenty-four hours later, because a restart after a setting change is the moment the change is expected to apply. It used to look at the date alone, so the box kept its old certificate for up to two months while the new name refused every connection, with a journal that announced the certificate loaded.

Reverse-proxy topology

Behind Caddy or Nginx, the proxy owns 80/443 and the certificate of the web names. The wizard still writes acme_enabled = true: OxiMail orders the certificate of its own hostname, which the mail ports use, and the proxy forwards the HTTP-01 challenge path to OxiMail’s http_bind. Two things matter:
  • SMTP (25/587/465) and IMAPS (993) are not proxied: OxiMail terminates TLS on those itself, with the certificate it ordered. The alternative is acme_enabled = false with [tls] cert_path/key_path pointing at the proxy’s certificate files; OxiMail then hot-reloads them when they change, but whoever holds the proxy holds the private key of the mail name. Choose one of the two. acme_enabled = true and a cert_path is not a mode: the mail ports start on the file, the first ACME renewal replaces it, the file watcher puts it back, and the TLSA records follow whichever renewed last. oximail doctor reports it under tls_cert_source, and oximail setup verify warns about it.
  • The proxy must forward everything except the ACME challenge path, with X-Forwarded-For from a [server] trusted_proxies address — see first boot for the generated snippet and the CORS rule. A proxy on the same machine reaches OxiMail through a Unix socket instead ([server] bind = "unix:<path>", what the wizard installs): only the proxy’s group may open it, the peer is the proxy, a request without a forwarding header naming a client is refused (400), and no trusted_proxies entry is needed; doctor client_address_mode warns when one is still listed. An entry of trusted_proxies that is not a loopback address lets whoever sits at that address set the client IP the box logs, rate-limits and bans. oximail doctor --reachability checks, under trusted_proxies, that each such entry still serves the box’s name on 443, so a proxy that moved away does not stay trusted.
What a proxy keeps, and a direct box does not have. A box that terminates 443 itself offers HTTP/2 and HTTP/1.1 (ALPN h2, http/1.1). It offers neither HTTP/3 nor Encrypted Client Hello: the TLS library OxiMail uses has no server side for ECH yet. If the web names need ECH, keep a proxy that supports it in front of them.

The DANE coupling

If you publish DANE TLSA records (email authentication), the record OxiMail publishes is the 3 1 1 pin, a hash of the certificate’s public key. A renewal certifies the same key by default ([tls] acme_reuse_key = true), so the pin does not change at renewal and a DANE-validating sender never meets a certificate that no longer matches the record it cached. acme_reuse_key = false gives a fresh key at every renewal: the pin then moves each time and the TLSA records have to follow. If the stored key cannot be read back, the renewal certifies a fresh key and logs an error saying the pin will change. dane_enabled = true (the default) still requires a configured [dns] provider: after a renewal OxiMail republishes the TLSA records itself (Cloudflare API, or a dynamic update on your own DNS server with rfc2136, see DNS providers), or logs the state of the records in manual mode. A server with DANE on and no DNS provider refuses to start rather than setting you up for a silent breakage. A certificate renewed outside OxiMail with a new key waits for its pin. With [tls] cert_path, the file watcher used to swap the renewed certificate in first and re-pin after it: when the source (a reverse proxy’s ACME client, say) made a new key at each renewal, every DANE-validating sender met a mismatch for at least the TTL, and deferred mail, at every renewal. When OxiMail publishes the TLSA records itself (cloudflare or rfc2136 provider, DANE on), the watcher now publishes the new key’s 3 1 1 pin beside the served one, keeps serving the current certificate, and swaps once the pin has been published for two TTLs (2 x 3600 s, RFC 7671 §8.1); the old pin is then retired. A served certificate within about three hours of its expiry is swapped at once. The journal says the wait and names the fix at the source: keep the key across renewals (reuse_private_keys in Caddy, for instance), and no renewal waits. oximail cert renew --force keeps the key as well when acme_reuse_key is on: only the certificate steps aside, and a copy of the key is filed as key.pem.pre-renew-<ts>.

Rotating the key on purpose

Changing the key is a deliberate act, done so that a validator never holds only a pin the served certificate does not match (publish before swap, RFC 7671 §8.1):
  1. Publish. Without --apply the verb says what it would do. --apply generates a new key, stages it and publishes its 3 1 1 pin beside the current one on every pinned port: 25, 465 and 587, or 25 alone on a backup MX (which ports). When [dns] provider is "cloudflare" or "rfc2136" and [smtp] dane_enabled = true, OxiMail publishes the records itself; otherwise the verb prints them for you to publish by hand, with a TTL of 3600 seconds at most.
  2. Wait. The server checks once an hour, and once at start. It waits until public DNS shows the new pin on every one of those ports, then two TTLs more (2 x 3600 s), so that every resolver holding the old set has fetched the new one. It never switches before that. If the new pin disappears from DNS after being seen, the wait starts over.
  3. Switch. The server orders a certificate over the staged key and serves it, without a restart. In that same case (cloudflare or rfc2136 provider and DANE on), the renewal callback then retires the old pin; otherwise remove the old 3 1 1 records yourself once the journal reports the switch. A switch that fails leaves the previous certificate in service and is retried at the next check.
Running the verb again while a rotation is under way shows where it stands (--json for scripts), and --apply then publishes again a pin that failed to publish. oximail dane rotate --cancel --apply abandons the rotation before the switch: the staged key is filed away and the new pin is withdrawn by OxiMail (cloudflare or rfc2136 provider and DANE on) or left for you to remove. Exit code 0 when done or read, 1 when the pin could not be published on every port (run --apply again), 2 when refused or failed. The rotation needs [tls] acme_enabled, because the switch is a certificate order; the verb refuses otherwise. Behind a reverse proxy, change the key where the certificate is made, then re-pin with oximail dane publish. oximail doctor reports a rotation under dane_key_rotation: its stage and age, with a warning when the new pin is still not visible after 24 hours, or when the switch is more than two hours late. The rotation state lives in the acme/ directory of the data directory (the parent of [storage] blob_path): key-rotation.json and the staged key key.next.pem, so a restart resumes where the rotation stopped. At the switch the previous key is kept as key.pem.pre-rotate-<ts>. The staged key file is renamed, never deleted: key.next.pem.switched-<ts> or key.next.pem.cancelled-<ts>.

Post-quantum key exchange

Every TLS surface OxiMail terminates itself — 25, 465, 587, 993, and 443 when no frontend sits in front of it — offers the hybrid group X25519MLKEM768 first, with classic X25519 as the fallback. Nothing is broken by this: a peer that does not know the hybrid group negotiates X25519 exactly as it did before. There is no setting to turn it on; it is the shipped posture. The threat it addresses is “harvest now, decrypt later” — an adversary recording today’s traffic to decrypt it when the machine to do so exists. Being late to this is not recoverable after the fact, which is why it is a default rather than an option. It is not theoretical for mail either: Gmail already offers the hybrid group on 25, 465, 587 and 993, so outbound mail to it negotiates post-quantum today. Verify rather than assume. This exact property was silently false here for months: the feature was compiled in while the installed crypto provider had no implementation for it, so every session fell back to classic X25519 while the configuration read as if it did not. A fallback of this kind is invisible — no error, no log line, nothing failing. With OpenSSL 3.5 or later:
Run it against each port you serve. A frontend in front of 443 answers for itself, not for OxiMail — check the mail ports separately. Outbound connections use the same stack. The HTTP client behind webhooks, push and link unfurling runs on the same TLS implementation as the server, so those fetches gained the hybrid group too. It trusts the operating system’s certificate store rather than a bundled root list, deliberately: an endpoint whose certificate chains to a private CA installed on the box keeps working.

Inspecting and troubleshooting

setup verify reads cert_strategy (operator-cert-path when cert_path is set, which wins; acme-http-01; none) and renewal_owner (operator, oximail, none) off the configuration, checks the certificate itself (a cert_path file absent, or an ACME certificate still pending while the box serves a self-signed one), names the three classic post-install failures with their remedy (DNS not propagated or resolving elsewhere, a CDN for instance; certificate pending; outbound port 25 blocked, a failure when the box sends directly and a warning when a relay carries every outbound message), and lists egress_endpoints, what the box must reach as deduced from its configuration (SMTP :25, DNS, the ACME authority when active, the Cloudflare API when configured, the usage report when active), for a default-deny firewall. Exit code 0, 1 degraded (a warning, an optional piece absent), 2 a check failing; every failed check carries a remediation field (CLI). doctor asks a second question the certificate check cannot: whether the service user can read the certificate, its key, the mail signing keys and the push key. A certificate that is valid when read as root and a symlink into another program’s store whose directories the oximail user cannot traverse start the service fine and encrypt nothing; the check asks the system rather than reimplementing the permission rules, so directory traversal is covered, and names the file, its owner and the exact link that blocks. It runs against the user of the unit this binary generates, so on a box still running as root it is a preflight of the switch. Common failures:
  • Port 80 unreachable — the HTTP-01 challenge cannot complete. Open it in the firewall/security group; it only ever serves challenges and redirects.
  • Rate-limited by Let’s Encrypt — repeated failed issuance attempts (usually a DNS mistake) can hit LE’s per-domain limits; fix the cause before retrying in a loop. The server’s own retries back off to one order a day (see “a failed renewal” above), so a manual oximail cert renew --force in a loop is what usually hits the limit.
  • DANE senders bouncing after a renewal — the served key changed and the TLSA records did not follow. With acme_reuse_key on, a renewal keeps the key; a new one means acme_reuse_key = false or a stored key that could not be read (the journal says which). Check the [dns] provider configuration and the renewal logs, and compare with oximail dane verify.
With built-in ACME, the certificate, its key and the ACME account (cert.pem, key.pem, account.json) live in acme/ under the data directory, the parent of [storage] blob_path (/var/lib/oximail/acme/ with the wizard’s default paths). oximail backup does not copy that directory: it saves the database and the blobs. Keep it in the host backup if you publish DANE, since a lost key.pem means a new key at the next issuance, so a new 3 1 1 pin. With cert_path/key_path, the files are wherever those keys point. The setup wizard creates an empty /etc/oximail/tls/, a convenient place for such a copy; the server itself writes nothing there.