# DNS Wildcard Certificate Generator 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 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. The container only needs to be running (with port 53 reachable) while a certificate is being requested or renewed. ## DNS Setup ### Step 1: Delegate a subdomain to the container (once per zone) Pick a subdomain, e.g. `acme.example.com`. Add these two records to the `example.com` zone: | Name | Type | Value | |------------------------|------|-------------------------| | `ns.acme.example.com` | A | `1.2.3.4` | | `acme.example.com` | NS | `ns.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 ``` 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. Then check the delegation works end to end while the container is running: ```bash dig @1.1.1.1 _acme-challenge.example.com TXT ``` ## Usage ### Quick Start ```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 -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 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 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 | ### Non-standard port 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: ```bash docker run -it --rm \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ -p 5555:5555/tcp -p 5555:5555/udp \ -e EMAIL="youremail@example.com" \ -e DOMAINS="-d example.com -d *.example.com" \ -e DNS_PORT="5555" \ dns-wildcard-cert ``` ## Certificate Renewal Run the same container again periodically, or use certbot's renew command: ```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 \ --entrypoint certbot \ dns-wildcard-cert renew ``` ## Certificate Location Certificates are stored in the `/etc/letsencrypt` volume: - Certificate: `/etc/letsencrypt/live//fullchain.pem` - Private key: `/etc/letsencrypt/live//privkey.pem` ## Docker Compose ```yaml version: '3.8' services: certbot: build: . ports: - "53:53/tcp" - "53:53/udp" environment: - EMAIL=youremail@example.com - DOMAINS=-d example.com -d *.example.com - STAGING=false volumes: - letsencrypt:/etc/letsencrypt - letsencrypt-lib:/var/lib/letsencrypt volumes: letsencrypt: letsencrypt-lib: ``` ## License MIT