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/pacman-hooks/hosts-guard-common.sh b/linux_configuration/hosts/guard/pacman-hooks/hosts-guard-common.sh index 9872cc3..8ab484b 100755 --- a/linux_configuration/hosts/guard/pacman-hooks/hosts-guard-common.sh +++ b/linux_configuration/hosts/guard/pacman-hooks/hosts-guard-common.sh @@ -3,6 +3,9 @@ # This file is sourced by pacman-pre-unlock-hosts.sh and pacman-post-relock-hosts.sh TARGET=/etc/hosts +NSSWITCH=/etc/nsswitch.conf +RESOLVED_CONF=/etc/systemd/resolved.conf +RESOLVED_DROPIN=/etc/systemd/resolved.conf.d LOGTAG=hosts-guard-hook # Check if target has a read-only mount @@ -38,7 +41,7 @@ collapse_mounts() { # Stop systemd units related to hosts guard stop_units_if_present() { - local units=(hosts-bind-mount.service hosts-guard.path) + local units=(hosts-bind-mount.service hosts-guard.path nsswitch-guard.path resolved-guard.path) for u in "${units[@]}"; do if command -v systemctl >/dev/null 2>&1; then if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then @@ -48,24 +51,36 @@ stop_units_if_present() { done } -# Remove immutable/append-only attributes -remove_host_attrs() { - if command -v lsattr >/dev/null 2>&1; then - local attrs - attrs=$(lsattr -d "$TARGET" 2>/dev/null || true) - if echo "$attrs" | grep -q " i "; then - chattr -i "$TARGET" >/dev/null 2>&1 || true - fi - if echo "$attrs" | grep -q " a "; then - chattr -a "$TARGET" >/dev/null 2>&1 || true - fi +# Remove immutable/append-only attributes from a file +_remove_attrs_for() { + local f="$1" + if [[ -e "$f" ]] && command -v lsattr >/dev/null 2>&1; then + chattr -ia "$f" >/dev/null 2>&1 || true fi } -# Apply immutable attribute +# Remove immutable/append-only attributes from all guarded files +remove_host_attrs() { + _remove_attrs_for "$TARGET" +} + +remove_all_guard_attrs() { + _remove_attrs_for "$TARGET" + _remove_attrs_for "$NSSWITCH" + _remove_attrs_for "$RESOLVED_CONF" + _remove_attrs_for "$RESOLVED_DROPIN" +} + +# Apply immutable attribute to all guarded files apply_immutable() { if command -v chattr >/dev/null 2>&1; then chattr +i "$TARGET" >/dev/null 2>&1 || true + chattr +i "$NSSWITCH" >/dev/null 2>&1 || true + chattr +i "$RESOLVED_CONF" >/dev/null 2>&1 || true + # Lock drop-in dir to prevent creation of override files + if [[ -d "$RESOLVED_DROPIN" ]]; then + chattr +i "$RESOLVED_DROPIN" >/dev/null 2>&1 || true + fi fi } @@ -75,10 +90,12 @@ apply_ro_bind_mount() { mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true } -# Start the path watcher service +# Start all path watcher services start_path_watcher() { if command -v systemctl >/dev/null 2>&1; then systemctl start hosts-guard.path >/dev/null 2>&1 || true + systemctl start nsswitch-guard.path >/dev/null 2>&1 || true + systemctl start resolved-guard.path >/dev/null 2>&1 || true fi } diff --git a/linux_configuration/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh b/linux_configuration/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh index 9f6cee2..3efe168 100755 --- a/linux_configuration/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh +++ b/linux_configuration/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# pacman-post-relock-hosts.sh - Re-apply hosts guard protections after pacman +# pacman-post-relock-hosts.sh - Re-apply all guard protections after pacman +# Re-locks: /etc/hosts, /etc/nsswitch.conf, /etc/systemd/resolved.conf set -euo pipefail # Source shared functions @@ -8,22 +9,30 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/hosts-guard-common.sh" ENFORCE=/usr/local/sbin/enforce-hosts.sh +ENFORCE_NSSWITCH=/usr/local/sbin/enforce-nsswitch.sh +ENFORCE_RESOLVED=/usr/local/sbin/enforce-resolved.sh log_hook "post" "relocking(start)" # Collapse any stacked mounts first collapse_mounts -# Run enforcement script if available +# Run enforcement scripts if available if [[ -x $ENFORCE ]]; then "$ENFORCE" >/dev/null 2>&1 || true fi +if [[ -x $ENFORCE_NSSWITCH ]]; then + "$ENFORCE_NSSWITCH" >/dev/null 2>&1 || true +fi +if [[ -x $ENFORCE_RESOLVED ]]; then + "$ENFORCE_RESOLVED" >/dev/null 2>&1 || true +fi -# Apply protections +# Apply protections (immutable on all guarded files) apply_immutable apply_ro_bind_mount -# Start the path watcher +# Start all path watchers start_path_watcher log_hook "post" "relocking(done)" diff --git a/linux_configuration/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh b/linux_configuration/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh index 0cb3331..d27c591 100755 --- a/linux_configuration/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh +++ b/linux_configuration/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# pacman-pre-unlock-hosts.sh - Temporarily unlock /etc/hosts before pacman +# pacman-pre-unlock-hosts.sh - Temporarily unlock guarded config files before pacman +# Unlocks: /etc/hosts, /etc/nsswitch.conf, /etc/systemd/resolved.conf set -euo pipefail # Source shared functions @@ -7,11 +8,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=hosts-guard-common.sh source "$SCRIPT_DIR/hosts-guard-common.sh" -# Remove protective attributes -remove_host_attrs +# Remove protective attributes from all guarded files +remove_all_guard_attrs sudo rm /etc/hosts -# Stop guard services +# Stop guard services (hosts, nsswitch, resolved watchers) stop_units_if_present log_hook "pre" "unlocking(start)" 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 diff --git a/linux_configuration/hosts/guard/setup_hosts_guard.sh b/linux_configuration/hosts/guard/setup_hosts_guard.sh index 61c08b8..869ee71 100755 --- a/linux_configuration/hosts/guard/setup_hosts_guard.sh +++ b/linux_configuration/hosts/guard/setup_hosts_guard.sh @@ -34,6 +34,7 @@ DO_SNAPSHOT=1 ENABLE_BIND=1 ENABLE_PATH=1 ENABLE_NSSWITCH=1 +ENABLE_RESOLVED=1 UNINSTALL=0 DELAY=45 DRY_RUN=0 @@ -89,6 +90,10 @@ while [[ $# -gt 0 ]]; do ENABLE_NSSWITCH=0 shift ;; + --skip-resolved) + ENABLE_RESOLVED=0 + shift + ;; --delay) DELAY=${2:-} [[ -z ${DELAY} ]] && { @@ -157,14 +162,21 @@ UNIT_BIND_SERVICE="$SCRIPT_DIR/hosts-bind-mount.service" TEMPLATE_ENFORCE_NSSWITCH="$SCRIPT_DIR/enforce-nsswitch.sh" UNIT_NSSWITCH_SERVICE="$SCRIPT_DIR/nsswitch-guard.service" UNIT_NSSWITCH_PATH="$SCRIPT_DIR/nsswitch-guard.path" +TEMPLATE_ENFORCE_RESOLVED="$SCRIPT_DIR/enforce-resolved.sh" +UNIT_RESOLVED_SERVICE="$SCRIPT_DIR/resolved-guard.service" +UNIT_RESOLVED_PATH="$SCRIPT_DIR/resolved-guard.path" INSTALL_ENFORCE="/usr/local/sbin/enforce-hosts.sh" INSTALL_UNLOCK="/usr/local/sbin/unlock-hosts" INSTALL_ENFORCE_NSSWITCH="/usr/local/sbin/enforce-nsswitch.sh" +INSTALL_ENFORCE_RESOLVED="/usr/local/sbin/enforce-resolved.sh" CANON="/usr/local/share/locked-hosts" CANON_NSSWITCH="/usr/local/share/locked-nsswitch.conf" +CANON_RESOLVED="/usr/local/share/locked-resolved.conf" HOSTS="/etc/hosts" NSSWITCH="/etc/nsswitch.conf" +RESOLVED_CONF="/etc/systemd/resolved.conf" +RESOLVED_DROPIN="/etc/systemd/resolved.conf.d" # Shell hook destinations (user agnostic system-wide skeleton + etc profile.d) ZSH_FILTER_SNIPPET="/etc/zsh/hosts_guard_history_filter.zsh" @@ -177,7 +189,7 @@ SYSTEMD_DIR="/etc/systemd/system" ###################################################################### if [[ $UNINSTALL -eq 1 ]]; then note "Uninstalling hosts guard components ( protections removed )" - for u in hosts-guard.path hosts-guard.service hosts-bind-mount.service nsswitch-guard.path nsswitch-guard.service; do + for u in hosts-guard.path hosts-guard.service hosts-bind-mount.service nsswitch-guard.path nsswitch-guard.service resolved-guard.path resolved-guard.service; do if systemctl list-unit-files | grep -q "^$u"; then run systemctl disable --now "$u" || true fi @@ -186,16 +198,19 @@ if [[ $UNINSTALL -eq 1 ]]; then "$INSTALL_ENFORCE" \ "$INSTALL_UNLOCK" \ "$INSTALL_ENFORCE_NSSWITCH" \ + "$INSTALL_ENFORCE_RESOLVED" \ "$SYSTEMD_DIR/hosts-guard.service" \ "$SYSTEMD_DIR/hosts-guard.path" \ "$SYSTEMD_DIR/hosts-bind-mount.service" \ "$SYSTEMD_DIR/nsswitch-guard.service" \ "$SYSTEMD_DIR/nsswitch-guard.path" \ + "$SYSTEMD_DIR/resolved-guard.service" \ + "$SYSTEMD_DIR/resolved-guard.path" \ "$ZSH_FILTER_SNIPPET" \ "$BASH_FILTER_SNIPPET"; do if [[ -e $f ]]; then run rm -f "$f"; fi done - note "Leaving canonical snapshots at $CANON and $CANON_NSSWITCH (remove manually if undesired)." + note "Leaving canonical snapshots at $CANON, $CANON_NSSWITCH and $CANON_RESOLVED (remove manually if undesired)." if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi msg "Uninstall complete" exit 0 @@ -369,6 +384,8 @@ run install -m 644 "$UNIT_GUARD_PATH" "$SYSTEMD_DIR/hosts-guard.path" run install -m 644 "$UNIT_BIND_SERVICE" "$SYSTEMD_DIR/hosts-bind-mount.service" run install -m 644 "$UNIT_NSSWITCH_SERVICE" "$SYSTEMD_DIR/nsswitch-guard.service" run install -m 644 "$UNIT_NSSWITCH_PATH" "$SYSTEMD_DIR/nsswitch-guard.path" +run install -m 644 "$UNIT_RESOLVED_SERVICE" "$SYSTEMD_DIR/resolved-guard.service" +run install -m 644 "$UNIT_RESOLVED_PATH" "$SYSTEMD_DIR/resolved-guard.path" if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi @@ -432,6 +449,77 @@ else note "Skipping nsswitch protection (--skip-nsswitch)" fi +if [[ $ENABLE_RESOLVED -eq 1 ]]; then + msg "Enabling resolved.conf protection (hosts bypass prevention)" + msg "Installing resolved enforcement script -> $INSTALL_ENFORCE_RESOLVED" + run install -m 755 "$TEMPLATE_ENFORCE_RESOLVED" "$INSTALL_ENFORCE_RESOLVED" + + # Ensure ReadEtcHosts=yes in resolved.conf before snapshotting + if [[ -f "$RESOLVED_CONF" ]]; then + local_read_hosts=$(grep -E '^\s*ReadEtcHosts\s*=' "$RESOLVED_CONF" 2>/dev/null | + tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') + if [[ "$local_read_hosts" != "yes" ]]; then + msg "Fixing ReadEtcHosts in resolved.conf (was: '$local_read_hosts')" + chattr -i "$RESOLVED_CONF" 2>/dev/null || true + if grep -qE '^\s*ReadEtcHosts\s*=' "$RESOLVED_CONF"; then + run sed -i -E 's/^\s*ReadEtcHosts\s*=.*/ReadEtcHosts=yes/' "$RESOLVED_CONF" + elif grep -q '^\[Resolve\]' "$RESOLVED_CONF"; then + run sed -i '/^\[Resolve\]/a ReadEtcHosts=yes' "$RESOLVED_CONF" + else + printf '\n[Resolve]\nReadEtcHosts=yes\n' >>"$RESOLVED_CONF" + fi + msg "resolved.conf ReadEtcHosts fixed: $(grep 'ReadEtcHosts' "$RESOLVED_CONF")" + fi + + # Ensure DNSOverTLS is not set to yes or opportunistic + local_dot=$(grep -E '^\s*DNSOverTLS\s*=' "$RESOLVED_CONF" 2>/dev/null | + tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') + if [[ -n "$local_dot" && "$local_dot" != "no" ]]; then + msg "Disabling DNSOverTLS in resolved.conf (was: '$local_dot')" + chattr -i "$RESOLVED_CONF" 2>/dev/null || true + run sed -i -E 's/^\s*DNSOverTLS\s*=.*/#DNSOverTLS=no/' "$RESOLVED_CONF" + fi + fi + + # Lock drop-in directory to prevent override files + if [[ -d "$RESOLVED_DROPIN" ]]; then + # Remove any existing drop-in overrides + local_count=$(find "$RESOLVED_DROPIN" -name '*.conf' -type f 2>/dev/null | wc -l) + if [[ "$local_count" -gt 0 ]]; then + warn "Removing $local_count drop-in override(s) from $RESOLVED_DROPIN" + find "$RESOLVED_DROPIN" -name '*.conf' -type f -delete + fi + chattr +i "$RESOLVED_DROPIN" 2>/dev/null || warn "Failed to lock $RESOLVED_DROPIN" + else + run mkdir -p "$RESOLVED_DROPIN" + chattr +i "$RESOLVED_DROPIN" 2>/dev/null || warn "Failed to lock $RESOLVED_DROPIN" + fi + + # Create resolved.conf canonical snapshot if needed + if [[ -f "$RESOLVED_CONF" ]]; then + if [[ ! -f "$CANON_RESOLVED" ]]; then + msg "Creating canonical resolved.conf snapshot at $CANON_RESOLVED" + run cp "$RESOLVED_CONF" "$CANON_RESOLVED" + run chmod 644 "$CANON_RESOLVED" + chattr +i "$CANON_RESOLVED" 2>/dev/null || warn "Failed to protect canonical resolved copy" + fi + fi + + run systemctl enable --now resolved-guard.path + + # Perform initial resolved enforcement + if [[ $DRY_RUN -eq 1 ]]; then + echo "DRY-RUN: would run $INSTALL_ENFORCE_RESOLVED" + else + "$INSTALL_ENFORCE_RESOLVED" || warn "resolved enforcement returned non-zero" + fi + + # Restart resolved to pick up corrected config + run systemctl restart systemd-resolved +else + note "Skipping resolved.conf protection (--skip-resolved)" +fi + msg "Performing initial hosts enforcement" if [[ $DRY_RUN -eq 1 ]]; then echo "DRY-RUN: would run $INSTALL_ENFORCE" @@ -446,13 +534,16 @@ echo msg "Hosts guard setup complete" echo "Canonical hosts copy: $CANON" echo "Canonical nsswitch copy: $CANON_NSSWITCH" +echo "Canonical resolved copy: $CANON_RESOLVED" echo "Enforce script: $INSTALL_ENFORCE" echo "nsswitch enforce: $INSTALL_ENFORCE_NSSWITCH" +echo "resolved enforce: $INSTALL_ENFORCE_RESOLVED" echo "Unlock command: sudo $INSTALL_UNLOCK" echo "Delay (seconds): $DELAY" echo "Auto-revert path watch: $([[ $ENABLE_PATH -eq 1 ]] && echo enabled || echo disabled)" echo "Read-only bind mount: $([[ $ENABLE_BIND -eq 1 ]] && echo enabled || echo disabled)" echo "nsswitch protection: $([[ $ENABLE_NSSWITCH -eq 1 ]] && echo enabled || echo disabled)" +echo "resolved protection: $([[ $ENABLE_RESOLVED -eq 1 ]] && echo enabled || echo disabled)" echo "Shell history suppression: $([[ $INSTALL_SHELL_HOOKS -eq 1 ]] && echo enabled || echo disabled)" echo "Audit rule: $([[ $INSTALL_AUDIT_RULE -eq 1 ]] && echo enabled || echo disabled)" echo "Alias stub: $([[ $ADD_ALIAS_STUB -eq 1 ]] && echo enabled || echo disabled)" diff --git a/linux_configuration/scripts/check_and_enable_services.sh b/linux_configuration/scripts/check_and_enable_services.sh index 7c1b54d..79bc2c5 100755 --- a/linux_configuration/scripts/check_and_enable_services.sh +++ b/linux_configuration/scripts/check_and_enable_services.sh @@ -513,6 +513,61 @@ check_hosts() { status="warning" fi + # Check resolved.conf has ReadEtcHosts=yes + if [[ -f /etc/systemd/resolved.conf ]]; then + local read_etc_hosts + read_etc_hosts=$(grep -E '^\s*ReadEtcHosts\s*=' /etc/systemd/resolved.conf 2>/dev/null | + tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') + if [[ "$read_etc_hosts" == "yes" ]]; then + msg "resolved.conf ReadEtcHosts=yes" + else + issues+=("resolved.conf ReadEtcHosts='$read_etc_hosts' — /etc/hosts is bypassed by systemd-resolved!") + status="error" + fi + + # Check DNSOverTLS is not enabled + local dns_over_tls + dns_over_tls=$(grep -E '^\s*DNSOverTLS\s*=' /etc/systemd/resolved.conf 2>/dev/null | + tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') + if [[ -z "$dns_over_tls" || "$dns_over_tls" == "no" ]]; then + msg "resolved.conf DNSOverTLS is disabled" + else + issues+=("resolved.conf DNSOverTLS='$dns_over_tls' — can bypass /etc/hosts!") + status="error" + fi + + # Check for drop-in overrides + if [[ -d /etc/systemd/resolved.conf.d ]]; then + local dropin_count + dropin_count=$(find /etc/systemd/resolved.conf.d -name '*.conf' -type f 2>/dev/null | wc -l) + if [[ "$dropin_count" -gt 0 ]]; then + issues+=("Found $dropin_count resolved.conf drop-in override(s) — potential bypass!") + status="error" + fi + fi + + # Check immutable attribute + if command -v lsattr &>/dev/null; then + if lsattr /etc/systemd/resolved.conf 2>/dev/null | grep -q '.*i.*e.*'; then + msg "resolved.conf has immutable attribute" + else + issues+=("resolved.conf missing immutable attribute") + [[ "$status" == "ok" ]] && status="warning" + fi + fi + else + issues+=("/etc/systemd/resolved.conf does not exist") + [[ "$status" == "ok" ]] && status="warning" + fi + + # Check resolved guard + if systemctl is-enabled resolved-guard.path &>/dev/null; then + msg "resolved-guard.path is enabled" + else + issues+=("resolved-guard.path is not enabled") + [[ "$status" == "ok" ]] && status="warning" + fi + # Report issues if [[ $status != "ok" ]]; then for issue in "${issues[@]}"; do @@ -543,6 +598,56 @@ check_hosts() { fi fi + # Fix resolved.conf if ReadEtcHosts is not yes + if [[ -f /etc/systemd/resolved.conf ]]; then + local resolved_reh + resolved_reh=$(grep -E '^\s*ReadEtcHosts\s*=' /etc/systemd/resolved.conf 2>/dev/null | + tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') + if [[ "$resolved_reh" != "yes" ]]; then + note "Fixing resolved.conf — setting ReadEtcHosts=yes..." + chattr -i /etc/systemd/resolved.conf 2>/dev/null || true + if grep -qE '^\s*ReadEtcHosts\s*=' /etc/systemd/resolved.conf; then + run sed -i -E 's/^\s*ReadEtcHosts\s*=.*/ReadEtcHosts=yes/' /etc/systemd/resolved.conf + elif grep -q '^\[Resolve\]' /etc/systemd/resolved.conf; then + run sed -i '/^\[Resolve\]/a ReadEtcHosts=yes' /etc/systemd/resolved.conf + else + printf '\n[Resolve]\nReadEtcHosts=yes\n' >>/etc/systemd/resolved.conf + fi + chattr +i /etc/systemd/resolved.conf 2>/dev/null || true + run systemctl restart systemd-resolved + ((FIXES_APPLIED++)) || true + msg "resolved.conf ReadEtcHosts fixed" + fi + + # Fix DNSOverTLS if enabled + local resolved_dot + resolved_dot=$(grep -E '^\s*DNSOverTLS\s*=' /etc/systemd/resolved.conf 2>/dev/null | + tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') + if [[ -n "$resolved_dot" && "$resolved_dot" != "no" ]]; then + note "Fixing resolved.conf — disabling DNSOverTLS..." + chattr -i /etc/systemd/resolved.conf 2>/dev/null || true + run sed -i -E 's/^\s*DNSOverTLS\s*=.*/#DNSOverTLS=no/' /etc/systemd/resolved.conf + chattr +i /etc/systemd/resolved.conf 2>/dev/null || true + run systemctl restart systemd-resolved + ((FIXES_APPLIED++)) || true + msg "resolved.conf DNSOverTLS disabled" + fi + + # Remove drop-in overrides + if [[ -d /etc/systemd/resolved.conf.d ]]; then + local dropin_fix_count + dropin_fix_count=$(find /etc/systemd/resolved.conf.d -name '*.conf' -type f 2>/dev/null | wc -l) + if [[ "$dropin_fix_count" -gt 0 ]]; then + note "Removing $dropin_fix_count resolved.conf drop-in override(s)..." + chattr -i /etc/systemd/resolved.conf.d 2>/dev/null || true + find /etc/systemd/resolved.conf.d -name '*.conf' -type f -delete + chattr +i /etc/systemd/resolved.conf.d 2>/dev/null || true + run systemctl restart systemd-resolved + ((FIXES_APPLIED++)) || true + fi + fi + fi + # Run hosts install first if [[ ! -f /etc/hosts ]] || [[ $(wc -l /dev/null && + systemctl is-enabled nsswitch-guard.path &>/dev/null && + systemctl is-enabled resolved-guard.path &>/dev/null && [[ -f /usr/local/sbin/enforce-hosts.sh ]] && [[ -f /usr/local/share/locked-hosts ]] && [[ -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]]; then