# πŸ” 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 - 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](/start/quickstart/) shows that loop and [Run it as a service](/start/service/) explains the reload. ## 🌐 Certificates from Let's Encrypt ```caddyfile { 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: ```text 🌐 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: ```text πŸ“ 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: ```bash curl -I https://example.com/ ``` ```text HTTP/2 200 content-type: text/html; charset=utf-8 etag: "493b-6ab1f452" server: Pingclair ``` ```bash echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates ``` ```text 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 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: ```caddyfile { email bonjour@pingclair.com } *.example.com { tls { auto dns cloudflare 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: ```bash curl -I https://anything.example.com/ ``` ```text HTTP/2 200 content-type: text/html; charset=utf-8 server: Pingclair ``` ```bash echo | openssl s_client -connect example.com:443 -servername anything.example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -ext subjectAltName ``` ```text 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 For private origins β€” a tunnel, an internal hostname, a lab machine β€” Pingclair can issue certificates from its internal authority: ```caddyfile 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 `/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: ```bash sudo PINGCLAIR_TLS_STORE=/var/lib/pingclair/.local/share/pingclair pingclair trust ``` ```text βœ… 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`: ```bash curl -s -o /dev/null -w '%{http_code}\n' https://internal.test/ ``` ```text 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. ## πŸ“œ Certificates you supply When another system issues your certificates, point `tls` at the files: ```caddyfile 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: ```text ❌ TLS certificate file does not exist: /etc/pingclair/certs/missing.crt ``` ## ⚠️ 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 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. ## 🧭 Next steps - [Run it as a service](/start/service/): the unit, its reload semantics, and its logs. - [`tls`](/reference/directives/#tls): every mode and option of the directive. - [Pingclairfile](/reference/pingclairfile/): addresses, matchers, and what the compiler accepts.