Let's Encrypt / ACME DNS-01

Obtain automatic TLS certificates using the DNS-01 challenge — DomU DNS handles the _acme-challenge TXT records directly. No HTTP port 80 required.

Prerequisites

Required: Your domain's NS records must point to your DomU DNS instance so Let's Encrypt can query _acme-challenge.<domain> TXT.

Step 1: Create a Named API Key

Named API keys are dedicated keys for external tools — separate from your root API key. Create one in the dashboard:

  1. Open the Dashboard → Settings → Security
  2. Scroll to API Keys → click + New API Key
  3. Enter a name (e.g. Traefik ACME) and save
  4. Copy the key — it is shown only once
Named API Keys are dedicated keys for external tools. Create them in the Dashboard under Settings → Security → API Keys. They are separate from your root API key.

Option A: Traefik (httpreq provider)

Traefik's built-in httpreq DNS provider uses Basic Auth. The Named API Key is used as the HTTP password.

Static configuration (traefik.yml)

yaml
certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: /acme/acme.json
      dnsChallenge:
        provider: httpreq

Environment variables

bash
HTTPREQ_ENDPOINT=http://<domudns-ip>/api/acme/httpreq
HTTPREQ_USERNAME=traefik          # any string
HTTPREQ_PASSWORD=<named-api-key>  # from DomU DNS dashboard

Traefik will call POST /api/acme/httpreq/present with the DNS-01 token and POST /api/acme/httpreq/cleanup after validation.

Option B: Certbot DNS Plugin

Install the certbot-dns-domudns plugin, then use it like any other Certbot DNS plugin.

Install

bash
pip install certbot certbot-dns-domudns

Credentials file /etc/letsencrypt/domudns.ini

ini
dns_domudns_url     = http://<domudns-ip>
dns_domudns_api_key = <named-api-key>
bash
chmod 600 /etc/letsencrypt/domudns.ini

Issue certificate (staging)

bash
certbot certonly \
  --authenticator dns-domudns \
  --dns-domudns-credentials /etc/letsencrypt/domudns.ini \
  --server https://acme-staging-v02.api.letsencrypt.org/directory \
  -d example.com -d '*.example.com'

Issue certificate (production)

bash
certbot certonly \
  --authenticator dns-domudns \
  --dns-domudns-credentials /etc/letsencrypt/domudns.ini \
  -d example.com -d '*.example.com'

Option C: acme.sh

Copy the hook script from the DomU DNS repository and use it with acme.sh.

Install acme.sh and the hook

bash
# Install acme.sh
curl https://get.acme.sh | sh

# Copy DomU DNS hook script
cp /path/to/domudns/scripts/dns_domudns.sh ~/.acme.sh/dnsapi/
chmod +x ~/.acme.sh/dnsapi/dns_domudns.sh

Issue certificate

bash
export DOMUDNS_URL=http://<domudns-ip>
export DOMUDNS_API_KEY=<named-api-key>

acme.sh --issue --dns dns_domudns -d example.com -d '*.example.com'

Option D: Proxmox Cluster

Proxmox VE uses acme.sh internally for ACME certificate management. The DomU DNS plugin must be installed manually on every node in the cluster.

Cluster note: The DNS plugin file must be present on every node that requests certificates. The plugin configuration (Datacenter → ACME) is cluster-wide and only needs to be created once.

Step 1 — Copy the plugin file to all nodes

Run this on a machine that has SSH access to all Proxmox nodes:

bash
for node in pve1 pve2 pve3; do
  scp dns_domudns.sh root@${node}:/usr/share/proxmox-acme/dnsapi/dns_domudns.sh
  ssh root@${node} chmod +x /usr/share/proxmox-acme/dnsapi/dns_domudns.sh
done

Step 2 — Register the plugin in the Proxmox schema

Proxmox reads /usr/share/proxmox-acme/dns-challenge-schema.json to populate the DNS API dropdown in the UI. Run the following on every node:

bash
python3 -c "
import json
with open('/usr/share/proxmox-acme/dns-challenge-schema.json', 'r') as f:
    schema = json.load(f)
schema['domudns'] = {
    'name': 'DomU DNS',
    'fields': {
        'DOMUDNS_URL': {
            'description': 'Base URL of the DomU DNS instance (no trailing slash)',
            'type': 'string'
        },
        'DOMUDNS_API_KEY': {
            'description': 'Named API key from DomU DNS dashboard',
            'type': 'string'
        }
    }
}
with open('/usr/share/proxmox-acme/dns-challenge-schema.json', 'w') as f:
    json.dump(schema, f, indent=3, sort_keys=True)
print('Done')
"
systemctl restart pvedaemon pveproxy
Note: The schema file is part of the libproxmox-acme-plugins package. An apt upgrade may overwrite it — re-run Step 2 after package updates.

Step 3 — Add the DNS plugin (once, cluster-wide)

In the Proxmox web UI: Datacenter → ACME → DNS Plugins → Add

Plugin ID domudns
DNS API domudns (select from dropdown)
API Data DOMUDNS_URL=http://<domudns-ip>
DOMUDNS_API_KEY=<named-api-key>

Step 4 — Configure certificate per node

For each node: Node → System → Certificates → ACME → Add Domain

Then click Order Certificates Now.

Manual API Test

Verify the full flow before using an ACME client:

bash
API=http://<domudns-ip>
KEY=<named-api-key>

# 1. Store challenge
curl -X POST $API/api/acme/dns-01/present \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain":"example.com","txt_value":"test-token-123"}'

# 2. Query DNS — must return "test-token-123"
dig @<domudns-ip> _acme-challenge.example.com TXT

# 3. Remove challenge
curl -X POST $API/api/acme/dns-01/cleanup \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain":"example.com"}'

Troubleshooting

Problem Solution
dig returns NXDOMAIN Challenge not stored yet, or TTL expired (default 60 s)
401 Unauthorized Use a Named API Key from Settings → Security → API Keys
Connection refused DomU DNS HTTP server not reachable on the configured port
Domain does not resolve NS records for the domain must point to your DomU DNS server
Rate limit exceeded Use staging: --server https://acme-staging-v02.api.letsencrypt.org/directory
Traefik: 401 Unauthorized HTTPREQ_PASSWORD must match the Named API Key exactly
Proxmox: domudns not in DNS API dropdown Run Step 2 (schema registration) on the node you are currently browsing from, then restart pvedaemon and pveproxy
Proxmox: domudns disappeared after upgrade apt upgrade overwrites the schema file — re-run Step 2 on all nodes