Decoupled, reusable-anywhere Let's Encrypt / ACME automation for full HTTPS/TLS with automatic certificate issuance, renewal, and key rotation — risk-free (validate → backup → atomic deploy → reachability probe → rollback-on-failure).
This module is designed to be wired into any project's deployment scripts. It carries zero project literals: every value (domains, ACME email, challenge type, cert directory, service-reload command) is injected via a config file or the environment. It is consumed by reference as a submodule.
Deployment scripts need HTTPS certificates that renew themselves and never take the served site down during a certificate operation. This module gives you:
- Issuance (
issue.sh) — first-time cert for one or more domains. - Renewal (
renew.sh) — window-aware; the timer/cron target. No-op until due. - Rotation (
rotate.sh) — force a new cert and private key on demand. - Status (
status.sh) — human text or JSON (subject, SANs, days-left, due). - Revocation (
revoke.sh) — revoke at the CA (key compromise), confirmed. - Deploy hook (
deploy-hook.sh) — atomic install + service reload (nginx / caddy / podman / anything). - Auto-renewal — a systemd timer and a cron fallback.
- A thin JSON API (
api/, Go) so deploy tooling can drive it programmatically.
Risk-free guarantee. Every issue/renew/rotate: the new cert is validated
before it can go live, the current cert is snapshotted, files are installed
atomically (mv on the same filesystem), the service is hot-reloaded, an
optional reachability probe runs, and any failure rolls back to the
previous certificate and reloads again. Validate the whole flow against the
staging CA first (LE_STAGING=1).
Host tools (no root required — everything runs rootless):
| Tool | Purpose | Install |
|---|---|---|
acme.sh |
ACME client (issue/renew/revoke) | scripts/setup.sh --install (rootless) |
openssl |
cert validation + status introspection | your distro |
curl |
ACME directory probe + post-deploy reachability | your distro |
go ≥1.23 |
build the optional api/ JSON wrapper (optional) |
your distro |
acme.sh is the currently supported ACME client. certbot is a documented
roadmap alternative — the scripts fail loudly if LE_CLIENT=certbot is set
(no silent no-op).
# 1. Copy and fill in the config for YOUR deployment.
cp config/lets_encrypt.conf.example /etc/lets_encrypt/lets_encrypt.conf
$EDITOR /etc/lets_encrypt/lets_encrypt.conf # domains, email, challenge, cert dir, reload cmd
# 2. Install acme.sh (rootless) + register the ACME account (STAGING first!).
scripts/setup.sh --config /etc/lets_encrypt/lets_encrypt.conf --install
# 3. Issue the certificate (with LE_STAGING=1 to rehearse, then flip to 0).
scripts/issue.sh --config /etc/lets_encrypt/lets_encrypt.conf
# 4. Enable auto-renewal (systemd timer OR cron — see systemd/).
# renew.sh is a no-op until the cert is inside its renewal window.Programmatic (JSON) use for deploy scripts:
cd api && go build -o le-api .
./le-api status --config /etc/lets_encrypt/lets_encrypt.conf # -> JSON
./le-api renew --config /etc/lets_encrypt/lets_encrypt.conf # -> {"command":"renew","ok":true,...}Everything is injected — see config/lets_encrypt.conf.example
for the full, commented contract. The load-bearing variables:
| Variable | Meaning |
|---|---|
LE_DOMAINS |
space-separated domains; first = primary (CN), rest = SANs |
LE_ACME_EMAIL |
ACME account contact email |
LE_CHALLENGE |
webroot (HTTP-01) or dns-01 |
LE_WEBROOT |
document root for HTTP-01 |
LE_DNS_PROVIDER |
acme.sh DNS hook for DNS-01 (e.g. dns_loopia); secrets via env |
LE_CERT_DIR |
where live cert/key/fullchain/chain are deployed |
LE_KEY_TYPE |
ec-256 | ec-384 | rsa-2048 | rsa-4096 |
LE_RELOAD_CMD |
atomic service reload, e.g. systemctl reload nginx |
LE_VALIDATE_URL |
optional HTTPS probe URL for the risk-free rollback gate |
LE_STAGING |
1 = staging CA (rehearse), 0 = production |
LE_RENEW_DAYS |
renew when fewer than N days of validity remain |
DNS-01 secrets (e.g. LOOPIA_User / LOOPIA_Password) are read from the
environment by acme.sh — export them from a gitignored secrets file, never
hardcode them in the config. See docs/DEFINITIONS.md.
This module is meant to be added by reference and driven from a consumer's deploy scripts:
git submodule add git@github.com:vasic-digital/lets_encrypt.git submodules/lets_encrypt
# consumer deploy script:
submodules/lets_encrypt/scripts/issue.sh --config config/lets_encrypt.confThe consumer owns the config (domains, cert dir, reload command); this module owns the mechanism. No consumer literal ever enters this module.
tests/run_all.sh # Go unit + shell unit + staging integration- Unit — Go (
api/) arg-parsing/config-injection/JSON-shape; shell config-injection + arg-parsing (+sh -n/bash -nparseability). - Integration — a real Let's Encrypt staging run: probes the staging ACME directory and registers a staging account. Full issuance is honestly SKIPPED without a reachable domain (never a fake pass).
No production certificates are ever issued by the test suite.
docs/USER_MANUAL.md— setup, issue, renew, rotate, revoke, auto-renewal install, troubleshooting.docs/DEFINITIONS.md— ACME/TLS terms, challenge types, DNS-01 provider setup, rate limits, staging vs production.
Apache License 2.0 — see LICENSE.