Skip to content

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.

  • 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.

{
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.com

The 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:

Terminal window
curl -I https://example.com/
HTTP/2 200
content-type: text/html; charset=utf-8
etag: "493b-6ab1f452"
server: Pingclair
Terminal window
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
subject=CN=example.com
issuer=C=US, O=Let's Encrypt, CN=YE2
notBefore=Sep 22 02:35:03 2026 GMT
notAfter=Dec 21 02:35:02 2026 GMT

Certificates 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 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:

Terminal window
curl -I https://anything.example.com/
HTTP/2 200
content-type: text/html; charset=utf-8
server: Pingclair
Terminal window
echo | openssl s_client -connect example.com:443 -servername anything.example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -ext subjectAltName
subject=CN=*.example.com
issuer=C=US, O=Let's Encrypt, CN=YE1
X509v3 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:

Terminal window
sudo PINGCLAIR_TLS_STORE=/var/lib/pingclair/.local/share/pingclair pingclair trust
✅ Internal CA root installed into the system trust store

The 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:

Terminal window
curl -s -o /dev/null -w '%{http_code}\n' https://internal.test/
200

pingclair 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.

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

  • contact email has forbidden domain "example.com". Let’s Encrypt rejects the reserved example domains as account contacts. Put a real mailbox in the email option.
  • NO_CERTIFICATE_SET in the log. The handshake presented a name the server has no certificate for. Read the log above it: a tls block without auto never 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-name shows 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.
  • 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.