A second certificate authority can provide another issuance path when Let’s Encrypt has a service problem or a rate limit prevents timely renewal. It does not repair broken DNS validation, an invalid API token or a restrictive CAA record. Trying another CA after every nonzero exit can create unnecessary orders while leaving the real fault unresolved.
This guide uses separate acme.sh configurations for Let’s Encrypt and ZeroSSL, exports certificates to protected staging directories, and deploys checked files to Nginx. Daily renewal checks are automatic. Switching to the secondary CA requires an explicit decision for a diagnosed CA incident; an unknown failure stops the process. The walkthrough is an example, not a claim about a customer deployment or measured results.
What a second CA can and cannot solve
ACME automates communication between a client and a certificate authority. Issuance and deployment are separate operations: a successful order does not prove that the web server has loaded the resulting certificate. A wrong file path or failed reload can leave the old certificate in service.
A standby CA is useful only if its account, validation method, trust chain and operating conditions have been checked. Both CAs can depend on the same DNS provider and credentials. For a small site, timely renewal monitoring and a tested recovery procedure may be more valuable than maintaining a second account. Additional setup and maintenance are real costs; redundancy is not a zero-effort guarantee.
Keep the two acme.sh configurations separate
Use a separate config-home and cert-home for each CA. Pass --server on each call instead of changing the global default while other jobs may be running. A default-CA change alone does not change the issuer of the certificate currently served by Nginx.
The example explicitly selects ECDSA P-256 with --keylength ec-256, then uses --ecc for renewal and export. Do not label an unspecified default key type as RSA. ZeroSSL uses EAB; the inspected acme.sh implementation supports obtaining EAB credentials using the registration email. If that path fails, check the account’s current documented EAB procedure. An environment variable expanded into a command argument does not, by itself, hide that argument from process inspection.
Choose DNS validation and check CAA
DNS-01 creates a TXT record and supports wildcard validation. HTTP-01 can be simpler for a small installation with a reliable webroot and avoids granting DNS API write access. A wildcard such as *.example.com covers www.example.com, but not the apex domain or a.b.example.com. Include the apex explicitly when needed.
If CAA records exist, confirm that both selected CAs are permitted for the requested names. The issue and issuewild properties may differ; obtain each CA identifier from its current documentation. A DNS API outage, propagation problem or token permission error can affect both CAs. DNS-01 for custom domains and multi-tenant services explains the wider validation workflow.
Define the example environment and failure policy
The commands target Debian 12, an existing Nginx server, Bash, systemd, OpenSSL and Cloudflare DNS. They run in a root shell because the example owns protected certificate paths and reloads Nginx. Replace example.com, the email address and credential placeholders with a test domain you control. Adapt package names, trust-store paths and service permissions on other systems. Back up the current Nginx TLS configuration and certificate paths first.
The daily job checks Let’s Encrypt first. A failed renewal creates a blocked file, preventing later timer runs until the incident is reviewed. ZeroSSL is attempted in that run only when ALLOW_FALLBACK=1 has explicitly been enabled for a diagnosed CA-side incident. This setting is not an error classifier: do not enable it for DNS, token, CAA or local filesystem faults. Each attempt has its own log; stale lines from a shared log never decide the route.
Install, register, issue and export the first certificates
Get the client from the official repository, record the revision and review upgrades in a test environment before using them. This direct checkout does not install a cron job. If you already use acme.sh, inspect the existing schedule and avoid two independent schedulers managing the same certificate.
apt-get update
apt-get install -y git curl openssl ca-certificates util-linux
git clone --depth 1 https://github.com/acmesh-official/acme.sh.git /opt/acme.sh
git -C /opt/acme.sh log -1 --format='%H %cs %s'
# Review and retain the chosen revision before running it.
/opt/acme.sh/acme.sh --version
install -d -m 700 /etc/acme-fallback /var/lib/acme-fallback/logs \
/etc/ssl/acme-example/releases
install -m 600 /dev/null /etc/acme-fallback/envFill /etc/acme-fallback/env with a DNS token scoped to the required zone and DNS edits, plus any zone-discovery read permission required by your dns_cf version. The file must remain owned and writable only by root, with mode 0600. It is sourced as shell code, so it must not contain untrusted input.
export CF_Token='REPLACE_WITH_SCOPED_DNS_API_TOKEN'
export CF_Account_ID='REPLACE_WITH_CLOUDFLARE_ACCOUNT_ID'
ALLOW_FALLBACK=0The following Bash script registers the accounts and performs the initial issuance at both CAs. That is two real certificate orders, each subject to the provider’s conditions and limits. A quoted Bash array keeps each domain argument separate and prevents wildcard expansion into local filenames. The two configurations use identical names and key types.
#!/usr/bin/env bash
set -euo pipefail
umask 077
source /etc/acme-fallback/env
ACME=/opt/acme.sh/acme.sh
STATE=/var/lib/acme-fallback
DOMAIN=example.com
DOMAINS=(-d "$DOMAIN" -d "*.$DOMAIN")
for ca in letsencrypt zerossl; do
install -d -m 700 "$STATE/$ca/config" "$STATE/$ca/certs" "$STATE/$ca/stage"
COMMON=(--home /opt/acme.sh --config-home "$STATE/$ca/config"
--cert-home "$STATE/$ca/certs" --server "$ca")
"$ACME" "${COMMON[@]}" --register-account -m ops@example.com
"$ACME" "${COMMON[@]}" --issue "${DOMAINS[@]}" --dns dns_cf --keylength ec-256
"$ACME" "${COMMON[@]}" --install-cert -d "$DOMAIN" --ecc \
--cert-file "$STATE/$ca/stage/leaf.pem" \
--key-file "$STATE/$ca/stage/key.pem" \
--fullchain-file "$STATE/$ca/stage/fullchain.pem" \
--reloadcmd 'true'
doneSave it as /root/acme-bootstrap.sh and run bash /root/acme-bootstrap.sh once for initial setup. Stop if either CA step fails. Before repeating a partial setup, inspect which steps completed; a skip result on an existing certificate is not a fresh issuance failure. Confirm that each stage directory contains leaf.pem, key.pem and fullchain.pem.
Use --install-cert to export files rather than configuring Nginx to read acme.sh’s internal directory layout. The stored --reloadcmd true is intentional: automatic renewal updates only the staging files. The deployment function below owns validation and the live reload. Account creation alone does not prove that DNS validation or issuance works for every domain.
Point the appropriate Nginx TLS server block at these stable paths. Keep the old configuration backup. The first service run will create the selected release and test Nginx before reloading. Its root master process must be able to read the protected files; a rootless service needs a different, narrowly authorized deployment arrangement.
ssl_certificate /etc/ssl/acme-example/current/fullchain.pem;
ssl_certificate_key /etc/ssl/acme-example/current/key.pem;Save the following as /usr/local/sbin/acme-fallback.sh. acme.sh return code 2 can mean a skipped renewal, including an unissued domain. The example requires an exported certificate before treating that code as a non-error and validates the certificate again before deployment. It never responds to every nonzero exit by forcing a new order.
#!/usr/bin/env bash
set -euo pipefail
umask 077
ACME=/opt/acme.sh/acme.sh
STATE=/var/lib/acme-fallback
LIVE=/etc/ssl/acme-example
DOMAIN=example.com
exec 9>"$STATE/renew.lock"
flock -n 9 || exit 0
[[ ! -e "$STATE/blocked" ]] || exit 1
# Trusted root-owned file, mode 0600; no untrusted input.
source /etc/acme-fallback/env
ALLOW_FALLBACK=${ALLOW_FALLBACK:-0}
acme_for() {
local ca=$1
shift
"$ACME" --home /opt/acme.sh \
--config-home "$STATE/$ca/config" \
--cert-home "$STATE/$ca/certs" --server "$ca" "$@"
}
renew_one() {
local ca=$1 log rc
log=$(mktemp "$STATE/logs/$ca.log.XXXXXXXX") || return 1
if acme_for "$ca" --renew -d "$DOMAIN" --ecc --log "$log"; then
return 0
else
rc=$?
# acme.sh uses 2 for skipped renewals, including unissued domains.
if [[ $rc -eq 2 && -s "$STATE/$ca/stage/leaf.pem" ]]; then
return 0
fi
printf 'Renewal failed for %s; inspect %s\n' "$ca" "$log" >&2
return 1
fi
}
deploy_one() {
local ca=$1 stage cert_key private_key release previous name
stage="$STATE/$ca/stage"
# Validate the first certificate in the fullchain that Nginx will load.
openssl x509 -in "$stage/fullchain.pem" -noout -checkend 86400 || return 1
for name in "$DOMAIN" "www.$DOMAIN"; do
openssl verify -purpose sslserver -verify_hostname "$name" \
-CApath /etc/ssl/certs -untrusted "$stage/fullchain.pem" \
"$stage/fullchain.pem" || return 1
done
openssl pkey -in "$stage/key.pem" -check -noout || return 1
cert_key=$(openssl x509 -in "$stage/fullchain.pem" -pubkey -noout |
openssl pkey -pubin -outform DER | sha256sum) || return 1
private_key=$(openssl pkey -in "$stage/key.pem" -pubout -outform DER |
sha256sum) || return 1
[[ "$cert_key" == "$private_key" ]] || return 1
if cmp -s "$stage/fullchain.pem" "$LIVE/current/fullchain.pem" &&
cmp -s "$stage/key.pem" "$LIVE/current/key.pem"; then
return 0
fi
release=$(mktemp -d "$LIVE/releases/release.XXXXXXXX") || return 1
install -m 600 "$stage/key.pem" "$release/key.pem" || return 1
install -m 600 "$stage/fullchain.pem" "$release/fullchain.pem" || return 1
previous=$(readlink -f "$LIVE/current" || true)
ln -sfn "$release" "$LIVE/next" || return 1
mv -Tf "$LIVE/next" "$LIVE/current" || return 1
if nginx -t && systemctl reload nginx; then
printf 'Certificate installed from %s\n' "$ca"
return 0
fi
if [[ -n "$previous" && -d "$previous" ]]; then
ln -sfn "$previous" "$LIVE/next" || return 1
mv -Tf "$LIVE/next" "$LIVE/current" || return 1
nginx -t && systemctl reload nginx || true
else
rm -f "$LIVE/current"
fi
return 1
}
selected=letsencrypt
if ! renew_one letsencrypt; then
# Stop later timer runs until the incident and Retry-After are reviewed.
touch "$STATE/blocked"
[[ "$ALLOW_FALLBACK" == 1 ]] || exit 1
# Set this flag only for a diagnosed CA-side incident, not a DNS/token error.
renew_one zerossl || exit 1
selected=zerossl
fi
# Deployment failure must never trigger another CA order.
if ! deploy_one "$selected"; then
touch "$STATE/blocked"
exit 1
fiThe wrapper is for one fixed domain on a root-managed server. It is not a general service that accepts tenant input as a path or shell command. It verifies expiry, names, trust and the key pair, then switches a single symlink to a new version directory. Failed Nginx validation or reload restores the previous link where one exists. A deployment failure never triggers an order at another CA.
Save the systemd service and timer as separate files. Root execution here matches the chosen filesystem paths and reload operation. Simply changing User=root to User=acme will not grant the required permissions; a dedicated account needs a separately designed deployment interface with narrowly scoped rights.
# /etc/systemd/system/acme-fallback.service
[Unit]
Description=Reviewable ACME renewal and controlled CA fallback
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=root
UMask=0077
ExecStart=/usr/local/sbin/acme-fallback.sh
# /etc/systemd/system/acme-fallback.timer
[Unit]
Description=Daily ACME renewal check
[Timer]
OnCalendar=daily
RandomizedDelaySec=1800
Persistent=true
[Install]
WantedBy=timers.target
bash -n /usr/local/sbin/acme-fallback.sh
chmod 700 /usr/local/sbin/acme-fallback.sh
systemctl daemon-reload
systemctl start acme-fallback.service
systemctl status acme-fallback.service --no-pager
journalctl -u acme-fallback.service --since today
# Enable the timer only after checking the first deployment.
systemctl enable --now acme-fallback.timer
systemctl list-timers acme-fallback.timerTest issuance separately from production trust
Use Let’s Encrypt staging with separate account/configuration directories and a separate test server. Its certificates are not trusted by public clients and must not be installed into the production certificate path. Do not assume ZeroSSL offers an equivalent staging endpoint; check the current account documentation first.
Exercise distinct failures: a CA-side outage, invalid DNS credentials, a lock collision, missing staging files and a failed Nginx reload. The expected response to an unknown failure is a blocked subsequent run and an alert. A canary does not mean forcing fresh issuance every day. Test the source revision and these local paths before enabling the timer.
Scale domain groups without sharing failure domains
Start with a small canary group, then distribute checks across batches. Limit concurrent writes to a shared DNS zone and preserve separate credentials or permissions where tenant boundaries require them. SAN groups and wildcards reduce some operational overhead but enlarge the impact of a shared key and can couple names during validation.
For larger installations, model the complete custom-domain validation flow and the responsibilities of each DNS service. Multi-provider DNS with octoDNS is related architecture work, not an automatic remedy for a failed CA order. Every authoritative provider still needs a consistent challenge record.
Diagnose the failed layer before switching CA
Read the log for the specific failed attempt, not a recent tail of a global log. Confirm whether the failure concerns account authorization, CAA, TXT publication, resolver reachability, the CA endpoint or local file installation. A bare HTTP status or a word such as “timeout” is insufficient evidence that the alternate CA will work.
If blocked exists, review the failure and the applicable Retry-After time before removing it. Enable the fallback flag only for the controlled incident, then restore it to 0. Once the cause is corrected and the wait has elapsed, remove the block and start the service once to observe the outcome. Keep private keys and DNS secrets out of tickets and shared logs.
A different certificate chain can affect older clients even when modern browsers trust it. Serving RSA and ECDSA certificates together can address some key-algorithm compatibility needs, but it does not make every CA root trusted by every client.
Use a canary and verify the served certificate
After a controlled deployment, verify the external endpoint rather than relying only on a successful command. The SNI and hostname checks below matter: a completed TCP or TLS connection alone is not proof of the expected certificate. Repeat the check for every relevant load-balancer or server endpoint.
openssl s_client -connect example.com:443 -servername example.com \
-verify_hostname example.com -verify_return_error </dev/null
openssl x509 -in /etc/ssl/acme-example/current/fullchain.pem \
-noout -issuer -dates -ext subjectAltNameKeep staging tests separate from that public trust check. Monitor the served certificate’s expiry independently, the systemd result, the block file and the age of staging files. The wrapper’s 24-hour deployment threshold is only a final guard; arrange renewal alerts and human response substantially earlier.
Respect rate limits and the client renewal schedule
The timer checks daily; it does not force a new order daily. Use --renew for routine renewal and let the client apply its supported renewal scheduling, including ARI where available. Forcing --issue is not equivalent to an ARI-coordinated renewal. Follow the CA’s Retry-After response and do not turn a rate-limit event into a rapid two-CA retry loop.
Planning SANs, wildcards and ACME issuance covers capacity planning. The official Let’s Encrypt limits distinguish ordinary orders from applicable renewal exemptions. Batch size, account limits and DNS provider limits remain relevant even with a secondary CA.
Protect credentials and certificate releases
DNS API tokens, CA account keys and certificate private keys are separate secrets. Protect configuration, certificate storage, staging files, logs and backups. Keep debug disabled for routine operations; sanitize any diagnostic log before sharing it. The example’s root-owned environment file is trusted executable input, not a place for arbitrary tenant values.
Retain a known-good release and the corresponding Nginx configuration until the new endpoint is verified. Separate source-code upgrades from an active certificate incident where possible. Record the client revision, the CA selected and the relevant request time without copying secrets into audit messages.
Keep the standby account ready without promising zero downtime
Both accounts must be usable before an incident. Registration, EAB and successful validation should be checked on a domain you control. Review ZeroSSL’s ACME account conditions from an accessible official source; if documentation is unavailable, do not infer pricing, unlimited issuance or a test endpoint.
Consider a hypothetical renewal failure while the deployed certificate remains valid. The operator confirms a CA-side incident and separately checks DNS, CAA and credentials. For one controlled run, they enable fallback and clear the reviewed block. If the primary still fails, the standby record is renewed or its existing valid certificate is exported and checked. Deployment proceeds only after the checks pass; the block remains for incident follow-up. No unmeasured downtime or cost outcome is implied.
Roll back files and plan the return to the primary
The release symlink keeps the key and full chain selected as a pair. If the Nginx test or reload fails, the script restores the previous release when available and reports failure. Confirm the actual service state afterward. On first installation there may be no previous symlink, so recovery depends on the separately backed-up TLS configuration and original certificate paths.
Returning to Let’s Encrypt is a planned renewal/deployment action, not a side effect of changing a global default. Avoid unnecessary forced issuance just to change the issuer. Keep the working certificate in service while diagnosing the primary path, and verify the replacement chain and external endpoint when the return is performed.
Handle DNS as its own dependency
DNS-01 depends on the authoritative DNS service publishing the expected TXT record and validators seeing it. Lowering a TTL does not instantly remove data already cached. Check actual authoritative answers and propagation behaviour rather than treating every delay as a CA outage.
If you use multiple authoritative DNS providers, ensure challenges reach each relevant provider consistently. Changing CA cannot repair inconsistent TXT data or a revoked API token. Limit shared-zone concurrency and avoid overlapping cleanup that removes another in-flight challenge.
Operational checks before widening the rollout
- Both accounts can issue for the exact apex and wildcard names, with appropriate CAA permission.
- Each CA has separate state and exported staging files; certificate/key and trust checks pass.
- Only one scheduler controls this workflow; skip results do not cause forced orders.
- Unknown failures stop later runs; CA fallback requires an explicit incident decision.
- Nginx validation and reload failures restore the prior selection and never trigger another CA.
- Expiry and failed services are monitored independently, with an accountable response owner.
Review issuance-rate planning and DNS consistency before expanding beyond the canary. The goal is a repeatable recovery path, not a guarantee that no dependency can fail.
Add RSA only when the compatibility requirement justifies it
The main example manages one ECDSA certificate pair. A dual-certificate deployment needs separate RSA certificate state, staging files and live paths. Select an RSA key size explicitly, such as --keylength 2048, and omit --ecc for the RSA renewal/export path. Do not write RSA and ECDSA outputs over the same files.
Test the web-server configuration and representative clients for both certificates. The RSA and ECDSA deployment guide provides the separate configuration context. Key-algorithm support and CA-root trust are different compatibility checks; dual certificates are not a universal cure for old TLS stacks.
Complete the test path before enabling production automation
Begin with a domain you control and complete initial issuance, a normal renewal check, a controlled secondary-CA run, deployment verification and rollback. Keep the official acme.sh documentation and the recorded client revision with the operational procedure. Only widen the rollout after the canary shows the expected behaviour.
For the next planning step, review rate-limit and certificate-group design and, if required, the separate dual-certificate configuration. Leave a valid deployed certificate in place while investigating failures, keep the fallback decision explicit, and make certificate expiry visible outside the renewal job itself.





