XTQ/Web/DocsDownload

HTTPS

TLS listeners, certificates, automatic Let's Encrypt certificates, and how the real client address is found behind proxies.

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-For is 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-For at all, CF-Connecting-IP is 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.