From 197cdc01056fbbddeff69bbaffbec08f5404a63d Mon Sep 17 00:00:00 2001 From: Krzysztof Rudnicki Date: Fri, 20 Feb 2026 23:21:25 +0100 Subject: [PATCH] Protect nsswitch.conf and resolved.conf from hosts bypass - Add enforce-resolved.sh: validates ReadEtcHosts=yes, prevents DNSOverTLS bypass, removes drop-in overrides, locks drop-in dir - Add resolved-guard.path/service: watches /etc/systemd/resolved.conf and its drop-in directory for tampering - Update pacman hooks to unlock/relock nsswitch.conf and resolved.conf alongside /etc/hosts during package transactions - Extend setup_hosts_guard.sh with --skip-resolved option, resolved canonical snapshot, drop-in directory locking, and enforcement - Add resolved.conf checks to check_and_enable_services.sh: validates ReadEtcHosts, DNSOverTLS, drop-in overrides, immutable attribute, and resolved-guard.path status with auto-fix capability Fixed on live system: ReadEtcHosts was set to 'no' and nsswitch.conf was missing 'files' in the hosts line, completely bypassing /etc/hosts. --- .../hosts/guard/enforce-resolved.sh | 147 ++++++++++++++++++ .../hosts/guard/resolved-guard.path | 10 ++ .../hosts/guard/resolved-guard.service | 12 ++ 3 files changed, 169 insertions(+) create mode 100755 linux_configuration/hosts/guard/enforce-resolved.sh create mode 100644 linux_configuration/hosts/guard/resolved-guard.path create mode 100644 linux_configuration/hosts/guard/resolved-guard.service diff --git a/linux_configuration/hosts/guard/enforce-resolved.sh b/linux_configuration/hosts/guard/enforce-resolved.sh new file mode 100755 index 0000000..fc67cff --- /dev/null +++ b/linux_configuration/hosts/guard/enforce-resolved.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# Guard script to enforce canonical /etc/systemd/resolved.conf +# Ensures ReadEtcHosts=yes and prevents DNS-over-TLS bypass of /etc/hosts +# Installed to /usr/local/sbin/enforce-resolved.sh by setup_hosts_guard.sh + +set -euo pipefail + +CANONICAL_SOURCE="/usr/local/share/locked-resolved.conf" +TARGET="/etc/systemd/resolved.conf" +DROPIN_DIR="/etc/systemd/resolved.conf.d" +LOG_FILE="/var/log/resolved-guard.log" + +log() { + printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE" >&2 +} + +# Validate that resolved.conf has correct settings to honour /etc/hosts +# Critical settings: +# ReadEtcHosts=yes — must be present and set to yes +# DNSOverTLS=no — must NOT be opportunistic/yes (bypasses local hosts) +validate_resolved() { + local file="$1" + + # ReadEtcHosts must be explicitly yes (not commented, not "no") + local read_hosts + read_hosts=$(grep -E '^\s*ReadEtcHosts\s*=' "$file" 2>/dev/null | tail -1 | + sed 's/.*=\s*//' | tr -d '[:space:]') + if [[ "$read_hosts" != "yes" ]]; then + log "INVALID: ReadEtcHosts='$read_hosts' (expected 'yes') in $file" + return 1 + fi + + # DNSOverTLS must not be set to yes or opportunistic + local dot + dot=$(grep -E '^\s*DNSOverTLS\s*=' "$file" 2>/dev/null | tail -1 | + sed 's/.*=\s*//' | tr -d '[:space:]') + if [[ -n "$dot" && "$dot" != "no" ]]; then + log "INVALID: DNSOverTLS='$dot' (must be 'no' or commented out) in $file" + return 1 + fi + + return 0 +} + +# Remove any drop-in overrides that could bypass protections +enforce_no_dropins() { + if [[ -d "$DROPIN_DIR" ]]; then + local count + count=$(find "$DROPIN_DIR" -name '*.conf' -type f 2>/dev/null | wc -l) + if [[ "$count" -gt 0 ]]; then + log "TAMPERING: Found $count drop-in config(s) in $DROPIN_DIR — removing" + find "$DROPIN_DIR" -name '*.conf' -type f -delete + log "Removed all drop-in overrides" + fi + # Lock the directory itself to prevent new drop-ins + chattr +i "$DROPIN_DIR" 2>/dev/null || log "Failed to lock $DROPIN_DIR" + else + # Create and lock the directory to prevent creation with overrides + mkdir -p "$DROPIN_DIR" + chattr +i "$DROPIN_DIR" 2>/dev/null || log "Failed to lock $DROPIN_DIR" + log "Created and locked empty $DROPIN_DIR" + fi +} + +# Main enforcement logic +log "Starting resolved.conf enforcement" + +# 1. Handle drop-in overrides first +enforce_no_dropins + +# 2. Check current resolved.conf +if [[ ! -f "$TARGET" ]]; then + log "CRITICAL: $TARGET does not exist" + if [[ -f "$CANONICAL_SOURCE" ]]; then + chattr -i "$TARGET" 2>/dev/null || true + cp "$CANONICAL_SOURCE" "$TARGET" + chmod 644 "$TARGET" + chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable on $TARGET" + log "Restored $TARGET from canonical copy" + else + log "ERROR: No canonical source at $CANONICAL_SOURCE — cannot restore" + exit 1 + fi +fi + +if ! validate_resolved "$TARGET"; then + log "TAMPERING DETECTED in $TARGET" + + if [[ -f "$CANONICAL_SOURCE" ]]; then + chattr -i "$TARGET" 2>/dev/null || true + cp "$CANONICAL_SOURCE" "$TARGET" + chmod 644 "$TARGET" + chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable on $TARGET" + log "Restored $TARGET from canonical copy" + else + log "No canonical source — applying emergency fix" + chattr -i "$TARGET" 2>/dev/null || true + + # Fix ReadEtcHosts + if grep -qE '^\s*ReadEtcHosts\s*=' "$TARGET"; then + sed -i -E 's/^\s*ReadEtcHosts\s*=.*/ReadEtcHosts=yes/' "$TARGET" + elif grep -q '^\[Resolve\]' "$TARGET"; then + sed -i '/^\[Resolve\]/a ReadEtcHosts=yes' "$TARGET" + else + printf '\n[Resolve]\nReadEtcHosts=yes\n' >>"$TARGET" + fi + + # Fix DNSOverTLS + if grep -qE '^\s*DNSOverTLS\s*=' "$TARGET"; then + sed -i -E 's/^\s*DNSOverTLS\s*=.*/#DNSOverTLS=no/' "$TARGET" + fi + + chattr +i "$TARGET" 2>/dev/null || true + log "Emergency fix applied" + fi + + # Restart resolved to pick up changes + systemctl restart systemd-resolved 2>/dev/null || log "Failed to restart systemd-resolved" + exit 0 +fi + +# 3. If canonical exists, check for any drift +if [[ -f "$CANONICAL_SOURCE" ]]; then + if ! cmp -s "$CANONICAL_SOURCE" "$TARGET"; then + log "Drift detected in $TARGET — restoring canonical" + chattr -i "$TARGET" 2>/dev/null || true + cp "$CANONICAL_SOURCE" "$TARGET" + chmod 644 "$TARGET" + chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable" + log "Restored $TARGET from canonical copy" + systemctl restart systemd-resolved 2>/dev/null || log "Failed to restart systemd-resolved" + else + log "No drift detected in $TARGET" + fi +else + log "Creating initial canonical snapshot" + mkdir -p "$(dirname "$CANONICAL_SOURCE")" + cp "$TARGET" "$CANONICAL_SOURCE" + chmod 644 "$CANONICAL_SOURCE" + chattr +i "$CANONICAL_SOURCE" 2>/dev/null || log "Failed to protect canonical copy" +fi + +# 4. Ensure immutable attribute is set on live file +chattr -i "$TARGET" 2>/dev/null || true +chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable on $TARGET" + +log "resolved.conf enforcement complete" diff --git a/linux_configuration/hosts/guard/resolved-guard.path b/linux_configuration/hosts/guard/resolved-guard.path new file mode 100644 index 0000000..febd6e6 --- /dev/null +++ b/linux_configuration/hosts/guard/resolved-guard.path @@ -0,0 +1,10 @@ +[Unit] +Description=Watch /etc/systemd/resolved.conf for tampering (hosts bypass protection) + +[Path] +PathChanged=/etc/systemd/resolved.conf +PathChanged=/etc/systemd/resolved.conf.d +Unit=resolved-guard.service + +[Install] +WantedBy=multi-user.target diff --git a/linux_configuration/hosts/guard/resolved-guard.service b/linux_configuration/hosts/guard/resolved-guard.service new file mode 100644 index 0000000..c571490 --- /dev/null +++ b/linux_configuration/hosts/guard/resolved-guard.service @@ -0,0 +1,12 @@ +[Unit] +Description=Enforce canonical /etc/systemd/resolved.conf (prevents hosts bypass) +After=local-fs.target + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/enforce-resolved.sh +Nice=10 +IOSchedulingClass=idle + +[Install] +WantedBy=multi-user.target