Automatic HTTPS
Automatic HTTPS
Section titled “Automatic HTTPS”Dwaar automatically provisions and renews TLS certificates via the ACME protocol. Point a domain’s DNS at your server, add it to your Dwaarfile, and Dwaar handles the rest — no certbot, no cron jobs, no manual certificate management.
Quick Start
Section titled “Quick Start”example.com { reverse_proxy localhost:3000}That’s the entire configuration. When Dwaar starts, it detects that example.com has no existing certificate, runs the ACME HTTP-01 challenge against Let’s Encrypt, downloads the certificate, and begins serving HTTPS on port 443. Port 80 is kept open to redirect HTTP requests to HTTPS.
Two requirements for automatic provisioning to work:
- The domain’s DNS A/AAAA record must resolve to this server’s public IP.
- Port 80 must be reachable from the public internet (Let’s Encrypt sends the HTTP-01 challenge over port 80).
How It Works
Section titled “How It Works”The challenge token is served directly by Dwaar’s request filter — no separate web server is needed. After validation completes, the token is cleaned up automatically.
Certificates use ECDSA P-256 keys and include a Subject Alternative Name (SAN) for the domain.
Challenge path and the HTTPS redirect
Section titled “Challenge path and the HTTPS redirect”The HTTP→HTTPS auto-redirect normally rewrites every plaintext request to its HTTPS counterpart. The only exception is /.well-known/acme-challenge/: those requests must be answered over plaintext so the ACME CA can fetch the token during validation.
As of 0.2.2 the bypass is gated on the HTTP method. Only GET requests to /.well-known/acme-challenge/... are served in the clear; POST, PUT, DELETE, and every other method fall through to the normal HTTPS redirect. This matches RFC 8555 §8.3, which specifies the challenge verification as a GET, and closes a narrow redirect-bypass vector on clients that tunnel arbitrary methods through the well-known path.
ACME Providers
Section titled “ACME Providers”| Provider | Directory URL | Notes |
|---|---|---|
| Let’s Encrypt (default) | https://acme-v02.api.letsencrypt.org/directory | Primary CA. Tried first for all domains. |
| Google Trust Services (fallback) | https://dv.acme-v02.api.pki.goog/directory | Automatic fallback if Let’s Encrypt fails. No extra configuration required. |
| Let’s Encrypt Staging | https://acme-staging-v02.api.letsencrypt.org/directory | Issues untrusted certs. Use for testing the provisioning flow without hitting rate limits. Enable with DWAAR_ACME_STAGING=1. |
Dwaar maintains separate ACME account credentials per CA under {acme_dir}/{ca_id}.json. Credentials are created on first use and reused on subsequent runs.
Certificate Renewal
Section titled “Certificate Renewal”The TlsBackgroundService runs inside Dwaar’s process as a Pingora background service. It wakes every 12 hours to:
- Refresh OCSP stapling responses for all cached certificates.
- Check whether any certificate expires within 30 days. If so, renew it immediately.
On startup, the service performs an immediate scan before entering the loop, so domains missing certificates are provisioned before traffic arrives.
A concurrency guard prevents double-issuance if a renewal is already in progress for a domain. Failed renewals are retried on the next 12-hour cycle — the existing (still-valid) certificate continues to serve traffic until renewal succeeds.
There is no short retry queue for a failed issuance or renewal inside the same cycle. After Let’s Encrypt and the Google Trust Services fallback both fail, Dwaar logs the failure and waits for the next 12-hour background scan, unless the process is restarted or the operator fixes the cause and triggers a new startup/config path. Plan alerting around that coarse retry interval.
Configuration
Section titled “Configuration”Default behavior (no explicit tls directive needed)
Section titled “Default behavior (no explicit tls directive needed)”example.com { reverse_proxy localhost:3000}Any domain block without a tls directive uses automatic HTTPS by default.
Explicit tls auto
Section titled “Explicit tls auto”example.com { reverse_proxy localhost:3000 tls auto}Identical to the default. Use this when you want to make the intent explicit in the Dwaarfile.
ACME contact email
Section titled “ACME contact email”Add an email address to global options so Let’s Encrypt can contact you about expiring certificates or policy changes:
{ email admin@example.com}
example.com { reverse_proxy localhost:3000}Disable automatic HTTPS entirely
Section titled “Disable automatic HTTPS entirely”{ auto_https off}
example.com { reverse_proxy localhost:3000}All sites serve plain HTTP. No certificates are provisioned.
Keep HTTPS but skip the HTTP→HTTPS redirect
Section titled “Keep HTTPS but skip the HTTP→HTTPS redirect”{ auto_https disable_redirects}
example.com { reverse_proxy localhost:3000}Dwaar provisions certificates and serves HTTPS normally, but does not automatically redirect http://example.com to https://example.com. Use this if you manage HTTP traffic separately (e.g. load balancer in front that handles redirects).
Disable TLS for a specific domain only
Section titled “Disable TLS for a specific domain only”example.com { reverse_proxy localhost:3000 tls off}That domain serves plain HTTP; all other domains in the same Dwaarfile continue to get certificates automatically.
Certificate Storage
Section titled “Certificate Storage”Certificates are stored in the configured certificate directory (default: /etc/dwaar/certs/).
| File | Permissions | Contents |
|---|---|---|
{domain}.pem | 0644 | Full certificate chain in PEM format (leaf cert + issuer) |
{domain}.key | 0600 | ECDSA P-256 private key in PKCS#8 PEM format |
Both files are written atomically via a temp file and rename() — a partial write never corrupts a serving certificate.
ACME account credentials are stored separately in the ACME directory (default: /etc/dwaar/acme/):
| File | Contents |
|---|---|
le.json | Let’s Encrypt account credentials |
gts.json | Google Trust Services account credentials |
SNI Resolution
Section titled “SNI Resolution”On each TLS handshake, Dwaar reads the SNI hostname from the TLS ClientHello and selects the matching certificate using the following priority order:
- Explicit cert path — from a
tls /cert.pem /key.pemdirective for that exact domain. - Exact domain —
{cert_dir}/{domain}.pem+{cert_dir}/{domain}.key. - Wildcard fallback — strips the first label and checks
*.{rest}(e.g.api.example.comfalls back to*.example.com). - Default domain — used for connections that send no SNI (IP-based clients). The first TLS-enabled domain in the config serves as the default.
Parsed certificates are held in a bounded LRU cache. After a renewal, the old cache entry is evicted immediately so the next handshake picks up the new certificate without waiting for natural LRU expiry.
If a cached certificate carries an OCSP staple (refreshed every 12 hours), it is attached to the ServerHello during the handshake.
Revocation handling (0.2.3)
Section titled “Revocation handling (0.2.3)”When a scheduled OCSP refresh returns CertRevoked, the ACME service now:
- Invalidates the revoked certificate from the in-memory LRU cache so no handshake can pick it up again.
- Deletes the on-disk
{domain}.pemand{domain}.keyfiles in the cert directory. - Kicks off a fresh ACME order (Let’s Encrypt first, Google Trust Services fallback) to re-issue the certificate.
Before 0.2.3, the cache retained the revoked entry until natural LRU eviction, so a revoked cert could continue to be served while re-issuance was in flight. The new eviction + delete + re-issue sequence closes that window. Re-issuance is gated by the same in-progress guard used for renewal, so a revocation detected on two domains in one refresh cycle still produces only one concurrent ACME run per domain.
Wildcard SNI enforcement (0.2.3)
Section titled “Wildcard SNI enforcement (0.2.3)”is_valid_sni_hostname now strictly follows RFC 6125 §6.4.3: * is only accepted as the entire first label of a DNS name. Dwaar rejects every other form at parse time and at SNI lookup time:
| Input | Status |
|---|---|
*.example.com | Accepted |
* (bare) | Rejected |
exam*ple.com | Rejected (partial-label wildcard) |
*ample.com | Rejected (prefix wildcard) |
foo.*.com | Rejected (wildcard not in leftmost label) |
**.example.com | Rejected (multiple * in label) |
The same validator is applied by CertStore::get / get_async / get_or_load before interpolating the hostname into a filesystem path, so a malformed SNI cannot be used to steer cert-store reads toward unexpected files.
Private key zeroization (0.2.3)
Section titled “Private key zeroization (0.2.3)”Sensitive key material is now wiped from memory as soon as it goes out of scope:
- ACME private key PEM —
generate_key_and_csrreturns the freshly-minted private key wrapped inZeroizing<String>. When the issuance pipeline finishes (success or failure), the PEM buffer is overwritten byte-by-byte before the allocation is freed. - Cached cert OCSP buffer —
CachedCertnow has a manualDropimplementation that zeroes the OCSP response DER buffer on eviction. This closes the window where a heap-allocated copy of the most recent OCSP response could linger in freed memory. - Private key inside OpenSSL — the parsed
PKeyis freed viaEVP_PKEY_free, which already callsOPENSSL_cleanseon the underlying key bytes.
This has no operator-visible effect — it is a defense-in-depth mitigation against heap-disclosure bugs and core-dump inspection.
Troubleshooting
Section titled “Troubleshooting”Challenge fails: connection refused or timeout
Section titled “Challenge fails: connection refused or timeout”Let’s Encrypt must reach http://{domain}/.well-known/acme-challenge/{token} on port 80. Check that:
- Port 80 is open in your firewall / security group.
- No other process (nginx, apache, existing Dwaar instance) is occupying port 80.
- The domain’s DNS resolves to this server’s IP. Use
dig +short example.comto verify.
domain does not resolve to this server
Section titled “domain does not resolve to this server”ACME validation is done by the CA’s validation servers, not from your machine. If the DNS hasn’t propagated yet, the challenge will fail. Wait for propagation and restart Dwaar, or use the Let’s Encrypt Staging environment while testing (DWAAR_ACME_STAGING=1) to avoid burning rate limits.
AllCasFailed: both Let’s Encrypt and Google Trust Services failed
Section titled “AllCasFailed: both Let’s Encrypt and Google Trust Services failed”Check the error details in the logs (dwaar --debug). Common causes:
- Port 80 is blocked — the challenge token was set but the CA could not fetch it.
- The domain resolves to a private IP (RFC 1918) — Let’s Encrypt does not issue for non-routable addresses.
- You have hit Let’s Encrypt’s rate limits (5 duplicate certificates per week per domain). Use Staging to test, then switch to production.
Certificate renewed but browser still sees the old cert
Section titled “Certificate renewed but browser still sees the old cert”The LRU cache is invalidated immediately after a successful renewal. If you are seeing a stale cert, the most likely cause is a CDN or upstream TLS terminator caching the old certificate. Dwaar itself will serve the new cert on the next handshake.
corrupt cert PEM — needs re-issuance
Section titled “corrupt cert PEM — needs re-issuance”A certificate file on disk failed to parse. Dwaar logs a warning and automatically re-issues the certificate. If re-issuance also fails, check filesystem permissions on the cert directory.
Related
Section titled “Related”- DNS Challenge (Wildcards) — use DNS-01 to provision
*.example.comcertificates without exposing port 80 - Manual Certificates — bring your own cert and key files with
tls /cert.pem /key.pem - Self-Signed Certificates —
tls internalfor local development - OCSP Stapling — how Dwaar fetches and staples OCSP responses