mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-11-10 19:27:05 +01:00
Merge hosts-guard legacy archive: setup_hosts_guard.sh and supporting files, superseded by guard-lib
This commit is contained in:
commit
fa36e1402e
@ -0,0 +1,33 @@
|
|||||||
|
# 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.
|
||||||
@ -0,0 +1,221 @@
|
|||||||
|
# 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
|
||||||
32
linux_configuration/scripts/periodic_background/hosts/guard/enforce-hosts.sh
Executable file
32
linux_configuration/scripts/periodic_background/hosts/guard/enforce-hosts.sh
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/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"
|
||||||
109
linux_configuration/scripts/periodic_background/hosts/guard/enforce-nsswitch.sh
Executable file
109
linux_configuration/scripts/periodic_background/hosts/guard/enforce-nsswitch.sh
Executable file
@ -0,0 +1,109 @@
|
|||||||
|
#!/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"
|
||||||
147
linux_configuration/scripts/periodic_background/hosts/guard/enforce-resolved.sh
Executable file
147
linux_configuration/scripts/periodic_background/hosts/guard/enforce-resolved.sh
Executable file
@ -0,0 +1,147 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Guard script to enforce canonical /etc/systemd/resolved.conf
|
||||||
|
# Ensures ReadEtcHosts=yes and prevents DNS-over-TLS bypass of /etc/hosts
|
||||||
|
# Installed to /usr/local/sbin/enforce-resolved.sh by setup_hosts_guard.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CANONICAL_SOURCE="/usr/local/share/locked-resolved.conf"
|
||||||
|
TARGET="/etc/systemd/resolved.conf"
|
||||||
|
DROPIN_DIR="/etc/systemd/resolved.conf.d"
|
||||||
|
LOG_FILE="/var/log/resolved-guard.log"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate that resolved.conf has correct settings to honour /etc/hosts
|
||||||
|
# Critical settings:
|
||||||
|
# ReadEtcHosts=yes — must be present and set to yes
|
||||||
|
# DNSOverTLS=no — must NOT be opportunistic/yes (bypasses local hosts)
|
||||||
|
validate_resolved() {
|
||||||
|
local file="$1"
|
||||||
|
|
||||||
|
# ReadEtcHosts must be explicitly yes (not commented, not "no")
|
||||||
|
local read_hosts
|
||||||
|
read_hosts=$(grep -E '^\s*ReadEtcHosts\s*=' "$file" 2>/dev/null | tail -1 |
|
||||||
|
sed 's/.*=\s*//' | tr -d '[:space:]')
|
||||||
|
if [[ "$read_hosts" != "yes" ]]; then
|
||||||
|
log "INVALID: ReadEtcHosts='$read_hosts' (expected 'yes') in $file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# DNSOverTLS must not be set to yes or opportunistic
|
||||||
|
local dot
|
||||||
|
dot=$(grep -E '^\s*DNSOverTLS\s*=' "$file" 2>/dev/null | tail -1 |
|
||||||
|
sed 's/.*=\s*//' | tr -d '[:space:]')
|
||||||
|
if [[ -n "$dot" && "$dot" != "no" ]]; then
|
||||||
|
log "INVALID: DNSOverTLS='$dot' (must be 'no' or commented out) in $file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove any drop-in overrides that could bypass protections
|
||||||
|
enforce_no_dropins() {
|
||||||
|
if [[ -d "$DROPIN_DIR" ]]; then
|
||||||
|
local count
|
||||||
|
count=$(find "$DROPIN_DIR" -name '*.conf' -type f 2>/dev/null | wc -l)
|
||||||
|
if [[ "$count" -gt 0 ]]; then
|
||||||
|
log "TAMPERING: Found $count drop-in config(s) in $DROPIN_DIR — removing"
|
||||||
|
find "$DROPIN_DIR" -name '*.conf' -type f -delete
|
||||||
|
log "Removed all drop-in overrides"
|
||||||
|
fi
|
||||||
|
# Lock the directory itself to prevent new drop-ins
|
||||||
|
chattr +i "$DROPIN_DIR" 2>/dev/null || log "Failed to lock $DROPIN_DIR"
|
||||||
|
else
|
||||||
|
# Create and lock the directory to prevent creation with overrides
|
||||||
|
mkdir -p "$DROPIN_DIR"
|
||||||
|
chattr +i "$DROPIN_DIR" 2>/dev/null || log "Failed to lock $DROPIN_DIR"
|
||||||
|
log "Created and locked empty $DROPIN_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main enforcement logic
|
||||||
|
log "Starting resolved.conf enforcement"
|
||||||
|
|
||||||
|
# 1. Handle drop-in overrides first
|
||||||
|
enforce_no_dropins
|
||||||
|
|
||||||
|
# 2. Check current resolved.conf
|
||||||
|
if [[ ! -f "$TARGET" ]]; then
|
||||||
|
log "CRITICAL: $TARGET does not exist"
|
||||||
|
if [[ -f "$CANONICAL_SOURCE" ]]; then
|
||||||
|
chattr -i "$TARGET" 2>/dev/null || true
|
||||||
|
cp "$CANONICAL_SOURCE" "$TARGET"
|
||||||
|
chmod 644 "$TARGET"
|
||||||
|
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable on $TARGET"
|
||||||
|
log "Restored $TARGET from canonical copy"
|
||||||
|
else
|
||||||
|
log "ERROR: No canonical source at $CANONICAL_SOURCE — cannot restore"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! validate_resolved "$TARGET"; then
|
||||||
|
log "TAMPERING DETECTED in $TARGET"
|
||||||
|
|
||||||
|
if [[ -f "$CANONICAL_SOURCE" ]]; then
|
||||||
|
chattr -i "$TARGET" 2>/dev/null || true
|
||||||
|
cp "$CANONICAL_SOURCE" "$TARGET"
|
||||||
|
chmod 644 "$TARGET"
|
||||||
|
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable on $TARGET"
|
||||||
|
log "Restored $TARGET from canonical copy"
|
||||||
|
else
|
||||||
|
log "No canonical source — applying emergency fix"
|
||||||
|
chattr -i "$TARGET" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Fix ReadEtcHosts
|
||||||
|
if grep -qE '^\s*ReadEtcHosts\s*=' "$TARGET"; then
|
||||||
|
sed -i -E 's/^\s*ReadEtcHosts\s*=.*/ReadEtcHosts=yes/' "$TARGET"
|
||||||
|
elif grep -q '^\[Resolve\]' "$TARGET"; then
|
||||||
|
sed -i '/^\[Resolve\]/a ReadEtcHosts=yes' "$TARGET"
|
||||||
|
else
|
||||||
|
printf '\n[Resolve]\nReadEtcHosts=yes\n' >>"$TARGET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fix DNSOverTLS
|
||||||
|
if grep -qE '^\s*DNSOverTLS\s*=' "$TARGET"; then
|
||||||
|
sed -i -E 's/^\s*DNSOverTLS\s*=.*/#DNSOverTLS=no/' "$TARGET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
chattr +i "$TARGET" 2>/dev/null || true
|
||||||
|
log "Emergency fix applied"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restart resolved to pick up changes
|
||||||
|
systemctl restart systemd-resolved 2>/dev/null || log "Failed to restart systemd-resolved"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. If canonical exists, check for any drift
|
||||||
|
if [[ -f "$CANONICAL_SOURCE" ]]; then
|
||||||
|
if ! cmp -s "$CANONICAL_SOURCE" "$TARGET"; then
|
||||||
|
log "Drift detected in $TARGET — restoring canonical"
|
||||||
|
chattr -i "$TARGET" 2>/dev/null || true
|
||||||
|
cp "$CANONICAL_SOURCE" "$TARGET"
|
||||||
|
chmod 644 "$TARGET"
|
||||||
|
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable"
|
||||||
|
log "Restored $TARGET from canonical copy"
|
||||||
|
systemctl restart systemd-resolved 2>/dev/null || log "Failed to restart systemd-resolved"
|
||||||
|
else
|
||||||
|
log "No drift detected in $TARGET"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Creating initial canonical snapshot"
|
||||||
|
mkdir -p "$(dirname "$CANONICAL_SOURCE")"
|
||||||
|
cp "$TARGET" "$CANONICAL_SOURCE"
|
||||||
|
chmod 644 "$CANONICAL_SOURCE"
|
||||||
|
chattr +i "$CANONICAL_SOURCE" 2>/dev/null || log "Failed to protect canonical copy"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Ensure immutable attribute is set on live file
|
||||||
|
chattr -i "$TARGET" 2>/dev/null || true
|
||||||
|
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable on $TARGET"
|
||||||
|
|
||||||
|
log "resolved.conf enforcement complete"
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
[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
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Watch /etc/hosts and trigger enforcement
|
||||||
|
|
||||||
|
[Path]
|
||||||
|
PathChanged=/etc/hosts
|
||||||
|
Unit=hosts-guard.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
[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
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
#!/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."
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
[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
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
[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
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
#!/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
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
#!/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
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
#!/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
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
#!/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
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Watch /etc/systemd/resolved.conf for tampering (hosts bypass protection)
|
||||||
|
|
||||||
|
[Path]
|
||||||
|
PathChanged=/etc/systemd/resolved.conf
|
||||||
|
PathChanged=/etc/systemd/resolved.conf.d
|
||||||
|
Unit=resolved-guard.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Enforce canonical /etc/systemd/resolved.conf (prevents hosts bypass)
|
||||||
|
After=local-fs.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/enforce-resolved.sh
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=idle
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
576
linux_configuration/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh
Executable file
576
linux_configuration/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh
Executable file
@ -0,0 +1,576 @@
|
|||||||
|
#!/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
|
||||||
Loading…
Reference in New Issue
Block a user