> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hill90.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Certificates

> Why the homelab needs two ACME challenge types, and how DNS-01 is implemented.

# Certificates

Traefik obtains all certificates from Let's Encrypt, using two different challenge
methods for two different kinds of hostname.

| Challenge | Used for                                                              | Resolver          |
| --------- | --------------------------------------------------------------------- | ----------------- |
| HTTP-01   | Public hostnames, including `auth.hill90.com`                         | `letsencrypt`     |
| DNS-01    | Tailscale-only hostnames (`traefik`, `portainer`, `grafana`, `vault`) | `letsencrypt-dns` |

## Why two

HTTP-01 requires Let's Encrypt's validation servers to reach the host on port 80. The
Tailscale-only surfaces have no public A record — that is the point of them — so HTTP-01
can never validate them.

Keycloak is the exception among administrative surfaces: it is publicly routed, so it has
a public A record and uses HTTP-01 like any other public host. See
[Surfaces](/homelab/overview#surfaces).

DNS-01 validates by publishing a TXT record instead, which works for any hostname
regardless of whether it is publicly reachable. It needs an API-driven way to write that
record, which is what `dns-manager` provides.

## The DNS-01 path

```
Let's Encrypt  ──DNS query──→  _acme-challenge.<host>.hill90.com
                                          ↑
Traefik ──→ Lego httpreq provider ──→ dns-manager ──→ Hostinger DNS API
```

`services/dns-manager/app.py` exposes three endpoints:

| Endpoint        | Purpose                                   |
| --------------- | ----------------------------------------- |
| `POST /present` | Create the DNS TXT record for a challenge |
| `POST /cleanup` | Delete it after validation                |
| `GET /health`   | Health check                              |

Two implementation details matter enough that they are called out in the source doc as
the mistakes that were actually made:

1. **The TXT value is not the token.** ACME DNS-01 requires
   `base64url(SHA256(keyAuth))`. Publishing the `token` field verbatim fails validation.
2. **`dns-manager` must return immediately.** Traefik handles the propagation wait itself
   via `delayBeforeCheck: 30s`. If the webhook sleeps, Traefik times out instead.

Traefik also does not interpolate `${VAR}` in its YAML config, so the ACME email is
hardcoded in `traefik.yml` while the CA server is passed through Docker Compose, which
does interpolate.

## Rate limits

Let's Encrypt production allows 50 certificates per registered domain per week and 5
validation failures per hostname per hour. Because of that, `make deploy-infra` uses
**staging** certificates by default; production certificates require
`make deploy-infra-production` or the `deploy-infra.yml` workflow, which sets them
explicitly.

The edge stack is deliberately excluded from push-triggered deploys for the same reason —
recreating Traefik re-requests certificates, and that should be a decision rather than a
side effect of a merge.

## Source documents

* [`docs/architecture/certificates.md`](https://github.com/jonhill90/Hill90/blob/main/docs/architecture/certificates.md)
  — full challenge flow, the `dns-manager` implementation, troubleshooting for failed
  issuance and DNS record verification
* [`docs/reference/dns.md`](https://github.com/jonhill90/Hill90/blob/main/docs/reference/dns.md)
  — DNS record inventory and the Hostinger API scripts
