Automatic HTTPS (ACME)
Get and auto-renew Let's Encrypt certificates with one config section — no certbot, no cron, no restarts.
rws obtains certificates from Let's Encrypt (or any ACME CA) by itself and
keeps them renewed. Point a domain's DNS at the server, then:
# rws.config.toml
port = 443
http_redirect_port = 80 # HTTP → HTTPS redirects; also answers ACME challenges
[acme]
domains = ["example.com", "www.example.com"]
email = "you@example.com"
rwsOn first start rws requests a certificate, stores it in .rws/acme/
(cert.pem, key.pem, account.key — keys 0600, the directory 0700,
and never served: the working directory is the web root), and starts serving HTTPS, HTTP/2, and
HTTP/3 with it. Later starts reuse the stored certificate. Renewal happens in
the background and is hot-swapped without dropping connections.
The prebuilt binaries (Homebrew, cargo binstall, Docker, GitHub Releases)
include it. When building from source, enable the acme feature:
cargo install rust-web-server --features acme,auth. A binary built without
it prints a warning if [acme] is configured instead of silently serving
plain HTTP.
Requirements
- DNS for every domain in
domainspoints at this server. - Port 80 is reachable from the internet. Let's Encrypt proves you control
the domain with an HTTP-01 challenge to
http://<domain>/.well-known/acme-challenge/…. - Binding ports 80/443 on Linux needs root or the capability:
sudo setcap cap_net_bind_service=+ep "$(which rws)".
IPv6 is handled: when a domain has an AAAA record, Let's Encrypt validates over IPv6, so the challenge and redirect listeners accept both IPv4 and IPv6.
How it works
- Startup. If no certificate exists, or it is due for renewal (see 4),
rwsruns the ACME flow before opening the HTTPS port: account (key saved to.rws/acme/account.key), order, HTTP-01 challenge, ECDSA P-256 key + CSR, finalize, download. - Challenges. At startup a temporary listener on the challenge port (default 80) answers them. Once the HTTP → HTTPS redirect listener is running on that port, it answers challenges itself — so renewals never compete with it for port 80.
- First-time failures are retried, e.g. while DNS for a new domain is still propagating: after 30 s, doubling up to every 15 minutes, until a certificate is issued.
- Renewal (since 18.0.0). A certificate is renewed at two-thirds of its
lifetime — day 60 of 90, day 30 of 45, day 4 of 6 — or earlier when the
CA's ACME Renewal Information (RFC 9773)
suggests a window that opens sooner, e.g. after a revocation.
rwsasks for it at every check (every 12 hours at most, every 6 when the CA has renewal information, sooner when renewal is due earlier or the CA'sRetry-Aftersays so) and picks a point in the window that's fixed per certificate. The renewal order names the certificate it replaces, so the CA can exempt it from rate limits. A failed renewal is retried within an hour. After renewing it reloads the TLS certificate in place (on Unix, via the sameSIGHUPreload used for config).renew_before_days, if set, replaces the two-thirds point (a CA window that opens earlier still wins). - Profiles.
profileorders a certificate profile the CA offers — Let's Encrypt hasclassic,tlsserverandshortlived(6-day certificates). A name the CA doesn't list stops provisioning with the list it offers. Unset, the CA picks its default.
The ACME client (RFC 8555) is implemented in-crate. It has been tested end to
end against Pebble, Let's Encrypt's
test CA, including a shortlived profile and renewal on Pebble's renewal
information through the redirect listener.
Testing with the staging CA
Let's Encrypt production has strict rate limits. Try your setup against staging first — its certificates aren't browser-trusted, but the limits are generous:
[acme]
domains = ["example.com"]
staging = trueDelete .rws/acme/ before switching back to production.
Before 17.113.1 the defaults were cert.pem, key.pem and acme_account.key
in the working directory — the web root, so the keys could be downloaded.
On upgrade, files found there are moved into .rws/acme/ (when the default
paths are in use), and the static server no longer serves *.pem, *.key
or rws.config.toml anywhere. Rotate a key that was exposed.
Private ACME CAs
To use step-ca, an internal Boulder, or Pebble, set the directory URL and trust the CA's root certificate (in addition to the public roots):
[acme]
domains = ["internal.example"]
directory = "https://ca.internal.example:9000/acme/acme/directory"
ca_file = "/etc/rws/internal-root.pem"Configuration reference
Every setting can go in the [acme] section of rws.config.toml, on the
command line (--acme-domains=...; not the last three), or in the environment.
[acme] key |
Environment variable | Default | Description |
|---|---|---|---|
domains |
RWS_CONFIG_ACME_DOMAINS |
— | Domains for the certificate. Setting this enables ACME. |
email |
RWS_CONFIG_ACME_EMAIL |
— | Contact email for expiry notices from the CA |
staging |
RWS_CONFIG_ACME_STAGING |
false |
Use the Let's Encrypt staging CA |
directory |
RWS_CONFIG_ACME_DIRECTORY |
Let's Encrypt | ACME directory URL of another CA |
ca_file |
RWS_CONFIG_ACME_CA_FILE |
— | Extra root CA(s) to trust for the directory (private CAs) |
challenge_port |
RWS_CONFIG_ACME_CHALLENGE_PORT |
80 |
Port the CA connects to for HTTP-01 |
cert_path |
RWS_CONFIG_ACME_CERT_PATH |
tls_cert_file, else .rws/acme/cert.pem |
Where the certificate chain is stored |
key_path |
RWS_CONFIG_ACME_KEY_PATH |
tls_key_file, else .rws/acme/key.pem |
Where the certificate key is stored (0600) |
renew_before_days |
RWS_CONFIG_ACME_RENEW_BEFORE_DAYS |
— | Renew this many days before expiry instead of at two-thirds of the lifetime (was 30 before 18.0.0) |
profile |
RWS_CONFIG_ACME_PROFILE |
— | Certificate profile to order (classic, tlsserver, shortlived, … as the CA offers) |
renew_check_interval_secs |
RWS_CONFIG_ACME_RENEW_CHECK_INTERVAL_SECS |
43200 |
Longest time between renewal checks |
account_key_path |
RWS_CONFIG_ACME_ACCOUNT_KEY_PATH |
.rws/acme/account.key |
Where the ACME account key is stored (0600) |
Top-level http_redirect_port (RWS_CONFIG_HTTP_REDIRECT_PORT) starts the
HTTP → HTTPS redirect listener; set it to the challenge port (80) in
production.
Using ACME from Rust
The rws binary does all of the above. In your own binary built on the
library, the equivalent is:
use rust_web_server::acme::{AcmeConfig, AcmeManager};
use rust_web_server::prelude::*;
#[tokio::main]
async fn main() {
let (listener, pool) = Server::setup().unwrap();
if let Some(cfg) = AcmeConfig::from_env() {
// Serve the issued certificate unless TLS paths were set explicitly.
cfg.install_as_tls_certificate();
let mgr = AcmeManager::new(cfg);
mgr.provision_until_ready().await; // retries until issued
tokio::spawn(mgr.run_renewal_loop());
}
let app = App::new();
tokio::join!(
Server::run_tls(listener, pool, app.clone()),
Server::run_quic(app),
Server::run_redirect(), // answers renewal challenges
);
}AcmeManager::provision_if_needed() (single attempt, returns the error) is
also available if you want to handle failures yourself.