Table of Contents

DNS

Changelog

  • 2024-07-17: Init page, on dehydrated and acme-dns.

Regarding hostnames

The preferred method of assigning an IP address and hostname, in order of preference:

  1. Assign IP address and domain name using DHCP
  2. Assign static IP address and domain name in /etc/hosts
  3. Assign loopback address and local hostname

There are expectations for how each name should be resolved:

Now consider the following scenarios:

This yields, in the case of electric hostname and pikachu domain name (ideally these two should be equivalent, by using assignment by DNS):

/etc/hosts
127.0.0.1 localhost
192.168.1.3 pikachu.pyuxiang.com pikachu electric

# If no domain name is needed/assigned:
# 127.0.1.1 electric

Some useful commands to test NS resolution behaviour: hostname -f and nslookup / dig.

DNS setup on client

One method to control how DNS flows is using Unbound. Alternatively, if using the stub listener, refer to this instead. Some conflicting information, but main takeaways:

systemd-networkd.service
[Match]
Name=wg0
 
[Network]
DNS=10.253.253.253
Domains=~pyuxiang.com

ACME client: Dehydrated

Why use dehydrated instead of certbot?

First steps

A quick rundown of the ACME process helps with context: ACME (Automated Certificate Management Environment) is fundamentally a protocol between clients and CA that automates certificates issuance, which used to require human intervention. Developed as part of Let's Encrypt (an ACME-supported CA), and formalised in RFC8555.

The ACMEv2 protocol is as follows:

  1. Client registers identity with server (optional depending on server, but usually provides useful things like expiry email reminders)
  2. Client generates CSR and sends an ACME HTTP request (one of HTTP-01, DNS-01, TLS-01).
  3. Server issues a string (challenge token) which it expects to read by accessing a URI or DNS record.
  4. Client interacts with the webserver to expose a URL path with file (.well-known), or modifies DNS TXT records (.acme-challenge). Then tells server it is ready.
  5. Server verifies the challenge tokens, and signs the CSR.
  6. Client uploads the certificate.

The server in question is a CA (e.g. Let's Encrypt, ZeroSSL, or self-hosted ACME-supported step-ca), while the ACME client is a script that ideally automates the steps above. Since the protocol is language-agnostic, many ACME clients exist: a few popular ones that commonly pop up include lego (Go), certbot (Python), acme.sh (Bash) and dehydrated (Bash). Just pick a favorite and go.

dehydrated is a single Bash script (~3k lines) that accepts three pieces of configuration located in /etc/dehydrated:

  1. domains.txt: A list of domain/aliases to generate certificates for.
  2. config: Configuration for dehydrated.
  3. hook.sh: Hook configuration.

Certficate generation/renewal is simply dehydrated -c.

Hooks

The ACME client can automate certificate/key management and communicate with the ACME server for challenge tokens. Additional functionality of (1) modifying DNS records, (2) uploading certificates to webserver, needs to be patched in using hooks/plugins. In the case of dehydrated, the hooks are implemented as an external client Bash script, which receives a command verb and relevant arguments. The documentation provides a limited list of commands and an hook boilerplate example.

An example set of commands to write for, using ACMEv2 DNS-01:

If using joohoi's acme-dns service, there is no need to start a DNS server, nor clean challenges. If running webserver on the same machine, one can also reference the certificate symlink directly, at /etc/dehydrated/certs/$DOMAIN/fullchain.pem, so the script effectively reduces to just "deploy_challenge":

/etc/dehydrated/hook.sh
#!/usr/bin/env bash
 
# Register with:
# curl -s -X POST https://auth.acme-dns.io/register
# then create CNAME DNS record: '.acme-challenge.$DOMAIN CNAME $DNS_SUBDOMAIN.acme-dns.io'
DNS_ENDPOINT="https://auth.acme-dns.io/update"
DNS_SUBDOMAIN="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
DNS_USER="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
DNS_PASS="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
HANDLER="${1}"
 
if [ "$HANDLER" = "deploy_challenge" ]; then
  TOKEN="${4}"
  curl -X POST $DNS_ENDPOINT -H "X-Api-User: $DNS_USER" -H "X-Api-Key: $DNS_PASS" \
    --data "{\"subdomain\": \"$DNS_SUBDOMAIN\", \"txt\": \"$TOKEN\"}" >/dev/null
 
elif [ "$HANDLER" = "deploy_cert" ]; then
  KEYFILE="${3}" CERTFILE="${4}" FULLCHAINFILE="${5}"
  scp {$KEYFILE,$CERTFILE,$FULLCHAINFILE} webserver:/var/www/certs/
fi

Configuration

Most of the defaults generally work fine. The full config is available, though I only needed to populate these variables:

/etc/dehydrated/config
# dehydrated configuration
BASEDIR=/etc/dehydrated
HOOK=/etc/dehydrated/hook.sh
DEHYDRATED_USER=xxxxxx
DEHYDRATED_GROUP=xxxxxx
 
# ACME server configuration
# Use "letsencrypt-test" staging server to avoid rate limits during testing
CA="letsencrypt"
CHALLENGETYPE="dns-01"
CONTACT_EMAIL=xxxxxx

For a single wildcard certificate, the domains file is straightfoward: sign for the base domain, with the wildcard as a SAN. The exact file I use is attached below:

/etc/dehydrated/domains.txt
pyuxiang.com *.pyuxiang.com

First register with ./dehydrated --register --accept-terms, then put ./dehydrated -c into a cron job and be done!

ACME Server: acme-dns

acme-dns is a simple DNS server that serves TXT records for the ACME challenges, and also supports basic authentication for updating said records over HTTP.

Usage

First sign up for the service:

user:~$ curl -sX POST https://auth.acme-dns.io/register
{
    "subdomain": "9d2c7b32-4af4-482c-9e46-718acf50539e",
    "username": "3d97e467-dd67-41d4-871f-5590b3d03c05",
    "password": "cLzdpV031ieuZzAE7jVNnX08uMqV0OsyIbf6Cqfm",
    ...
}

Push ACME challenge tokens:

user:~$ cat header.txt
X-Api-User: 3d97e467-dd67-41d4-871f-5590b3d03c05
X-Api-Key: cLzdpV031ieuZzAE7jVNnX08uMqV0OsyIbf6Cqfm
 
user:~$ cat body.txt
{
    "subdomain": "9d2c7b32-4af4-482c-9e46-718acf50539e",
    "txt": "w3LreTPDfo3GbaoRmoneFgKbmpGdweWbrlpg04-1xY0"
}
 
user:~$ curl -sX POST -H @header.txt --data @body.txt https://auth.acme-dns.io/update
{
    "txt": "w3LreTPDfo3GbaoRmoneFgKbmpGdweWbrlpg04-1xY0"
}

Verify challenge tokens are updated. Note that up to two TXT challenges can be cached in the "auth.acme-dns.io" service (typically needed for simultaneous root domain and wildcard domain validation).

user:~$ dig _acme-challenge.pyuxiang.com TXT
...
;; QUESTION SECTION:
;_acme-challenge.pyuxiang.com.  IN      TXT
 
;; ANSWER SECTION:
_acme-challenge.pyuxiang.com. 3430 IN   CNAME   9d2c7b32-4af4-482c-9e46-718acf50539e.auth.acme-dns.io.
9d2c7b32-4af4-482c-9e46-718acf50539e.auth.acme-dns.io. 1 IN TXT "w3LreTPDfo3GbaoRmoneFgKbmpGdweWbrlpg04-1xY0"
...