acme.sh
acme.sh is a pure shell client — useful when Python (certbot) or Go (lego) aren't available, or simply when minimum runtime footprint matters. This page covers the acme.sh-specific bits; for the general onboarding flow and trust-store setup see First Certificate.
neilpang/acme.sh:latest Docker image)acme.sh's CLI has been stable for years — --issue / --renew / --revoke and their flags work identically across 2.x and 3.x. If you are on an older version and a flag is missing, upgrade is the recommended path.
What changes vs certbot
-
Key type: acme.sh generates RSA 2048 by default. This is refused by Certeasy under the default
signature.min-rsa-bits: 3072policy — always pass--keylengthexplicitly:--keylength 3072or4096for RSA--keylength ec-256orec-384for ECDSA
-
CSR EKU — read this carefully: acme.sh's built-in OpenSSL template declares both
serverAuthandclientAuthin the CSR's Extended Key Usage, regardless of intended purpose. By default Certeasy rejects this combination, returningbadCSR: EKU 1.3.6.1.5.5.7.3.2 in CSR not allowed by policy.Two paths to make it work:
- Strict (preferred from June 2026): drop
clientAuthfrom acme.sh's CSR template. The CA/B Forum baseline forbids theserverAuth + clientAuthcombination on publicly-trusted server certificates from June 2026 onwards — production deployments should align even when fronted by an internal ADCS. The cleanest way is to maintain a private fork ofacme.shor to override its OpenSSL config file (~/.acme.sh/openssl.cnfif you use--certhome). - Pragmatic (existing fleet): add
clientAuthto the policy'scsr.allowed-extra-ekuon the Certeasy side. See Configuration → Issuance policies → EKU. This unblocks acme.sh as-is; plan a migration before June 2026.
- Strict (preferred from June 2026): drop
-
Trust store: acme.sh uses curl under the hood. Point both
--ca-bundleand theCA_BUNDLE/CURL_CA_BUNDLEenv vars at your OS bundle:
# Debian / Ubuntu
export CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
export CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
# RHEL / CentOS / Rocky
export CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt
export CURL_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt
--ca-bundle is for the ACME server's TLS cert; CURL_CA_BUNDLE covers the underlying curl invocations acme.sh uses for some HTTP calls. Setting both is belt-and-braces.
- Built-in scheduler: acme.sh installs its own daily cron entry via
--install-cronjob, no systemd timer to write.
HTTP-01 (standalone)
acme.sh --issue \
--server https://acme.corp.internal/acme/directory \
--ca-bundle /etc/ssl/certs/ca-certificates.crt \
--standalone \
--keylength 3072 \
-d app.corp.internal
Cert + key land under ~/.acme.sh/app.corp.internal/:
fullchain.cer— leaf + chain.app.corp.internal.cer— leaf only.app.corp.internal.key— private key.ca.cer— chain only.
acme.sh's --standalone binds port 80 with a tiny built-in server for the duration of the challenge.
HTTP-01 (webroot)
acme.sh --issue \
--server https://acme.corp.internal/acme/directory \
--ca-bundle /etc/ssl/certs/ca-certificates.crt \
--webroot /var/www/html \
--keylength 3072 \
-d app.corp.internal
acme.sh writes the challenge token under /var/www/html/.well-known/acme-challenge/. The web server must serve that path over HTTP without authentication.
TLS-ALPN-01
acme.sh --issue \
--server https://acme.corp.internal/acme/directory \
--ca-bundle /etc/ssl/certs/ca-certificates.crt \
--alpn \
--keylength 3072 \
-d app.corp.internal
Same constraint as HTTP-01 standalone but on port 443.
DNS-01 (for wildcards)
acme.sh has its own catalogue of DNS plugins under ~/.acme.sh/dnsapi/. Configure with env vars and pass --dns dns_<provider>:
By default, acme.sh resolves the just-installed _acme-challenge.<domain> TXT
record through public DoH resolvers (Cloudflare, Google) before notifying
Certeasy. On an internal-only deployment this check will always fail —
the names do not exist publicly — and worse, the internal domain name is
leaked in cleartext to the public resolvers.
Pass --dnssleep <N> to skip the pre-check and wait N seconds before
notifying the server. A short value such as --dnssleep 1 is enough when
the DNS plugin updates the authoritative server synchronously
(dns_nsupdate, BIND/Knot RFC 2136, MS DNS via dns_namesilo etc.);
larger values may be required for providers with slow API propagation.
# Example: RFC 2136 / nsupdate
export NSUPDATE_SERVER='10.0.0.53'
export NSUPDATE_KEY='/etc/acme.sh/keyfile'
acme.sh --issue \
--server https://acme.corp.internal/acme/directory \
--ca-bundle /etc/ssl/certs/ca-certificates.crt \
--dns dns_nsupdate \
--dnssleep 1 \
--keylength 3072 \
-d '*.corp.internal'
The full list of plugins is in acme.sh's DNS API documentation.
Renewal
acme.sh installs its own daily cron entry. Enable it once after the first issuance:
acme.sh --install-cronjob
The cron job runs acme.sh --cron daily; certs within 30 days of expiration are renewed automatically. To force a one-off renewal:
acme.sh --renew -d app.corp.internal --force
Revocation
acme.sh --revoke -d app.corp.internal \
--server https://acme.corp.internal/acme/directory \
--ca-bundle /etc/ssl/certs/ca-certificates.crt