#!/usr/bin/env bash # setup-bind.sh — initialize BIND for the DNS Web Manager # # Detects BIND layout (Debian/Ubuntu vs RHEL/CentOS/Rocky) by checking which # files exist, then ensures: # - bind package installed # - zone directory exists with correct ownership # - named.conf.local is included from named.conf # - /etc/named.conf.local (or /etc/bind/named.conf.local) exists & writable # # Usage: # sudo bash scripts/setup-bind.sh # actually run all steps # sudo bash scripts/setup-bind.sh --dry-run # print what would run, don't execute # # Idempotent: safe to run multiple times. Existing configs are preserved; # named.conf.local is only created if missing. set -euo pipefail DRY_RUN=0 if [[ "${1:-}" == "--dry-run" ]]; then DRY_RUN=1 fi # ─── helpers ──────────────────────────────────────────────────────────────── log() { printf '\033[1;34m[setup-bind]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[setup-bind]\033[0m %s\n' "$*" >&2; } err() { printf '\033[1;31m[setup-bind]\033[0m %s\n' "$*" >&2; } run() { if [[ $DRY_RUN -eq 1 ]]; then printf ' [DRY-RUN] %s\n' "$*" else "$@" fi } must_be_root() { if [[ $EUID -ne 0 ]]; then err "Must run as root (use sudo)" exit 1 fi } # ─── detect BIND layout ───────────────────────────────────────────────────── # Logic mirrors app.py's auto-detection: prefer RHEL paths when both exist. detect_layout() { if [[ -d /etc/bind ]]; then # Debian/Ubuntu bind9 — /etc/bind exists BIND_USER="bind" BIND_GROUP="bind" ZONES_DIR="/etc/bind/zones" NAMED_CONF="/etc/bind/named.conf" NAMED_CONF_LOCAL="/etc/bind/named.conf.local" PKG_MANAGER="apt-get" PKG_INSTALL=(apt-get install -y bind9 bind9utils dnsutils) SERVICE_NAME="named" elif [[ -f /etc/named.conf ]]; then # RHEL/CentOS/Rocky bind — /etc/named.conf exists, /etc/bind usually doesn't BIND_USER="named" BIND_GROUP="named" ZONES_DIR="/var/named" NAMED_CONF="/etc/named.conf" NAMED_CONF_LOCAL="/etc/named.conf.local" PKG_MANAGER="yum" PKG_INSTALL=(yum install -y bind bind-utils) SERVICE_NAME="named" else # No BIND config found at all — assume Debian (most common for fresh installs) # and let the package install create the structure. BIND_USER="bind" BIND_GROUP="bind" ZONES_DIR="/etc/bind/zones" NAMED_CONF="/etc/bind/named.conf" NAMED_CONF_LOCAL="/etc/bind/named.conf.local" PKG_MANAGER="apt-get" PKG_INSTALL=(apt-get install -y bind9 bind9utils dnsutils) SERVICE_NAME="named" warn "No BIND config detected; defaulting to Debian/Ubuntu layout" fi log "Detected layout:" log " bind user/group : $BIND_USER:$BIND_GROUP" log " zones dir : $ZONES_DIR" log " named.conf : $NAMED_CONF" log " named.conf.local: $NAMED_CONF_LOCAL" log " pkg manager : $PKG_MANAGER" } # ─── steps ────────────────────────────────────────────────────────────────── install_bind() { log "Step 1: ensure BIND package is installed" if command -v named >/dev/null 2>&1; then log " named already installed: $(named -v 2>&1 | head -1)" return fi warn " named not found; installing via $PKG_MANAGER" run "${PKG_INSTALL[@]}" } ensure_zones_dir() { log "Step 2: ensure zones directory exists with correct ownership" if [[ ! -d "$ZONES_DIR" ]]; then log " $ZONES_DIR does not exist; creating" run mkdir -p "$ZONES_DIR" else log " $ZONES_DIR already exists" fi # group-writable + setgid so future files inherit the group run chown "$BIND_USER:$BIND_GROUP" "$ZONES_DIR" run chmod 2770 "$ZONES_DIR" } ensure_named_conf_local() { log "Step 3: ensure named.conf.local exists and is writable by web" if [[ ! -f "$NAMED_CONF_LOCAL" ]]; then log " $NAMED_CONF_LOCAL does not exist; creating empty file" run touch "$NAMED_CONF_LOCAL" run chown "root:$BIND_GROUP" "$NAMED_CONF_LOCAL" run chmod 640 "$NAMED_CONF_LOCAL" else log " $NAMED_CONF_LOCAL already exists; preserving" # Don't change ownership if file already has content — it may be managed # by someone else. Just make sure web process can write to it. run chmod 644 "$NAMED_CONF_LOCAL" fi } ensure_include_in_named_conf() { log "Step 4: ensure named.conf includes named.conf.local" if [[ ! -f "$NAMED_CONF" ]]; then warn " $NAMED_CONF not found; skipping include check (bind not configured?)" return fi if grep -qF "$NAMED_CONF_LOCAL" "$NAMED_CONF"; then log " $NAMED_CONF already includes $NAMED_CONF_LOCAL" return fi warn " $NAMED_CONF does NOT include $NAMED_CONF_LOCAL — appending" if [[ $DRY_RUN -eq 1 ]]; then printf ' [DRY-RUN] echo "include \\"%s\\";" >> %s\n' "$NAMED_CONF_LOCAL" "$NAMED_CONF" else printf '\ninclude "%s";\n' "$NAMED_CONF_LOCAL" >> "$NAMED_CONF" fi } restart_named() { log "Step 5: restart named so include takes effect" if ! systemctl is-active --quiet "$SERVICE_NAME"; then warn " $SERVICE_NAME not running; skipping restart" return fi if [[ $DRY_RUN -eq 1 ]]; then printf ' [DRY-RUN] systemctl restart %s\n' "$SERVICE_NAME" else run systemctl restart "$SERVICE_NAME" sleep 1 if systemctl is-active --quiet "$SERVICE_NAME"; then log " $SERVICE_NAME is active" else err " $SERVICE_NAME failed to start; check 'journalctl -xe -u $SERVICE_NAME'" fi fi } # ─── main ─────────────────────────────────────────────────────────────────── main() { must_be_root detect_layout if [[ $DRY_RUN -eq 1 ]]; then log "DRY-RUN mode: no changes will be made" fi install_bind ensure_zones_dir ensure_named_conf_local ensure_include_in_named_conf restart_named log "Done. Verify with:" log " named-checkconf" log " rndc status | grep 'number of zones'" } main "$@"