mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
Archive legacy hosts-guard subsystem: superseded by guard-lib
setup_hosts_guard.sh was NEXT_SESSION_PROMPT.md's next split target, but
live-machine inspection showed it (and 16 supporting files - enforce-*.sh,
6 systemd units, install_pacman_hooks.sh, pacman-hooks/, psychological/,
both READMEs) is fully retired in favor of guard-lib, migrated 2026-07-04.
User confirmed archive over split, then confirmed the larger 17-file scope
once the directory was surveyed. Full history preserved via git filter-repo,
live at github.com/kuhyx/testsAndMisc-archive/tree/main/linux_configuration/scripts/periodic_background/hosts/guard.
plugins/{nsswitch,resolved}-plugin.sh stay - they're guard-lib's live
plugins, referenced by absolute path from /etc/guard-lib/targets/*.conf.
check_and_enable_services.sh's check_hosts() rewritten to check guard-lib
state (guardctl file-guard status) instead of the retired units/scripts it
used to check and self-repair - verified live, now reports the hosts
service ok against the actual migrated machine instead of a false error.
copilot-instructions.md and one install.sh comment updated to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
015eb48714
commit
327d48badd
@ -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 <name>`) 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>`, `} >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 <pkg>` 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 <filename-stem>` 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 <changed>` 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/<slug>-<date>.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 <tmp> 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 <test> <subject> [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 <outdir> <subject> <min>`
|
||||
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 <pre-split-rev> <old> <new>...`** —
|
||||
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 <spec>`** — 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 <spec>`** — 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 <each current
|
||||
path> --path <each historical path from `git log --all --follow
|
||||
--name-only`>`, 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/<dir>`, **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_<lib>.sh` per lib, sourcing the harness, asserting against real
|
||||
function calls (not mocks at the call-site level).
|
||||
- A `test_<entry-script>.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.
|
||||
|
||||
@ -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."
|
||||
}
|
||||
@ -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 <name>` (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 <all staged 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."
|
||||
]
|
||||
}
|
||||
@ -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 <install|status|unlock|uninstall> <name>`; 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 <name>`, edit, then let the path-watcher re-lock it (or `guardctl file-guard enforce <name>`).
|
||||
- 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_<pkg>` + `prompt_for_<pkg>_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` |
|
||||
|
||||
@ -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() { # <user> <systemctl args...>
|
||||
# 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 <name> 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/<name>.conf), so a missing instance and an unhealthy
|
||||
# one both fail this check without needing separate handling.
|
||||
guard_lib_instance_healthy() { # <name>
|
||||
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"
|
||||
|
||||
@ -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.
|
||||
@ -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
|
||||
@ -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"
|
||||
@ -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"
|
||||
@ -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"
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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."
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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.
|
||||
# ============================================================================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user