Using acme.sh to get certs, but Stalwart does not refresh "Valid From" and "Expires" in TLS -> Certificates

Issue Description

I use acme.sh to get my certificates, but when I get a new cert (letsencrypt) Stalwart does not show the new “Valid From” and “Expires” date in the TLS → Certificates page.
Also when I restart Stalwart, the page shows the old dates

I use stalwart 0.16.5

Expected Behavior

Normally the TLS → Certificates page should show the correct dates

Actual Behavior

I see the old dates on the Stalwart TLS->Certificates UI:

Expires
Expiration date of the certificate

Aug 12, 2026, 9:34 PM
Valid From
Issuance date of the certificate

May 14, 2026, 9:34 PM
Issuer
Certificate issuer

C=US, O=Let’s Encrypt, CN=E8

#########

A have checked with “sslscan https://mail.schleicher.li:465” and get the following output

SSL Certificate:
Signature Algorithm: ecdsa-with-SHA384
ECC Curve Name: prime256v1
ECC Key Strength: 128

Subject: schleicher.li
Altnames: DNS:.lug-bruchsal.de, DNS:.lugbruchsal.de, DNS:*.schleicher.li, DNS:lug-bruchsal.de, DNS:lugbruchsal.de, DNS:schleicher.li
Issuer: E8

Not valid before: May 15 05:54:53 2026 GMT
Not valid after: Aug 13 05:54:52 2026 GMT

The dates are different, Stalwart use the new cert but in the UI it shows the old dates

Reproduction Steps

  1. create with acme.sh certs.
  2. configure the certs in Stalwart-UI
  3. renew the acme.sh certs and restart stalwart
  4. Stalwart-UI shows still the old Cert in the UI

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

Let me research how Stalwart handles TLS certificate loading and storage to understand this issue.

The root cause is clear. Here is what’s happening and how to fix it.

The UI displays stale metadata because the Certificate object in Stalwart’s store was never updated

When you use manual mode (i.e. you give Stalwart a file path or inline PEM for the cert), Stalwart parses the certificate at save time and caches the read-only metadata fields (notValidBefore, notValidAfter, issuer, SANs) in its database. Just replacing the file on disk – or even restarting the service – does not re-parse the file and update those cached fields. That is why:

  • sslscan shows the new dates (Stalwart is serving the new file correctly from disk), but
  • the WebUI still shows the old dates (it is displaying the stale cached metadata from the last time you saved the Certificate object).

What to do

After acme.sh renews your certificate, you need to trigger a TLS certificate reload, which re-reads the file and updates the metadata. Per the docs, this can be done without stopping the server:

  • From the WebUI (Settings › TLS › Certificates, then trigger the reload action), or
  • From the CLI:
stalwart-cli --url https://<your-host> --api-key <TOKEN> create action/ReloadTlsCertificates

You can automate this in your acme.sh --reloadcmd hook (or a cron job / systemd unit that fires after renewal) so it runs automatically every time a new cert is installed. For example, as a --reloadcmd:

stalwart-cli --url https://mail.example.com --api-key <TOKEN> create action/ReloadTlsCertificates

After the reload, the WebUI will show the correct new “Valid From” and “Expires” dates.

Relevant docs: TLS Certificates - Reloading certificates and the Caddy reverse proxy page (which shows a full systemd + CLI reload example you can adapt for acme.sh).

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

I already copy the new acme.sh certs to the Stalwart machine run the following command.

root@stalwart:~# /root/.cargo/bin/stalwart-cli --url https://mail.schleicher.li --api-key “here my API KEY” create action/ReloadTlsCertificates
Created Action bvanvhq

But it does not change the TLS → Certificates dates in the WEB-UI.
Also a “systemctl restart stalwart” does not fix it.

Stalwart parses the certificate info only when the certificate is created. When rotating an external certificate, you need to delete the old certificate from Stalwart and add it again after each change. This could be automated using either the JMAP API or the CLI.

Thanks for the answer.

I already run the CLI command “… server reload-certificates”.

Is it really required to re-configure (deleted and add) the new certificate or ist it Just required to Display the correct expire Date?

Hi @stalwart,

based on the issues touched upon by this thread, I just wanted to point out that I believe action/ReloadTlsCertificates behaves in a misleading and potentially disruptive way in v0.16.x.

I generate certificates externally with acme.sh and expose a folder containing the cert and key to Stalwart via a Docker bind mount. In stalwart the cert/key are configured as a Text value read from file. After acme.sh runs, a cronjob replaces the cert/key on disk and then calls stalwart-cli […] create action/ReloadTlsCertificates.

Before I upgraded to v0.16.x last week, this worked perfectly (using the legacy API via curl rather than stalwart-cli).

Today, though, I was suddenly served a self-signed rcgen certificate. Luckily, this was caught quickly by UptimeKuma and by my mail client refusing to connect. This was confusing, since the acme.sh cronjob had run successfully just the night before, and I had verified only a couple of hours earlier that openssl s_client -connect mail.example.org:465 returned the correct cert.

I found these two lines in the log:

2026-06-22T18:35:55Z INFO Certificate expired and removed (tls.expired-certificate-removed) details = ["mail.example.org", "mta-sts.example.org"]
2026-06-22T18:36:12Z WARN No TLS certificates available (tls.no-certificates-available) total = 0

What seems to be happening:

  1. On the surface, action/ReloadTlsCertificates does what the name suggests. It reloads the changed cert/key files from disk.
  2. Even though, as you stated, the date shown in the web UI is not updated, the reloaded cert is served to the clients. Checking with openssl s_client -connect … of course also shows that everything is fine (or rather seems to be).
  3. However, once the expiry shown in the web UI date passes, the certificate is “removed” (cf. log) and a self-signed one is generated in its place, regardless of whether a fresh cert/key was already reloaded (and is actively being served to the clients!).
  4. In addition, the certificate “object” still shows up in the web UI. It also shows up under Network > General. So a) it is unclear where it is being “removed” from and b) the web UI currently doesn’t show that a self-signed cert is used instead.
  5. All of this is easy to get caught out by, because action/ReloadTlsCertificates appears to work, and there is no way to tell that it won’t anymore, especially since it is being served/openssl s_client -connect … shows the reloaded cert right up until the expiry date shown in the web UI hits.
  6. This also raises the question of what action/ReloadTlsCertificates is actually for, if a reloaded cert on disk still has to be recreated to remain valid?

My preferred fix would be to restore the v0.15.x behavior, where reloaded certificates were actually persisted for as long as they themselves are valid.

If that’s not feasible, a note in the docs would help at a minimum. Better still would be a WARN log line each time action/ReloadTlsCertificates runs, making clear that the reloaded cert persists only temporarily, until the original expiry date is reached.

Even though this might come across as slightly nitpicky, my points come from a genuine appreciation for this project, which I would love to see grow further. I believe that addressing small (but still somewhat disruptive) inconveniences like this, even if only via the docs, is an important part of keeping users happy and admin overhead low.

Thank you for your time and all the work so far!

This has been fixed. The fix will be included in v0.16.11.

Wow, that was quick, thank you so much!