Skip to content

HTTPS

A site block whose address is a public name gets HTTPS without a tls directive: Pingclair asks Let’s Encrypt for a certificate over ACME, answers the HTTP-01 challenge on port 80, stores the result, and renews it in the background. The other three ways to get a certificate — DNS-01, a local authority, and files you supply — are covered below with what each one requires and what it actually does in v0.2.0-rc.3.

  • A name that resolves to this host. Check it before blaming 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 pingclair@pingclair.com
}
example.com {
file_server /var/lib/pingclair/html
}

There is nothing else to configure. At startup the server authorises 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 what is actually served, 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

The certificate material is kept in the service user’s data directory, /var/lib/pingclair/.local/share/pingclair — the path the binary resolves from that account’s home, which is also what PINGCLAIR_TLS_STORE names when a command runs as somebody else.

DNS-01 proves control of a name by publishing a TXT record instead of answering on port 80, which is what a wildcard certificate requires. The configuration needs the provider block:

{
email pingclair@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 are easy to miss. The auto line inside the block is what puts the name on the issuance list; without it the server logs authorised for 0 hostname(s) and never asks for a certificate, leaving every handshake to fail with NO_CERTIFICATE_SET. And the token is a Cloudflare API token with Zone:DNS:Edit for the zone that holds the name.

⚠️ DNS-01 issuance does not complete in v0.2.0-rc.3. The challenge itself runs: the record is published, propagation is confirmed against the resolver the configuration names, and the authority is asked to validate. The order then comes back invalid within a second, on every name tried, and the certificate is never stored:

📡 Published the DNS-01 record for _acme-challenge.example.com via cloudflare
👍 DNS-01 record for _acme-challenge.example.com is visible
🚀 Verification triggered for example.com
⏳ Polling order status...
⚠️ Eager issuance failed for example.com: Order ended in state: Invalid

Until that is fixed, use HTTP-01 for public names. A wildcard name therefore cannot be served with a certificate yet; the alternative is one name per certificate, or a certificate you issue elsewhere and supply as files.

🏛️ 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 be its own authority:

https://internal.test {
tls internal
file_server /var/lib/pingclair/html
}

The site answers with a certificate issued by CN=Pingclair Local Authority for ten years, and the root is published in the store:

Terminal window
sudo ls -l /var/lib/pingclair/.local/share/pingclair/internal/
-rw------- 1 pingclair pingclair 652 Sep 22 03:40 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 matters: pingclair trust looks in the store of the user who runs it, which for root is /root/.local/share/pingclair, while the service uses /var/lib/pingclair/.local/share/pingclair. Without the prefix it answers No internal CA root at /root/.local/share/pingclair/internal/root.crt.

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.

When another system issues your certificates, point tls at the files:

https://byo.test {
tls {
cert /etc/pingclair/certs/byo.crt
key /etc/pingclair/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 refuses a path that does not exist rather than failing at the first handshake:

❌ 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 DNS-01 does not complete in this release.
  • 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. Fix the cause before retrying, or the retries themselves become the error.
  • 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.