diff --git a/README.md b/README.md index 32cca1f..c64e5c7 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,83 @@ # DNS Wildcard Certificate Generator -A Docker container for easily obtaining wildcard SSL certificates from Let's Encrypt using the `certbot-dns-standalone` plugin. +Docker container that obtains wildcard certificates from Let's Encrypt. It uses the +`certbot-dns-standalone` plugin, which runs its own tiny DNS server to answer the +ACME DNS-01 challenge. No API credentials for your DNS provider are needed. ## How It Works -This uses the `dns-standalone` authenticator which runs its own DNS server to respond to ACME DNS-01 challenges. You need to configure your DNS to delegate `_acme-challenge` queries to this container. +1. You delegate one subdomain (e.g. `acme.example.com`) to the machine running this container. +2. For every domain you want a certificate for, you point its `_acme-challenge` record at that subdomain via CNAME. +3. When certbot runs, Let's Encrypt looks up `_acme-challenge.`, follows the CNAME, + and lands on the container's DNS server, which answers with the challenge token. -## Prerequisites - -1. A server with port 53 (DNS) available -2. DNS configuration to route challenge queries to your server (see DNS Setup below) +The container only needs to be running (with port 53 reachable) while a certificate is +being requested or renewed. ## DNS Setup -### Option 1: Direct NS Record +### Step 1: Delegate a subdomain to the container (once per zone) -Point `_acme-challenge` records to your certbot server using CNAME and NS records: +Pick a subdomain, e.g. `acme.example.com`. Add these two records to the `example.com` zone: -```dns -; For acme.example.com as your certbot endpoint -acme IN NS ns.acme.example.com. -ns.acme IN A 1.2.3.4 +| Name | Type | Value | +|------------------------|------|-------------------------| +| `ns.acme.example.com` | A | `1.2.3.4` | +| `acme.example.com` | NS | `ns.acme.example.com.` | -; For each domain you want certificates for -_acme-challenge.example.com IN CNAME acme.example.com. +`1.2.3.4` is the public IP of the machine running this container. Port 53 (TCP and UDP) +must be reachable from the internet. + +### Step 2: Add a CNAME for every name you request (once per name) + +Let's Encrypt validates each name in the certificate separately. For each name it queries +`_acme-challenge.`. So you need one CNAME per **distinct name**, all pointing at the +subdomain from Step 1. + +Rule of thumb: take every `-d` entry, drop the leading `*.` if present, and prefix the +result with `_acme-challenge.`. Duplicates collapse into one record. + +| You request (`-d`) | Record needed | +|--------------------------------|--------------------------------------------------------| +| `example.com` | `_acme-challenge.example.com CNAME acme.example.com.` | +| `*.example.com` | same record as above (already covered) | +| `*.ai.example.com` | `_acme-challenge.ai.example.com CNAME acme.example.com.` | +| `*.dev.ai.example.com` | `_acme-challenge.dev.ai.example.com CNAME acme.example.com.` | +| `other-domain.org` | `_acme-challenge.other-domain.org CNAME acme.example.com.` | + +Notes: + +- `example.com` and `*.example.com` share one record. A wildcard one level deeper + (`*.ai.example.com`) is a **different name** and needs its own record. +- Other domains you own (`other-domain.org`) can reuse the same `acme.example.com` + delegation. Only Step 2 is needed for them. +- The CNAME target can be anything under the delegated subdomain. The container answers + every TXT query it receives, regardless of name. `acme.example.com.` is the simplest choice. + +### Watch out: wildcard TXT records + +If your zone has a wildcard record like `*.example.com TXT "v=spf1 ..."`, then +`_acme-challenge.ai.example.com` silently resolves to that SPF string instead of failing. +Let's Encrypt then reports an *incorrect TXT record*. An explicit CNAME for +`_acme-challenge.ai.example.com` overrides the wildcard, so adding the record from Step 2 +fixes this. + +### Verify before running certbot + +Ask the authoritative name server directly (no cache): + +```bash +dig +norecurse @ _acme-challenge.ai.example.com TXT ``` -Where `1.2.3.4` is the IP of the server running this container. +You want to see a `CNAME acme.example.com.` line in the answer. If you see an SPF string +or nothing, the record is missing or the wildcard is winning. -### Option 2: DNS Proxy/Forwarding +Then check the delegation works end to end while the container is running: -If you already run a DNS server, configure it to forward `_acme-challenge` queries to the container. +```bash +dig @1.1.1.1 _acme-challenge.example.com TXT +``` ## Usage @@ -42,10 +89,14 @@ docker run -it --rm \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ -p 53:53/tcp -p 53:53/udp \ -e EMAIL="youremail@example.com" \ - -e DOMAINS="-d example.com -d *.example.com" \ + -e DOMAINS="-d example.com -d *.example.com -d *.ai.example.com" \ + -e STAGING="true" \ dns-wildcard-cert ``` +Run with `STAGING="true"` first. Once it succeeds, run again without it to get a real +certificate. Let's Encrypt rate-limits failed attempts against production. + ### Build Locally ```bash @@ -54,53 +105,21 @@ docker build -t dns-wildcard-cert . ### Environment Variables -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `EMAIL` | Yes | - | Email for Let's Encrypt registration | -| `DOMAINS` | Yes | - | Domain flags (e.g., `-d example.com -d *.example.com`) | -| `DNS_ADDRESS` | No | `0.0.0.0` | IPv4 address to bind DNS server | -| `DNS_IPV6_ADDRESS` | No | `::` | IPv6 address to bind DNS server | -| `DNS_PORT` | No | `53` | Port for DNS server | -| `STAGING` | No | `false` | Use Let's Encrypt staging server (for testing) | -| `DRY_RUN` | No | `false` | Perform a dry run without saving certificates | +| Variable | Required | Default | Description | +|--------------------|----------|-----------|----------------------------------------------------------| +| `EMAIL` | Yes | - | Email for Let's Encrypt registration | +| `DOMAINS` | Yes | - | Domain flags, e.g. `-d example.com -d *.example.com` | +| `DNS_ADDRESS` | No | `0.0.0.0` | IPv4 address to bind the DNS server to | +| `DNS_IPV6_ADDRESS` | No | `::` | IPv6 address to bind the DNS server to | +| `DNS_PORT` | No | `53` | Port for the DNS server (needs forwarding if not 53) | +| `STAGING` | No | `false` | Use the Let's Encrypt staging server (for testing) | +| `DRY_RUN` | No | `false` | Perform a dry run without saving certificates | -### Examples +### Non-standard port -**Test with staging server first:** -```bash -docker run -it --rm \ - -v "/etc/letsencrypt:/etc/letsencrypt" \ - -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ - -p 53:53/tcp -p 53:53/udp \ - -e EMAIL="youremail@example.com" \ - -e DOMAINS="-d example.com -d *.example.com" \ - -e STAGING="true" \ - dns-wildcard-cert -``` +If port 53 is taken on the host, run the container on another port and have your existing +DNS server forward `_acme-challenge` queries there: -**Dry run (no certificates saved):** -```bash -docker run -it --rm \ - -p 53:53/tcp -p 53:53/udp \ - -e EMAIL="youremail@example.com" \ - -e DOMAINS="-d example.com -d *.example.com" \ - -e DRY_RUN="true" \ - dns-wildcard-cert -``` - -**Bind to specific IP:** -```bash -docker run -it --rm \ - -v "/etc/letsencrypt:/etc/letsencrypt" \ - -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ - -p 1.2.3.4:53:53/tcp -p 1.2.3.4:53:53/udp \ - -e EMAIL="youremail@example.com" \ - -e DOMAINS="-d example.com -d *.example.com" \ - -e DNS_ADDRESS="0.0.0.0" \ - dns-wildcard-cert -``` - -**Use non-standard port (with DNS forwarding):** ```bash docker run -it --rm \ -v "/etc/letsencrypt:/etc/letsencrypt" \ @@ -114,7 +133,7 @@ docker run -it --rm \ ## Certificate Renewal -For renewal, you can run the same container periodically or use certbot's renew command: +Run the same container again periodically, or use certbot's renew command: ```bash docker run -it --rm \ @@ -130,7 +149,7 @@ docker run -it --rm \ Certificates are stored in the `/etc/letsencrypt` volume: - Certificate: `/etc/letsencrypt/live//fullchain.pem` -- Private Key: `/etc/letsencrypt/live//privkey.pem` +- Private key: `/etc/letsencrypt/live//privkey.pem` ## Docker Compose @@ -156,17 +175,6 @@ volumes: letsencrypt-lib: ``` -## Parameter Changes - -**Note:** The old certbot-dns-standalone parameter format has changed: - -| Old Format | New Format | -|------------|------------| -| `--authenticator certbot-dns-standalone:dns-standalone` | `--authenticator dns-standalone` | -| `--certbot-dns-standalone:dns-standalone-address=` | `--dns-standalone-address=` | -| `--certbot-dns-standalone:dns-standalone-ipv6-address=` | `--dns-standalone-ipv6-address=` | -| `--certbot-dns-standalone:dns-standalone-port=` | `--dns-standalone-port=` | - ## License MIT