Traefik with Mail-in-a-box as the dns-01 Provider
Published: 2024-01-03
Problem
I need to replace Cloudflare as my DNS provider. I run Mail-in-a-Box which can serve DNS and has a good API but Traefik can't directly use it.
Background
Let's Encrypt
Let's encrypt provides free TLS certificates by giving you a token and asking you to post it somewhere authoritative that they can then check to verify your identity.
The two main challenge types are http-01
(you essentially make the token available at http://<DOMAIN>/.well-known/acme-challenge/<TOKEN>
) and dns-01
(create a TXT record _acme-challenge.<DOMAIN>
with <TOKEN>
as the value). The http-01
method is preferred since it doesn't require additional administrative access but it's not feasible for sites that are internal-only.
Traefik
Traefik uses the Lego ACME library for interacting with Let's Encrypt. While Lego doesn't have a dedicated MiaB provider it does have one named exec
that can run an arbitrary executable. We can use this to call to any DNS API that's not already supported.
Mail-in-a-Box
Mail-in-a-Box is a great option for self-hosting email, calendar, and contacts functionality. It prefers to be configured as the authoritative DNS for each domain it manages since it can seamlessly handle all the DNS entries needed to run smoothly. Creating and deleting DNS entries using the API using curl
is easy:
curl -X POST \
-d "${value}" \
--user "${MIAB_Username}:${MIAB_Password}" \
"https://${MIAB_Server}/admin/dns/custom/${fqdn}/txt"
Solution
Make a shim script that Traefik/Lego can call with the exec
provider and that then makes the appropriate API call to Mail-in-a-Box. Here's what I came up with. It uses the same environment variables as acme.sh (MIAB_Username
, MIAB_Password
, and MIAB_Server
).
/usr/local/bin/miab-dns.sh
#!/bin/bash
set -eu -o pipefail
# See https://go-acme.github.io/lego/dns/exec/ for details about this wrapper
# Usage: Set MIAB_Username, MIAB_Password, and MIAB_Server environment variables
action="${1}"; shift
# Remove trailing period since traefik always sends it but the MIAB API only
# accepts it for CNAMEs
# shellcheck disable=SC2001
fqdn="$(echo "${1}" | sed 's/\.$//')"; shift
value="${1}"; shift
case "${action}" in
present)
# shellcheck disable=SC2154
curl -X POST \
-d "${value}" \
--user "${MIAB_Username}:${MIAB_Password}" \
"https://${MIAB_Server}/admin/dns/custom/${fqdn}/txt"
;;
cleanup)
# shellcheck disable=SC2154
curl -X DELETE \
-d "${value}" \
--user "${MIAB_Username}:${MIAB_Password}" \
"https://${MIAB_Server}/admin/dns/custom/${fqdn}/txt"
;;
esac
systemd Example
There are a hundred ways to configure and run Traefik but I'll share a complete systemd solution as one example.
/etc/systemd/system/traefik.service
[Unit]
Description=Traefik Proxy
Documentation=https://doc.traefik.io/traefik/
After=network-online.target
[Service]
User=traefik
Group=traefik
EnvironmentFile=/root/.traefik.env
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
Environment="EXEC_PATH=/usr/local/bin/miab-dns.sh"
ExecStart=/usr/local/bin/traefik \
--log.level=INFO \
--entrypoints.web.address=:80 \
--entrypoints.web.http.redirections.entrypoint.to=websecure \
--entrypoints.websecure.address=:443 \
--entrypoints.websecure.http.tls.certresolver=miab \
--certificatesresolvers.miab.acme.dnschallenge=true \
--certificatesresolvers.miab.acme.dnschallenge.provider=exec \
--certificatesresolvers.miab.acme.email=mark@mrkc.net \
--certificatesresolvers.miab.acme.storage=%S/traefik/acme.json
ConfigurationDirectory=traefik.d
ProtectSystem=true
ProtectHome=true
PrivateMounts=true
StateDirectory=traefik
StateDirectoryMode=0750
PrivateTmp=true
PrivateDevices=true
[Install]
WantedBy=multi-user.target
/root/.traefik.env
Make sure this file is secure (e.g. chmod 0640
)!
MIAB_Username=REDACTED
MIAB_Password=REDACTED
MIAB_Server=REDACTED