246 lines
8.0 KiB
Bash
246 lines
8.0 KiB
Bash
#!/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
|