TLS listeners
A listener speaks TLS when its address is on the top-level https line:
bind 0.0.0.0:80
https 0.0.0.0:443
Sites without their own bind then answer on both. TLS 1.2 and 1.3 are offered, and HTTP/2 is negotiated with clients that support it, HTTP/1.1 with the rest.
A site that only answers over TLS binds just that address:
https 0.0.0.0:443
site example.com {
bind 0.0.0.0:443
proxy 127.0.0.1:3000
}
Which certificate is served
The certificate is picked per connection by the name the client asks for (SNI):
- a certificate issued by Let's Encrypt for exactly that name, if there is one,
- otherwise the fallback certificate.
The fallback is the PEM certificate chain in XTQW_CERT with the PEM private key in XTQW_KEY, when both are set. Without them, xTQ Web makes a self-signed certificate at startup for the names in XTQW_TLS_NAMES (comma-separated, default localhost) and logs:
xtq-web: no certificate configured, using a self-signed one for ["localhost"]
A self-signed certificate is enough behind a proxy that does not check the origin's certificate, such as Cloudflare in "Full" mode. For "Full (strict)", use a Cloudflare Origin CA certificate as XTQW_CERT and XTQW_KEY. A wildcard certificate of your own works the same way, as the fallback for every name.
[Service]
Environment=XTQW_CERT=/etc/xtq-web/tls/fullchain.pem
Environment=XTQW_KEY=/etc/xtq-web/tls/key.pem
The fallback files are read once at startup. After replacing them, restart xTQ Web. The xtq-web user must be able to read them.
Automatic HTTPS with Let's Encrypt
Set XTQW_ACME=1 and xTQ Web gets a certificate from Let's Encrypt for every exact hostname in your sites, and renews it on its own.
sudo systemctl edit xtq-web
[Service]
Environment=XTQW_ACME=1
sudo systemctl restart xtq-web
The log says which names it manages, then reports each certificate:
xtq-web: acme: managing certificates for ["example.com", "www.example.com"]
xtq-web: acme: certificate issued for example.com
Which hostnames
By default, every hostname from every site line that contains a dot, has no *, and is not an IP address. site * and site *.example.com are skipped, since wildcard certificates can't be issued this way.
XTQW_ACME_HOSTS replaces that list with your own, comma-separated:
Environment=XTQW_ACME_HOSTS=example.com,www.example.com
Requirements
Certificates are issued with the HTTP-01 challenge. For each hostname:
- Its DNS must point at this server, or at a proxy in front of it.
- Let's Encrypt must be able to fetch
http://<hostname>/.well-known/acme-challenge/<token>on port 80 from the internet, and that request must reach an xTQ Web listener. Open port 80 in the firewall.
Pending challenges are answered on every listener, plain or TLS, before any site or route is looked at. A redirect, an ip rule or a challenge in your config does not get in the way. If a proxy or CDN in front redirects HTTP to HTTPS, Let's Encrypt follows the redirect and the answer comes from the TLS listener.
You don't need a TLS listener for issuance itself, but without an https line the certificates are never served.
Renewal and retries
At startup, and after every reload, xTQ Web loads the certificates it already has on disk and issues the missing ones. It then checks twice a day and renews a certificate when fewer than 30 days are left. Hostnames are handled one after another.
If an order fails, the error is logged, for example:
xtq-web: acme: example.com: order is Invalid: Let's Encrypt could not fetch the challenge
The next attempt is at the next check, 12 hours later. After fixing DNS or the firewall, reload or restart to try again at once. A name without a certificate gets the fallback certificate in the meantime.
The dashboard lists every issued certificate with the days it has left, and /.xtq/metrics exports them as xtq_certificate_days_left.
Staging
While testing, use Let's Encrypt's staging service, which has much higher rate limits but issues certificates browsers don't trust:
Environment=XTQW_ACME_STAGING=1
Staging certificates are stored apart, in a staging folder inside the ACME directory, so switching back to production does not keep serving them.
Where certificates are stored
In XTQW_ACME_DIR, default /var/lib/xtq-web/acme:
| Path | Contents |
|---|---|
account.json |
The Let's Encrypt account, created on first use |
<hostname>/cert.pem |
The certificate chain |
<hostname>/key.pem |
Its private key |
staging/... |
The same, for staging |
The account is created without an email address and accepts Let's Encrypt's terms of service. Keep this directory between restarts and upgrades, or certificates are issued again. In Docker it is the xtq-web-acme volume.
Redirecting HTTP to HTTPS
Two sites with the same name, one per listener:
bind 0.0.0.0:80
https 0.0.0.0:443
site example.com www.example.com {
bind 0.0.0.0:80
redirect https://{host}{uri} 301
}
site example.com www.example.com {
bind 0.0.0.0:443
proxy 127.0.0.1:3000
}
Certificate challenges still work on port 80, because they are answered before the redirect.
The real client address
Behind a proxy, a load balancer or a CDN, every connection comes from the proxy. The real client is named in X-Forwarded-For. Since anyone can send that header, xTQ Web believes it only from addresses in trust.
trust 127.0.0.1 ::1 10.0.0.0/8 cloudflare
Entries are addresses, CIDR ranges and the word cloudflare, which adds Cloudflare's published edge ranges. Without a trust line, 127.0.0.1 and ::1 are trusted. A trust line replaces that default, so list loopback again if a local proxy should stay trusted.
The client address is found like this:
- If the connection does not come from a trusted address, the connection's address is the client.
- Otherwise
X-Forwarded-Foris read from right to left. Trusted addresses are skipped, and the first address that is not trusted is the client. Everything to its left was written by the client itself and is ignored. - If every entry is trusted, the leftmost one is used. An entry that is not an address stops the walk, and the last address read is used.
- If there is no
X-Forwarded-Forat all,CF-Connecting-IPis used when present, else the connection's address.
With trust 127.0.0.1 cloudflare:
| Connection from | Headers | Client |
|---|---|---|
198.51.100.4 |
X-Forwarded-For: 203.0.113.9 |
198.51.100.4, not trusted so the header is ignored |
127.0.0.1 |
X-Forwarded-For: 198.51.100.4 |
198.51.100.4 |
127.0.0.1 |
X-Forwarded-For: 203.0.113.9, 198.51.100.4 |
198.51.100.4, the forged left entry is ignored |
127.0.0.1 |
X-Forwarded-For: 198.51.100.4, 173.245.48.1 |
198.51.100.4, the Cloudflare hop is skipped |
127.0.0.1 |
CF-Connecting-IP: 198.51.100.4 |
198.51.100.4 |
127.0.0.1 |
none | 127.0.0.1 |
The client address is what ip and not ip check, and what php sites get as X-Xtq-Client-Ip and use for the per-address fair share.
trust also decides what reaches backends behind proxy. From a trusted address, the incoming X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host are kept and extended. From anyone else they are dropped and set fresh. See Reverse proxy.
Without the right trust line, every client looks like the proxy in front. Allow lists then match everyone or no one, and per-address limits treat all visitors as one.
