Skip to content

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.

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:

  1. The domain’s DNS A/AAAA record must resolve to this server’s public IP.
  2. Port 80 must be reachable from the public internet (Let’s Encrypt sends the HTTP-01 challenge over port 80).

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.

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.

ProviderDirectory URLNotes
Let’s Encrypt (default)https://acme-v02.api.letsencrypt.org/directoryPrimary CA. Tried first for all domains.
Google Trust Services (fallback)https://dv.acme-v02.api.pki.goog/directoryAutomatic fallback if Let’s Encrypt fails. No extra configuration required.
Let’s Encrypt Staginghttps://acme-staging-v02.api.letsencrypt.org/directoryIssues 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.

The TlsBackgroundService runs inside Dwaar’s process as a Pingora background service. It wakes every 12 hours to:

  1. Refresh OCSP stapling responses for all cached certificates.
  2. 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.

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.

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.

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
}
{
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).

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.

Certificates are stored in the configured certificate directory (default: /etc/dwaar/certs/).

FilePermissionsContents
{domain}.pem0644Full certificate chain in PEM format (leaf cert + issuer)
{domain}.key0600ECDSA 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/):

FileContents
le.jsonLet’s Encrypt account credentials
gts.jsonGoogle Trust Services account credentials

On each TLS handshake, Dwaar reads the SNI hostname from the TLS ClientHello and selects the matching certificate using the following priority order:

  1. Explicit cert path — from a tls /cert.pem /key.pem directive for that exact domain.
  2. Exact domain{cert_dir}/{domain}.pem + {cert_dir}/{domain}.key.
  3. Wildcard fallback — strips the first label and checks *.{rest} (e.g. api.example.com falls back to *.example.com).
  4. 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.

When a scheduled OCSP refresh returns CertRevoked, the ACME service now:

  1. Invalidates the revoked certificate from the in-memory LRU cache so no handshake can pick it up again.
  2. Deletes the on-disk {domain}.pem and {domain}.key files in the cert directory.
  3. 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.

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:

InputStatus
*.example.comAccepted
* (bare)Rejected
exam*ple.comRejected (partial-label wildcard)
*ample.comRejected (prefix wildcard)
foo.*.comRejected (wildcard not in leftmost label)
**.example.comRejected (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.

Sensitive key material is now wiped from memory as soon as it goes out of scope:

  • ACME private key PEMgenerate_key_and_csr returns the freshly-minted private key wrapped in Zeroizing<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 bufferCachedCert now has a manual Drop implementation 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 PKey is freed via EVP_PKEY_free, which already calls OPENSSL_cleanse on 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.

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.com to verify.

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.

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.