HTTPS
📌 TLS examples below use ./certs/ in the working directory. Supply your own certificate, matching private key, or client CA file there; validation reads these files too.
A site block with a public hostname enables HTTPS without a tls
directive: Pingclair obtains a certificate from Let’s Encrypt over ACME, answers
the HTTP-01 challenge on port 80, stores the result, and renews it in the
background. The other three paths — DNS-01, the internal authority, and
files you supply — are covered below.
🧾 Before you start
Section titled “🧾 Before you start”- A name that resolves to this host. Verify DNS resolution before
troubleshooting the server:
dig +short A example.com. - Ports 80 and 443 reachable from the internet. The HTTP-01 challenge is served on port 80, and the certificate is used on 443.
- An email address for the ACME account. It must be a real mailbox: Let’s
Encrypt refuses the reserved example domains, and the issuance fails with
contact email has forbidden domain "example.com".
The configuration below replaces /etc/Pingclair/Pingclairfile, which the
service runs. Validate before reloading; Quickstart shows
that loop and Run it as a service explains the reload.
🌐 Certificates from Let’s Encrypt
Section titled “🌐 Certificates from Let’s Encrypt”{ email bonjour@pingclair.com}
example.com { file_server /var/lib/pingclair/html}At startup, the server authorizes the hostname, starts the ACME flow, and serves the challenge:
🌐 Automatic public certificates authorised for 1 hostname(s)🚀 Eager issuance for 1 hostname(s)🔐 Starting ACME flow for domains: ["example.com"]🔐 Serving ACME challenge for token: Ix9X74-tENLdJY0F6f7kUe3TXkoXOxOyTb8iHcnv9Z4✅ Certificate stored successfully: example.com🎉 Certificate issuance complete for example.comThe challenge request in the access log comes from the certificate authority, not from a browser:
📝 Access ... path="/.well-known/acme-challenge/Ix9X74-..." status=200 user_agent="Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)"Verify the HTTPS response and certificate from another machine:
curl -I https://example.com/HTTP/2 200content-type: text/html; charset=utf-8etag: "493b-6ab1f452"server: Pingclairecho | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -datessubject=CN=example.comissuer=C=US, O=Let's Encrypt, CN=YE2notBefore=Sep 22 02:35:03 2026 GMTnotAfter=Dec 21 02:35:02 2026 GMTCertificates are stored in the service user’s data directory,
/var/lib/pingclair/.local/share/pingclair. The binary resolves this path from
the account’s home directory. When a command runs as a different user, set
PINGCLAIR_TLS_STORE to point at this path.
📡 DNS-01 and wildcards
Section titled “📡 DNS-01 and wildcards”DNS-01 proves control of a name by publishing a TXT record instead of answering on port 80. A wildcard certificate requires it, and so does a host whose port 80 is closed.
📌 DNS-01 supports Cloudflare; other provider names are refused. The server publishes the ACME TXT digest and preserves other TXT records at the challenge name.
The configuration needs the provider block:
{ email bonjour@pingclair.com}
*.example.com { tls { auto dns cloudflare <token> resolvers 1.1.1.1 propagation_delay 10s } file_server /var/lib/pingclair/html}Two details require attention. First, the auto line inside the block puts the
name on the issuance list; without it the server logs authorised for 0 hostname(s) and never requests a certificate, so every handshake fails with
NO_CERTIFICATE_SET. Second, the token is a Cloudflare API token with
Zone:DNS:Edit for the zone that holds the name.
🃏 One wildcard certificate covers matching subdomains. A *.example.com site orders *.example.com
itself: one certificate, obtained at startup, served to every name beneath it.
A wildcard covers exactly one label, so the apex needs its own entry — write
*.example.com, example.com if the site answers at example.com too, and each
subject is ordered as written. Individual subdomain names are not included in the wildcard certificate’s
Certificate Transparency entry.
Any name under the site is served by that one leaf. From another machine:
curl -I https://anything.example.com/HTTP/2 200content-type: text/html; charset=utf-8server: Pingclairecho | openssl s_client -connect example.com:443 -servername anything.example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -ext subjectAltNamesubject=CN=*.example.comissuer=C=US, O=Let's Encrypt, CN=YE1X509v3 Subject Alternative Name: DNS:*.example.com🏛️ Certificates from the internal authority
Section titled “🏛️ Certificates from the internal authority”For private origins — a tunnel, an internal hostname, a lab machine — Pingclair can issue certificates from its internal authority:
https://internal.test { tls internal file_server /var/lib/pingclair/html}The internal authority has a root and an intermediate that signs 90-day leaves. The root is at <store>/pki/authorities/local/root.crt.
Clients do not trust it yet, so a request without -k fails. Install the root
into the system trust store:
sudo PINGCLAIR_TLS_STORE=/var/lib/pingclair/.local/share/pingclair pingclair trust✅ Internal CA root installed into the system trust storeThe PINGCLAIR_TLS_STORE prefix is required because pingclair trust looks in
the store of the user who runs it. For root, that is
/root/.local/share/pingclair, not the service account’s store at
/var/lib/pingclair/.local/share/pingclair. Without the prefix, the command searches root’s own store instead of the service store.
After trusting the root, the same request succeeds without -k:
curl -s -o /dev/null -w '%{http_code}\n' https://internal.test/200pingclair untrust removes it again, with the same store prefix.
📌 Upgrade note. The old internal/ directory is not migrated. Version 0.2.0 creates a new authority, and every client must trust its root again.
📜 Certificates you supply
Section titled “📜 Certificates you supply”When another system issues your certificates, point tls at the files:
https://byo.test { tls { cert ./certs/byo.crt key ./certs/byo.key } file_server /var/lib/pingclair/html}The files must be readable by the pingclair user, because the service runs as
that user. validate reads and parses the files and checks that the private key matches the certificate, without opening listeners. A missing file is refused:
❌ TLS certificate file does not exist: /etc/pingclair/certs/missing.crt
⚠️ HTTPS setup failures
Section titled “⚠️ HTTPS setup failures”contact email has forbidden domain "example.com". Let’s Encrypt rejects the reserved example domains as account contacts. Put a real mailbox in theemailoption.NO_CERTIFICATE_SETin the log. The handshake presented a name the server has no certificate for. Read the log above it: atlsblock withoutautonever starts issuance, and verify the DNS challenge settings.- The challenge is never served. Port 80 is blocked by a firewall, or
something else holds the port. The authority has to reach
http://your-name/.well-known/acme-challenge/from the internet. - The name does not resolve to this host.
dig +short A your-nameshows what the authority will connect to, which is not always what you expect after a recent change. - Repeated failures. Let’s Encrypt rate-limits failed validations per hostname. Resolve the underlying cause before retrying to avoid exceeding the validation limit.
🧭 Next steps
Section titled “🧭 Next steps”- Run it as a service: the unit, its reload semantics, and its logs.
tls: every mode and option of the directive.- Pingclairfile: addresses, matchers, and what the compiler accepts.
