TLS certificates for fleet hosts
Public names on the open network are a kind of address in space: browsers, APIs, and reverse proxies ask whether the machine answering that name is the one they meant to reach. TLS certificates are how we answer that question with cryptography instead of hope. Linux Directory helps you issue and renew those certificates on enrolled hosts through a native ACME client inside lindir-client — no certbot dependency, no separate certificate daemon required for the common case.
This guide is the operational map: how policies are shaped, how challenges prove control of a name, how renewal fits into sync, and how discovery helps you bring existing PEMs under management. For the product overview, see the Operator Handbook. Fleet API tables live in Fleet; agent endpoints in Client.
What problem this solves
Many fleets already terminate HTTPS on nginx, Caddy, or a similar proxy. Certificates expire. Let’s Encrypt and other ACME certificate authorities issue short-lived certificates (typically about ninety days) so that compromise windows stay bounded and operators are nudged to automate renewal.
Lindir’s certificate feature:
- Stores policies on the server (domains, challenge type, DNS credentials, install paths, optional reload command).
- Assigns those policies to hosts or labels (never the entire fleet in one assignment).
- Lets each host pull its assigned policies on sync, issue or renew when due, write PEMs to disk, optionally run a reload hook, and report status back.
A single host may hold many policies at once — different domains, different DNS providers, different file paths — each renewed independently.
Pieces of the model
| Piece | Role |
|---|---|
| Policy | Named ACME configuration: domains, dns-01 or http-01, provider + encrypted credential (or webroot), certPath / keyPath, optional reloadHook, renewBeforeDays, ACME directory URL, contact email |
| Target | Assignment of a policy to a host or label |
| Status | Per-(host, policy) row: notAfter, fingerprint, last success, last error |
| Discover job | One-shot scan of on-disk PEMs; findings help prefill new policies; certs already managed by Lindir are tagged |
Credentials for DNS APIs are stored encrypted on the server (AES-GCM keyed from the bootstrap material). Plaintext is delivered only to the assigned host when it pulls policies. Admin APIs show whether a credential is set, not the secret itself.
Challenges: proving you control the name
ACME asks the subscriber to prove control of each identifier (usually a DNS name) before a certificate is issued.
DNS-01 (preferred when a provider is configured)
The client places a TXT record at _acme-challenge.<name> whose value is derived from the challenge token and the ACME account key. The certificate authority queries public DNS from multiple vantage points. When those views agree that the correct TXT is present, authorization succeeds.
Lindir implements DNS-01 for:
| Provider id | Notes |
|---|---|
gandi |
LiveDNS API (Bearer PAT, or legacy Apikey …) |
hostinger |
DNS zone API (Bearer token) |
hetzner |
DNS Console API |
ovh |
Credential form appKey:appSecret:consumerKey optionally [:ovh-eu] (or another OVH endpoint suffix) |
ionos |
Credential form publicprefix.secret |
infomaniak |
API token |
scaleway |
Secret key (and project context as required by the API) |
cloudflare |
API token |
After writing the TXT record, the client waits until public resolvers (DNS-over-HTTPS via Cloudflare and Google) show the expected value, then allows time for the authority’s secondary validators. Old TXT values with long TTLs can linger in recursive caches worldwide; that is why challenge records should use a short TTL (Lindir requests sixty seconds where the provider accepts it) and why failed attempts may need patience until stale cache entries expire.
HTTP-01
The client writes a file under webroot/.well-known/acme-challenge/<token> that the authority fetches over HTTP on port 80. Use this when the host already serves that webroot publicly and you do not have (or need) DNS API access. The policy must set webroot; dnsProvider is left empty.
Lifecycle on the host
Each lindir-client sync:
- Claims any certificate discovery jobs (if queued).
- Loads all cert policies assigned via host or label.
- For each policy: if the PEM at
certPathis missing or withinrenewBeforeDaysof expiry (default 30), runs ACME issue; otherwise reports status only (“still valid”). - On successful issue: writes
certPathandkeyPath, runsreloadHookif set (bounded by a timeout so a hung reload cannot hold the sync lock forever), reports success. - On failure: reports the error for that policy; other policies on the same host still run.
ACME account keys live under /var/lib/lindir/acme/ (or a cache-relative path when /var/lib/lindir is absent). Issued PEMs go exactly where the policy says — use distinct paths per policy so one renewal does not overwrite another.
Audit: a real ACME issue or renew records fleet.cert_issued (actor host:<hostname>). Failures record fleet.cert_issue_failed. Routine “still valid” check-ins do not clutter the audit log.
Discovery: finding certificates already on disk
Hosts often already hold Let’s Encrypt trees under /etc/letsencrypt/live/… or PEMs beside a reverse proxy. Discover on host (Admin Compliance → Certificates, or CLI) enqueues a job; on the next sync the client walks default roots (Lindir cert dirs, Let’s Encrypt live, common nginx/Apache/Caddy SSL paths) or optional custom paths.
Each finding includes domains (from the leaf certificate’s SAN/CN), paths, expiry, fingerprint, and issuer. If the paths match an assigned Lindir policy, the finding is tagged managed so operators open the existing policy instead of duplicating it. Unmanaged findings can Use to prefill the create form.
Admin surfaces
| Surface | What to do |
|---|---|
| Admin UI Compliance → Certificates | Create/edit policies, assign host/label, watch status, discover and prefill |
lindir-admin fleet certs … |
list, create, assign, unassign, delete, status, discover, discover-status |
| HTTP | /v1/fleet/certs/* (admin), /v1/client/fleet/certs* (host) |
Staging (safe dry-run against Let’s Encrypt’s staging directory):
acmeDirectoryUrl = https://acme-staging-v02.api.letsencrypt.org/directory
Production directory (default):
https://acme-v02.api.letsencrypt.org/directory
Verifying a certificate on the host
After a successful issue, confirm the files the proxy actually reads:
openssl x509 -in /path/from/policy/fullchain.pem -noout -subject -dates
Expect the intended CN/SAN and a notAfter about ninety days out for Let’s Encrypt. In Admin, Certificates → Status should show that expiry without a lasting error. Audit should show fleet.cert_issued for that host when the issue actually ran.
A careful first issue
Automation that talks to the public Internet benefits from a rehearsal. The same protocol that issues production certificates also issues staging certificates that browsers will not trust — which is exactly what you want while wiring DNS credentials and paths.
- Create the policy with
acmeDirectoryUrlset to the Let’s Encrypt staging directory (see above). - Assign it to one host; run
sudo lindir-client sync(or wait for the timer). - Read Admin Certificates → Status and, on the host,
openssl x509 -in … -noout -subject -dates. - When staging succeeds, switch the policy to the production directory (or clear
acmeDirectoryUrlto use the default) and sync again so a trusted certificate is written to the same paths.
Rate limits at the certificate authority are real: too many failed production attempts against the same name can pause further issuance for a time. Staging exists so those limits stay out of the critical path while you learn the shape of your DNS and webroot.
When something fails
Failures are reported per policy on the host status row and, when an ACME issue actually ran, as fleet.cert_issue_failed in Audit. Useful patterns:
| Symptom | Likely cause | What to check |
|---|---|---|
| Incorrect TXT / authorization failure | Stale _acme-challenge TXT still cached, or the wrong zone updated |
Remove leftover challenge records in the DNS panel; wait for the previous TTL; confirm the TXT at the FQDN the CA expects |
| DNS API error / 401 | Credential scope or format | Provider token permissions; OVH/IONOS credential shape (see provider table) |
| Timeout waiting for DNS | Propagation slower than expected, or API write did not stick | Query the name via public DoH; confirm the provider accepted the record; retry after the zone settles |
| HTTP-01 failure | Port 80 or webroot not reachable from the Internet | That webroot/.well-known/acme-challenge/ is served on the hostname being validated |
| Cert issued, site still old | Proxy reading different files, or reload not run | Align certPath/keyPath with the listener config; set reloadHook; confirm with openssl on the paths the proxy uses |
| Sync never reaches ACME | Sync flock held, or earlier step blocking | systemctl status lindir-sync.service; ensure no hung sync holds /run/lindir/ |
One host’s failure does not stop other policies on that host. Fix the failing policy, sync again, and leave the healthy PEMs alone.
Operational notes that save real time
- Many policies, one host — assign freely; keep
certPath/keyPathunique. - Reload hooks — e.g.
systemctl reload nginxso listeners pick up new PEMs without a full restart when possible. - DNS cache after failed attempts — if Let’s Encrypt reports an incorrect TXT during secondary validation, a previous challenge value may still be cached at some resolvers. Delete leftover
_acme-challengerecords in the DNS panel if needed, then wait for the old TTL to drain before the next attempt. - Zone shape — Lindir maps names to a zone apex using the last two DNS labels for provider APIs. Unusual public suffixes (multi-part TLDs) may need care; prefer names that match how the DNS provider lists the zone.
- Server TLS vs host TLS — certificates managed here are for fleet hosts (sites and services on enrolled machines). Terminating TLS for the Lindir server itself remains a reverse-proxy / platform concern (see production hardening in the handbook).
Further reading
- Operator handbook — TLS certificates
- Fleet — Certificates
- Client — sync order and client ACME endpoints
- Admin Web UI — Compliance → Certificates
- RFC 8555 — Automatic Certificate Management Environment (ACME)
