diff --git a/NEXT_SESSION_PROMPT.md b/NEXT_SESSION_PROMPT.md index 1fdc78b0..3d2f6450 100644 --- a/NEXT_SESSION_PROMPT.md +++ b/NEXT_SESSION_PROMPT.md @@ -1,15 +1,16 @@ -# Next session: §3's next restructure — setup_hosts_guard.sh +# Next session: pick the next §3/§4 target — no split candidate is currently in scope > **Paste this whole file into a fresh Claude session opened in `~/testsAndMisc`.** > It is self-contained. Do not go looking for the previous session's context. -Over-cap: **12** (11 shell, 2 kotlin, 1 dart). Working tree clean, `main` in -sync as of the diagnose_pacman_hook_stall.sh split commit (see §0.5). +Over-cap: **11** (10 shell, 2 kotlin, 1 dart — `focus_owner`'s two Kotlin files +count once regardless of listing style). Working tree clean, `main` in sync +as of the `setup_hosts_guard.sh` archive commit (see §0). Two standing decisions from the user, already made — do **not** re-litigate: 1. **100% line coverage on what you extract** — same bar every session so far - has applied. + has applied. Does not apply to archived/deleted code (no split happened). 2. **Split blind where there is no device.** Do not deploy to the phone, do not ask to. Prove splits with tests, hashes and real runs instead. @@ -17,254 +18,208 @@ Two standing decisions from the user, already made — do **not** re-litigate: ## 0. What just happened (read this, don't repeat it) -Two sessions ago: `install_plagiarism_tools.sh` was split (534→94 lines + -3 libs, 100% coverage) — then the user said they no longer use the -plagiarism tools at all, so the whole thing was archived to -`kuhyx/testsAndMisc-archive` with full git history via `git filter-repo`. -One session ago: the same one-line "do you still use this?" check-in was -asked about `libre_translate.sh` _before_ any code was touched, and got the -same answer — also archived, not split. +`setup_hosts_guard.sh` (576 lines) was this session's assigned split target. +The dead-code check-in (per the §0 lesson from two sessions before this one) +turned up evidence the file might be fully superseded — confirmed by +inspecting live machine state: the legacy pacman hooks it installs were +absent from `/etc/pacman.d/hooks/`, its systemd units were all `disabled`, +and guard-lib's `/etc/guard-lib/targets/{hosts,nsswitch,resolved}.conf` were +all already populated. The user confirmed: archive, don't split. -**Lesson (2 for 2, now reinforced a third way):** keep asking the check-in -before every future §3/§4 split. It is not a formality; it has changed scope -in both of the two times it's been tried. +Scope grew past the one named file once the directory was surveyed: the +**whole legacy hosts-guard subsystem** (17 files — `setup_hosts_guard.sh`, +`enforce-hosts.sh`, `enforce-nsswitch.sh`, `enforce-resolved.sh`, 6 systemd +units, `install_pacman_hooks.sh`, `pacman-hooks/*`, `psychological/*`, both +READMEs) only existed to support the retired system, so it went to +`testsAndMisc-archive` together via `git filter-repo` (same procedure as the +plagiarism-tools and libre_translate archives). `plugins/nsswitch-plugin.sh` +and `plugins/resolved-plugin.sh` were **kept** — they're guard-lib's live +plugins, registered by absolute path in `/etc/guard-lib/targets/*.conf`. -## 0.5. What just happened (this session) — diagnose_pacman_hook_stall.sh split, confirmed live +`check_and_enable_services.sh`'s `check_hosts()` function was rewritten in +the same commit (user confirmed this too) to check guard-lib state +(`guardctl file-guard status `) instead of the legacy systemd +units/enforce-scripts it used to check and self-repair. Verified live: +`sudo bash check_and_enable_services.sh --status` now reports the hosts +service `ok` against the actual migrated machine, where the old code would +have reported `error` (three units it expected enabled were `disabled` by +design). `linux_configuration/.github/copilot-instructions.md` (always-loaded) +and one comment in `hosts/install.sh` were also updated to stop naming the +archived files. -Asked the dead-code check-in first, per §0's lesson. User confirmed this -script **is** still in active use (unlike the last two candidates) — so the -split proceeded as originally planned. 493 lines → 249 (entry) + 6 libs -(pacman_hook_stall_setup.sh, _load.sh, _capture.sh, _watch.sh, _usage.sh, -_summary.sh), all at 100% line coverage, plus a PATH-shim test suite (56 -lib-level + 13 entry-level subprocess assertions, 0 failed). +**Lesson (now 3 for 4):** the check-in keeps changing scope, and this time it +changed scope _twice_ — first archive-vs-split, then "just this file" vs +"the whole dead subsystem." Keep surveying the directory (`grep -rl`, not +just the named file) before proposing an archive's file list, and keep +confirming scope growth with the user before executing it. -**Two things worth knowing before touching this area again:** +## 0.1 Known defect found but NOT fixed this session — flag again if touching guard-lib -1. `run_one`/`cleanup`/`log_size`/`main` had to stay together in the entry - script — they share `PACMAN_PID`/`LAST_ELAPSED` across the `EXIT` trap, - and a prior session (commit `2527fb3`) already proved that splitting - `run_one` into a lib produces a **real** SC2153/SC2034 pair (not a - suppressible false positive), because a sourced lib has no way to declare - where its globals come from. If a future split looks like it wants to - move a function that both writes a global the entry script reads AND - reads a global only the entry script sets, do the same scratch - shellcheck-dry-run check (with `.shellcheckrc`'s `external-sources=true` / - `source-path=SCRIPTDIR`, which is what the real hook uses) **before** - committing to a lib boundary, not after. -2. **kcov cannot see `done `, `} >redirect`, or lines inside an - inline awk script**, no matter how thoroughly the code executes (verified - empirically with `bash -x` on isolated repros — those lines never emit a - trace event). If a coverage gap on a future file's line list looks - suspiciously like a loop-closing or group-closing line, or an awk block, - check whether it's this exact class before assuming the test is weak. The - user was asked and chose "refactor the code" over "fix the coverage - tool" — that decision holds for this repo going forward unless - re-raised. -3. **Test files are not exempt from the 250-line cap.** The entry-script - subprocess test grew to 295 lines while adding coverage and had to be - split itself, mid-session, after `check_file_length.sh --all` caught it. - Budget for this: a test file that drives real subprocess timing (unshare, - real sleep, multiple flag combinations) grows fast — split its fixture - setup into a shared harness early rather than after it's already over. -4. Evidence + contract: - `docs/superpowers/evidence/split-diagnose-pacman-hook-stall-2026-08-18.json` - and the matching `contracts/` file are the current format reference — - more detailed than the plagiarism-tools pair since this split involved - several genuine (not mechanical) function rewrites, all individually - justified there. +`/etc/guard-lib/targets/nsswitch.conf` and `.../resolved.conf` both have +`PLUGIN=` pointing at the **repo checkout** +(`/home/kuhy/testsAndMisc/linux_configuration/scripts/periodic_background/hosts/guard/plugins/*.sh`) +instead of `/usr/local/share/guard-lib-plugins/`, which +`migrate_hosts_guard_to_guard_lib.sh`'s own header warns against by name +("pointing it at a repo checkout... silently breaks enforcement the day that +directory moves"). `/usr/local/share/guard-lib-plugins/` does not exist on +this machine at all — the migration's plugin-install step appears to have +never run. Not fixed this session: it needs a sudo mutation of a live +security-enforcement system plus `guardctl` re-registration, which the user +was not asked to approve. If a future session touches guard-lib, nsswitch, +resolved, or moves/renames this checkout, raise this first. -## 1. This session's task: `setup_hosts_guard.sh` +## 1. This session's task: none pre-selected — ask the user -Location: -`linux_configuration/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh` -(576 lines). +Every file currently over the 250-line cap is already on an explicit +out-of-scope list from a prior session (§3 below, or the equivalent of the +old §4). There is no unclaimed split candidate right now. Options, in +roughly ascending order of friction: -**Ask the dead-code check-in first** (see §0.5) — the streak is 2-for-3 so -far (2 archived, 1 confirmed live); don't assume either outcome. +- Re-run `bash meta/scripts/check_file_length.sh --all` fresh (files may have + grown/shrunk since this doc was written) and see if a genuinely new + candidate appeared. +- Ask the user whether any §3/§4 file should be taken out of its hold (e.g. + "is `install_leechblock.sh`/`block_compulsive_opening.sh` still deployed at + `/usr/local/…`? still preferred by `pacman_wrapper.sh:831`?" — re-verify, + don't assume the old note is still true). +- Ask the user for a completely different task. -**Before you start**, confirm scope hasn't drifted: `git status --short` and -`wc -l` on the target file, and re-run -`bash meta/scripts/check_file_length.sh --all` to get the current exact -list — files change size between sessions, and this file's line count may -not still be 576. +**Ask before picking** — do not silently start splitting a file that a prior +session explicitly parked, and do not assume "no candidate" means "nothing to +do here" without checking with the user first. -Check what this file references before planning a split boundary — -`hosts_guard_migrate.sh`/`hosts_guard_rollback.sh` already exist as siblings -in `linux_configuration/scripts/single_use/fixes/lib/`, and -`fixes/migrate_hosts_guard_to_guard_lib.sh` is the entry script that -migrated _to_ this guard-lib system. Read that migration script's header -comment first — it explains what guard-lib actually does and may reveal -whether `setup_hosts_guard.sh` itself is now redundant with the migrated -state, which is exactly the kind of thing the dead-code check-in is for. - -## 3. §4 — do not touch this session +## 2. §3 — do not touch this session (unless the user lifts it) `install_leechblock.sh` (485) and `block_compulsive_opening.sh` (705) are copied to `/usr/local/…`, and `pacman_wrapper.sh:831` prefers the deployed copy on **every pacman invocation**. Splitting them naively breaks every -`pacman -S` on this machine. Out of scope until `setup_hosts_guard.sh` is -done (or archived) and the user explicitly says to continue into §4. +`pacman -S` on this machine. Re-verify this is still true (grep +`pacman_wrapper.sh` for the deployed-copy preference) before assuming the old +note still holds — a lot has changed guard-lib-side this session, worth the +30-second re-check. You cannot run `sudo pacman -S` from the Bash tool — it deadlocks on `db.lck`. Hand the user a `! sudo pacman -S ` line plus the expected output if `pacman_wrapper.sh` ever needs live verification. -## 4. Out of scope until the user says otherwise +## 3. Out of scope until the user says otherwise -`setup_midnight_shutdown.sh` (1734), `check_and_enable_services.sh` (1301), +`setup_midnight_shutdown.sh` (1734), `check_and_enable_services.sh` (1225, +edited this session but not split — same out-of-scope reasoning as before), `generate_study_materials.sh` (1017), `pacman_wrapper.sh` (929), -`setup_night_lockdown.sh` (918), `hosts/install.sh` (912), -`EnforcementRunner.kt` (564), `DevicePolicyBridge.kt` (415), +`setup_night_lockdown.sh` (918), `hosts/install.sh` (913, one comment edited +this session), `EnforcementRunner.kt` (564), `DevicePolicyBridge.kt` (415), `status_page_state.dart` (307) — check `check_file_length.sh --all` for the current exact list before assuming this is still accurate, since files change size between sessions. -`pacman_wrapper.sh` carries the same live-deployment trap as §3/§4. The -Kotlin and Dart files need a different verification stack — `focus_owner` -gradle needs `JAVA_HOME=/usr/lib/jvm/java-21-openjdk` and `--rerun-tasks`; a -plain `gradlew test` reports `UP-TO-DATE` and proves nothing. +`pacman_wrapper.sh` carries the same live-deployment trap as §2. The Kotlin +and Dart files need a different verification stack — `focus_owner` gradle +needs `JAVA_HOME=/usr/lib/jvm/java-21-openjdk` and `--rerun-tasks`; a plain +`gradlew test` reports `UP-TO-DATE` and proves nothing. -## 5. Rules that will bite you (carried forward, still true) +## 4. Rules that will bite you (carried forward, still true) -- **Ask before large scope changes.** Two of the last three sessions turned - into archive-and-delete mid-session because the user volunteered new - information partway through (or, this session, confirmed the file WAS - still live). That is fine and expected — just don't assume silence means - "keep going as originally scoped" if something about the target file's - relevance seems worth a quick check. +- **Ask before large scope changes.** Three of the last four sessions have + changed scope mid-session because the user volunteered new information, or + a check-in (dead-code, or in this session's case a directory survey) + surfaced something bigger than the named file. That is fine and expected — + just don't assume silence means "keep going as originally scoped." +- **Survey the whole directory before scoping an archive, not just the named + file.** `grep -rl ` across the repo, then `ls` the file's + own directory — this session's file list grew from 1 to 17 files this way. - **No suppressions, ever.** No `# noqa`, no per-file-ignore added without asking every time, no shellcheck disable beyond what's already justified - inline. If a coverage or lint finding looks like it fundamentally - conflicts with the file's purpose, ask before working around it — this - session's kcov-blind-spot finding (§0.5.2) is the exact pattern: verify - the finding is real (bash -x trace check) before assuming it's a tool - limitation to route around vs. a real behavioral gap to test. + inline. - **A file must not assign a global it never reads** (SC2034). The hook runs `shellcheck` with **no `-x`**, but `.shellcheckrc`'s `external-sources=true` + `source-path=SCRIPTDIR` means a `source=` - annotation on the _sourcing_ file still resolves correctly — the failure - mode that actually breaks is a _lib_ file trying to reference a global - only the entry script sets, since a lib has no annotation pointing - "upward" to whoever sources it. Constants travel with their readers; a - global genuinely written on both sides of a seam stays in the entry - script. -- **New sourced libs need a shebang AND the executable bit**, even though - they're sourced, not executed — `lib/hosts_guard_migrate.sh` and this - session's 6 new libs both carry `100755` in the git index, matching - convention. The hook reads the **git index**: stage with - `git add --chmod=+x`, and note that a plain `git add` afterwards resets - it. + annotation on the _sourcing_ file still resolves correctly. +- **New sourced libs need a shebang AND the executable bit**, staged with + `git add --chmod=+x` — a plain `git add` afterwards resets it. - `pre-commit run --files ` before committing, but run it **after** - `git add`, not before — index-mode checks (executable bit) only fire - against what's staged. **`prettier` and `ci-mirror` run on pre-push.** - `npx prettier --write` any `.md`/`.json` you touch (evidence/contract files - included — they are JSON and prettier will reformat them). + `git add`. **`prettier` and `ci-mirror` run on pre-push.** `npx prettier +--write` any `.md`/`.json` you touch (evidence/contract files included). - **Every commit touching code needs evidence** in `docs/superpowers/evidence/-.json`. Staging **≥4 code files also needs** a fresh `docs/superpowers/contracts/*.json`. Put the - **measured** coverage number in it, not a rounded-up one. This session's - pair (`split-diagnose-pacman-hook-stall-2026-08-18.json` in both - `evidence/` and `contracts/`) is the current best format reference — - it documents several genuine mid-session findings (a real bug, a - memory-safety catch, a test-file-too-long split) as they happened, which - is the level of detail to aim for when the session isn't a pure mechanical - move. + **measured** number in it, not a rounded-up one. This session's pair + (`archive-setup-hosts-guard-2026-08-18.json` in both `evidence/` and + `contracts/`) documents an archive-plus-live-script-fix, not a pure split — + read it if the next task is another archive-vs-split judgment call. +- **An archive does not need new tests.** The 100%-coverage standing decision + binds code that gets _split_, not code that gets _deleted_. Neither this + session's archive nor the two before it added tests for the archived code. - Work directly on `main`. `git stash` and branch creation are blocked; use `git worktree add --detach` for a clean baseline (but NEVER `git filter-repo` inside a worktree — it shares the object store with the main repo and will corrupt it; always a throwaway `git clone` for that). - **Do not wire the file-length hook into pre-commit.** It lands last, once `check_file_length.sh --all` exits 0. -- Watch `jscpd` (fails above 2%). Measure in a **clean HEAD worktree** - (`git worktree add --detach HEAD`, copy the new files in, run jscpd - there, remove the worktree) — the working tree reads higher because of - vendored `.venv`, and several near-identical test files sharing a fixture - preamble is exactly the shape that trips this gate. +- Watch `jscpd` (fails above 2%). Measure in a **clean HEAD worktree**. - Cap pytest memory: `systemd-run --user --scope -p MemorySwapMax=0 -p MemoryMax=2G`. -- **Never let a test allocate unbounded real memory.** This session's - `--with-load` entry test would have `dd`ed 13-20+ GB into `/dev/shm` using - this box's real available memory before it was caught in review — any - script under test that computes an allocation size from real - `/proc/meminfo` needs its floor/ceiling constants made env-overridable - (`: "${VAR:=default}"`, not a bare or `readonly` assignment) so tests can - pin them to a small, bounded amount instead of trusting the live system - state to stay favorable. +- **Never let a test allocate unbounded real memory.** -## 6. Tooling reference +## 5. Tooling reference **`meta/scripts/check_file_length.sh --all`** — the 250-line gate. Run -**before every commit**. Applies to test files too, not just the file -you're splitting. +**before every commit**. Applies to test files too, not just the file you're +splitting. **`meta/scripts/shell_coverage.sh [min]`** — kcov wrapper, 100% minimum. Hand it the test script **directly**; `bash script.sh` -instruments the bash binary and silently reports 0/0. If a test suite for -one subject is split across multiple test files, `shell_coverage.sh` only -takes one at a time — run kcov manually into the same output dir for each -test file, then call `meta/scripts/shell_coverage_report.py ` -once to get the merged number (it sums hits per line-number across every -`cov.xml` it finds, so multiple runs against the same subject combine -correctly). +instruments the bash binary and silently reports 0/0. **`meta/scripts/extract_shell_functions.py`** — moves functions -brace-by-brace. **Never slice by line range**. **Do not trust where it puts -the source line** — verify with `grep -c` on the anchor and check placement. -The `--header` text is written **verbatim, uncommented** — pass a full -`#!/bin/bash\n# comment` string, not bare prose, or the generated lib won't -parse. +brace-by-brace. **Never slice by line range**. Verify placement with `grep -c` +on the anchor. **`meta/scripts/verify_shell_split.sh ...`** — -proves a split moved every function verbatim (hashes function bodies only, -blind to top-level code). For a **partial** split list the old path among the -new paths too, or it reports a false `DIFFERENCE`. A function that was -genuinely rewritten (not moved) will legitimately show as `DIFFERENCE` — -that's expected, not a bug; explain it in evidence rather than trying to make -the tool agree. +proves a split moved every function verbatim. For a **partial** split list +the old path among the new paths too. -**`meta/scripts/mutate_shell.py `** — mutation testing against specs in -`meta/scripts/fixtures/mutations/`. No spec exists for anything outside -`phone_focus_mode/`; optional, not required — 100% line coverage with real -assertions is the stated bar. +**`meta/scripts/mutate_shell.py `** — mutation testing; optional, not +required. -## 7. Testing pattern to follow +**Archiving a dead file/subsystem** (no dedicated script — hand-run each +time): clone to a scratch dir, `git filter-repo --force --path --path `, clone `testsAndMisc-archive` separately, add the filtered +clone as a remote, `git merge --allow-unrelated-histories`, remove stray +remote-tracking branches and the remote, push, verify with `gh api +repos/kuhyx/testsAndMisc-archive/contents/`, **then** `git rm` from +`testsAndMisc`. Read `docs/superpowers/evidence/archive-setup-hosts-guard-2026-08-18.json` +or `archive-plagiarism-tools-2026-08-18.json` for the full worked example. + +## 6. Testing pattern to follow (for an actual split, if one gets picked) The established pattern for `single_use/`-adjacent shell scripts with no -prior test coverage (now used across three splits): +prior test coverage (used across three prior splits): - A `lib/tests/` directory sibling to the split libs. - A `*_harness.sh` file (sourced, not executed) with `_t_pass`/`_t_fail`/ `_t_eq` helpers, `mktemp -d` + `trap cleanup EXIT`, and PATH-shim fakes for - any external tool the code shells out to (record invocation to a file, - minimum realistic behavior). + any external tool the code shells out to. - One `test_.sh` per lib, sourcing the harness, asserting against real function calls (not mocks at the call-site level). - A `test_.sh` that runs the **actual entry script as a - subprocess** against the same fakes, since `verify_shell_split.sh` and kcov - are blind to code that only lives in the entry script's `main()`. If this - test file threatens to grow past 250 lines (it will, once you're covering - several arg combinations and timing branches), split the fixture setup - into its own `*_entry_harness.sh` early and write two or more thinner test - files against it — this session had to do this mid-stream and it's - cheaper to plan for upfront. + subprocess** against the same fakes. If this test file threatens to grow + past 250 lines, split the fixture setup into its own `*_entry_harness.sh` + early. - Sharp edges hit and fixed across sessions, watch for all of them again: - - `local a="$1" b="${a}"` **fails under `set -u`** — bash evaluates all - RHS expressions in the new scope before any assignment lands. Split into - separate `local` statements. + - `local a="$1" b="${a}"` **fails under `set -u`** — split into separate + `local` statements. - A function that calls `exit` (not `return`) on a fatal condition will kill the whole test script if called directly inside an `if` condition. Run it in a subshell: `if (fn args); then ...`. - A backgrounded process needs `disown` before `kill`/`wait` reliably - affects only it, not the calling job — this bit both a `stop_load` test - and the entry script's SIGTERM test this session. + affects only it. - **SIGINT delivery to a backgrounded job was unreliable in this - environment; SIGTERM was not.** If a script's trap handles both signals - identically (`trap handler EXIT INT TERM`), prefer testing via SIGTERM. + environment; SIGTERM was not.** - If a script under test computes a real resource allocation (memory, disk) from live system state, make the relevant constants - env-overridable and pin them to something tiny in every test — never - trust "the box currently has enough headroom" to stay true. + env-overridable and pin them to something tiny in every test. -Check the target file for anything that shells out (pacman? systemctl? -journalctl? guardctl?) before assuming a prior session's fakes apply — each -file is a different domain, so the PATH-shim list will differ. +Check the target file for anything that shells out before assuming a prior +session's fakes apply — each file is a different domain. diff --git a/docs/superpowers/contracts/archive-setup-hosts-guard-2026-08-18.json b/docs/superpowers/contracts/archive-setup-hosts-guard-2026-08-18.json new file mode 100644 index 00000000..053f4f29 --- /dev/null +++ b/docs/superpowers/contracts/archive-setup-hosts-guard-2026-08-18.json @@ -0,0 +1,21 @@ +{ + "title": "Archive the legacy hosts-guard subsystem and repoint check_and_enable_services.sh at guard-lib", + "objective": "setup_hosts_guard.sh was NEXT_SESSION_PROMPT.md's next split target, but live-machine inspection showed the whole legacy hosts-guard subsystem it belongs to (17 files) has been fully superseded by guard-lib (migrate_hosts_guard_to_guard_lib.sh, run 2026-07-04). The user confirmed archiving the whole subsystem instead of splitting it, and confirmed fixing the one live script (check_and_enable_services.sh) that still checked and self-repaired against the retired system.", + "acceptance_criteria": [ + "All 17 legacy hosts-guard files (setup_hosts_guard.sh, enforce-hosts.sh, enforce-nsswitch.sh, enforce-resolved.sh, hosts-bind-mount.service, hosts-guard.path, hosts-guard.service, nsswitch-guard.path, nsswitch-guard.service, resolved-guard.path, resolved-guard.service, install_pacman_hooks.sh, pacman-hooks/hosts-guard-common.sh, pacman-hooks/pacman-post-relock-hosts.sh, pacman-hooks/pacman-pre-unlock-hosts.sh, psychological/unlock-hosts.sh, README.md, README_FOR_LLM.md) exist in kuhyx/testsAndMisc-archive with git history intact, including both pre-reorg path prefixes (hosts/guard/* and linux_configuration/hosts/guard/*).", + "The archive push completed and was verified live on GitHub via the API before any deletion happened in the main repo.", + "The same 17 files are removed from testsAndMisc via git rm; plugins/nsswitch-plugin.sh and plugins/resolved-plugin.sh (guard-lib's live plugins) are untouched.", + "check_and_enable_services.sh's check_hosts() no longer references any deleted file or legacy systemd unit name; it checks guard-lib instance health via guardctl file-guard status instead, and running it --status against the live (already-migrated) machine reports the hosts service as ok, not error.", + "linux_configuration/.github/copilot-instructions.md (always-loaded) contains no remaining references to the archived files or the legacy mechanism.", + "bash meta/scripts/check_file_length.sh --all reports fewer or equal over-cap files than before this session (12 -> 11), and check_and_enable_services.sh (already over cap, on the out-of-scope list) did not grow.", + "pre-commit run --files passes on the full staged set for both the deletion+edit commit and the evidence/contract commit." + ], + "out_of_scope": [ + "Splitting check_and_enable_services.sh itself -- it is on NEXT_SESSION_PROMPT.md §4's out-of-scope list; edited in place only.", + "Fixing the pre-existing defect where /etc/guard-lib/targets/{nsswitch,resolved}.conf point PLUGIN= at the repo checkout instead of /usr/local/share/guard-lib-plugins/ -- flagged in evidence risks[], not fixed (requires sudo mutation of live security enforcement + guardctl re-registration, not something the user was asked to approve this session).", + "Updating linux_configuration/docs/security-hardening/{01-current-state,03-implementation,04-agent-personas,05-llm-readmes}.md -- these predate the 2026-07-04 guard-lib migration itself and were already stale before this session; fixing them is a larger doc-accuracy pass outside what was scoped.", + "§4's other listed files (setup_midnight_shutdown.sh, generate_study_materials.sh, pacman_wrapper.sh, setup_night_lockdown.sh, install_leechblock.sh, block_compulsive_opening.sh, the Kotlin/Dart files) -- untouched, per standing scope.", + "Any test suite for the archived subsystem -- it never had one, and the standing 100%-coverage decision applies to code that is split, not code that is deleted/archived (same precedent as archive-plagiarism-tools and archive-libre-translate, neither of which added tests)." + ], + "verifier": "gh api repos/kuhyx/testsAndMisc-archive/contents/linux_configuration/scripts/periodic_background/hosts/guard confirming all 17 files present before the git rm commit; sudo bash check_and_enable_services.sh --status showing the Hosts File and Guards section all-OK against live guard-lib state; bash -n + shellcheck on check_and_enable_services.sh and hosts/install.sh; bash meta/scripts/check_file_length.sh --all; pre-commit run --files on the full staged set." +} diff --git a/docs/superpowers/evidence/archive-setup-hosts-guard-2026-08-18.json b/docs/superpowers/evidence/archive-setup-hosts-guard-2026-08-18.json new file mode 100644 index 00000000..317fff3e --- /dev/null +++ b/docs/superpowers/evidence/archive-setup-hosts-guard-2026-08-18.json @@ -0,0 +1,61 @@ +{ + "intent": "Archive the pre-guard-lib hosts-guard subsystem (setup_hosts_guard.sh and everything that only existed to support it), since guardctl/guard-lib has fully replaced it on this machine, then update the one live script that still checked and self-repaired against the retired system.", + "scope": [ + "linux_configuration/scripts/periodic_background/hosts/guard/{setup_hosts_guard.sh, enforce-hosts.sh, enforce-nsswitch.sh, enforce-resolved.sh, hosts-bind-mount.service, hosts-guard.path, hosts-guard.service, nsswitch-guard.path, nsswitch-guard.service, resolved-guard.path, resolved-guard.service, install_pacman_hooks.sh, pacman-hooks/*, psychological/unlock-hosts.sh, README.md, README_FOR_LLM.md} (deleted, moved to testsAndMisc-archive)", + "linux_configuration/scripts/periodic_background/hosts/guard/plugins/{nsswitch-plugin.sh, resolved-plugin.sh} left in place — these are guard-lib's live plugins, referenced by absolute path from /etc/guard-lib/targets/{nsswitch,resolved}.conf on this machine", + "linux_configuration/scripts/periodic_background/check_and_enable_services.sh check_hosts() rewritten to check guard-lib state (guardctl file-guard status) instead of the legacy systemd units/enforce-scripts/canonical-snapshot paths", + "linux_configuration/.github/copilot-instructions.md: 8 references to the archived files updated to describe the guard-lib mechanism instead", + "linux_configuration/scripts/periodic_background/hosts/install.sh: one comment describing enforcement mechanism updated (mentioned hosts-guard.path/enforce-hosts.sh/hosts-bind-mount.service by name)", + "Not in scope: linux_configuration/docs/security-hardening/{01-current-state,03-implementation,04-agent-personas,05-llm-readmes}.md — these are dated design/planning docs that already predated the 2026-07-04 guard-lib migration (i.e. were already stale before this session, not freshly broken by it); fixing them is a larger doc-accuracy pass the user did not scope in" + ], + "changes": [ + "Confirmed live-machine state before touching anything: legacy pacman hooks (10-unlock-etc-hosts.hook, 90-relock-etc-hosts.hook) absent from /etc/pacman.d/hooks/, legacy systemd units (hosts-guard.path/.service, nsswitch-guard.*, resolved-guard.*) all `disabled`, and /etc/guard-lib/targets/{hosts,nsswitch,resolved}.conf all populated — confirming the migration is complete and setup_hosts_guard.sh is dead code.", + "Asked the user (dead-code check-in per NEXT_SESSION_PROMPT.md §0/§0.5 lesson) whether to archive vs split; user confirmed archive.", + "Surveyed the whole hosts/guard/ directory and its cross-references (grep -rl) rather than archiving only the named file; found the legacy subsystem is 17 files across enforce-*.sh, systemd units, pacman-hooks/, psychological/, install_pacman_hooks.sh, and both READMEs, versus plugins/ (2 files) which is guard-lib's live plugin layer. Asked the user to confirm this larger scope before touching anything; confirmed.", + "Verified two potential blockers before archiving: hosts/install.sh's reference to enforce-hosts.sh is a descriptive comment only (not a live call — fixed for accuracy, not correctness); single_use/fixes/lib/hosts_guard_rollback.sh's do_rollback() restores from a captured $STATE_DIR snapshot and the deployed /usr/local/sbin/*.sh copies, not the repo checkout, so archiving the repo source does not break --rollback today.", + "Enumerated every historical path for the 17 files (git log --all --follow --name-only --diff-filter=A) to build a complete git filter-repo --path list, including two pre-reorg prefixes: hosts/guard/* (before the scripts/periodic_background/ reorg) and linux_configuration/hosts/guard/* (older still, before the scripts/ reorg — needed for enforce-resolved.sh and resolved-guard.path/.service specifically).", + "Cloned testsAndMisc into a throwaway scratchpad location (never a worktree — filter-repo rewrites refs and a worktree shares the object store), ran git filter-repo --force with the full 34-entry --path list, verified all 17 files present with history (17 relevant commits).", + "Cloned testsAndMisc-archive separately, added the filtered clone as a remote, merged with --allow-unrelated-histories, removed the stray remote-tracking branches (copilot-wt, copilot/worktree-*, fix-steam-backlog-needy-streamer) picked up from the source repo's other refs, removed the remote itself, then pushed to origin/main.", + "Verified the push landed via `gh api repos/kuhyx/testsAndMisc-archive/contents/.../hosts/guard` before touching the main repo — confirmed all 16 top-level entries (files + the 2 subdirs) present.", + "Only after the archive push was confirmed live: git rm the same 17 files from testsAndMisc.", + "Rewrote check_and_enable_services.sh's check_hosts(): replaced per-unit systemctl is-enabled/is-active checks and /usr/local/sbin/enforce-*.sh + /usr/local/share/locked-*.conf existence checks with a new guard_lib_instance_healthy() helper wrapping `guardctl file-guard status ` (checks 'path unit: active' and an immutable target attr) for the hosts/nsswitch/resolved instances. hosts_pacman_hooks_installed() lost its legacy-hook fallback branch (guard-lib hooks only, since they're confirmed installed). The repair path that used to re-run setup_hosts_guard.sh and install_pacman_hooks.sh now re-runs migrate_hosts_guard_to_guard_lib.sh instead, which the migration script's own header documents as idempotent. Removed the now-dead HOSTS_GUARD_SCRIPT/HOSTS_PACMAN_HOOKS_SCRIPT constants, added GUARD_LIB_MIGRATE_SCRIPT.", + "Updated linux_configuration/.github/copilot-instructions.md (an always-loaded instruction file) at all 8 locations that named archived files or the legacy mechanism by name, replacing them with the guard-lib equivalent (guardctl, migrate_hosts_guard_to_guard_lib.sh, the generic pacman hooks, plugins/)." + ], + "verification": [ + { + "command": "bash -n linux_configuration/scripts/periodic_background/check_and_enable_services.sh && shellcheck linux_configuration/scripts/periodic_background/check_and_enable_services.sh", + "result": "pass", + "evidence": "Syntax OK; shellcheck clean, no warnings." + }, + { + "command": "sudo bash linux_configuration/scripts/periodic_background/check_and_enable_services.sh --status", + "result": "pass", + "evidence": "Hosts File and Guards section: all 11 checks report [OK], including \"guard-lib 'hosts' instance is active and enforced\", \"guard-lib 'nsswitch' instance is active and enforced\", \"guard-lib 'resolved' instance is active and enforced\", and \"Pacman hooks installed\". Overall exit code 0; Summary table shows hosts=ok. Before this edit, the same run against the same live machine state would have reported hosts-guard.path/nsswitch-guard.path/resolved-guard.path as disabled (status=error) because those legacy checks were being run against a migrated machine — this is the actual bug this session's rewrite fixes, not just a cosmetic rename." + }, + { + "command": "gh api repos/kuhyx/testsAndMisc-archive/contents/linux_configuration/scripts/periodic_background/hosts/guard --jq '.[].name'", + "result": "pass", + "evidence": "Returned README.md, README_FOR_LLM.md, enforce-hosts.sh, enforce-nsswitch.sh, enforce-resolved.sh, hosts-bind-mount.service, hosts-guard.path, hosts-guard.service, install_pacman_hooks.sh, nsswitch-guard.path, nsswitch-guard.service, pacman-hooks, psychological, resolved-guard.path, resolved-guard.service, setup_hosts_guard.sh -- confirmed live on GitHub before the main-repo deletion commit was made." + }, + { + "command": "bash meta/scripts/check_file_length.sh --all", + "result": "pass", + "evidence": "11 files over cap (was 12 before this session — setup_hosts_guard.sh at 576 lines is gone). check_and_enable_services.sh is still over cap (1225 lines, was 1301) but net shrank; it is on NEXT_SESSION_PROMPT.md §4's explicit out-of-scope list for splitting, so being over cap is expected and unchanged in kind, just smaller." + }, + { + "command": "pre-commit run --files ", + "result": "pass", + "evidence": "All hooks passed except ai-evidence-contract (expected — this file is what satisfies it) and workflow-contract, addressed by the accompanying contracts/ file. shellcheck, shfmt-equivalent whitespace/EOF checks, codespell, secret scanning all clean." + } + ], + "risks": [ + "/etc/guard-lib/targets/{nsswitch,resolved}.conf point their PLUGIN= paths at the repo checkout (/home/kuhy/testsAndMisc/linux_configuration/scripts/periodic_background/hosts/guard/plugins/*.sh) instead of /usr/local/share/guard-lib-plugins/, which migrate_hosts_guard_to_guard_lib.sh's own header explicitly warns against ('an ABSOLUTE plugin path... pointing it at a repo checkout... silently breaks enforcement the day that directory moves'). /usr/local/share/guard-lib-plugins/ does not exist on this machine at all -- the migration's plugin-install step appears not to have run. This is a pre-existing defect, unrelated to and not caused by this session's archive (plugins/ was left in place specifically so this would keep working), but it means moving or renaming the testsAndMisc checkout will silently break nsswitch/resolved enforcement. Flagged for the user; not fixed in this commit since it requires a sudo mutation of a live security-enforcement system and guardctl re-registration, which is out of the two things the user approved this session.", + "linux_configuration/docs/security-hardening/{01-current-state,03-implementation,04-agent-personas,05-llm-readmes}.md still describe the pre-guard-lib system in detail and were not updated -- they were already stale before this session (guard-lib migration was 2026-07-04), so this is not a regression, but a reader following those docs today will find dead paths.", + "The 4 dated security-hardening docs above were left untouched by choice, not oversight -- see 'Not in scope' in scope[]." + ], + "rollback": [ + "In testsAndMisc: git revert the deletion commit to restore the 17 files (content/permissions unchanged from their last live commit, so a straight revert is safe) and git revert the check_and_enable_services.sh/copilot-instructions.md/install.sh edit commit to restore the legacy-aware checks.", + "The testsAndMisc-archive copy can stay regardless of rollback -- redundant, not harmful.", + "Restoring the legacy hosts-guard system itself (not just the repo files) requires sudo scripts/single_use/fixes/migrate_hosts_guard_to_guard_lib.sh --rollback, which is unaffected by this commit since it restores from /var/lib/guard-lib-migration/ and the already-deployed /usr/local/sbin/*.sh copies, not the repo checkout." + ] +} diff --git a/linux_configuration/.github/copilot-instructions.md b/linux_configuration/.github/copilot-instructions.md index 05958489..cc27707f 100644 --- a/linux_configuration/.github/copilot-instructions.md +++ b/linux_configuration/.github/copilot-instructions.md @@ -7,7 +7,7 @@ This repo automates Linux desktop bootstrap, hardening, and i3 setup. It’s pri - fresh-install/: end-to-end bootstrap for Arch/Ubuntu workstations. Reads package lists, configures pacman/makepkg, sets up GPU drivers, i3, hosts guard, pacman wrapper, and useful services. Example: `fresh-install/main.sh` orchestrates most steps and sources `detect_gpu*.sh`. - hosts/: manages a highly-opinionated `/etc/hosts` via StevenBlack upstream with custom edits, plus “guard” friction: - `hosts/install.sh` builds and locks `/etc/hosts` (immutable/append-only; selective unblocks; custom blocks). - - `hosts/guard/` installs enforcement: `enforce-hosts.sh`, path-watcher `hosts-guard.path` -> `hosts-guard.service`, optional RO bind mount, pacman hooks, and a delayed editor `psychological/unlock-hosts.sh`. + - `hosts/guard/` enforcement is provided by guard-lib (`guardctl`, installed outside this repo): a `file-guard` instance per target (`hosts`, `nsswitch`, `resolved`) with a path-watcher, optional RO bind mount, generic pacman hooks, and per-target plugins under `hosts/guard/plugins/`. The pre-guard-lib scripts (`setup_hosts_guard.sh`, `enforce-*.sh`, per-target systemd units, `psychological/unlock-hosts.sh`) are archived at github.com/kuhyx/testsAndMisc-archive — see `scripts/single_use/fixes/migrate_hosts_guard_to_guard_lib.sh` for how the migration works. - scripts/periodic_background/digital_wellbeing/pacman/: a policy-aware pacman wrapper with friction mechanics. - `pacman_wrapper.sh` intercepts transactions, runs hosts-guard pre/post hooks, handles stale db lock, auto-wires maintenance services, and enforces package policy (blocked/whitelisted lists); adds weekend-only “Steam” challenge and a VirtualBox challenge powered by `words.txt`. - `install_pacman_wrapper.sh` backs up `/usr/bin/pacman` to `pacman.orig` and symlinks to the wrapper. @@ -17,7 +17,7 @@ This repo automates Linux desktop bootstrap, hardening, and i3 setup. It’s pri ## Conventions you should follow -- Bash style: use `set -e` or `set -euo pipefail`, re-exec with sudo if not root, be idempotent, and log to `/var/log/*` with timestamps. Examples: `setup_periodic_system.sh`, `hosts/guard/setup_hosts_guard.sh`. +- Bash style: use `set -e` or `set -euo pipefail`, re-exec with sudo if not root, be idempotent, and log to `/var/log/*` with timestamps. Examples: `setup_periodic_system.sh`, `scripts/single_use/fixes/migrate_hosts_guard_to_guard_lib.sh`. - Install via templates: scripts under `scripts/periodic_background/system-maintenance/bin` and `.../systemd` are templates. The setup script substitutes placeholders like `__HOSTS_INSTALL_SCRIPT__` and `__PACMAN_WRAPPER_INSTALL__` before installing to `/usr/local/bin` and `/etc/systemd/system`. Don’t edit installed copies directly; modify templates and the setup script. - Package lists: `fresh-install/pacman_packages.txt` and `aur_packages.txt` treat any line not starting with lowercase alnum as a comment. @@ -29,20 +29,20 @@ This repo automates Linux desktop bootstrap, hardening, and i3 setup. It’s pri - Pacman wrapper only: `sudo scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh` (backs up pacman and wires the wrapper). The wrapper auto-runs hosts-guard pre/post hooks and can self-setup periodic services when missing. - Hosts guard: - `sudo hosts/install.sh` to (re)build `/etc/hosts` from cache/upstream then lock it. - - `sudo hosts/guard/setup_hosts_guard.sh` to install guard layers; then `hosts/guard/install_pacman_hooks.sh` to add pacman pre/post unlock hooks. - - To edit `/etc/hosts`: run `/usr/local/sbin/unlock-hosts` (delays, opens editor, re-applies protections). + - Guard layers (hosts/nsswitch/resolved) are managed by `guardctl file-guard `; see `scripts/single_use/fixes/migrate_hosts_guard_to_guard_lib.sh` to (re)install them. + - To edit a guarded file: `sudo guardctl file-guard unlock `, edit, then let the path-watcher re-lock it (or `guardctl file-guard enforce `). - i3 config: `i3-configuration/install.sh` (copies `i3` and `i3blocks`, adjusts font size; installs required tools conditionally for Arch/Ubuntu). ## Integration points and gotchas - Pacman interception: `pacman_wrapper.sh` sets `PACMAN_BIN=/usr/bin/pacman.orig` and symlinks `/usr/bin/pacman` -> wrapper. Keep this invariant when changing the wrapper. -- Hosts hooks: Wrapper calls `/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh` and `...post-relock-hosts.sh` if installed; keep paths stable or update both installer and wrapper. +- Hosts hooks: pacman transactions are unlocked/relocked by guard-lib's generic hooks (`/etc/pacman.d/hooks/10-guard-lib-unlock-all.hook`, `90-guard-lib-relock-all.hook`), which iterate every registered `file-guard` instance rather than being hosts-specific. - Logs: check `/var/log/periodic-system-maintenance.log` and `/var/log/hosts-file-monitor.log` for service behavior; timer and services live under `scripts/periodic_background/system-maintenance/systemd/` (templates). - Browser pre-exec: setup creates `/usr/local/bin/browser-preexec-wrapper` and symlinks common browser names to it; it silently re-runs the hosts installer before launching the real binary in `/usr/bin`. ## Patterns to reuse when adding features -- Follow the sudo re-exec + idempotent install pattern from `setup_periodic_system.sh` and `hosts/guard/setup_hosts_guard.sh`. +- Follow the sudo re-exec + idempotent install pattern from `setup_periodic_system.sh` and `scripts/single_use/fixes/migrate_hosts_guard_to_guard_lib.sh`. - Add new periodic behaviors as templates under `scripts/periodic_background/system-maintenance/bin` and `.../systemd`, then extend `setup_periodic_system.sh` to install/enable them. - Extend package policy by updating `scripts/periodic_background/digital_wellbeing/pacman/pacman_blocked_keywords.txt` or by adding `check_for_` + `prompt_for__challenge` blocks in the wrapper. - Run `scripts/meta/shell_check.sh` to detect things to fix before committing. @@ -51,7 +51,7 @@ This repo automates Linux desktop bootstrap, hardening, and i3 setup. It’s pri For in-depth understanding of specific components, see these dedicated guides: -- **Hosts Guard**: [hosts/guard/README_FOR_LLM.md](../hosts/guard/README_FOR_LLM.md) - Protection layers, canonical copies, path watchers +- **Hosts Guard**: [scripts/single_use/fixes/migrate_hosts_guard_to_guard_lib.sh](../scripts/single_use/fixes/migrate_hosts_guard_to_guard_lib.sh) header comment - guard-lib migration, protection layers, canonical copies, path watchers. The pre-guard-lib README is archived at github.com/kuhyx/testsAndMisc-archive. - **Pacman Wrapper**: [scripts/periodic_background/digital_wellbeing/pacman/README_FOR_LLM.md](../scripts/periodic_background/digital_wellbeing/pacman/README_FOR_LLM.md) - Policy files, integrity checks, challenges - **Midnight Shutdown**: [scripts/periodic_background/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md](../scripts/periodic_background/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md) - Schedule protection, timer system - **Compulsive Block**: [scripts/periodic_background/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md](../scripts/periodic_background/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md) - App launch limiting @@ -61,7 +61,7 @@ For in-depth understanding of specific components, see these dedicated guides: | Component | Purpose | Key Files | | ----------------- | ----------------------------- | --------------------------------------------------------------------------- | -| Hosts Guard | Block websites via /etc/hosts | `hosts/install.sh`, `hosts/guard/*` | +| Hosts Guard | Block websites via /etc/hosts | `hosts/install.sh`, `guardctl` (guard-lib), `hosts/guard/plugins/*` | | Pacman Wrapper | Block package installation | `scripts/periodic_background/digital_wellbeing/pacman/*` | | Midnight Shutdown | Auto-shutdown at night | `scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh` | | Compulsive Block | Limit app launches | `scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh` | diff --git a/linux_configuration/scripts/periodic_background/check_and_enable_services.sh b/linux_configuration/scripts/periodic_background/check_and_enable_services.sh index 8634db24..22585ddb 100755 --- a/linux_configuration/scripts/periodic_background/check_and_enable_services.sh +++ b/linux_configuration/scripts/periodic_background/check_and_enable_services.sh @@ -39,8 +39,7 @@ MIDNIGHT_SHUTDOWN_SCRIPT="$CONFIG_DIR/scripts/periodic_background/digital_wellbe STARTUP_MONITOR_SCRIPT="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/setup_pc_startup_monitor.sh" PERIODIC_SYSTEM_SCRIPT="$CONFIG_DIR/scripts/periodic_background/setup_periodic_system.sh" HOSTS_INSTALL_SCRIPT="$CONFIG_DIR/scripts/periodic_background/hosts/install.sh" -HOSTS_GUARD_SCRIPT="$CONFIG_DIR/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh" -HOSTS_PACMAN_HOOKS_SCRIPT="$CONFIG_DIR/scripts/periodic_background/hosts/guard/install_pacman_hooks.sh" +GUARD_LIB_MIGRATE_SCRIPT="$CONFIG_DIR/scripts/single_use/fixes/migrate_hosts_guard_to_guard_lib.sh" COMPULSIVE_BLOCK_SCRIPT="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh" LEECHBLOCK_SCRIPT="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/install_leechblock.sh" REMOVE_GUEST_MODE_SCRIPT="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/remove_guest_mode.sh" @@ -105,10 +104,20 @@ user_systemctl() { # # guard-lib pair as authoritative (see pacman_hooks_manage_guard_lib) — match it, # while still accepting the legacy pair if an older install is present. hosts_pacman_hooks_installed() { - { [[ -f /etc/pacman.d/hooks/10-guard-lib-unlock-all.hook ]] && - [[ -f /etc/pacman.d/hooks/90-guard-lib-relock-all.hook ]]; } || - { [[ -f /etc/pacman.d/hooks/10-unlock-etc-hosts.hook ]] && - [[ -f /etc/pacman.d/hooks/90-relock-etc-hosts.hook ]]; } + [[ -f /etc/pacman.d/hooks/10-guard-lib-unlock-all.hook ]] && + [[ -f /etc/pacman.d/hooks/90-guard-lib-relock-all.hook ]] +} + +# True if guard-lib's file-guard instance is installed AND healthy: +# the path unit is active and the target carries the immutable attribute. +# `guardctl file-guard status` exits 1 for an unregistered instance (missing +# /etc/guard-lib/targets/.conf), so a missing instance and an unhealthy +# one both fail this check without needing separate handling. +guard_lib_instance_healthy() { # + local name="$1" out + out=$(guardctl file-guard status "$name" 2>/dev/null) || return 1 + grep -q '^path unit: active$' <<<"$out" || return 1 + grep -q '^target attrs: .*i' <<<"$out" || return 1 } # Replay a drift manifest written at install time. The manifest holds plain @@ -625,50 +634,15 @@ check_hosts() { status="warning" fi - # Check hosts guard path watcher - if systemctl is-enabled hosts-guard.path &>/dev/null; then - msg "hosts-guard.path is enabled" + # Check the guard-lib "hosts" file-guard instance (path unit active + + # immutable attribute on the target). Replaces the legacy hosts-guard.path + # / hosts-bind-mount.service / enforce-hosts.sh checks now that this + # machine's migration to guard-lib is complete — see + # migrate_hosts_guard_to_guard_lib.sh. + if guard_lib_instance_healthy hosts; then + msg "guard-lib 'hosts' instance is active and enforced" else - issues+=("hosts-guard.path is not enabled") - status="error" - fi - - if systemctl is-active hosts-guard.path &>/dev/null; then - msg "hosts-guard.path is active" - else - issues+=("hosts-guard.path is not active") - status="warning" - fi - - # Check hosts bind mount service - if systemctl is-enabled hosts-bind-mount.service &>/dev/null; then - msg "hosts-bind-mount.service is enabled" - else - issues+=("hosts-bind-mount.service is not enabled") - status="warning" - fi - - # Check enforcement script - if [[ -f /usr/local/sbin/enforce-hosts.sh ]]; then - msg "Enforcement script exists at /usr/local/sbin/enforce-hosts.sh" - else - issues+=("enforce-hosts.sh not found") - status="error" - fi - - # Check unlock script - if [[ -f /usr/local/sbin/unlock-hosts ]]; then - msg "Unlock script exists at /usr/local/sbin/unlock-hosts" - else - issues+=("unlock-hosts not found") - status="warning" - fi - - # Check locked hosts snapshot - if [[ -f /usr/local/share/locked-hosts ]]; then - msg "Canonical hosts snapshot exists at /usr/local/share/locked-hosts" - else - issues+=("Canonical hosts snapshot not found") + issues+=("guard-lib 'hosts' instance is missing or unhealthy") status="error" fi @@ -695,27 +669,11 @@ check_hosts() { status="error" fi - # Check nsswitch guard - if systemctl is-enabled nsswitch-guard.path &>/dev/null; then - msg "nsswitch-guard.path is enabled" + # Check the guard-lib "nsswitch" file-guard instance + if guard_lib_instance_healthy nsswitch; then + msg "guard-lib 'nsswitch' instance is active and enforced" else - issues+=("nsswitch-guard.path is not enabled") - status="error" - fi - - # Check nsswitch enforcement script - if [[ -f /usr/local/sbin/enforce-nsswitch.sh ]]; then - msg "nsswitch enforcement script exists at /usr/local/sbin/enforce-nsswitch.sh" - else - issues+=("enforce-nsswitch.sh not found") - status="error" - fi - - # Check canonical nsswitch snapshot - if [[ -f /usr/local/share/locked-nsswitch.conf ]]; then - msg "Canonical nsswitch snapshot exists at /usr/local/share/locked-nsswitch.conf" - else - issues+=("Canonical nsswitch snapshot not found at /usr/local/share/locked-nsswitch.conf") + issues+=("guard-lib 'nsswitch' instance is missing or unhealthy") status="error" fi @@ -766,27 +724,11 @@ check_hosts() { [[ "$status" == "ok" ]] && status="warning" fi - # Check resolved guard - if systemctl is-enabled resolved-guard.path &>/dev/null; then - msg "resolved-guard.path is enabled" + # Check the guard-lib "resolved" file-guard instance + if guard_lib_instance_healthy resolved; then + msg "guard-lib 'resolved' instance is active and enforced" else - issues+=("resolved-guard.path is not enabled") - status="error" - fi - - # Check resolved enforcement script - if [[ -f /usr/local/sbin/enforce-resolved.sh ]]; then - msg "resolved enforcement script exists at /usr/local/sbin/enforce-resolved.sh" - else - issues+=("enforce-resolved.sh not found") - status="error" - fi - - # Check canonical resolved.conf snapshot - if [[ -f /usr/local/share/locked-resolved.conf ]]; then - msg "Canonical resolved.conf snapshot exists at /usr/local/share/locked-resolved.conf" - else - issues+=("Canonical resolved.conf snapshot not found at /usr/local/share/locked-resolved.conf") + issues+=("guard-lib 'resolved' instance is missing or unhealthy") status="error" fi @@ -881,46 +823,28 @@ check_hosts() { fi fi - # Run hosts guard setup (also installs nsswitch-guard and resolved-guard) - if ! systemctl is-enabled hosts-guard.path &>/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/sbin/enforce-nsswitch.sh ]] || - [[ ! -f /usr/local/sbin/enforce-resolved.sh ]] || - [[ ! -f /usr/local/share/locked-nsswitch.conf ]] || - [[ ! -f /usr/local/share/locked-resolved.conf ]]; then - note "Setting up hosts guard (includes nsswitch-guard and resolved-guard)..." - if [[ -f $HOSTS_GUARD_SCRIPT ]]; then - run bash "$HOSTS_GUARD_SCRIPT" + # Re-run the guard-lib migration if any instance is missing/unhealthy + # or the pacman hooks aren't installed. The migration script is + # documented as idempotent (see its header), so re-running it against + # an already-migrated, healthy machine is a no-op. + if ! guard_lib_instance_healthy hosts || + ! guard_lib_instance_healthy nsswitch || + ! guard_lib_instance_healthy resolved || + ! hosts_pacman_hooks_installed; then + note "Repairing guard-lib hosts/nsswitch/resolved instances..." + if [[ -f $GUARD_LIB_MIGRATE_SCRIPT ]]; then + run bash "$GUARD_LIB_MIGRATE_SCRIPT" ((FIXES_APPLIED++)) || true else - err_missing_script "Hosts guard script not found: $HOSTS_GUARD_SCRIPT" - fi - fi - - # Install pacman hooks if missing - if ! hosts_pacman_hooks_installed; then - note "Installing pacman hooks..." - if [[ -f $HOSTS_PACMAN_HOOKS_SCRIPT ]]; then - run bash "$HOSTS_PACMAN_HOOKS_SCRIPT" - ((FIXES_APPLIED++)) || true - else - err_missing_script "Pacman hooks script not found: $HOSTS_PACMAN_HOOKS_SCRIPT" + err_missing_script "Guard-lib migration script not found: $GUARD_LIB_MIGRATE_SCRIPT" fi fi # Re-verify after fixes if [[ $DRY_RUN -eq 0 ]]; then - if systemctl is-enabled hosts-guard.path &>/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/sbin/enforce-nsswitch.sh ]] && - [[ -f /usr/local/sbin/enforce-resolved.sh ]] && - [[ -f /usr/local/share/locked-hosts ]] && - [[ -f /usr/local/share/locked-nsswitch.conf ]] && - [[ -f /usr/local/share/locked-resolved.conf ]] && + if guard_lib_instance_healthy hosts && + guard_lib_instance_healthy nsswitch && + guard_lib_instance_healthy resolved && hosts_pacman_hooks_installed; then # Downgrade to warning if only minor issues remain (immutable attr, etc.) status="ok" diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/README.md b/linux_configuration/scripts/periodic_background/hosts/guard/README.md deleted file mode 100644 index f9dd70b4..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Hosts Guard Components - -This directory contains templates for hardening /etc/hosts against impulsive tampering by adding friction, NOT providing absolute security against a determined root user. - -Components: - -1. enforce-hosts.sh – Idempotent script that: compares /etc/hosts with canonical copy at /usr/local/share/locked-hosts and restores if different; reapplies immutable attribute. -2. systemd units (to be installed under /etc/systemd/system): - - hosts-guard.service (oneshot enforcement) - - hosts-guard.path (triggers on PathChanged=/etc/hosts) - - hosts-bind-mount.service (bind mounts /etc/hosts read-only after boot) -3. psychological/ directory – scripts that add delay + journaling before allowing a maintenance/unlock operation. -4. pacman hooks – automatically unlock/re-lock /etc/hosts around package transactions so pacman never fails due to the read-only bind mount. - -Install Flow (suggested): - -1. After generating /etc/hosts via your existing hosts/install.sh, copy it to /usr/local/share/locked-hosts. -2. Install enforce-hosts.sh to /usr/local/sbin/ (chmod 755). -3. Place units and enable: - systemctl daemon-reload - systemctl enable --now hosts-guard.path - systemctl enable --now hosts-bind-mount.service -4. (Optional) Use psychological/unlock-hosts.sh as the ONLY sanctioned way to modify hosts (it removes protections temporarily, launches an editor after a delay, and re-enforces on close). -5. Make pacman automatic (recommended): - ./install_pacman_hooks.sh - This installs hooks under /etc/pacman.d/hooks that: - - PreTransaction: temporarily disable guard and make /etc/hosts writable - - PostTransaction: re-run enforcement and re-enable guard (bind mount + path watcher) - -Limitations: - -- A root user can still disable units, remount, remove attributes. -- Purpose is to interrupt habit loops and create intentional friction. diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/README_FOR_LLM.md b/linux_configuration/scripts/periodic_background/hosts/guard/README_FOR_LLM.md deleted file mode 100644 index 08751af6..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/README_FOR_LLM.md +++ /dev/null @@ -1,221 +0,0 @@ -# Hosts Guard System - LLM Reference Guide - -> **For AI assistants**: This document explains how the hosts guard system works so you can make correct modifications. - -## System Purpose - -Prevent tampering with `/etc/hosts` to maintain website blocking (YouTube, social media, etc.) as part of a digital wellbeing system. - -## Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ PROTECTION LAYERS │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ Layer 1: Immutable Attribute │ -│ ───────────────────────────── │ -│ /etc/hosts has chattr +i (cannot be modified even by root) │ -│ │ -│ Layer 2: Canonical Copy │ -│ ─────────────────────── │ -│ /usr/local/share/locked-hosts contains the "true" version │ -│ If /etc/hosts differs, it gets overwritten from this copy │ -│ │ -│ Layer 3: Path Watcher (systemd) │ -│ ────────────────────────────── │ -│ hosts-guard.path watches /etc/hosts for ANY change │ -│ hosts-guard.service runs enforce-hosts.sh when triggered │ -│ │ -│ Layer 4: Read-Only Bind Mount │ -│ ──────────────────────────── │ -│ hosts-bind-mount.service mounts /etc/hosts read-only │ -│ Even if chattr is removed, write operations fail │ -│ │ -│ Layer 5: Custom Entries Protection │ -│ ───────────────────────────────── │ -│ /etc/hosts.custom-entries.state tracks blocked domains │ -│ Prevents removal of domains from install.sh │ -│ │ -│ Layer 6: nsswitch.conf Protection (NEW) │ -│ ─────────────────────────────────────── │ -│ Prevents bypass via /etc/nsswitch.conf manipulation │ -│ Ensures "files" always appears in hosts: line before "dns" │ -│ nsswitch-guard.path watches for changes │ -│ Canonical copy at /usr/local/share/locked-nsswitch.conf │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -## File Locations - -| File | Purpose | Protection | -| ---------------------------------------------- | ----------------------------- | ----------------------- | -| `/etc/hosts` | Active hosts file | chattr +i, bind mount | -| `/usr/local/share/locked-hosts` | Canonical source of truth | chattr +i | -| `/etc/hosts.custom-entries.state` | Tracks custom blocked domains | chattr +i | -| `/etc/hosts.stevenblack` | Cached upstream hosts file | None | -| `/etc/nsswitch.conf` | Name service switch config | chattr +i, path watcher | -| `/usr/local/share/locked-nsswitch.conf` | Canonical nsswitch copy | chattr +i | -| `/usr/local/sbin/enforce-hosts.sh` | Restoration script | File permissions | -| `/usr/local/sbin/enforce-nsswitch.sh` | nsswitch enforcement | File permissions | -| `/usr/local/sbin/unlock-hosts` | Psychological unlock script | File permissions | -| `/etc/systemd/system/hosts-guard.path` | Path watcher unit | systemd | -| `/etc/systemd/system/hosts-guard.service` | Enforcement service | systemd | -| `/etc/systemd/system/hosts-bind-mount.service` | RO bind mount | systemd | -| `/etc/systemd/system/nsswitch-guard.path` | nsswitch watcher | systemd | -| `/etc/systemd/system/nsswitch-guard.service` | nsswitch enforce | systemd | - -## Key Scripts - -### hosts/install.sh - -- Downloads StevenBlack hosts list (cached at `/etc/hosts.stevenblack`) -- Adds custom blocking entries (YouTube, etc.) -- Comments out allowed sites (4chan, Facebook) -- Runs protection check for custom entries -- Sets up initial immutable attribute - -### hosts/guard/setup_hosts_guard.sh - -Installs all protection layers: - -- Creates canonical snapshot -- Installs enforce-hosts.sh and unlock-hosts scripts -- Enables systemd path watcher -- Enables bind mount service -- Installs shell history suppression hooks - -### hosts/guard/enforce-hosts.sh - -Called when tampering detected: - -```bash -# Compares /etc/hosts to canonical -# If different: restores from canonical, logs event -# Re-applies chattr +i -``` - -### hosts/guard/psychological/unlock-hosts.sh - -Legitimate edit workflow: - -1. Prompts for reason (logged) -2. Stops protection services -3. Waits 45 seconds (cooling off) -4. Opens editor -5. Updates canonical if changes made -6. Re-enables all protections - -## Pacman Integration - -The pacman wrapper calls these hooks during package transactions: - -- `/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh` - Before transaction -- `/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh` - After transaction - -These temporarily unlock hosts for package manager operations. - -## Common Tasks - -### Adding a New Blocked Domain - -1. Edit `hosts/install.sh` -2. Find the heredoc section after `# Custom blocking entries` -3. Add line: `0.0.0.0 newdomain.com` -4. Run: `sudo ~/linux-configuration/hosts/install.sh` - -```bash -# Example: Block example.com -# In hosts/install.sh, add to heredoc: -0.0.0.0 example.com -0.0.0.0 www.example.com -``` - -### Allowing a Previously Blocked Domain - -**This is intentionally difficult.** You must: - -1. Remove entry from install.sh heredoc -2. Remove protection: `sudo chattr -i /etc/hosts.custom-entries.state` -3. Edit state file to remove domain -4. Re-run install.sh - -### Checking Protection Status - -```bash -# Check immutable attribute -lsattr /etc/hosts -# Should show: ----i--------e-- /etc/hosts - -# Check services -systemctl status hosts-guard.path hosts-guard.service hosts-bind-mount.service - -# Check canonical exists -ls -la /usr/local/share/locked-hosts -``` - -### Legitimate Editing - -```bash -sudo /usr/local/sbin/unlock-hosts -# Enter reason when prompted -# Wait 45 seconds -# Edit in your $EDITOR -# Changes auto-saved to canonical -``` - -## nsswitch.conf Protection (Layer 6) - -**Why this matters:** A user could bypass ALL /etc/hosts protections by simply editing `/etc/nsswitch.conf` and removing `files` from the `hosts:` line. This protection layer prevents that. - -### How it works: - -- `nsswitch-guard.path` watches `/etc/nsswitch.conf` for changes -- `nsswitch-guard.service` runs `enforce-nsswitch.sh` when triggered -- Canonical copy stored at `/usr/local/share/locked-nsswitch.conf` -- Validates that `hosts:` line contains `files` before `dns` -- Auto-restores from canonical if tampered - -### Check nsswitch protection status: - -```bash -lsattr /etc/nsswitch.conf -systemctl status nsswitch-guard.path -``` - -## Troubleshooting - -### "Cannot modify /etc/hosts" - -This is expected! Use the unlock script: - -```bash -sudo /usr/local/sbin/unlock-hosts -``` - -### Path watcher not running - -```bash -sudo systemctl start hosts-guard.path -sudo systemctl enable hosts-guard.path -``` - -### Bind mount preventing access - -```bash -# Temporarily disable (not recommended) -sudo systemctl stop hosts-bind-mount.service -``` - -### Custom entries protection blocking install - -The protection mechanism detected you're trying to remove previously blocked domains. This is intentional. To proceed, manually edit the state file (see "Allowing a Previously Blocked Domain"). - -## DO NOT - -1. ❌ Edit `/etc/nsswitch.conf` to bypass hosts (this defeats the purpose) -2. ❌ Stop `hosts-guard.path` without understanding consequences -3. ❌ Delete `/usr/local/share/locked-hosts` (breaks restoration) -4. ❌ Remove entries from install.sh without updating state file -5. ❌ Use `chattr -i` without going through unlock-hosts diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/enforce-hosts.sh b/linux_configuration/scripts/periodic_background/hosts/guard/enforce-hosts.sh deleted file mode 100755 index 2eba90ad..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/enforce-hosts.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# Template guard script to enforce canonical /etc/hosts -# This will be installed into /usr/local/sbin/enforce-hosts.sh by a setup script. - -set -euo pipefail - -CANONICAL_SOURCE="/usr/local/share/locked-hosts" -TARGET="/etc/hosts" -LOG_FILE="/var/log/hosts-guard.log" - -log() { - printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE" >&2 -} - -if [[ ! -f $CANONICAL_SOURCE ]]; then - log "Canonical hosts not found at $CANONICAL_SOURCE; aborting enforcement" - exit 0 -fi - -if ! cmp -s "$CANONICAL_SOURCE" "$TARGET"; then - log "Difference detected – restoring $TARGET from canonical copy" - cp "$CANONICAL_SOURCE" "$TARGET" - chmod 644 "$TARGET" -else - log "No drift detected (contents identical)" -fi - -# Re-apply protective attributes: immutable first, then read-only bind mount handled by separate unit -chattr -i -a "$TARGET" 2> /dev/null || true -chattr +i "$TARGET" || log "Failed to set immutable attribute" - -log "Enforcement complete" diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/enforce-nsswitch.sh b/linux_configuration/scripts/periodic_background/hosts/guard/enforce-nsswitch.sh deleted file mode 100755 index e751a8a5..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/enforce-nsswitch.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/bin/bash -# Template guard script to enforce canonical /etc/nsswitch.conf -# Ensures "hosts:" line always contains "files" before "dns" -# This prevents bypassing /etc/hosts by removing "files" from nsswitch.conf -# Installed to /usr/local/sbin/enforce-nsswitch.sh by setup_hosts_guard.sh - -set -euo pipefail - -CANONICAL_SOURCE="/usr/local/share/locked-nsswitch.conf" -TARGET="/etc/nsswitch.conf" -LOG_FILE="/var/log/nsswitch-guard.log" - -log() { - printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE" >&2 -} - -# Function to validate that "hosts:" line has correct format -# Must contain "files" before "dns" (or "dns" not present) -validate_hosts_line() { - local line="$1" - - # Check if "files" is present - if ! echo "$line" | grep -qw "files"; then - return 1 - fi - - # If dns is present, files must come before it - if echo "$line" | grep -qw "dns"; then - local files_pos dns_pos - files_pos=$(echo "$line" | grep -bo '\bfiles\b' | head -1 | cut -d: -f1) - dns_pos=$(echo "$line" | grep -bo '\bdns\b' | head -1 | cut -d: -f1) - if [[ -n "$files_pos" && -n "$dns_pos" && "$files_pos" -gt "$dns_pos" ]]; then - return 1 - fi - fi - - return 0 -} - -# Check current nsswitch.conf hosts line -current_hosts_line=$(grep '^hosts:' "$TARGET" 2>/dev/null || echo "") - -if [[ -z "$current_hosts_line" ]]; then - log "CRITICAL: No 'hosts:' line found in $TARGET - restoring from canonical" - 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 attribute on $TARGET" - log "Restored $TARGET from canonical copy" - else - log "ERROR: Canonical source not found at $CANONICAL_SOURCE" - exit 1 - fi - exit 0 -fi - -if ! validate_hosts_line "$current_hosts_line"; then - log "TAMPERING DETECTED: 'hosts:' line is invalid or missing 'files' before 'dns'" - log "Current line: $current_hosts_line" - - 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 attribute on $TARGET" - log "Restored $TARGET from canonical copy" - else - log "ERROR: Canonical source not found at $CANONICAL_SOURCE" - # Emergency fix: add "files" back to hosts line - chattr -i "$TARGET" 2>/dev/null || true - if grep -q '^hosts:.*dns' "$TARGET"; then - sed -i 's/^hosts:\(.*\)dns/hosts:\1files dns/' "$TARGET" - elif grep -q '^hosts:.*resolve' "$TARGET"; then - sed -i 's/^hosts:\(.*\)resolve/hosts: files\1resolve/' "$TARGET" - else - sed -i 's/^hosts:/hosts: files/' "$TARGET" - fi - chattr +i "$TARGET" 2>/dev/null || true - log "Emergency fix applied: added 'files' to hosts line" - fi - exit 0 -fi - -# If canonical exists, check for any drift -if [[ -f "$CANONICAL_SOURCE" ]]; then - if ! cmp -s "$CANONICAL_SOURCE" "$TARGET"; then - log "Drift detected in $TARGET (but hosts line valid) - 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 attribute" - log "Restored $TARGET from canonical copy" - 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 - -# Ensure immutable attribute is set -chattr -i "$TARGET" 2>/dev/null || true -chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute on $TARGET" - -log "nsswitch.conf enforcement complete" diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/enforce-resolved.sh b/linux_configuration/scripts/periodic_background/hosts/guard/enforce-resolved.sh deleted file mode 100755 index fc67cffd..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/enforce-resolved.sh +++ /dev/null @@ -1,147 +0,0 @@ -#!/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/scripts/periodic_background/hosts/guard/hosts-bind-mount.service b/linux_configuration/scripts/periodic_background/hosts/guard/hosts-bind-mount.service deleted file mode 100644 index 998c2f4d..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/hosts-bind-mount.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=Bind mount /etc/hosts over itself as read-only (friction layer) -After=local-fs.target -Before=network.target - -[Service] -Type=oneshot -ExecStart=/bin/mount --bind /etc/hosts /etc/hosts -ExecStart=/bin/mount -o remount,ro,bind /etc/hosts -ExecStartPost=/usr/bin/logger -t hosts-bind-mount "Hosts file bind-mounted read-only" -RemainAfterExit=yes - -[Install] -WantedBy=multi-user.target diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/hosts-guard.path b/linux_configuration/scripts/periodic_background/hosts/guard/hosts-guard.path deleted file mode 100644 index 0e3371cc..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/hosts-guard.path +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Watch /etc/hosts and trigger enforcement - -[Path] -PathChanged=/etc/hosts -Unit=hosts-guard.service - -[Install] -WantedBy=multi-user.target diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/hosts-guard.service b/linux_configuration/scripts/periodic_background/hosts/guard/hosts-guard.service deleted file mode 100644 index 0cf52b94..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/hosts-guard.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Enforce canonical /etc/hosts contents -After=local-fs.target - -[Service] -Type=oneshot -ExecStart=/usr/local/sbin/enforce-hosts.sh -Nice=10 -IOSchedulingClass=idle - -[Install] -WantedBy=multi-user.target diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/install_pacman_hooks.sh b/linux_configuration/scripts/periodic_background/hosts/guard/install_pacman_hooks.sh deleted file mode 100755 index e7f7b19d..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/install_pacman_hooks.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -require_root() { if [[ $EUID -ne 0 ]]; then exec sudo -E bash "$0" "$@"; fi; } -require_root "$@" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -HOOKS_DIR="/etc/pacman.d/hooks" - -install -d -m 755 "$HOOKS_DIR" - -# Pre-transaction hook -cat > "$HOOKS_DIR/10-unlock-etc-hosts.hook" << 'HOOK' -[Trigger] -Operation = Upgrade -Operation = Install -Operation = Remove -Type = Package -Target = * - -[Action] -Description = Temporarily unlocking /etc/hosts for transaction -When = PreTransaction -Exec = /bin/bash /usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh -NeedsTargets -HOOK - -# Post-transaction hook -cat > "$HOOKS_DIR/90-relock-etc-hosts.hook" << 'HOOK' -[Trigger] -Operation = Upgrade -Operation = Install -Operation = Remove -Type = Package -Target = * - -[Action] -Description = Re-locking /etc/hosts after transaction -When = PostTransaction -Exec = /bin/bash /usr/local/share/hosts-guard/pacman-post-relock-hosts.sh -NeedsTargets -HOOK - -# Place helper scripts into a shared location -install -d -m 755 /usr/local/share/hosts-guard -install -m 755 "$SCRIPT_DIR/pacman-hooks/hosts-guard-common.sh" /usr/local/share/hosts-guard/ -install -m 755 "$SCRIPT_DIR/pacman-hooks/pacman-pre-unlock-hosts.sh" /usr/local/share/hosts-guard/ -install -m 755 "$SCRIPT_DIR/pacman-hooks/pacman-post-relock-hosts.sh" /usr/local/share/hosts-guard/ - -echo "Pacman hooks installed into $HOOKS_DIR." diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/nsswitch-guard.path b/linux_configuration/scripts/periodic_background/hosts/guard/nsswitch-guard.path deleted file mode 100644 index 769d6986..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/nsswitch-guard.path +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Watch /etc/nsswitch.conf for tampering (hosts bypass protection) - -[Path] -PathChanged=/etc/nsswitch.conf -Unit=nsswitch-guard.service - -[Install] -WantedBy=multi-user.target diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/nsswitch-guard.service b/linux_configuration/scripts/periodic_background/hosts/guard/nsswitch-guard.service deleted file mode 100644 index 448b5e36..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/nsswitch-guard.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Enforce canonical /etc/nsswitch.conf (prevents hosts bypass) -After=local-fs.target - -[Service] -Type=oneshot -ExecStart=/usr/local/sbin/enforce-nsswitch.sh -Nice=10 -IOSchedulingClass=idle - -[Install] -WantedBy=multi-user.target diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/pacman-hooks/hosts-guard-common.sh b/linux_configuration/scripts/periodic_background/hosts/guard/pacman-hooks/hosts-guard-common.sh deleted file mode 100755 index 8f329f08..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/pacman-hooks/hosts-guard-common.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash -# Shared functions for hosts-guard pacman hooks -# 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 - -require_root() { - if [[ $EUID -ne 0 ]]; then - echo "hosts-guard pacman hook must run as root" >&2 - exit 1 - fi -} - -# Check if target has a read-only mount -is_ro_mount() { - findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro -} - -# Count mount layers for the target -mount_layers_count() { - awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0 -} - -# Collapse all bind mount layers -collapse_mounts() { - local i=0 - if command -v mountpoint >/dev/null 2>&1; then - while mountpoint -q "$TARGET"; do - umount -l "$TARGET" >/dev/null 2>&1 || break - i=$((i + 1)) - ((i > 20)) && break - done - else - local cnt - cnt=$(mount_layers_count) - while ((cnt > 1)); do - umount -l "$TARGET" >/dev/null 2>&1 || break - i=$((i + 1)) - ((i > 20)) && break - cnt=$(mount_layers_count) - done - fi -} - -# Stop systemd units related to hosts guard -stop_units_if_present() { - 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 - systemctl stop "$u" >/dev/null 2>&1 || true - fi - fi - done -} - -# 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 -} - -# 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 -} - -# Apply a single read-only bind mount layer -apply_ro_bind_mount() { - mount --bind "$TARGET" "$TARGET" >/dev/null 2>&1 || true - mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true -} - -# 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 -} - -# Log to system logger and run log file -log_hook() { - local phase="$1" - local state="$2" - logger -t "$LOGTAG" "$phase: $state" - echo "$(date -Is) $phase-$state" >>/run/hosts-guard-hook.log 2>/dev/null || true -} diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh b/linux_configuration/scripts/periodic_background/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh deleted file mode 100755 index d82cd6f5..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -# 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 -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=hosts-guard-common.sh -source "$SCRIPT_DIR/hosts-guard-common.sh" - -require_root - -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 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 (immutable on all guarded files) -apply_immutable -apply_ro_bind_mount - -# Start all path watchers -start_path_watcher - -log_hook "post" "relocking(done)" - -exit 0 diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh b/linux_configuration/scripts/periodic_background/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh deleted file mode 100755 index 332281d0..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -# 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 -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=hosts-guard-common.sh -source "$SCRIPT_DIR/hosts-guard-common.sh" - -require_root - -log_hook "pre" "unlocking(start)" - -# Remove protective attributes from all guarded files -remove_all_guard_attrs - -# Stop guard services (hosts, nsswitch, resolved watchers) -stop_units_if_present - -# Collapse any existing mount layers -collapse_mounts - -# Ensure writable by remounting if still read-only -if is_ro_mount; then - mount -o remount,rw "$TARGET" >/dev/null 2>&1 || collapse_mounts -fi - -log_hook "pre" "unlocking(done)" - -exit 0 diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/psychological/unlock-hosts.sh b/linux_configuration/scripts/periodic_background/hosts/guard/psychological/unlock-hosts.sh deleted file mode 100755 index 62a781c8..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/psychological/unlock-hosts.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash -# Guided, delayed unlock procedure to intentionally slow down impulsive edits. -set -euo pipefail - -TARGET=/etc/hosts -CANON=/usr/local/share/locked-hosts -LOG=/var/log/hosts-guard.log -SYSLOG_TAG=hosts-unlock -EDITOR_CMD=${EDITOR:-nano} -DELAY_SECONDS=45 - -log() { printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG" >&2; } - -require_root() { if [[ $EUID -ne 0 ]]; then exec sudo -E bash "$0" "$@"; fi; } -require_root "$@" - -echo "Reason for editing /etc/hosts (will be logged):" >&2 -read -r -p "Enter reason: " REASON -if [[ -z ${REASON// /} ]]; then - echo "Empty reason not allowed. Aborting." >&2 - exit 1 -fi -log "Requested intentional /etc/hosts modification session. Reason: $REASON" -logger -t "$SYSLOG_TAG" "session_start user=${SUDO_USER:-$USER} reason='$REASON'" -echo "This action is logged. A cooling-off delay of $DELAY_SECONDS seconds applies." >&2 - -for s in hosts-bind-mount.service hosts-guard.path; do - if systemctl is-active --quiet "$s"; then - log "Stopping $s" - systemctl stop "$s" || true - fi - if systemctl is-enabled --quiet "$s"; then - log "(Will re-enable later)" - fi -done - -# Remove attributes to allow edit -chattr -i -a "$TARGET" 2>/dev/null || true - -echo "Countdown:" >&2 -for ((i = DELAY_SECONDS; i > 0; i--)); do - printf '\rEdit window opens in %2d seconds... Press Ctrl+C to abort.' "$i" >&2 - sleep 1 -done -echo >&2 - -# Launch editor -sha_before=$(sha256sum "$TARGET" | awk '{print $1}') -"$EDITOR_CMD" "$TARGET" -sha_after=$(sha256sum "$TARGET" | awk '{print $1}') - -if [[ $sha_before == "$sha_after" ]]; then - log "No changes made to $TARGET. Reason: $REASON" - logger -t "$SYSLOG_TAG" "no_change user=${SUDO_USER:-$USER} reason='$REASON'" -else - log "Changes detected. Updating canonical copy and re-enforcing. Reason: $REASON" - logger -t "$SYSLOG_TAG" "modified user=${SUDO_USER:-$USER} reason='$REASON'" - cp "$TARGET" "$CANON" -fi - -# Re-run enforcement -/usr/local/sbin/enforce-hosts.sh || log "Enforcement script returned non-zero" - -# Restart watchers and bind mount -systemctl start hosts-guard.path || true -systemctl start hosts-bind-mount.service || true - -log "Unlock session complete. Reason: $REASON" -logger -t "$SYSLOG_TAG" "session_end user=${SUDO_USER:-$USER} reason='$REASON'" -echo "Done." >&2 diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/resolved-guard.path b/linux_configuration/scripts/periodic_background/hosts/guard/resolved-guard.path deleted file mode 100644 index febd6e66..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/resolved-guard.path +++ /dev/null @@ -1,10 +0,0 @@ -[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/scripts/periodic_background/hosts/guard/resolved-guard.service b/linux_configuration/scripts/periodic_background/hosts/guard/resolved-guard.service deleted file mode 100644 index c5714909..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/resolved-guard.service +++ /dev/null @@ -1,12 +0,0 @@ -[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/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh b/linux_configuration/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh deleted file mode 100755 index 9db4b6d4..00000000 --- a/linux_configuration/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh +++ /dev/null @@ -1,576 +0,0 @@ -#!/bin/bash -# One-shot installer for hosts guard + psychological friction + read-only bind mount -# Layers implemented: -# - Canonical snapshot of /etc/hosts at /usr/local/share/locked-hosts -# - Enforcement script (/usr/local/sbin/enforce-hosts.sh) -# - Systemd path-based auto-revert (hosts-guard.path + hosts-guard.service) -# - Read-only bind mount (hosts-bind-mount.service) -# - Delayed edit workflow (/usr/local/sbin/unlock-hosts) -# -# This script is idempotent. Re-running updates installed artifacts safely. -# -# Usage: -# sudo ./setup_hosts_guard.sh [options] -# Options: -# --force-snapshot Overwrite canonical snapshot even if it exists -# --no-snapshot Skip creating canonical snapshot (assume already present) -# --skip-bind Do not enable read-only bind mount service -# --skip-path-watch Do not enable path watch auto-revert -# --delay N Set unlock delay seconds (default 45) -# --dry-run Show actions without performing changes -# --uninstall Remove installed units/scripts (does NOT restore original hosts) -# -h|--help Show help -# -# Exit codes: -# 0 success, 1 generic failure, 2 argument error - -set -euo pipefail - -###################################################################### -# Defaults / Config -###################################################################### -FORCE_SNAPSHOT=0 -DO_SNAPSHOT=1 -ENABLE_BIND=1 -ENABLE_PATH=1 -ENABLE_NSSWITCH=1 -ENABLE_RESOLVED=1 -UNINSTALL=0 -DELAY=45 -DRY_RUN=0 -INSTALL_SHELL_HOOKS=1 -INSTALL_AUDIT_RULE=1 -ADD_ALIAS_STUB=1 - -###################################################################### -# Helpers -###################################################################### -msg() { printf '\e[1;32m[+]\e[0m %s\n' "$*"; } -note() { printf '\e[1;34m[i]\e[0m %s\n' "$*"; } -warn() { printf '\e[1;33m[!]\e[0m %s\n' "$*"; } -err() { printf '\e[1;31m[x]\e[0m %s\n' "$*" >&2; } -run() { - if [[ $DRY_RUN -eq 1 ]]; then - printf 'DRY-RUN:' - if [ "$#" -gt 0 ]; then - printf ' %q' "$@" - fi - printf '\n' - else - "$@" - fi -} - -require_root() { if [[ $EUID -ne 0 ]]; then exec sudo -E bash "$0" "$@"; fi; } - -usage() { sed -n '1,/^set -euo pipefail/p' "$0" | sed 's/^# \{0,1\}//'; } - -###################################################################### -# Parse args -###################################################################### -while [[ $# -gt 0 ]]; do - case "$1" in - --force-snapshot) - FORCE_SNAPSHOT=1 - shift - ;; - --no-snapshot) - DO_SNAPSHOT=0 - shift - ;; - --skip-bind) - ENABLE_BIND=0 - shift - ;; - --skip-path-watch) - ENABLE_PATH=0 - shift - ;; - --skip-nsswitch) - ENABLE_NSSWITCH=0 - shift - ;; - --skip-resolved) - ENABLE_RESOLVED=0 - shift - ;; - --delay) - DELAY=${2:-} - [[ -z ${DELAY} ]] && { - err '--delay requires value' - exit 2 - } - shift 2 - ;; - --dry-run) - DRY_RUN=1 - shift - ;; - --no-shell-hooks) - INSTALL_SHELL_HOOKS=0 - shift - ;; - --shell-hooks) - INSTALL_SHELL_HOOKS=1 - shift - ;; - --no-audit) - INSTALL_AUDIT_RULE=0 - shift - ;; - --audit) - INSTALL_AUDIT_RULE=1 - shift - ;; - --no-alias-stub) - ADD_ALIAS_STUB=0 - shift - ;; - --alias-stub) - ADD_ALIAS_STUB=1 - shift - ;; - --uninstall) - UNINSTALL=1 - shift - ;; - -h | --help) - usage - exit 0 - ;; - *) - err "Unknown argument: $1" - usage - exit 2 - ;; - esac -done - -require_root "$@" - -###################################################################### -# Paths -###################################################################### -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" - -TEMPLATE_ENFORCE="$SCRIPT_DIR/enforce-hosts.sh" -TEMPLATE_UNLOCK="$SCRIPT_DIR/psychological/unlock-hosts.sh" -UNIT_GUARD_SERVICE="$SCRIPT_DIR/hosts-guard.service" -UNIT_GUARD_PATH="$SCRIPT_DIR/hosts-guard.path" -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" -BASH_FILTER_SNIPPET="/etc/profile.d/hosts_guard_history_filter.sh" - -SYSTEMD_DIR="/etc/systemd/system" - -###################################################################### -# Uninstall flow -###################################################################### -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 resolved-guard.path resolved-guard.service; do - if systemctl list-unit-files | grep -q "^$u"; then - run systemctl disable --now "$u" || true - fi - done - for f in \ - "$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, $CANON_NSSWITCH and $CANON_RESOLVED (remove manually if undesired)." - if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi - msg "Uninstall complete" - exit 0 -fi - -###################################################################### -# Pre-flight checks -###################################################################### -note "Script directory: $SCRIPT_DIR" -note "Repository root: $REPO_ROOT" - -for req in "$TEMPLATE_ENFORCE" "$TEMPLATE_UNLOCK" "$UNIT_GUARD_SERVICE"; do - [[ -f $req ]] || { - err "Missing template: $req" - exit 1 - } -done - -if [[ ! -f $HOSTS ]]; then - err "$HOSTS does not exist. Run your hosts/install.sh first." - exit 1 -fi - -###################################################################### -# Snapshot -###################################################################### -if [[ $DO_SNAPSHOT -eq 1 ]]; then - if [[ -f $CANON && $FORCE_SNAPSHOT -eq 0 ]]; then - note "Canonical snapshot exists (use --force-snapshot to overwrite)" - else - msg "Creating canonical snapshot at $CANON" - run install -m 644 -D "$HOSTS" "$CANON" - fi -else - note "Skipping snapshot creation (--no-snapshot)" -fi - -###################################################################### -# Install scripts -###################################################################### -msg "Installing enforcement script -> $INSTALL_ENFORCE" -run install -m 755 "$TEMPLATE_ENFORCE" "$INSTALL_ENFORCE" - -msg "Installing unlock script -> $INSTALL_UNLOCK" -run install -m 755 "$TEMPLATE_UNLOCK" "$INSTALL_UNLOCK" - -# Adjust delay in unlock script if different from default -if [[ $DELAY -ne 45 ]]; then - msg "Adjusting unlock delay to $DELAY seconds" - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would patch $INSTALL_UNLOCK" - else - # Replace DELAY_SECONDS=... line - sed -i -E "s/^(DELAY_SECONDS=).*/\\1$DELAY/" "$INSTALL_UNLOCK" || warn "Failed to adjust delay" - fi -fi - -###################################################################### -# Install shell history filters (optional) -###################################################################### -if [[ $INSTALL_SHELL_HOOKS -eq 1 ]]; then - msg "Installing shell history suppression hooks for unlock command" - # Pattern matches commands invoking unlock-hosts (with or without sudo) & setup script force snapshot - # Zsh: use zshaddhistory function - if command -v zsh >/dev/null 2>&1; then - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would create $ZSH_FILTER_SNIPPET" - else - cat >"$ZSH_FILTER_SNIPPET" <<'ZEOF' -# Added by hosts guard setup – suppress unlock-hosts commands from Zsh history -autoload -Uz add-zsh-hook 2>/dev/null || true -_hosts_guard_history_filter() { - emulate -L zsh - setopt extendedglob - local line="$1" - local _pattern='(^|;|&&|\|\|)\s*(sudo\s+)?(/usr/local/sbin/)?unlock-hosts(\s|;|$)' - if [[ $line =~ ${_pattern} ]]; then - return 1 - fi - return 0 -} -if typeset -f add-zsh-hook >/dev/null 2>&1; then - add-zsh-hook zshaddhistory _hosts_guard_history_filter 2>/dev/null || true -else - zshaddhistory() { _hosts_guard_history_filter "$1"; } -fi -ZEOF - chmod 644 "$ZSH_FILTER_SNIPPET" - fi - fi - - # Bash: rely on HISTCONTROL and PROMPT_COMMAND filter - if command -v bash >/dev/null 2>&1; then - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would create $BASH_FILTER_SNIPPET" - else - cat >"$BASH_FILTER_SNIPPET" <<'BEOF' -# Added by hosts guard setup – suppress unlock-hosts commands from Bash history -export HISTCONTROL=ignoredups:erasedups -_hosts_guard_hist_filter() { - local last_cmd - local _pattern='(^|;|&&|\|\|)\s*(sudo\s+)?(/usr/local/sbin/)?unlock-hosts(\s|;|$)' - last_cmd=$(history 1 2>/dev/null | sed -E 's/^ *[0-9]+ +//') - if [[ -n $last_cmd && $last_cmd =~ ${_pattern} ]]; then - local id - id=$(history 1 2>/dev/null | awk '{print $1}') - if [[ -n $id ]]; then history -d $id 2>/dev/null || true; fi - history -w 2>/dev/null || true - history -c || true - history -r 2>/dev/null || true - fi -} -case :${PROMPT_COMMAND-}: in - *:_hosts_guard_hist_filter:* ) ;; - * ) PROMPT_COMMAND="_hosts_guard_hist_filter${PROMPT_COMMAND:+;${PROMPT_COMMAND}}" ;; -esac -BEOF - chmod 644 "$BASH_FILTER_SNIPPET" - fi - fi -else - note "Skipping shell history hooks (--no-shell-hooks)" -fi - -###################################################################### -# Add alias stub to discourage raw invocation (shell-level friction) -###################################################################### -if [[ $ADD_ALIAS_STUB -eq 1 ]]; then - PROFILE_STUB="/etc/profile.d/hosts_guard_alias_stub.sh" - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would create $PROFILE_STUB" - else - cat >"$PROFILE_STUB" <<'ASTUB' -# Added by hosts guard setup – discourages casual use of unlock-hosts name -if command -v unlock-hosts >/dev/null 2>&1; then - alias unlock-hosts='command_not_found_handle 2>/dev/null || echo "Use: sudo /usr/local/sbin/unlock-hosts (logged & delayed)"' -fi -ASTUB - chmod 644 "$PROFILE_STUB" - fi -fi - -###################################################################### -# Audit rule to record executions (requires auditd) -###################################################################### -if [[ $INSTALL_AUDIT_RULE -eq 1 ]]; then - if command -v auditctl >/dev/null 2>&1; then - audit_rule_str="-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock" - audit_rule_args=(-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock) - if auditctl -l 2>/dev/null | grep -Fq "/usr/local/sbin/unlock-hosts"; then - note "Audit rule already present" - else - run auditctl "${audit_rule_args[@]}" || warn "Failed to add audit rule (runtime)" - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would create /etc/audit/rules.d/hosts_unlock.rules" - else - echo "$audit_rule_str" >/etc/audit/rules.d/hosts_unlock.rules - fi - fi - else - warn "auditctl not found; skipping audit rule (install auditd to enable)" - fi -fi - -###################################################################### -# Install systemd units -###################################################################### -msg "Deploying systemd units" -run install -m 644 "$UNIT_GUARD_SERVICE" "$SYSTEMD_DIR/hosts-guard.service" -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 - -###################################################################### -# Enable / Start -###################################################################### -if [[ $ENABLE_PATH -eq 1 ]]; then - msg "Enabling path watch (auto-revert)" - run systemctl enable --now hosts-guard.path -else - note "Skipping path watch (--skip-path-watch)" -fi - -if [[ $ENABLE_BIND -eq 1 ]]; then - msg "Enabling read-only bind mount" - run systemctl enable --now hosts-bind-mount.service -else - note "Skipping bind mount (--skip-bind)" -fi - -if [[ $ENABLE_NSSWITCH -eq 1 ]]; then - msg "Enabling nsswitch.conf protection (hosts bypass prevention)" - msg "Installing nsswitch enforcement script -> $INSTALL_ENFORCE_NSSWITCH" - run install -m 755 "$TEMPLATE_ENFORCE_NSSWITCH" "$INSTALL_ENFORCE_NSSWITCH" - - # Ensure 'files' is present in the hosts line before snapshotting - if [[ -f "$NSSWITCH" ]]; then - hosts_line=$(grep '^hosts:' "$NSSWITCH" 2>/dev/null || echo "") - if [[ -n "$hosts_line" ]] && ! echo "$hosts_line" | grep -qw 'files'; then - msg "Adding 'files' to nsswitch.conf hosts line (was: $hosts_line)" - if echo "$hosts_line" | grep -qw 'resolve'; then - run sed -i 's/^hosts:\(.*\)resolve/hosts: files\1resolve/' "$NSSWITCH" - elif echo "$hosts_line" | grep -qw 'dns'; then - run sed -i 's/^hosts:\(.*\)dns/hosts:\1files dns/' "$NSSWITCH" - else - run sed -i 's/^hosts:/hosts: files/' "$NSSWITCH" - fi - msg "nsswitch.conf hosts line fixed: $(grep '^hosts:' "$NSSWITCH")" - fi - fi - - # Create nsswitch canonical snapshot if needed - if [[ -f "$NSSWITCH" ]]; then - if [[ ! -f "$CANON_NSSWITCH" ]]; then - msg "Creating canonical nsswitch.conf snapshot at $CANON_NSSWITCH" - run cp "$NSSWITCH" "$CANON_NSSWITCH" - run chmod 644 "$CANON_NSSWITCH" - chattr +i "$CANON_NSSWITCH" 2>/dev/null || warn "Failed to protect canonical nsswitch copy" - fi - fi - - run systemctl enable --now nsswitch-guard.path - - # Perform initial nsswitch enforcement - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would run $INSTALL_ENFORCE_NSSWITCH" - else - "$INSTALL_ENFORCE_NSSWITCH" || warn "nsswitch enforcement returned non-zero" - fi -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 - # `|| 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:]') || 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 - 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 - # `|| 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:]') || 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 - 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. - # 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 - -msg "Performing initial hosts enforcement" -if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would run $INSTALL_ENFORCE" -else - "$INSTALL_ENFORCE" || warn "Enforcement returned non-zero" -fi - -###################################################################### -# Summary -###################################################################### -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)" -echo -echo "Test flow:" -echo " sudo sed -i '1s/.*/# tamper test/' /etc/hosts # Should revert automatically" -echo " sudo $INSTALL_UNLOCK # Intentional edit workflow" -echo -echo "Uninstall:" -echo " sudo $0 --uninstall" -echo "(Optional) Skip shell history hooks: --no-shell-hooks" -echo -exit 0 diff --git a/linux_configuration/scripts/periodic_background/hosts/install.sh b/linux_configuration/scripts/periodic_background/hosts/install.sh index b5d7f502..ac54630c 100755 --- a/linux_configuration/scripts/periodic_background/hosts/install.sh +++ b/linux_configuration/scripts/periodic_background/hosts/install.sh @@ -905,8 +905,9 @@ fi # reverts unrelated unstaged edits. # # Enforcement does not depend on these sources being immutable: /etc/hosts -# itself is chattr +i, hosts-guard.path watches and re-enforces it via -# enforce-hosts.sh against the /usr/local/share/locked-hosts canonical snapshot, -# and hosts-bind-mount.service pins it. Editing these sources changes nothing -# until install.sh is re-run as root, which regenerates the guarded artifacts. +# itself is chattr +i, guard-lib's "hosts" file-guard instance (guardctl +# file-guard status hosts) watches and re-enforces it against its canonical +# snapshot, and the same instance's bind mount pins it. Editing these sources +# changes nothing until install.sh is re-run as root, which regenerates the +# guarded artifacts. # ============================================================================