docs: restructure DNS setup section in README
Build Container / docker (push) Successful in 20s

Split DNS setup into one-time delegation (NS + A) and per-name CNAME
records, with a table showing which _acme-challenge record each -d
entry needs. Nested wildcards like *.ai.example.com need their own
record, which was not obvious before. Add a note on wildcard TXT
records shadowing missing challenge names, plus dig commands to verify
before running certbot. Drop the obsolete plugin parameter table and
redundant run examples.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lyu3TWGffosqSY6kDpXCd1
This commit is contained in:
2026-09-11 13:30:03 +02:00
co-authored by Claude Fable 5.1
parent e110b47acb
commit 28864708c8
+80 -72
View File
@@ -1,36 +1,83 @@
# DNS Wildcard Certificate Generator # 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 ## 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.<domain>`, follows the CNAME,
and lands on the container's DNS server, which answers with the challenge token.
## Prerequisites The container only needs to be running (with port 53 reachable) while a certificate is
being requested or renewed.
1. A server with port 53 (DNS) available
2. DNS configuration to route challenge queries to your server (see DNS Setup below)
## DNS Setup ## 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 | Name | Type | Value |
; For acme.example.com as your certbot endpoint |------------------------|------|-------------------------|
acme IN NS ns.acme.example.com. | `ns.acme.example.com` | A | `1.2.3.4` |
ns.acme IN A 1.2.3.4 | `acme.example.com` | NS | `ns.acme.example.com.` |
; For each domain you want certificates for `1.2.3.4` is the public IP of the machine running this container. Port 53 (TCP and UDP)
_acme-challenge.example.com IN CNAME acme.example.com. 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.<name>`. 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 @<your-dns-provider-ns> _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 ## Usage
@@ -42,10 +89,14 @@ docker run -it --rm \
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
-p 53:53/tcp -p 53:53/udp \ -p 53:53/tcp -p 53:53/udp \
-e EMAIL="youremail@example.com" \ -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 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 ### Build Locally
```bash ```bash
@@ -55,52 +106,20 @@ docker build -t dns-wildcard-cert .
### Environment Variables ### Environment Variables
| Variable | Required | Default | Description | | Variable | Required | Default | Description |
|----------|----------|---------|-------------| |--------------------|----------|-----------|----------------------------------------------------------|
| `EMAIL` | Yes | - | Email for Let's Encrypt registration | | `EMAIL` | Yes | - | Email for Let's Encrypt registration |
| `DOMAINS` | Yes | - | Domain flags (e.g., `-d example.com -d *.example.com`) | | `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_ADDRESS` | No | `0.0.0.0` | IPv4 address to bind the DNS server to |
| `DNS_IPV6_ADDRESS` | No | `::` | IPv6 address to bind DNS server | | `DNS_IPV6_ADDRESS` | No | `::` | IPv6 address to bind the DNS server to |
| `DNS_PORT` | No | `53` | Port for DNS server | | `DNS_PORT` | No | `53` | Port for the DNS server (needs forwarding if not 53) |
| `STAGING` | No | `false` | Use Let's Encrypt staging server (for testing) | | `STAGING` | No | `false` | Use the Let's Encrypt staging server (for testing) |
| `DRY_RUN` | No | `false` | Perform a dry run without saving certificates | | `DRY_RUN` | No | `false` | Perform a dry run without saving certificates |
### Examples ### Non-standard port
**Test with staging server first:** If port 53 is taken on the host, run the container on another port and have your existing
```bash DNS server forward `_acme-challenge` queries there:
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
```
**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 ```bash
docker run -it --rm \ docker run -it --rm \
-v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/etc/letsencrypt:/etc/letsencrypt" \
@@ -114,7 +133,7 @@ docker run -it --rm \
## Certificate Renewal ## 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 ```bash
docker run -it --rm \ docker run -it --rm \
@@ -130,7 +149,7 @@ docker run -it --rm \
Certificates are stored in the `/etc/letsencrypt` volume: Certificates are stored in the `/etc/letsencrypt` volume:
- Certificate: `/etc/letsencrypt/live/<domain>/fullchain.pem` - Certificate: `/etc/letsencrypt/live/<domain>/fullchain.pem`
- Private Key: `/etc/letsencrypt/live/<domain>/privkey.pem` - Private key: `/etc/letsencrypt/live/<domain>/privkey.pem`
## Docker Compose ## Docker Compose
@@ -156,17 +175,6 @@ volumes:
letsencrypt-lib: 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 ## License
MIT MIT