fix(hosts-guard): stop setup aborting silently, so the guards actually enable

setup_hosts_guard.sh died partway on every run under `set -euo pipefail`,
which is why hosts-guard.path / hosts-bind-mount.service / nsswitch-guard.path
sat installed-but-disabled since 2026-02-02 and resolved-guard.path was never
installed at all — /etc/hosts had no watcher to restore it if tampered.

Two aborts, both silent:

1. `local_dot=$(grep -E '^\s*DNSOverTLS\s*=' ... | tail -1 | sed ...)` — grep
   exits 1 when the key is absent, pipefail propagates, the assignment returns
   1, set -e kills the script. DNSOverTLS is absent from a default
   resolved.conf, so this fired on EVERY run, right before resolved-guard.path
   was enabled. The fix-if-missing branch below it was unreachable because
   detecting "missing" aborted first. Same trap on the ReadEtcHosts probe.
   check_and_enable_services.sh already guarded its copy with `|| true`.

2. `systemctl restart systemd-resolved` — resolved is MASKED here (dnsmasq
   serves DNS), so the restart failed and aborted the script on its last step,
   after everything was already configured. Now skipped with a note.

Before: exit 1, resolved-guard disabled, nsswitch-guard start-limit-hit.
After: exit 0; hosts-guard.path, hosts-bind-mount.service, nsswitch-guard.path
and resolved-guard.path all enabled+active. DNS still resolves; getent ahosts
on doubleclick/googlesyndication/facebook returns 0.0.0.0; /etc/hosts still
immutable at 176888 lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
This commit is contained in:
Krzysztof Rudnicki 2026-07-17 08:25:51 +02:00
parent aba288d418
commit 779ace0c87
2 changed files with 47 additions and 4 deletions

View File

@ -0,0 +1,26 @@
{
"intent": "setup_hosts_guard.sh aborted partway on every run under `set -euo pipefail`, silently leaving the hosts guards half-configured or disabled. hosts-guard.path, hosts-bind-mount.service and nsswitch-guard.path were installed 2026-02-02 but disabled, and resolved-guard.path/enforce-resolved.sh were never installed at all — so /etc/hosts had no watcher to restore it if tampered. Make the installer run to completion so all four guards actually get enabled.",
"scope": [
"linux_configuration/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh",
"Non-goals: blocklist content, dnsmasq config, the other drifted services (thesis_tracker, focus_mode, guest_mode_removal, workout_locker) which the author will repair separately"
],
"changes": [
"Added `|| true` to the ReadEtcHosts and DNSOverTLS detection assignments. grep exits 1 when a key is absent; with set -euo pipefail that aborted the whole script — silently skipping the very fix-if-missing branch below it. DNSOverTLS is absent from a default resolved.conf, so the run died there on EVERY invocation, right before resolved-guard.path was enabled. (check_and_enable_services.sh already guarded its copy of the same grep with `|| true`; this brings the installer in line.)",
"Guarded the final `systemctl restart systemd-resolved`: systemd-resolved is MASKED on this host (dnsmasq serves DNS), so restarting it failed and set -e aborted the script on its last step — after everything was already configured, making a successful run report failure. Now skips with a note when resolved is masked/disabled."
],
"verification": [
{
"command": "sudo bash setup_hosts_guard.sh; systemctl is-enabled/is-active for the four guard units; getent ahosts on blocked domains; bash -n + shellcheck --severity=warning",
"result": "pass",
"evidence": "Before: exit 1, resolved-guard.path disabled/inactive, nsswitch-guard.path failed (unit-start-limit-hit). After: exit 0, and hosts-guard.path / hosts-bind-mount.service / nsswitch-guard.path / resolved-guard.path are all enabled+active. DNS still resolves (dnsmasq untouched); getent ahosts doubleclick.net / googlesyndication.com / facebook.com all return 0.0.0.0; /etc/hosts still immutable with 176888 lines. parse OK, shellcheck clean."
}
],
"risks": [
"The guards are now actually live: /etc/hosts has a read-only bind mount and a path watcher that reverts edits, and nsswitch.conf/resolved.conf are enforced against canonical snapshots. Editing those files now requires the documented unlock path (sudo unlock-hosts) or `setup_hosts_guard.sh --uninstall`.",
"nsswitch-guard hit 'unit-start-limit-hit' once during the first run (the enforcement rewrites the file it watches, re-triggering itself). It settled to active, but a rapid drift/restore cycle could trip systemd's start limit again."
],
"rollback": [
"sudo bash setup_hosts_guard.sh --uninstall (disables the units and removes them; canonical snapshots are left in place).",
"Validate after rollback: the four units are gone/disabled, /etc/hosts is no longer bind-mounted (findmnt /etc/hosts is empty), and DNS still resolves."
]
}

View File

@ -456,8 +456,13 @@ if [[ $ENABLE_RESOLVED -eq 1 ]]; then
# Ensure ReadEtcHosts=yes in resolved.conf before snapshotting
if [[ -f "$RESOLVED_CONF" ]]; then
# `|| true` is REQUIRED: grep exits 1 when the key is absent, and under
# `set -euo pipefail` that aborts the whole script — silently skipping the
# very fix below that exists to handle the absent case. Without it the
# "key missing" branch is unreachable. (check_and_enable_services.sh
# already guards its copy of this grep the same way.)
local_read_hosts=$(grep -E '^\s*ReadEtcHosts\s*=' "$RESOLVED_CONF" 2>/dev/null |
tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]')
tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') || true
if [[ "$local_read_hosts" != "yes" ]]; then
msg "Fixing ReadEtcHosts in resolved.conf (was: '$local_read_hosts')"
chattr -i "$RESOLVED_CONF" 2>/dev/null || true
@ -472,8 +477,12 @@ if [[ $ENABLE_RESOLVED -eq 1 ]]; then
fi
# Ensure DNSOverTLS is not set to yes or opportunistic
# `|| true`: same trap as above. DNSOverTLS is absent from a default
# resolved.conf, so without this the script aborted here on every run —
# right before resolved-guard.path was enabled, which is why that guard
# was never installed.
local_dot=$(grep -E '^\s*DNSOverTLS\s*=' "$RESOLVED_CONF" 2>/dev/null |
tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]')
tail -1 | sed 's/.*=\s*//' | tr -d '[:space:]') || true
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
@ -514,8 +523,16 @@ if [[ $ENABLE_RESOLVED -eq 1 ]]; then
"$INSTALL_ENFORCE_RESOLVED" || warn "resolved enforcement returned non-zero"
fi
# Restart resolved to pick up corrected config
run systemctl restart systemd-resolved
# Restart resolved to pick up corrected config.
# systemd-resolved is not necessarily the resolver: on this host it is MASKED
# and dnsmasq serves DNS. Restarting a masked unit fails, and under `set -e`
# that aborted the script on its very last step — after everything was already
# configured, so the run reported failure despite having succeeded.
if systemctl is-enabled systemd-resolved.service &>/dev/null; then
run systemctl restart systemd-resolved || warn "Failed to restart systemd-resolved"
else
note "systemd-resolved is masked/disabled — skipping restart (DNS is served elsewhere, e.g. dnsmasq)"
fi
else
note "Skipping resolved.conf protection (--skip-resolved)"
fi