automate/020_acme-dns_docker-compose...

246 lines
8.0 KiB
Bash
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
#
# ACME-DNS Docker Setup Script (Production Hardened)
# Generates validated, secure configuration for joohoi/acme-dns
# https://github.com/joohoi/acme-dns
#
set -euo pipefail
IFS=$'\n\t'
# ======================
# CONFIGURATION (EDIT THESE)
# ======================
SETUP_DIR="/var/tmp/acme-dns-setup"
MAIN_DOMAIN="example.com" # Your registered domain (e.g., yoursite.com)
ACME_DNS_SUBDOMAIN="auth" # Creates auth.yoursite.com
ACME_DNS_FQDN="${ACME_DNS_SUBDOMAIN}.${MAIN_DOMAIN}"
PUBLIC_IP="1.2.3.4" # Public server IP (MUST be reachable on UDP/TCP 53)
ADMIN_EMAIL="admin@${MAIN_DOMAIN}" # SOA contact (converted to DNS format)
LE_NOTIFICATION_EMAIL="admin@${MAIN_DOMAIN}" # Let's Encrypt expiry notices
# API Configuration
API_PORT="443" # 443 required for letsencrypt modes
API_TLS_MODE="letsencryptstaging" # none | letsencrypt | letsencryptstaging | cert
CUSTOM_TLS_CERT_PRIVKEY="/etc/acme-dns/certs/privkey.pem"
CUSTOM_TLS_CERT_FULLCHAIN="/etc/acme-dns/certs/fullchain.pem"
# Operational
DNS_PORT="53" # MUST be 53 for public DNS delegation
LOG_LEVEL="info" # error | warning | info | debug
# ======================
# VALIDATION & SAFETY
# ======================
validate_domain() {
local domain="$1"
# Basic domain validation (RFC 1035 compliant pattern)
if [[ ! "$domain" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]{2,})+$ ]]; then
echo "❌ ERROR: Invalid domain format: '${domain}'" >&2
echo " Must be a valid domain (e.g., example.com)" >&2
exit 1
fi
}
if ! command -v docker &>/dev/null; then
echo "❌ ERROR: Docker not found. Install Docker first." >&2
exit 1
fi
# Critical placeholder validation
if [[ "${MAIN_DOMAIN}" == "example.com" || "${PUBLIC_IP}" == "1.2.3.4" ]]; then
echo -e "\n⚠ \033[1;31mCRITICAL WARNING\033[0m: Using default placeholder values!" >&2
echo " Edit CONFIGURATION section BEFORE proceeding." >&2
read -p "Continue anyway? (y/N): " -r || exit 1
[[ ! "$REPLY" =~ ^[Yy]$ ]] && echo "Aborted." && exit 1
fi
# Validate domains
validate_domain "${MAIN_DOMAIN}"
validate_domain "${ACME_DNS_FQDN}"
# Port conflict detection (precise regex to avoid false positives like port 530)
for port in "${DNS_PORT}" "${API_PORT}"; do
if ss -tuln 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 1} END {exit 0}'; then
echo "⚠️ WARNING: Host port ${port} is in use!" >&2
echo " acme-dns requires exclusive access to these ports." >&2
read -p "Continue? (y/N): " -r || exit 1
[[ ! "$REPLY" =~ ^[Yy]$ ]] && exit 1
fi
done
# ======================
# CONFIG GENERATION (TOML-SAFE)
# ======================
generate_config() {
local nsadmin="${ADMIN_EMAIL//@/.}" # SOA format: admin@domain.com → admin.domain.com
cat <<EOF
[general]
listen = "0.0.0.0:${DNS_PORT}"
protocol = "both"
domain = "${ACME_DNS_FQDN}"
nsname = "${ACME_DNS_FQDN}"
nsadmin = "${nsadmin}"
records = [
"${ACME_DNS_FQDN}. A ${PUBLIC_IP}",
"${ACME_DNS_FQDN}. NS ${ACME_DNS_FQDN}."
]
debug = false
[database]
engine = "sqlite3"
connection = "/var/lib/acme-dns/acme-dns.db"
[api]
ip = "0.0.0.0"
port = "${API_PORT}"
disable_registration = false
tls = "${API_TLS_MODE}"
EOF
# TLS-specific configurations (TOML-safe injection)
case "${API_TLS_MODE}" in
letsencrypt | letsencryptstaging)
cat <<EOF
notification_email = "${LE_NOTIFICATION_EMAIL}"
acme_cache_dir = "api-certs"
EOF
;;
cert)
cat <<EOF
tls_cert_privkey = "${CUSTOM_TLS_CERT_PRIVKEY}"
tls_cert_fullchain = "${CUSTOM_TLS_CERT_FULLCHAIN}"
EOF
;;
esac
# SECURITY: Hardcoded CORS policy (avoids TOML syntax errors from variable interpolation)
cat <<EOF
corsorigins = [
"https://*.${MAIN_DOMAIN}",
"https://${MAIN_DOMAIN}"
]
use_header = false
header_name = "X-Forwarded-For"
[logconfig]
loglevel = "${LOG_LEVEL}"
logtype = "stdout"
logformat = "text"
EOF
}
generate_docker_compose() {
cat <<EOF
version: '3.8'
services:
acme-dns:
image: joohoi/acme-dns:latest
container_name: acme-dns
restart: unless-stopped
ports:
- "${DNS_PORT}:${DNS_PORT}/udp"
- "${DNS_PORT}:${DNS_PORT}/tcp"
- "${API_PORT}:${API_PORT}/tcp"
volumes:
- ./config:/etc/acme-dns:ro
- ./data:/var/lib/acme-dns
# SECURITY HARDENING (critical for public DNS service)
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Required for ports <1024
security_opt:
- no-new-privileges:true
# Prevent privilege escalation
read_only: true
tmpfs:
- /tmp
EOF
}
# ======================
# EXECUTION
# ======================
echo -e "\n🔧 \033[1mACME-DNS Setup Generator (Production Hardened)\033[0m"
echo " Setup Directory : ${SETUP_DIR}"
echo " ACME-DNS Domain : ${ACME_DNS_FQDN}"
echo " Public IP : ${PUBLIC_IP}"
echo " Admin Contact : ${ADMIN_EMAIL} (SOA record)"
echo " API Mode : ${API_TLS_MODE} @ port ${API_PORT}"
echo -e " \033[33m⚠ CORS restricted to: *.${MAIN_DOMAIN}, ${MAIN_DOMAIN}\033[0m"
echo
# Final confirmation with path safety check
if [[ "${SETUP_DIR}" =~ ^/(bin|etc|usr|var/lib|root) ]]; then
echo "❌ ERROR: Unsafe SETUP_DIR path detected. Aborting." >&2
exit 1
fi
read -p "⚠️ This will CREATE/OVERWRITE ${SETUP_DIR}. Confirm? (y/N): " -r || exit 1
[[ ! "$REPLY" =~ ^[Yy]$ ]] && echo "Aborted by user." && exit 0
# Atomic directory setup
rm -rf "${SETUP_DIR}" 2>/dev/null || true
mkdir -p "${SETUP_DIR}"/{config,data}
docker pull joohoi/acme-dns:latest >/dev/null 2>&1
generate_config >"${SETUP_DIR}/config/config.cfg"
generate_docker_compose >"${SETUP_DIR}/docker-compose.yml"
# ======================
# ACTIONABLE DEPLOYMENT GUIDE
# ======================
cat <<EOF
✅ Configuration generated in ${SETUP_DIR}
❗❗ CRITICAL DEPLOYMENT STEPS (NON-NEGOTIABLE) ❗❗
1. 🌐 DNS CONFIGURATION (at registrar for ${MAIN_DOMAIN}):
| Type | Host | Value | TTL |
|------|---------------|------------------------------------|------|
| A | ${ACME_DNS_FQDN} | ${PUBLIC_IP} | Auto |
| NS | ${ACME_DNS_FQDN} | ${ACME_DNS_FQDN}. | Auto |
🔑 MUST-HAVES:
- NS record VALUE MUST END WITH TRAILING DOT (.)
- Verify propagation: dig +short NS ${ACME_DNS_FQDN}
- Wait for global propagation (use https://dnschecker.org)
2. 🔒 PRE-DEPLOY SECURITY CHECKLIST:
[ ] Edit config/config.cfg:
• After initial client registrations: set disable_registration=true
• For production: Change API_TLS_MODE to "letsencrypt" (staging avoids rate limits)
• Review CORS origins (currently restricted to your domain)
[ ] If using "cert" mode: Place certs in host's ./config/certs/ and verify paths
[ ] Backup strategy: ./data/acme-dns.db contains critical registration tokens
[ ] Firewall: Allow UDP/TCP 53 + TCP ${API_PORT} (sudo ufw allow 53; sudo ufw allow ${API_PORT}/tcp)
3. 🚀 DEPLOY:
cd "${SETUP_DIR}"
docker compose up -d # Modern Docker (v20.10+)
# Legacy systems: docker-compose up -d
4. ✅ VALIDATE:
docker logs -f acme-dns # Confirm "Serving DNS on :53" and "API listening on :${API_PORT}"
# For staging mode (self-signed):
curl -k https://localhost:${API_PORT}/health
# For production mode (valid cert):
curl https://localhost:${API_PORT}/health
💡 TROUBLESHOOTING:
- Port 53 conflict? → sudo systemctl stop systemd-resolved; sudo systemctl disable systemd-resolved
- "permission denied" on ports? → Ensure NET_BIND_SERVICE capability is present (in docker-compose.yml)
- DNS queries failing? → Check firewall allows UDP 53; verify NS record has trailing dot
- API returns 403? → Check CORS origins match your client's origin exactly
📌 PRODUCTION ESSENTIALS:
• Use "letsencryptstaging" for ALL testing (avoids LE rate limits)
• NEVER expose disable_registration=false permanently
• Monitor ./data/acme-dns.db backups (contains token secrets)
• This service is part of your PKI infrastructure - treat with highest security priority
• Full docs: https://github.com/joohoi/acme-dns/blob/master/README.md
EOF