mirror of
https://github.com/kuhyx/scripts.git
synced 2026-07-04 12:43:05 +02:00
feat: more etc hosts friction
This commit is contained in:
parent
cadc2bfe1a
commit
a69dfff125
25
hosts/guard/README.md
Normal file
25
hosts/guard/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
Limitations:
|
||||
- A root user can still disable units, remount, remove attributes.
|
||||
- Purpose is to interrupt habit loops and create intentional friction.
|
||||
32
hosts/guard/enforce-hosts.sh
Normal file
32
hosts/guard/enforce-hosts.sh
Normal 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"
|
||||
14
hosts/guard/hosts-bind-mount.service
Normal file
14
hosts/guard/hosts-bind-mount.service
Normal file
@ -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
|
||||
9
hosts/guard/hosts-guard.path
Normal file
9
hosts/guard/hosts-guard.path
Normal file
@ -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
|
||||
12
hosts/guard/hosts-guard.service
Normal file
12
hosts/guard/hosts-guard.service
Normal file
@ -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
|
||||
59
hosts/guard/psychological/unlock-hosts.sh
Normal file
59
hosts/guard/psychological/unlock-hosts.sh
Normal file
@ -0,0 +1,59 @@
|
||||
#!/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
|
||||
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 "$@"
|
||||
|
||||
log "Requested intentional /etc/hosts modification session."
|
||||
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."
|
||||
else
|
||||
log "Changes detected. Updating canonical copy and re-enforcing."
|
||||
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."
|
||||
echo "Done." >&2
|
||||
217
hosts/guard/setup_hosts_guard.sh
Normal file
217
hosts/guard/setup_hosts_guard.sh
Normal file
@ -0,0 +1,217 @@
|
||||
#!/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
|
||||
UNINSTALL=0
|
||||
DELAY=45
|
||||
DRY_RUN=0
|
||||
|
||||
######################################################################
|
||||
# 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: %s\n' "$*"; else eval "$@"; 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 ;;
|
||||
--delay) DELAY=${2:-} ; [[ -z ${DELAY} ]] && { err '--delay requires value'; exit 2; } ; shift 2 ;;
|
||||
--dry-run) DRY_RUN=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"
|
||||
|
||||
INSTALL_ENFORCE="/usr/local/sbin/enforce-hosts.sh"
|
||||
INSTALL_UNLOCK="/usr/local/sbin/unlock-hosts"
|
||||
CANON="/usr/local/share/locked-hosts"
|
||||
HOSTS="/etc/hosts"
|
||||
|
||||
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; 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" \
|
||||
"$SYSTEMD_DIR/hosts-guard.service" \
|
||||
"$SYSTEMD_DIR/hosts-guard.path" \
|
||||
"$SYSTEMD_DIR/hosts-bind-mount.service"; do
|
||||
if [[ -e $f ]]; then run rm -f "$f"; fi
|
||||
done
|
||||
note "Leaving canonical snapshot at $CANON (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 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"
|
||||
|
||||
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
|
||||
|
||||
msg "Performing initial 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 copy: $CANON"
|
||||
echo "Enforce script: $INSTALL_ENFORCE"
|
||||
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
|
||||
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
|
||||
exit 0
|
||||
Loading…
Reference in New Issue
Block a user