From bf7dca0ced023c52aa8da7a4647e1d0c32c23af5 Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 27 Oct 2026 12:52:44 +0100 Subject: [PATCH 1/4] Add script 'switch-resolver-to-bind9.sh'. --- README-bind9-direct-resolver.md | 163 +++++++++++++++++++++++++ switch-resolver-to-bind9.sh | 203 ++++++++++++++++++++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 README-bind9-direct-resolver.md create mode 100755 switch-resolver-to-bind9.sh diff --git a/README-bind9-direct-resolver.md b/README-bind9-direct-resolver.md new file mode 100644 index 0000000..20023c9 --- /dev/null +++ b/README-bind9-direct-resolver.md @@ -0,0 +1,163 @@ +# Debian 13: Systemresolver von systemd-resolved auf lokalen BIND9 umstellen + +**Stand:** 27. Oktober 2026 +**Beispielsystem:** `lists.mx` (produktiver Mail-/Listenserver, Debian 13) +**Skript:** `switch-resolver-to-bind9.sh` + +## 1. Ziel und Hintergrund + +Auf dem Host läuft bereits ein rekursiver BIND9 auf `127.0.0.1:53` und `[::1]:53`, einschließlich eigener DNSSEC-Validierung (`dnssec-validation auto;`). Bislang nutzten Programme `/etc/resolv.conf -> /run/systemd/resolve/stub-resolv.conf` und damit `127.0.0.53` (`systemd-resolved`), der wiederum an BIND9 weiterleitete. + +Beobachtetes Problem: Die IPv6-PTR-Abfrage für `2607:f8b0:4864:20::72e` ergab über `127.0.0.53` `SERVFAIL` mit `DNSSEC validation failed: failed-auxiliary`, über `127.0.0.1` jedoch korrekt `mail-qk1-x72e.google.com.`. Mit `DNSSEC=no` in `systemd-resolved` funktionierte die PTR-Auflösung, aber Anfragen für die absichtlich DNSSEC-fehlerhafte Zone `www.dnssec-failed.org` liefen über `127.0.0.53` weiterhin in einen Timeout. Ein Loopback-Mitschnitt zeigte vielfach wiederholte Abfragen trotz `SERVFAIL`-Antworten von BIND9. Der DNS-Proxy auf `127.0.0.54` übergab dagegen `SERVFAIL` korrekt. + +**Ziel:** BIND9 direkt über die glibc-/NSS-DNS-Auflösung nutzen; `systemd-resolved` nach erfolgreichem Test stoppen und deaktivieren. **Keine Änderung** an Maildiensten, BIND-Konfiguration, Interfaces, Firewall oder Routing. + +## 2. Zielkonfiguration + +`/etc/nsswitch.conf`: + +```text +hosts: files myhostname dns +``` + +`/etc/resolv.conf` (reguläre Datei statt Symlink): + +```text +# Local BIND9 recursive resolver; DNSSEC validated by BIND9. +nameserver 127.0.0.1 +options edns0 trust-ad +search warenform.de +``` + +`search warenform.de` ist **systemspezifisch**. Das Skript übernimmt eine bereits vorhandene `search`-Zeile aus der bestehenden Konfiguration; es erfindet keine Suchdomäne. Bei `lists.mx` ist sie bereits gesetzt. `trust-ad` bedeutet **nicht**, dass glibc selbst DNSSEC prüft. Das validierende Glied ist BIND9. Programme mit eigenen Resolver-Bibliotheken können eine abweichende DNS-Konfiguration haben; dies ggf. anwendungsspezifisch prüfen. + +Die lokale DNS-Auflösung setzt die Verfügbarkeit von BIND9 voraus. Mit nur `127.0.0.1` gibt es bewusst **keinen externen Fallback**, der lokale DNSSEC-Validierung umgehen könnte. + +## 3. Voraussetzungen + +- Debian 13, Root-Shell; BIND9 ist installiert, gestartet und lauscht auf IPv4-Loopback `127.0.0.1:53`. +- `dig`, `getent`, `systemctl`, die üblichen Coreutils sowie Bash sind verfügbar. +- BIND9 kann rekursiv auflösen und validiert DNSSEC unabhängig von `systemd-resolved`. +- Änderungen werden in einem Wartungsfenster bzw. mit vorhandener administrativer Konsole vorgenommen. Keine Netzwerkdienste oder Interfaces neu starten. +- Vorhandene dynamische DNS-Konfiguration (DHCP, VPN, Split-DNS, interne Domains) prüfen; das Skript bildet **keine** `systemd-resolved`-Link-Konfiguration nach. +- Eventuelle lokale Abhängigkeiten von `systemd-resolved`, `resolvectl`, LLMNR/mDNS und DNS-Stub-Adressen `127.0.0.53`/`127.0.0.54` vor dem Abschalten ausschließen. + +**Achtung:** Auf `lists.mx` wurde `networking.service` zuvor als `failed` gemeldet. Das ist ein **separates** Thema und wird von diesem Skript ausdrücklich nicht behandelt. Insbesondere kein `systemctl restart networking` ausführen. + +## 4. DNSSEC-Vorabprüfung + +```bash +dig @127.0.0.1 cloudflare.com A +dnssec +adflag +dig @127.0.0.1 www.dnssec-failed.org A +time=5 +tries=1 +dig @127.0.0.1 -x 2607:f8b0:4864:20::72e +``` + +Erwartung: `cloudflare.com` → `NOERROR` und Flag `ad`, `www.dnssec-failed.org` → `SERVFAIL` (nicht Timeout), PTR → `mail-qk1-x72e.google.com.` (Stand des beobachteten Beispiels; DNS-Daten können sich ändern). Das Skript führt einen Positiv- und Negativ-DNSSEC-Test zusätzlich selbst aus. Die Testdomain ist absichtlich fehlerhaft signiert. + +## 5. Skriptinstallation und Verwendung + +Datei an einen geeigneten Ort kopieren, beispielsweise: + +```bash +install -o root -g root -m 0755 switch-resolver-to-bind9.sh \ + /usr/local/sbin/switch-resolver-to-bind9.sh + +bash -n /usr/local/sbin/switch-resolver-to-bind9.sh + +# Nur lesen und prüfen (Standard): +/usr/local/sbin/switch-resolver-to-bind9.sh --check + +# Umstellung mit interaktiver Bestätigung (exakt 'yes'): +/usr/local/sbin/switch-resolver-to-bind9.sh --apply + +# Rückbau des durch dieses Skript vorgenommenen Zustands: +/usr/local/sbin/switch-resolver-to-bind9.sh --rollback +``` + +Statusausgaben verwenden die bekannten Muster `[ Info ]`, `[ ok ]`, `[ failed ]`, `[ skipped ]`. Keine automatische Paketinstallation und kein Reboot. Die Änderung erfolgt **nicht** schon beim Aufruf ohne Parameter. + +### Was `--apply` tut + +1. Prüft Root-Rechte, benötigte Kommandos und BIND9; DNSSEC-Positiv- und Negativtest. +2. Zeigt den geplanten Eingriff und verlangt `Einverstanden [yes/no, default: no]?`. +3. Legt eine Sicherung von **Datei bzw. Symlink** `/etc/resolv.conf`, `/etc/nsswitch.conf` und dem bisherigen `systemd-resolved`-Dienststatus unter `/var/backups/oopen-bind9-direct-resolver/run-.../` an (Verzeichnis nur für Root). +4. Ändert, soweit erforderlich, die `hosts:`-NSS-Zeile und erstellt `/etc/resolv.conf` mit `nameserver 127.0.0.1`. Eine vorhandene `search`-Zeile wird übernommen. +5. Prüft normale Vorwärts- und Reverse-Auflösung. +6. Stoppt und deaktiviert `systemd-resolved` und prüft BIND9/DNSSEC erneut. +7. Bei einem Fehler versucht das Skript automatisch einen Rückbau der gesicherten Dateien und des gespeicherten Dienstzustands. Ein automatischer Rückbau ist eine Schutzmaßnahme, **keine Garantie** bei Stromausfall oder hartem Prozessabbruch. + +Bereits vollständig umgestellte Systeme werden ohne Änderungen mit `[ skipped ]` erkannt. Existiert noch ein offener, vom Skript angelegter Rückbauzustand, bricht ein weiterer `--apply`-Lauf ab. So wird verhindert, dass die ursprüngliche Sicherung unbemerkt überschrieben wird. + +### Besonderheit bei `lists.mx` (bereits manuell umgestellt) + +Auf `lists.mx` wurden `/etc/nsswitch.conf` und `/etc/resolv.conf` am 27.10.2026 bereits manuell auf direkten BIND9 umgestellt. Das Skript erkennt dies und kann bei `--apply` den Dienst `systemd-resolved` noch stoppen und deaktivieren, sofern dies noch nicht erfolgt ist. **Der Skript-Rollback stellt dann den Zustand unmittelbar vor diesem Skriptlauf wieder her, nicht automatisch den historischen systemd-resolved-Zustand vom Morgen.** + +Die zuvor manuell erstellte Sicherung liegt unter: + +```text +/root/dns-resolver-backup-20261027-120829 +``` + +Dort ist `resolv.conf` ein relativer Symlink. Er muss bei manueller Wiederherstellung an seinem **ursprünglichen Ort** `/etc/resolv.conf` wieder angelegt werden. Der historische Zustand lässt sich mit gesichertem `nsswitch.conf` und dem ursprünglichen Symlink wiederherstellen; dafür `systemd-resolved` zuerst wieder aktivieren. + +## 6. Abschließende Prüfungen + +```bash +cat /etc/resolv.conf +grep '^hosts:' /etc/nsswitch.conf +systemctl is-active bind9 +systemctl is-active systemd-resolved +systemctl is-enabled systemd-resolved +getent ahostsv4 oopen.de +getent hosts 2607:f8b0:4864:20::72e +dig cloudflare.com A +dnssec +adflag +dig www.dnssec-failed.org A +time=5 +tries=1 +``` + +Erwartung: BIND9 `active`, `systemd-resolved` `inactive` und `disabled`, erfolgreiche normale Auflösung, DNSSEC-Positivtest `ad`, Negativtest `SERVFAIL`. Bei `dig` muss `SERVER: 127.0.0.1#53` erscheinen. Zusätzlich Mail-Logs und anwendungsspezifische Namensauflösung kontrollieren. Änderungen bestehen über einen Neustart fort, **sofern** kein anderer Netzwerkdienst `/etc/resolv.conf` ersetzt und BIND9 beim Boot zuverlässig startet; einen Neustart des Produktivservers führt das Skript nicht aus. + +## 7. Rollback + +Falls `--apply` dieses Skripts zuvor erfolgreich gelaufen ist: + +```bash +/usr/local/sbin/switch-resolver-to-bind9.sh --rollback +``` + +Das stellt die bei **diesem Skriptlauf** gesicherten Dateien und den Dienststatus nach Bestätigung wieder her. Die Sicherungsdateien bleiben archiviert. Falls die Dateien **vor dem ersten Skriptlauf** schon direkt BIND9 benutzten, bringt dieser Rollback folgerichtig nicht den ursprünglichen systemd-Stub zurück. + +**Manueller Rückbau auf `lists.mx` in den früheren Zustand** (nur bei gezieltem Bedarf und mit konsistenter DNS-Prüfung): + +```bash +# Ursprünglichen NSS-Zustand wiederherstellen: +cp -a /root/dns-resolver-backup-20261027-120829/nsswitch.conf /etc/nsswitch.conf + +# Dienst starten, Stub-Datei prüfen: +systemctl enable --now systemd-resolved +test -f /run/systemd/resolve/stub-resolv.conf + +# Ziel-Symlink atomar am ursprünglichen Ort wiederherstellen: +ln -s /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf.rollback-link +mv -Tf /etc/resolv.conf.rollback-link /etc/resolv.conf + +# Ergebnis prüfen: +getent ahostsv4 oopen.de +dig -x 2607:f8b0:4864:20::72e +``` + +Die frühere Einstellung `DNSSEC=no` in `/etc/systemd/resolved.conf.d/90-local-bind9.conf` bleibt bei diesem manuellen Rückbau erhalten, solange sie nicht separat geändert wird. DNSSEC bleibt dann bei BIND9 aktiv. + +## 8. Grenzen und Sicherheit + +- Das Skript setzt einen bereits gesunden lokalen BIND9 voraus und ist **kein** BIND9-Installationsskript. +- Kein automatisches Umschalten auf öffentliche Resolver; bei BIND-Ausfall würde lokale DNS-Auflösung ausfallen. +- Ein vereinfachter `hosts:`-Eintrag `files myhostname dns` ist auf diesem Server geprüft. Systeme mit weiteren NSS-Modulen wie `mymachines`, `mdns` oder LDAP benötigen eine angepasste Variante. +- `systemd-resolved` kann von speziellen Diensten per D-Bus oder `resolvectl` verwendet werden. Das muss vor dem Deaktivieren separat berücksichtigt werden. +- `systemd-resolved` wird nur gestoppt/deaktiviert, **nicht deinstalliert oder maskiert**. +- Bei fehlgeschlagenem Rollback nicht mehrfach blind ausführen: Ausgabe und gesichertes Backup-Verzeichnis überprüfen. + +## 9. Quellen + +- [systemd-resolved – Modes für /etc/resolv.conf](https://www.freedesktop.org/software/systemd/man/latest/systemd-resolved.service.html) +- [nss-resolve – NSS-Reihenfolge und systemd-resolved](https://www.freedesktop.org/software/systemd/man/latest/nss-resolve.html) +- [Debian: resolvconf-Kompatibilitätsmodus von resolvectl](https://manpages.debian.org/trixie/systemd-resolved/resolvconf.1.en.html) diff --git a/switch-resolver-to-bind9.sh b/switch-resolver-to-bind9.sh new file mode 100755 index 0000000..50f57e4 --- /dev/null +++ b/switch-resolver-to-bind9.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# Debian 13: switch system name resolution from systemd-resolved to local BIND9. +# No package installation, BIND configuration or networking changes. +set -Eeuo pipefail +umask 077 + +readonly STATE_DIR=/var/backups/oopen-bind9-direct-resolver +readonly CURRENT_FILE="$STATE_DIR/current" +readonly RESOLV=/etc/resolv.conf +readonly NSS=/etc/nsswitch.conf +readonly EXPECTED_NSS='hosts: files myhostname dns' + +MODE="${1:---check}" +case "$MODE" in --check|--apply|--rollback|--help) ;; *) echo "Usage: $0 [--check|--apply|--rollback|--help]" >&2; exit 2;; esac + +info() { printf '[ Info ] %s\n' "$*"; } +ok() { printf '[ ok ] %s\n' "$*"; } +skipped() { printf '[ skipped ] %s\n' "$*"; } +failed() { printf '[ failed ] %s\n' "$*" >&2; } +fatal() { failed "$*"; exit 1; } +echononl(){ printf '%-56s' "$*"; } +require_root(){ [[ "${EUID}" -eq 0 ]] || fatal 'Please run as root.'; } +need(){ command -v "$1" >/dev/null 2>&1 || fatal "Missing command: $1"; } +confirm(){ local answer; read -r -p 'Einverstanden [yes/no, default: no]? ' answer || true; [[ "$answer" == yes ]]; } + +# Only this script's state file controls rollback. Older manual backups are untouched. +# Each run's backup retains the exact file or symlink, along with active/enabled state. +state_dir_from_marker(){ + [[ -f "$CURRENT_FILE" ]] || return 1 + local p + IFS= read -r p < "$CURRENT_FILE" || true + [[ "$p" == "$STATE_DIR"/run-* && -d "$p" && -f "$p/metadata" ]] || fatal 'Invalid resolver rollback marker.' + printf '%s\n' "$p" +} + +is_direct(){ + [[ -f "$RESOLV" && ! -L "$RESOLV" ]] && + [[ "$(awk '$1=="nameserver" {print $2}' "$RESOLV")" == '127.0.0.1' ]] && + grep -Eq '^[[:space:]]*hosts:[[:space:]]*files[[:space:]]+myhostname[[:space:]]+dns[[:space:]]*$' "$NSS" +} + +is_fully_switched(){ + is_direct && ! systemctl is-active --quiet systemd-resolved && + [[ "$(systemctl is-enabled systemd-resolved 2>/dev/null || true)" == disabled ]] +} + +# BIND must reject a known bogus signature, not merely answer normal queries. +preflight(){ + echononl 'BIND9 on 127.0.0.1:53' + if dig @127.0.0.1 oopen.de A +time=3 +tries=1 +short | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then ok 'reachable'; else echo; fatal 'Local BIND9 does not resolve oopen.de'; fi + echononl 'DNSSEC positive test (AD flag)' + local reply + reply=$(dig @127.0.0.1 cloudflare.com A +dnssec +adflag +time=5 +tries=1) + if grep -q 'status: NOERROR' <<< "$reply" && grep -Eq 'flags:.*[[:space:]]ad[;[:space:]]' <<< "$reply"; then ok 'validated'; else echo; fatal 'BIND9 DNSSEC positive test failed'; fi + echononl 'DNSSEC negative test (SERVFAIL)' + reply=$(dig @127.0.0.1 www.dnssec-failed.org A +time=5 +tries=1) + if grep -q 'status: SERVFAIL' <<< "$reply"; then ok 'rejected as expected'; else echo; fatal 'BIND9 does not reject invalid DNSSEC test zone'; fi +} + +show_check(){ + require_root + need dig; need systemctl; need getent + printf '\n=== DNS resolver status ===\n' + printf 'resolv.conf: '; if [[ -L "$RESOLV" ]]; then readlink "$RESOLV"; else echo 'regular file'; fi + grep -E '^[[:space:]]*(nameserver|search|options)[[:space:]]' "$RESOLV" || true + grep -E '^hosts:' "$NSS" || true + printf 'systemd-resolved active: %s\n' "$(systemctl is-active systemd-resolved 2>/dev/null || true)" + printf 'systemd-resolved enabled: %s\n' "$(systemctl is-enabled systemd-resolved 2>/dev/null || true)" + if is_fully_switched; then ok 'Already directly using BIND9 (resolved disabled)'; + elif is_direct; then info 'BIND9 direct configuration is present; resolved still active/enabled'; + else info 'Not fully configured for direct BIND9'; fi + if [[ -f "$CURRENT_FILE" ]]; then info "Rollback state: $(state_dir_from_marker)"; fi + preflight +} + +restore_files(){ + local path="$1" + [[ -d "$path" && -e "$path/nsswitch.conf" ]] || fatal 'Incomplete backup: NSS' + [[ -e "$path/resolv.conf" || -L "$path/resolv.conf" ]] || fatal 'Incomplete backup: resolv.conf' + cp -a --remove-destination "$path/nsswitch.conf" "$NSS" + cp -a --remove-destination "$path/resolv.conf" "$RESOLV" +} + +restore_services(){ + local path="$1" enabled active + enabled=$(sed -n 's/^resolved_enabled=//p' "$path/metadata") + active=$(sed -n 's/^resolved_active=//p' "$path/metadata") + case "$enabled" in + enabled|enabled-runtime) systemctl enable systemd-resolved >/dev/null ;; + disabled) systemctl disable systemd-resolved >/dev/null ;; + static|indirect|masked|alias|'') info "Previous enabled state '$enabled'; no enable-state change" ;; + *) info "Unrecognized previous enabled state '$enabled'; no enable-state change" ;; + esac + case "$active" in + active) systemctl start systemd-resolved ;; + inactive|failed) systemctl stop systemd-resolved ;; + *) info "Previous active state '$active'; no service-state change" ;; + esac +} + +rollback_to(){ + local path="$1" + info "Restoring config from $path" + # Stop resolved first only when it was previously not active; otherwise start after files. + restore_files "$path" + restore_services "$path" + ok 'Configuration and resolved service state restored' +} + +apply(){ + require_root + for cmd in dig getent systemctl cp mv mktemp grep awk sed date; do need "$cmd"; done + [[ -f "$NSS" ]] || fatal "$NSS missing" + [[ -e "$RESOLV" || -L "$RESOLV" ]] || fatal "$RESOLV missing" + [[ ! -f "$CURRENT_FILE" ]] || { + if is_fully_switched; then skipped 'Already installed; no changes'; exit 0; fi + fatal 'An earlier script run has rollback state. Use --check or --rollback first.'; + } + if is_fully_switched; then skipped 'Already directly using BIND9; no changes'; exit 0; fi + info 'Preflight checks (no changes)' + preflight + printf '\nPlanned changes:\n hosts: files myhostname dns\n resolv.conf: nameserver 127.0.0.1\n stop and disable systemd-resolved\n' + info 'Network interfaces, routes, BIND9 and MTA are not changed.' + confirm || { skipped 'Abbruch durch User'; exit 0; } + + mkdir -p "$STATE_DIR" + chmod 700 "$STATE_DIR" + local path timestamp + timestamp=$(date +%Y%m%d-%H%M%S) + path=$(mktemp -d "$STATE_DIR/run-${timestamp}-XXXXXX") + cp -a "$RESOLV" "$path/resolv.conf" + cp -a "$NSS" "$path/nsswitch.conf" + { + printf 'resolved_active=%s\n' "$(systemctl is-active systemd-resolved 2>/dev/null || true)" + printf 'resolved_enabled=%s\n' "$(systemctl is-enabled systemd-resolved 2>/dev/null || true)" + } > "$path/metadata" + printf '%s\n' "$path" > "$CURRENT_FILE" + info "Backup: $path" + + # Roll back this transaction automatically if a command or signal fails. + trap 'rc=$?; trap - ERR INT TERM; failed "Apply failed (exit $rc). Rolling back"; rollback_to "$path" || failed "Automatic rollback failed; backup: $path"; exit "$rc"' ERR + trap 'exit 130' INT + trap 'exit 143' TERM + + if ! grep -Eq '^[[:space:]]*hosts:[[:space:]]*files[[:space:]]+myhostname[[:space:]]+dns[[:space:]]*$' "$NSS"; then + sed -i -E '/^[[:space:]]*hosts:/c\hosts: files myhostname dns' "$NSS" + else skipped 'NSS already configured'; fi + + if [[ ! -f "$RESOLV" || -L "$RESOLV" ]] || ! grep -Eq '^nameserver[[:space:]]+127\.0\.0\.1$' "$RESOLV"; then + local search_domain="" + search_domain=$(awk '$1=="search" { $1=""; sub(/^[[:space:]]+/, ""); print; exit }' "$RESOLV" 2>/dev/null || true) + rm -f "$RESOLV" + { + printf '# Local BIND9 recursive resolver; DNSSEC validated by BIND9.\n' + printf 'nameserver 127.0.0.1\noptions edns0 trust-ad\n' + [[ -z "$search_domain" ]] || printf 'search %s\n' "$search_domain" + } > "$RESOLV" + chmod 644 "$RESOLV" + else skipped 'resolv.conf already uses local BIND9'; fi + + echononl 'NSS forward resolution' + getent ahostsv4 oopen.de >/dev/null && ok 'works' || { echo; false; } + echononl 'NSS reverse resolution' + getent hosts 2607:f8b0:4864:20::72e >/dev/null && ok 'works' || { echo; false; } + echononl 'Resolver configuration' + is_direct && ok 'direct BIND9' || { echo; false; } + + systemctl stop systemd-resolved + systemctl disable systemd-resolved >/dev/null + echononl 'systemd-resolved disabled' + is_fully_switched && ok 'yes' || { echo; false; } + preflight + trap - ERR INT TERM + ok 'Switch completed successfully' + info "Rollback available: $0 --rollback" +} + +rollback(){ + require_root + local path + path=$(state_dir_from_marker) || fatal 'No rollback state from this script exists' + info "Rollback backup: $path" + confirm || { skipped 'Abbruch durch User'; exit 0; } + rollback_to "$path" + rm -f "$CURRENT_FILE" + ok 'Rollback completed; preserved backup remains in place' +} + +case "$MODE" in + --help) + cat <<'HELP' +Usage: switch-resolver-to-bind9.sh [--check|--apply|--rollback|--help] + --check read-only checks (default) + --apply verify BIND9 DNSSEC; back up; switch DNS/NSS; stop+disable resolved + --rollback restore files and resolved service state saved before last --apply + --help this help +Notes: run as root; requires BIND9 already working; no package changes. +HELP + ;; + --check) show_check ;; + --apply) apply ;; + --rollback) rollback ;; +esac From 7b4d0218e746f0c4aea6123582227491ddc328af Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 27 Oct 2026 13:10:41 +0100 Subject: [PATCH 2/4] switch-resolver-to-bind9.sh: some modifications on script output. --- switch-resolver-to-bind9.sh | 72 +++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/switch-resolver-to-bind9.sh b/switch-resolver-to-bind9.sh index 50f57e4..7a0ea9a 100755 --- a/switch-resolver-to-bind9.sh +++ b/switch-resolver-to-bind9.sh @@ -13,10 +13,31 @@ readonly EXPECTED_NSS='hosts: files myhostname dns' MODE="${1:---check}" case "$MODE" in --check|--apply|--rollback|--help) ;; *) echo "Usage: $0 [--check|--apply|--rollback|--help]" >&2; exit 2;; esac +# Colorize status labels on terminals; respect NO_COLOR and redirected output. +if [[ -t 1 && ! -v NO_COLOR && "${TERM:-}" != dumb ]]; then + GREEN=$'\033[32m' + GREY=$'\033[90m' + RED=$'\033[31m' + YELLOW=$'\033[33m' + RESET=$'\033[0m' +else + GREEN='' GREY='' RED='' YELLOW='' RESET='' +fi + info() { printf '[ Info ] %s\n' "$*"; } -ok() { printf '[ ok ] %s\n' "$*"; } -skipped() { printf '[ skipped ] %s\n' "$*"; } -failed() { printf '[ failed ] %s\n' "$*" >&2; } +ok() { printf '%s[ ok ]%s %s\n' "$GREEN" "$RESET" "$*"; } +skipped() { printf '%s[ skipped ]%s %s\n' "$GREY" "$RESET" "$*"; } +failed() { printf '%s[ failed ]%s %s\n' "$RED" "$RESET" "$*" >&2; } +# Delimited summary at the end of --check / before an interactive --apply. +status_banner(){ + local color="$1" message="$2" detail="${3:-}" + printf '\n%s------------------------------------------------------------%s\n' "$color" "$RESET" + printf '%s%s%s\n' "$color" "$message" "$RESET" + [[ -z "$detail" ]] || printf '%s%s%s\n' "$color" "$detail" "$RESET" + printf '%s------------------------------------------------------------%s\n' "$color" "$RESET" +} +# Every invocation finishes with one additional empty output line. +trap 'printf "\n"' EXIT fatal() { failed "$*"; exit 1; } echononl(){ printf '%-56s' "$*"; } require_root(){ [[ "${EUID}" -eq 0 ]] || fatal 'Please run as root.'; } @@ -47,14 +68,14 @@ is_fully_switched(){ # BIND must reject a known bogus signature, not merely answer normal queries. preflight(){ echononl 'BIND9 on 127.0.0.1:53' - if dig @127.0.0.1 oopen.de A +time=3 +tries=1 +short | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then ok 'reachable'; else echo; fatal 'Local BIND9 does not resolve oopen.de'; fi + if dig @127.0.0.1 oopen.de A +time=3 +tries=1 +short | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then ok 'reachable'; else echo; failed 'Local BIND9 does not resolve oopen.de'; return 1; fi echononl 'DNSSEC positive test (AD flag)' local reply reply=$(dig @127.0.0.1 cloudflare.com A +dnssec +adflag +time=5 +tries=1) - if grep -q 'status: NOERROR' <<< "$reply" && grep -Eq 'flags:.*[[:space:]]ad[;[:space:]]' <<< "$reply"; then ok 'validated'; else echo; fatal 'BIND9 DNSSEC positive test failed'; fi + if grep -q 'status: NOERROR' <<< "$reply" && grep -Eq 'flags:.*[[:space:]]ad[;[:space:]]' <<< "$reply"; then ok 'validated'; else echo; failed 'BIND9 DNSSEC positive test failed'; return 1; fi echononl 'DNSSEC negative test (SERVFAIL)' reply=$(dig @127.0.0.1 www.dnssec-failed.org A +time=5 +tries=1) - if grep -q 'status: SERVFAIL' <<< "$reply"; then ok 'rejected as expected'; else echo; fatal 'BIND9 does not reject invalid DNSSEC test zone'; fi + if grep -q 'status: SERVFAIL' <<< "$reply"; then ok 'rejected as expected'; else echo; failed 'BIND9 does not reject invalid DNSSEC test zone'; return 1; fi } show_check(){ @@ -66,11 +87,29 @@ show_check(){ grep -E '^hosts:' "$NSS" || true printf 'systemd-resolved active: %s\n' "$(systemctl is-active systemd-resolved 2>/dev/null || true)" printf 'systemd-resolved enabled: %s\n' "$(systemctl is-enabled systemd-resolved 2>/dev/null || true)" - if is_fully_switched; then ok 'Already directly using BIND9 (resolved disabled)'; - elif is_direct; then info 'BIND9 direct configuration is present; resolved still active/enabled'; - else info 'Not fully configured for direct BIND9'; fi - if [[ -f "$CURRENT_FILE" ]]; then info "Rollback state: $(state_dir_from_marker)"; fi - preflight + local readiness=0 + if preflight; then readiness=1; fi + if is_fully_switched; then + if (( readiness )); then + status_banner "$GREY" 'Umstellung auf BIND9 bereits durchgeführt.' + else + status_banner "$YELLOW" 'Umstellung bereits durchgeführt, aber DNS-Prüfung fehlgeschlagen.' + fi + elif [[ -f "$CURRENT_FILE" ]]; then + status_banner "$YELLOW" 'Umstellung kann nicht ausgeführt werden.' \ + 'Ein vorheriger Rollback-Stand ist vorhanden; zunächst prüfen oder zurückbauen.' + elif (( readiness )); then + status_banner "$GREEN" 'Umstellung kann durchgeführt werden.' + else + status_banner "$YELLOW" 'Umstellung kann nicht ausgeführt werden.' \ + 'BIND9-/DNSSEC-Voraussetzungen nicht erfüllt; bitte Fehler prüfen.' + fi + if [[ -f "$CURRENT_FILE" ]]; then + printf '\n=== Rollback information ===\n' + info 'Backup from before the last --apply is available' + info "Backup: $(state_dir_from_marker)" + info "Restore with: $0 --rollback" + fi } restore_files(){ @@ -113,12 +152,17 @@ apply(){ [[ -f "$NSS" ]] || fatal "$NSS missing" [[ -e "$RESOLV" || -L "$RESOLV" ]] || fatal "$RESOLV missing" [[ ! -f "$CURRENT_FILE" ]] || { - if is_fully_switched; then skipped 'Already installed; no changes'; exit 0; fi + if is_fully_switched; then status_banner "$GREY" 'Umstellung auf BIND9 bereits durchgeführt.'; skipped 'No changes'; exit 0; fi + status_banner "$YELLOW" 'Umstellung kann nicht ausgeführt werden.' 'Vorheriger Rollback-Stand vorhanden.' fatal 'An earlier script run has rollback state. Use --check or --rollback first.'; } - if is_fully_switched; then skipped 'Already directly using BIND9; no changes'; exit 0; fi + if is_fully_switched; then status_banner "$GREY" 'Umstellung auf BIND9 bereits durchgeführt.'; skipped 'No changes'; exit 0; fi info 'Preflight checks (no changes)' - preflight + if ! preflight; then + status_banner "$YELLOW" 'Umstellung kann nicht ausgeführt werden.' 'BIND9-/DNSSEC-Voraussetzungen nicht erfüllt.' + return 1 + fi + status_banner "$GREEN" 'Umstellung kann durchgeführt werden.' printf '\nPlanned changes:\n hosts: files myhostname dns\n resolv.conf: nameserver 127.0.0.1\n stop and disable systemd-resolved\n' info 'Network interfaces, routes, BIND9 and MTA are not changed.' confirm || { skipped 'Abbruch durch User'; exit 0; } From b83cc8fc5e3232849c7d9f3d7ef457d00f395145 Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 27 Oct 2026 13:25:59 +0100 Subject: [PATCH 3/4] switch-resolver-to-bind9.sh: some further changes in script output. --- switch-resolver-to-bind9.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/switch-resolver-to-bind9.sh b/switch-resolver-to-bind9.sh index 7a0ea9a..e54cfa9 100755 --- a/switch-resolver-to-bind9.sh +++ b/switch-resolver-to-bind9.sh @@ -99,7 +99,8 @@ show_check(){ status_banner "$YELLOW" 'Umstellung kann nicht ausgeführt werden.' \ 'Ein vorheriger Rollback-Stand ist vorhanden; zunächst prüfen oder zurückbauen.' elif (( readiness )); then - status_banner "$GREEN" 'Umstellung kann durchgeführt werden.' + status_banner "$GREEN" 'Umstellung kann durchgeführt werden.' \ + "Ausführen mit: $0 --apply" else status_banner "$YELLOW" 'Umstellung kann nicht ausgeführt werden.' \ 'BIND9-/DNSSEC-Voraussetzungen nicht erfüllt; bitte Fehler prüfen.' @@ -162,7 +163,8 @@ apply(){ status_banner "$YELLOW" 'Umstellung kann nicht ausgeführt werden.' 'BIND9-/DNSSEC-Voraussetzungen nicht erfüllt.' return 1 fi - status_banner "$GREEN" 'Umstellung kann durchgeführt werden.' + status_banner "$GREEN" 'Umstellung kann durchgeführt werden.' \ + "Ausführen mit: $0 --apply" printf '\nPlanned changes:\n hosts: files myhostname dns\n resolv.conf: nameserver 127.0.0.1\n stop and disable systemd-resolved\n' info 'Network interfaces, routes, BIND9 and MTA are not changed.' confirm || { skipped 'Abbruch durch User'; exit 0; } From 968d756d9386bb44a920c21e655936efa8e91fa9 Mon Sep 17 00:00:00 2001 From: Christoph Date: Tue, 27 Oct 2026 18:39:27 +0100 Subject: [PATCH 4/4] fix: handle Postfix DNS resolution after switching to BIND9 --- switch-resolver-to-bind9.sh | 80 +++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/switch-resolver-to-bind9.sh b/switch-resolver-to-bind9.sh index e54cfa9..75d3a44 100755 --- a/switch-resolver-to-bind9.sh +++ b/switch-resolver-to-bind9.sh @@ -11,7 +11,7 @@ readonly NSS=/etc/nsswitch.conf readonly EXPECTED_NSS='hosts: files myhostname dns' MODE="${1:---check}" -case "$MODE" in --check|--apply|--rollback|--help) ;; *) echo "Usage: $0 [--check|--apply|--rollback|--help]" >&2; exit 2;; esac +case "$MODE" in --check|--apply|--rollback|--repair-postfix|--help) ;; *) echo "Usage: $0 [--check|--apply|--rollback|--repair-postfix|--help]" >&2; exit 2;; esac # Colorize status labels on terminals; respect NO_COLOR and redirected output. if [[ -t 1 && ! -v NO_COLOR && "${TERM:-}" != dumb ]]; then @@ -78,6 +78,60 @@ preflight(){ if grep -q 'status: SERVFAIL' <<< "$reply"; then ok 'rejected as expected'; else echo; failed 'BIND9 does not reject invalid DNSSEC test zone'; return 1; fi } +# Debian Postfix: systemd instance startup refreshes the chroot resolver files. +# This is deliberately limited to running Postfix instances. No data_directory +# files are ever removed. A queue flush remains a separate admin decision. +postfix_units(){ + local unit + while read -r unit _; do + [[ "$unit" == postfix@*.service ]] || continue + printf '%s\n' "$unit" + done < <(systemctl list-units --type=service --state=active --no-legend --plain 'postfix@*.service' 2>/dev/null) +} + +sync_postfix(){ + local -a units=() + local unit queue_dir chroot_resolv + if ! command -v postconf >/dev/null 2>&1; then + skipped 'Postfix not installed; no MTA restart required' + return 0 + fi + mapfile -t units < <(postfix_units) + if (( ${#units[@]} == 0 )); then + if systemctl is-active --quiet postfix.service; then + units=(postfix.service) + else + skipped 'No active Postfix service; no MTA restart required' + return 0 + fi + fi + info 'Restarting active Postfix systemd service(s) to refresh chroot DNS' + for unit in "${units[@]}"; do + systemctl restart "$unit" || { failed "Postfix restart failed: $unit"; return 1; } + if ! systemctl is-active --quiet "$unit"; then + failed "Postfix service not active after restart: $unit" + return 1 + fi + ok "Postfix restarted: $unit" + done + # Only assert a chroot resolver copy when it exists; custom setups may + # use multiple queue directories or run SMTP clients without chroot. + queue_dir=$(postconf -h queue_directory 2>/dev/null || true) + if [[ -n "$queue_dir" && -f "$queue_dir/etc/resolv.conf" ]]; then + chroot_resolv="$queue_dir/etc/resolv.conf" + if cmp -s "$RESOLV" "$chroot_resolv"; then + ok "Postfix chroot resolver synchronized: $chroot_resolv" + else + failed "Postfix chroot resolver differs: $chroot_resolv" + failed 'Check Debian postfix configure-instance.sh / active instances' + return 1 + fi + else + info 'No default Postfix chroot resolv.conf found (possibly non-chroot or custom layout)' + fi + info 'Deferred mail is not flushed automatically; if appropriate run: postqueue -f' +} + show_check(){ require_root need dig; need systemctl; need getent @@ -145,6 +199,7 @@ rollback_to(){ restore_files "$path" restore_services "$path" ok 'Configuration and resolved service state restored' + sync_postfix } apply(){ @@ -166,7 +221,7 @@ apply(){ status_banner "$GREEN" 'Umstellung kann durchgeführt werden.' \ "Ausführen mit: $0 --apply" printf '\nPlanned changes:\n hosts: files myhostname dns\n resolv.conf: nameserver 127.0.0.1\n stop and disable systemd-resolved\n' - info 'Network interfaces, routes, BIND9 and MTA are not changed.' + info 'Network interfaces, routes and BIND9 are unchanged; active Postfix will restart.' confirm || { skipped 'Abbruch durch User'; exit 0; } mkdir -p "$STATE_DIR" @@ -216,11 +271,25 @@ apply(){ echononl 'systemd-resolved disabled' is_fully_switched && ok 'yes' || { echo; false; } preflight + sync_postfix trap - ERR INT TERM - ok 'Switch completed successfully' + ok 'Switch completed successfully' info "Rollback available: $0 --rollback" } +repair_postfix(){ + require_root + need systemctl; need postconf; need cmp + if ! is_direct; then + fatal 'Resolver is not configured for direct BIND9; use --check first' + fi + if ! preflight; then + fatal 'Local DNSSEC preflight failed; Postfix unchanged' + fi + sync_postfix + ok 'Postfix DNS synchronization completed' +} + rollback(){ require_root local path @@ -235,15 +304,18 @@ rollback(){ case "$MODE" in --help) cat <<'HELP' -Usage: switch-resolver-to-bind9.sh [--check|--apply|--rollback|--help] +Usage: switch-resolver-to-bind9.sh [--check|--apply|--rollback|--repair-postfix|--help] --check read-only checks (default) --apply verify BIND9 DNSSEC; back up; switch DNS/NSS; stop+disable resolved --rollback restore files and resolved service state saved before last --apply + --repair-postfix restart active Postfix instance(s) to refresh chroot DNS --help this help +After Postfix restart: consider postqueue -f to retry deferred messages. Notes: run as root; requires BIND9 already working; no package changes. HELP ;; --check) show_check ;; --apply) apply ;; --rollback) rollback ;; + --repair-postfix) repair_postfix ;; esac