--- id: tls-sources title: A certificate comes from somewhere other than this server's own ACME client status: open crates: [didbot-serve] dependsOn: [agent-accounts] exitCriterion: > A deployment serves agent hostnames over TLS from a certificate this server did not obtain, swaps it without a restart or a dropped connection, and says which source produced the one it is serving. --- # tls-sources Obtaining a wildcard over ACME DNS-01 is the default and should stay the default: it needs no operator, renews itself, and works with the zone credential the deployment already holds. It is not the only place a certificate can come from, and some of the alternatives are what a deployment with an existing PKI actually has. The deliverable is the **seam** first and the implementations second, the same way [`DnsProvider`](../crates/didbot-dns/src/lib.rs) is the seam under [dns-providers](dns-providers.md). A certificate source has to answer three things: produce a certificate now, say when it should next be replaced, and hand over a replacement without interrupting anything. ## The sources - [ ] **Another ACME certificate authority.** Mostly a directory URL — ZeroSSL, Buypass, Google Trust Services. Cheap to support and worth having, because a deployment blocked by one authority's rate limits or outage has somewhere to go. - [ ] **A certificate the operator supplies**, as files. A half-declared hook for this already exists and is worth knowing about before anything is built on it: `TlsSection::cert_source` (`crates/didbot-config/src/sections.rs:90`) is documented as "`acme` or a path to a fixed pair", and **nothing reads it** — `didbot-pds`'s `--tls` accepts `plain` or `acme` and nothing else. A deployment that set `cert_source` to a path today would be silently ignored. Either wire it as part of this item or delete it; a configuration key that does nothing is worse than an absent one. This is the case for an existing internal PKI, a corporate authority, or a purchased wildcard, and it is the one that most needs a **reload trigger**: a file that changes on disk is not an event, so it needs a watcher or a signal. Renewal is somebody else's job here, which changes what a renewal *failure* even means. - [ ] **A certificate from a secret store or KMS**, fetched rather than read, which is the same shape as the file source with a different retrieval and a credential of its own. - [ ] **Terminating at a reverse proxy the operator runs**, with this server speaking plain HTTP behind it. This is legitimate **only when the hop is loopback on one host** and the operator has chosen it deliberately. Terminating at a load balancer and carrying plaintext across a network to the instance is a different thing and is not acceptable: the distinction is the wire, not the pattern. ## What every source has to satisfy - [ ] **Hot reload, without dropping or stalling connections.** Whatever replaces a certificate does it by swapping shared state behind rustls's certificate resolution, never by rebinding the listener. This is not a nicety: `com.atproto.sync.subscribeRepos` is the firehose, and consumers hold it open for hours. The default ACME source establishes this behaviour; every source here inherits the requirement rather than reinventing it. - [ ] **Say which source produced what is being served**, on the health surface. A deployment that cannot answer "where did this certificate come from" cannot answer the question that matters after an incident. - [ ] **Distinguish failing at startup from failing at renewal.** With no certificate at all the server cannot serve; with a valid one and a failing refresh it must keep serving and complain loudly. The consequence is the same for every source and is severe: [deploy](deploy.md) puts it plainly — an expired wildcard stops every agent resolving at once. - [ ] **A source that cannot renew must say so**, rather than presenting as one that has not needed to yet. An operator-supplied file has no renewal story by construction, and that is a fact the deployment should surface before the expiry rather than at it. ## Provenance, because this certificate is load-bearing for identity - [ ] **CAA records naming the intended issuer**, and **certificate transparency watched for the zone** — [deployment](../docs/deployment.md) already asks for both. Widening the set of sources widens what a mis-issuance could look like, so these stop being hygiene and start being the check. - [ ] **Consider `_acme-challenge` delegation** ([dns-providers](dns-providers.md)) so the credential that answers a challenge is not the credential that controls the zone. It belongs to both epics and is the one change here that narrows a blast radius rather than widening a choice. ## Done Nothing closed yet.