Available — Local Digital Products
HCA · Studio
TR — Contact ↗
Technical Note 07 · Server

The acme.sh staging trap, scheduled for 60 days from now

This is the hardest kind of TLS failure to notice, because today everything is fine. The certificate is valid, the browser is happy, monitoring is green. The problem surfaces on the day the renewal cron runs — and on that day the whole site throws a security warning.

Tool
acme.sh
Symptom
None today, total in 60 days
Evidence
Le_API line
Short answer

acme.sh keeps a per-domain configuration file at ~/.acme.sh/<domain>/<domain>.conf. The Le_API value in it decides which certificate authority that domain renews from, and it is fixed at the moment of the first issuance. If the certificate was ever issued with --staging, or the default CA was set to staging, every subsequent renewal comes from staging. Staging certificates are trusted by no browser. As long as the currently installed production certificate is still valid the problem is invisible; on renewal day the site becomes unreachable. Check with grep -r acme-staging ~/.acme.sh/*/*.conf.

Why it is silent

Let's Encrypt runs two separate environments. Production signs with roots that browsers trust. Staging speaks the same protocol and runs the same flow, but signs with a deliberately fake root — and says so in the name: "Pretend Pear", "Artificial Apricot".

Staging exists so you can rehearse automation without burning through strict rate limits. The trouble starts when a rehearsal becomes a permanent setting. acme.sh writes the domain configuration at first issuance and never asks again:

# ~/.acme.sh/example.com/example.com.conf
Le_API='https://acme-staging-v02.api.letsencrypt.org/directory'

As long as that line stands, every run of acme.sh --cron goes to staging. And if the certificate currently installed is a valid production one issued by hand, there is no symptom at all. This is a genuinely time-delayed fault: its trigger is not a code change but the calendar.

Detection

One line scans every domain on the server at once:

grep -r 'acme-staging' ~/.acme.sh/*/*.conf

Empty output means you are clean. Every matching line is a domain that will break at its next renewal.

You can also ask the live server who actually signed the certificate it is serving:

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -dates

If the issuer line contains "(STAGING)" or "Pretend", the problem is already live today. If it does not, but grep found a match, the problem is scheduled.

One more trap: a single server may host dozens of domains and only one of them may point at staging. Run the check across the whole directory, not per domain.

The fix

Editing the configuration file by hand is not enough, because acme.sh can regenerate it. The correct route is to reissue the certificate with the production server named explicitly:

acme.sh --issue -d example.com -d www.example.com \
  --server letsencrypt \
  --webroot /usr/local/lsws/Example/html \
  --force

--force is required because acme.sh would otherwise skip the job: the existing certificate has not expired yet. --server letsencrypt pins Le_API back to production.

So the same mistake does not repeat on new domains, set the default CA once:

acme.sh --set-default-ca --server letsencrypt

Do not forget the install step — obtaining a certificate is not the same as deploying it. Define the target files and the reload command with --install-cert so renewals complete end to end:

acme.sh --install-cert -d example.com \
  --key-file       /etc/letsencrypt/live/example.com/privkey.pem \
  --fullchain-file /etc/letsencrypt/live/example.com/fullchain.pem \
  --reloadcmd      "/usr/local/lsws/bin/lswsctrl reload"

Verification

After the fix, check three things in order:

# 1) Does the configuration point at production now?
grep Le_API ~/.acme.sh/example.com/example.com.conf

# 2) Who signed what the server is actually serving?
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -enddate

# 3) Does a renewal dry run complete cleanly?
acme.sh --cron --home ~/.acme.sh --dry-run

The third step is the valuable one: instead of waiting for renewal day, you see today what will happen on it.

Early warning

What faults in this class have in common is that they stay invisible until you go looking. A simple scheduled check removes the surprise: read the certificate's expiry and issuer once a day, and alert when fewer than 21 days remain or the issuer is not what you expect.

Run the check from outside, over a TLS connection, not against the file on disk. The file can be correct while the web server is still serving something else; what matters is the certificate a visitor is handed.

Summary

Staging environments exist for testing, and test settings have a habit of leaking into permanent configuration. In acme.sh that leak hides in a single line and stays quiet for weeks. When you inherit a server, or whenever you are investigating certificate trouble, make grep -r 'acme-staging' ~/.acme.sh/*/*.conf the first command you type — it costs three seconds and saves a day later.

Quick reference
The trap
Le_API is fixed at first issuance; once staging, always staging
Why silent
No symptom while the installed production certificate is still valid
Trigger
Not a deploy — the calendar, on the day the renewal cron runs
Detect
grep -r 'acme-staging' ~/.acme.sh/*/*.conf
Verify
Look for (STAGING) in openssl x509 -noout -issuer
Fix
Reissue with --server letsencrypt --force
Prevent
acme.sh --set-default-ca --server letsencrypt
Monitor
Check issuer and days remaining from outside, over TLS
Frequently Asked Questions

About the staging certificate trap.

My certificate looks valid right now. Am I still at risk?

Possibly yes. If the certificate currently installed is a valid production one issued by hand, no browser will complain. But if the acme.sh configuration points at staging, the renewal cron will install an untrusted certificate over it on the day it runs. Checking today is far cheaper than waiting for renewal day.

Can I just edit the line in the configuration file?

Not reliably. acme.sh can regenerate that file during its own operations and your manual edit may disappear. The correct approach is to reissue with --server letsencrypt --force, which both writes the file correctly and installs a production certificate immediately.

How do I recognise a staging certificate?

By the issuer field. Staging certificates are signed with openly fake names such as (STAGING) Pretend Pear or (STAGING) Artificial Apricot. Pipe openssl s_client output into openssl x509 -noout -issuer and the name appears directly.

Is checking one domain on a server enough?

No. The problem is per domain; on a server with ten domains only one may point at staging while the rest look healthy, and that one will break on renewal day. Always run the check across the whole ~/.acme.sh directory.

Availability and Quotes

Have an idea?
Half a sentence is enough.