Files
pi-kit/pikit-prep.sh

639 lines
19 KiB
Bash
Executable File

#!/bin/bash
# Pi-Kit DietPi image prep + check script
# Cleans host-unique data and optionally verifies the image state.
set -euo pipefail
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
SCRIPT_NAME="$(basename "$SCRIPT_PATH")"
PIKIT_HOST="${PIKIT_HOST:-pikit.local}"
PIKIT_USER="${PIKIT_USER:-dietpi}"
PIKIT_SSH_KEY="${PIKIT_SSH_KEY:-$HOME/.ssh/pikit}"
PIKIT_SSH_OPTS="${PIKIT_SSH_OPTS:-}"
PIKIT_REMOTE_TMP="${PIKIT_REMOTE_TMP:-/tmp/pikit-prep.sh}"
PIKIT_SELF_DELETE="${PIKIT_SELF_DELETE:-0}"
PIKIT_FORCE_PASSWORD_CHANGE="${PIKIT_FORCE_PASSWORD_CHANGE:-1}"
MODE="both"
LOCAL_ONLY=0
ERRORS=0
WARNINGS=0
usage() {
cat <<'USAGE'
Usage: pikit-prep.sh [--prep-only|--check-only] [--local]
Defaults to prep + check (combined). When run on a non-DietPi host, it will
copy itself to the Pi (/tmp) and run with sudo, then clean up.
Options:
--prep-only Run prep only (no check)
--check-only Run checks only (no prep)
--local Force local execution (no SSH copy)
--help Show this help
Env:
PIKIT_FORCE_PASSWORD_CHANGE=0 Skip forcing a password change (default is on)
USAGE
}
status() {
local level="$1"
shift
printf '[%s] %s\n' "$level" "$*"
case "$level" in
FAIL) ERRORS=$((ERRORS + 1)) ;;
WARN) WARNINGS=$((WARNINGS + 1)) ;;
esac
}
section() {
printf '\n== %s ==\n' "$1"
}
is_dietpi() {
grep -qi "dietpi" /etc/os-release 2>/dev/null
}
require_root() {
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
echo "[FAIL] This script must run as root (use sudo)." >&2
exit 1
fi
}
parse_args() {
for arg in "$@"; do
case "$arg" in
--prep-only) MODE="prep" ;;
--check-only) MODE="check" ;;
--local) LOCAL_ONLY=1 ;;
--help|-h) usage; exit 0 ;;
*)
echo "[FAIL] Unknown argument: $arg" >&2
usage
exit 1
;;
esac
done
}
run_remote() {
local forward=()
for arg in "$@"; do
[ "$arg" = "--local" ] && continue
forward+=("$arg")
done
if ! command -v scp >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then
echo "[FAIL] ssh/scp not available for remote prep" >&2
exit 1
fi
scp -i "$PIKIT_SSH_KEY" $PIKIT_SSH_OPTS "$SCRIPT_PATH" "${PIKIT_USER}@${PIKIT_HOST}:${PIKIT_REMOTE_TMP}"
ssh -i "$PIKIT_SSH_KEY" $PIKIT_SSH_OPTS "${PIKIT_USER}@${PIKIT_HOST}" \
"sudo PIKIT_SELF_DELETE=1 bash ${PIKIT_REMOTE_TMP} --local ${forward[*]}; rc=\$?; rm -f ${PIKIT_REMOTE_TMP}; exit \$rc"
exit $?
}
clean_logs_dir() {
local dir="$1" pattern="${2:-*}"
if [ -d "$dir" ]; then
find "$dir" -type f -name "$pattern" -print -delete 2>/dev/null | sed 's/^/[CLEANED] /' || true
status CLEANED "logs $pattern in $dir"
else
status SKIP "$dir (missing)"
fi
}
truncate_file() {
local file="$1"
if [ -e "$file" ]; then
:> "$file" && status CLEANED "truncated $file" || status FAIL "truncate $file"
else
status SKIP "$file (missing)"
fi
}
clean_file() {
local path="$1"
if [ -e "$path" ]; then
rm -f "$path" && status CLEANED "$path" || status FAIL "$path"
else
status SKIP "$path (missing)"
fi
}
clean_dir_files() {
local dir="$1" pattern="$2"
if [ -d "$dir" ]; then
find "$dir" -type f -name "$pattern" -print -delete 2>/dev/null | sed 's/^/[CLEANED] /' || true
status CLEANED "files $pattern in $dir"
else
status SKIP "$dir (missing)"
fi
}
truncate_dir() {
local dir="$1"
if [ -d "$dir" ]; then
# keep systemd-private dirs intact while services run
find "$dir" -mindepth 1 -maxdepth 1 ! -path "$dir/systemd-private-*" -exec rm -rf {} + 2>/dev/null
status CLEANED "$dir"
else
status SKIP "$dir (missing)"
fi
}
clean_backups() {
local dir="$1"
if [ -d "$dir" ]; then
find "$dir" -type f \( -name '*~' -o -name '*.bak*' -o -name '*.orig*' \) -print -delete 2>/dev/null | sed 's/^/[CLEANED] /' || true
status CLEANED "backup/editor files in $dir"
else
status SKIP "$dir (missing)"
fi
}
clean_home_dir() {
local dir="$1"
shift
local keep=("$@")
if [ -d "$dir" ]; then
shopt -s dotglob nullglob
for entry in "$dir"/*; do
local base
base="$(basename "$entry")"
case "$base" in
.|..) continue ;;
esac
local keep_it=0
for k in "${keep[@]}"; do
if [ "$base" = "$k" ]; then
keep_it=1
break
fi
done
if [ "$keep_it" -eq 0 ]; then
rm -rf "$entry" && status CLEANED "$dir/$base" || status FAIL "$dir/$base"
fi
done
shopt -u dotglob nullglob
else
status SKIP "$dir (missing)"
fi
}
prep_image() {
section "Prep"
# --- Identity ---
truncate -s 0 /etc/machine-id && status CLEANED /etc/machine-id || status FAIL /etc/machine-id
mkdir -p /var/lib/dbus || true
rm -f /var/lib/dbus/machine-id
ln -s /etc/machine-id /var/lib/dbus/machine-id && status CLEANED "/var/lib/dbus/machine-id -> /etc/machine-id" || status FAIL "/var/lib/dbus/machine-id"
clean_file /var/lib/systemd/random-seed
# --- SSH host keys ---
if ls /etc/ssh/ssh_host_* >/dev/null 2>&1; then
rm -f /etc/ssh/ssh_host_* && status CLEANED "SSH host keys" || status FAIL "SSH host keys"
else
status SKIP "SSH host keys (none)"
fi
# --- SSH client traces ---
:> /root/.ssh/known_hosts 2>/dev/null && status CLEANED "/root/.ssh/known_hosts" || status SKIP "/root/.ssh/known_hosts"
:> /home/dietpi/.ssh/known_hosts 2>/dev/null && status CLEANED "/home/dietpi/.ssh/known_hosts" || status SKIP "/home/dietpi/.ssh/known_hosts"
:> /home/dietpi/.ssh/authorized_keys 2>/dev/null && status CLEANED "/home/dietpi/.ssh/authorized_keys" || status SKIP "/home/dietpi/.ssh/authorized_keys"
:> /root/.ssh/authorized_keys 2>/dev/null && status CLEANED "/root/.ssh/authorized_keys" || status SKIP "/root/.ssh/authorized_keys"
# --- Default login ---
if id -u dietpi >/dev/null 2>&1; then
echo "dietpi:pikit" | chpasswd && status CLEANED "reset dietpi password" || status FAIL "reset dietpi password"
mkdir -p /var/lib/pikit
rm -f /var/lib/pikit/first-login.notice
case "${PIKIT_FORCE_PASSWORD_CHANGE,,}" in
1|true|yes|on)
chage -d 0 dietpi && status CLEANED "force dietpi password change on next login" || status FAIL "force dietpi password change"
:> /var/lib/pikit/first-login.notice && chmod 644 /var/lib/pikit/first-login.notice \
&& status CLEANED "first-login notice armed" || status FAIL "first-login notice"
;;
*) ;;
esac
else
status SKIP "dietpi user missing"
fi
# --- Shell history ---
:> /root/.bash_history 2>/dev/null && status CLEANED "/root/.bash_history" || status SKIP "/root/.bash_history"
:> /home/dietpi/.bash_history 2>/dev/null && status CLEANED "/home/dietpi/.bash_history" || status SKIP "/home/dietpi/.bash_history"
# --- Home directories ---
clean_home_dir /home/dietpi .bashrc .bash_logout .profile .ssh RESCUE.md
clean_home_dir /root .bashrc .bash_logout .profile .ssh RESCUE.md
# --- Ready flag ---
clean_file /var/run/pikit-ready
# --- First-boot state + TLS ---
clean_dir_files /var/lib/pikit/firstboot "*"
clean_file /var/lib/pikit/firstboot/firstboot.done
clean_file /var/lib/pikit/firstboot/state.json
clean_file /var/lib/pikit/firstboot/firstboot.log
clean_file /var/lib/pikit/firstboot/firstboot.error
clean_file /var/lib/pikit/firstboot/firstboot.lock
clean_file /etc/pikit/certs/pikit-ca.crt
clean_file /etc/pikit/certs/pikit-ca.key
clean_file /etc/pikit/certs/pikit-ca.srl
clean_file /etc/pikit/certs/pikit.local.crt
clean_file /etc/pikit/certs/pikit.local.key
clean_file /etc/pikit/certs/pikit.local.csr
clean_file /var/www/pikit-web/assets/pikit-ca.crt
clean_file /var/www/pikit-web/assets/pikit-ca.sha256
clean_file /var/lib/pikit-update/state.json
clean_file /var/run/pikit-update.lock
# --- Backup/editor cruft ---
clean_backups /var/www/pikit-web
clean_backups /usr/local/bin
# --- Logs ---
clean_dir_files /var/log "*"
clean_dir_files /var/log/nginx "*"
if [ -d /var/log/journal ]; then
find /var/log/journal -type f -print -delete 2>/dev/null | sed 's/^/[CLEANED] /' || true
status CLEANED "/var/log/journal"
else
status SKIP "/var/log/journal (missing)"
fi
if [ -d /var/crash ]; then
find /var/crash -type f -print -delete 2>/dev/null | sed 's/^/[CLEANED] /' || true
status CLEANED "/var/crash"
else
status SKIP "/var/crash (missing)"
fi
if command -v pihole >/dev/null 2>&1; then
pihole -f >/dev/null 2>&1 && status CLEANED "pihole logs via pihole -f" || status FAIL "pihole -f"
clean_logs_dir /var/log/pihole '*'
clean_file /etc/pihole/pihole-FTL.db
fi
if [ -x /opt/AdGuardHome/AdGuardHome ]; then
clean_logs_dir /var/opt/AdGuardHome/data/logs '*'
clean_file /opt/AdGuardHome/data/querylog.db
fi
if command -v ufw >/dev/null 2>&1; then
truncate_file /var/log/ufw.log
fi
if command -v fail2ban-client >/dev/null 2>&1; then
truncate_file /var/log/fail2ban.log
fi
clean_logs_dir /var/log/unbound '*'
clean_logs_dir /var/log/dnsmasq '*'
clean_logs_dir /var/log/powerdns '*'
clean_logs_dir /var/lib/technitium-dns/Logs '*'
clean_logs_dir /var/log/jellyfin '*'
clean_logs_dir /var/lib/jellyfin/log '*'
clean_logs_dir /var/log/jellyseerr '*'
clean_logs_dir /opt/jellyseerr/logs '*'
clean_logs_dir /var/log/ustreamer '*'
clean_logs_dir /var/log/gitea '*'
clean_logs_dir /var/lib/gitea/log '*'
clean_logs_dir /var/log/fmd '*'
clean_logs_dir /var/log/uptime-kuma '*'
clean_logs_dir /opt/uptime-kuma/data/logs '*'
clean_logs_dir /var/log/romm '*'
clean_logs_dir /var/log/privatebin '*'
clean_logs_dir /var/log/crafty '*'
clean_logs_dir /var/log/rustdesk '*'
clean_logs_dir /var/log/memos '*'
clean_logs_dir /var/lib/memos/logs '*'
clean_logs_dir /var/log/traccar '*'
clean_logs_dir /var/log/webmin '*'
clean_logs_dir /var/log/homarr '*'
clean_logs_dir /var/log/termix '*'
clean_logs_dir /var/log/syncthing '*'
clean_logs_dir /var/log/netdata '*'
clean_logs_dir /var/lib/netdata/dbengine '*'
clean_logs_dir /var/log/AdGuardHome '*'
clean_logs_dir /var/log/mysql '*'
clean_logs_dir /var/log/mariadb '*'
clean_logs_dir /var/log/postgresql '*'
truncate_file /var/log/redis/redis-server.log
clean_logs_dir /var/log/influxdb '*'
clean_logs_dir /var/log/prometheus '*'
clean_logs_dir /var/log/grafana '*'
clean_logs_dir /var/log/loki '*'
clean_logs_dir /var/log/caddy '*'
clean_logs_dir /var/log/apache2 '*'
clean_logs_dir /var/log/lighttpd '*'
clean_logs_dir /var/log/samba '*'
clean_logs_dir /var/log/mosquitto '*'
clean_logs_dir /var/log/openvpn '*'
clean_logs_dir /var/log/wireguard '*'
clean_logs_dir /var/log/node-red '*'
truncate_file /var/log/nodered-install.log
clean_logs_dir /var/log/transmission-daemon '*'
clean_logs_dir /var/log/deluge '*'
clean_logs_dir /var/log/qbittorrent '*'
clean_logs_dir /var/log/paperless-ngx '*'
clean_logs_dir /var/log/photoprism '*'
clean_logs_dir /var/log/navidrome '*'
clean_logs_dir /var/log/minio '*'
clean_logs_dir /var/log/nzbget '*'
clean_logs_dir /var/log/sabnzbd '*'
clean_logs_dir /var/log/jackett '*'
clean_logs_dir /var/log/radarr '*'
clean_logs_dir /var/log/sonarr '*'
clean_logs_dir /var/log/lidarr '*'
clean_logs_dir /var/log/prowlarr '*'
clean_logs_dir /var/log/bazarr '*'
clean_logs_dir /var/log/overseerr '*'
clean_logs_dir /var/log/emby-server '*'
truncate_file /home/homeassistant/.homeassistant/home-assistant.log
clean_logs_dir /home/homeassistant/.homeassistant/logs '*'
truncate_file /var/www/nextcloud/data/nextcloud.log
truncate_file /var/www/owncloud/data/owncloud.log
if [ -d /var/lib/docker/containers ]; then
find /var/lib/docker/containers -type f -name '*-json.log' -print0 2>/dev/null | while IFS= read -r -d '' f; do
:> "$f" && status CLEANED "truncated $f" || status FAIL "truncate $f"
done
else
status SKIP "/var/lib/docker/containers (missing)"
fi
clean_file /var/log/wtmp.db
clean_dir_files /var/tmp/dietpi/logs "*"
# --- Caches ---
apt-get clean >/dev/null 2>&1 && status CLEANED "apt cache" || status FAIL "apt cache"
rm -rf /var/lib/apt/lists/* /var/cache/apt/* 2>/dev/null && status CLEANED "apt lists/cache" || status FAIL "apt lists/cache"
find /var/cache/debconf -type f -print -delete 2>/dev/null | sed 's/^/[CLEANED] /' || true
status CLEANED "/var/cache/debconf files"
# --- Temp directories ---
truncate_dir /tmp
truncate_dir /var/tmp
# --- DietPi RAMlog store (preserve log dir structure) ---
if [ -d /var/log ]; then
local log_group="adm"
if ! getent group "$log_group" >/dev/null 2>&1; then
log_group="root"
fi
install -d -m 0755 -o root -g "$log_group" /var/log/nginx \
&& status CLEANED "/var/log/nginx (ensured)" \
|| status FAIL "/var/log/nginx"
else
status SKIP "/var/log (missing)"
fi
if [ -x /boot/dietpi/func/dietpi-ramlog ]; then
/boot/dietpi/func/dietpi-ramlog 1 >/dev/null 2>&1 \
&& status CLEANED "DietPi RAMlog store" \
|| status FAIL "DietPi RAMlog store"
else
status SKIP "DietPi RAMlog (missing)"
fi
# --- DHCP leases ---
clean_file /var/lib/dhcp/dhclient.eth0.leases
# --- Nginx caches ---
if [ -d /var/lib/nginx ]; then
find /var/lib/nginx -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + 2>/dev/null
status CLEANED "/var/lib/nginx/*"
else
status SKIP "/var/lib/nginx"
fi
status DONE "Prep complete"
}
check_home_dir() {
local dir="$1"
shift
local keep=("$@")
if [ -d "$dir" ]; then
local extra=()
shopt -s dotglob nullglob
for entry in "$dir"/*; do
local base
base="$(basename "$entry")"
case "$base" in
.|..) continue ;;
esac
local keep_it=0
for k in "${keep[@]}"; do
if [ "$base" = "$k" ]; then
keep_it=1
break
fi
done
if [ "$keep_it" -eq 0 ]; then
extra+=("$base")
fi
done
shopt -u dotglob nullglob
if [ "${#extra[@]}" -gt 0 ]; then
status WARN "extra files in $dir: ${extra[*]}"
else
status OK "home clean: $dir"
fi
else
status FAIL "missing home dir: $dir"
fi
}
check_file_missing() {
local path="$1"
if [ -e "$path" ]; then
status FAIL "unexpected file exists: $path"
else
status OK "missing as expected: $path"
fi
}
check_file_empty_or_missing() {
local path="$1"
if [ -e "$path" ]; then
if [ -s "$path" ]; then
status FAIL "file not empty: $path"
else
status OK "empty file: $path"
fi
else
status OK "missing (ok): $path"
fi
}
check_image() {
section "Check"
section "Identity files"
if [ -e /etc/machine-id ]; then
if [ -s /etc/machine-id ]; then
status FAIL "/etc/machine-id not empty"
else
status OK "/etc/machine-id empty"
fi
else
status FAIL "/etc/machine-id missing"
fi
if [ -L /var/lib/dbus/machine-id ]; then
status OK "/var/lib/dbus/machine-id symlink present"
else
status WARN "dbus machine-id missing or not a symlink"
fi
check_file_missing /var/lib/systemd/random-seed
section "SSH host keys"
if ls /etc/ssh/ssh_host_* >/dev/null 2>&1; then
status FAIL "SSH host keys still present"
else
status OK "no SSH host keys"
fi
section "SSH client traces"
check_file_empty_or_missing /root/.ssh/known_hosts
check_file_empty_or_missing /home/dietpi/.ssh/known_hosts
check_file_empty_or_missing /home/dietpi/.ssh/authorized_keys
check_file_empty_or_missing /root/.ssh/authorized_keys
section "Home directories"
check_home_dir /home/dietpi .bashrc .bash_logout .profile .ssh RESCUE.md
check_home_dir /root .bashrc .bash_logout .profile .ssh RESCUE.md
section "Ready flag"
check_file_missing /var/run/pikit-ready
section "Firstboot state"
if [ -d /var/lib/pikit/firstboot ]; then
local count
count="$(find /var/lib/pikit/firstboot -type f 2>/dev/null | wc -l | tr -d ' ')"
if [ "$count" -gt 0 ]; then
status WARN "firstboot files still present: $count"
else
status OK "firstboot dir empty"
fi
else
status OK "firstboot dir missing (ok)"
fi
section "TLS certs"
check_file_missing /etc/pikit/certs/pikit-ca.crt
check_file_missing /etc/pikit/certs/pikit-ca.key
check_file_missing /etc/pikit/certs/pikit.local.crt
check_file_missing /etc/pikit/certs/pikit.local.key
check_file_missing /var/www/pikit-web/assets/pikit-ca.crt
check_file_missing /var/www/pikit-web/assets/pikit-ca.sha256
check_file_missing /var/lib/pikit-update/state.json
check_file_missing /var/run/pikit-update.lock
section "Logs"
if [ -d /var/log ]; then
local nonempty
nonempty="$(find /var/log -type f -size +0c 2>/dev/null | wc -l | tr -d ' ')"
if [ "$nonempty" -gt 0 ]; then
status WARN "/var/log has non-empty files: $nonempty"
else
status OK "/var/log empty"
fi
else
status WARN "/var/log missing"
fi
if [ -d /var/log/nginx ]; then
local nginx_nonempty
nginx_nonempty="$(find /var/log/nginx -type f -size +0c 2>/dev/null | wc -l | tr -d ' ')"
if [ "$nginx_nonempty" -gt 0 ]; then
status WARN "/var/log/nginx has non-empty files: $nginx_nonempty"
else
status OK "/var/log/nginx empty"
fi
else
status WARN "/var/log/nginx missing"
fi
section "DietPi RAM logs"
if [ -d /var/tmp/dietpi/logs/dietpi-ramlog_store ]; then
status OK "DietPi RAMlog store present"
else
status FAIL "DietPi RAMlog store missing"
fi
section "Caches"
du -sh /var/cache/apt /var/lib/apt/lists /var/cache/debconf 2>/dev/null || true
section "Temp dirs"
local tmp_extra
tmp_extra="$(find /tmp /var/tmp -maxdepth 1 -mindepth 1 ! -name 'systemd-private-*' ! -path '/var/tmp/dietpi' 2>/dev/null | wc -l | tr -d ' ')"
if [ "$tmp_extra" -gt 0 ]; then
status WARN "temp dirs not empty"
else
status OK "temp dirs empty"
fi
section "DHCP lease"
check_file_missing /var/lib/dhcp/dhclient.eth0.leases
section "Nginx cache dirs"
if [ -d /var/lib/nginx ]; then
local nginx_cache
nginx_cache="$(find /var/lib/nginx -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')"
if [ "$nginx_cache" -gt 0 ]; then
status WARN "nginx cache dirs present: $nginx_cache"
else
status OK "/var/lib/nginx empty"
fi
else
status OK "/var/lib/nginx missing (ok)"
fi
}
finalize() {
section "Summary"
status OK "warnings: $WARNINGS"
status OK "errors: $ERRORS"
if [ "$ERRORS" -gt 0 ]; then
echo "[FAIL] Prep/check completed with errors."
exit 1
fi
echo "[OK] Prep/check completed."
}
maybe_self_delete() {
if [ "$PIKIT_SELF_DELETE" -eq 1 ] && [[ "$SCRIPT_PATH" == /tmp/* ]]; then
rm -f "$SCRIPT_PATH" || true
fi
}
main() {
parse_args "$@"
if [ "$LOCAL_ONLY" -eq 0 ] && ! is_dietpi; then
run_remote "$@"
fi
require_root
case "$MODE" in
prep) prep_image ;;
check) check_image ;;
both)
prep_image
check_image
;;
esac
finalize
maybe_self_delete
}
main "$@"