Merge pull request #10379 from AksharP5/fix/factory-reset-password-hashes
Erase old password hashes during factory reset
This commit is contained in:
2 files changed
+173
-14
No files matched your search
@@ -297,19 +297,34 @@ rebuild_next_boot() {
|
||||
umount "$next$esp_mount"
|
||||
}
|
||||
|
||||
# Both the staged system and the retained baseline must lose the old hashes.
|
||||
scrub_factory_accounts() {
|
||||
local root="$1" user users
|
||||
|
||||
users=$(awk -F: '$3 >= 1000 && $3 < 60000 { print $1 }' "$root/etc/passwd") || return 1
|
||||
for user in $users; do
|
||||
userdel --root "$root" "$user" 2>>"$LOG_FILE" || return 1
|
||||
rm -rf "${root:?}/home/$user" || return 1
|
||||
done
|
||||
|
||||
# passwd --lock preserves the hash. Replace it, then remove the backups
|
||||
# that userdel and usermod leave behind.
|
||||
usermod --root "$root" --password '!' root >>"$LOG_FILE" 2>&1 || return 1
|
||||
rm -f "$root/etc/"{shadow-,gshadow-,passwd-,group-,subuid-,subgid-}
|
||||
}
|
||||
|
||||
# Remove the seller's account material and machine identity from the retained
|
||||
# @factory baseline so it can neither be mounted for recovery nor restore the
|
||||
# seller's account on a future reset. Idempotent (a scrubbed baseline has no
|
||||
# uid>=1000 accounts left to remove).
|
||||
sanitize_factory_baseline() {
|
||||
local factory="$1" user
|
||||
local factory="$1"
|
||||
btrfs property set -ts "$factory" ro false
|
||||
|
||||
for user in $(awk -F: '$3 >= 1000 && $3 < 60000 { print $1 }' "$factory/etc/passwd"); do
|
||||
userdel --root "$factory" "$user" 2>>"$LOG_FILE" || true
|
||||
rm -rf "${factory:?}/home/$user"
|
||||
done
|
||||
passwd --root "$factory" --lock root >>"$LOG_FILE" 2>&1 || true
|
||||
if ! scrub_factory_accounts "$factory"; then
|
||||
btrfs property set -ts "$factory" ro true
|
||||
fail "could not remove account credentials from the factory baseline (see $LOG_FILE)"
|
||||
fi
|
||||
rm -f "$factory"/etc/ssh/ssh_host_*
|
||||
rm -f "$factory"/etc/NetworkManager/system-connections/*
|
||||
rm -rf "$factory"/var/lib/NetworkManager/* "$factory/var/lib/tailscale" "$factory/var/lib/iwd"
|
||||
@@ -337,17 +352,13 @@ stage_full_reset() {
|
||||
rm -rf "$next"/var/lib/NetworkManager/* "$next/var/lib/tailscale" "$next/var/lib/iwd"
|
||||
rm -f "$next/var/lib/sddm/state.conf" "$next/etc/sddm.conf.d/autologin.conf"
|
||||
|
||||
# A factory snapshot from a normal (normal) install contains the original
|
||||
# A factory snapshot from a normal install contains the original
|
||||
# user account; first-boot setup must start from none. A leftover account
|
||||
# would keep its password hash and group memberships (including wheel), so
|
||||
# failure here has to abort the reset, not be shrugged off.
|
||||
local user
|
||||
for user in $(awk -F: '$3 >= 1000 && $3 < 60000 { print $1 }' "$next/etc/passwd"); do
|
||||
log "Removing user $user from the factory system"
|
||||
userdel --root "$next" "$user" 2>>"$LOG_FILE" ||
|
||||
fail "could not remove user $user from the factory system (see $LOG_FILE)"
|
||||
done
|
||||
passwd --root "$next" --lock root >>"$LOG_FILE" 2>&1 || true
|
||||
log "Removing account credentials from the factory system"
|
||||
scrub_factory_accounts "$next" ||
|
||||
fail "could not remove account credentials from the factory system (see $LOG_FILE)"
|
||||
|
||||
# @factory itself survives the wipe as the baseline for future resets. If it
|
||||
# came from a normal install it still holds the seller's account and
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
if (( EUID != 0 )); then
|
||||
if unshare --user --map-root-user true 2>/dev/null; then
|
||||
exec unshare --user --map-root-user bash "$0"
|
||||
fi
|
||||
pass "no unprivileged user namespace; skipping factory account cleanup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
# Load the production functions without self-elevation or the reset entrypoint.
|
||||
awk '
|
||||
/^[a-z_]+\(\) \{/ { copying = 1 }
|
||||
copying { print }
|
||||
/^}/ { copying = 0 }
|
||||
' "$ROOT/bin/omarchy-system-factory-reset" >"$test_tmp/functions"
|
||||
|
||||
cat >"$test_tmp/reset" <<'SH'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
source "$1/functions"
|
||||
TOP_MNT="$2"
|
||||
NEXT_NAME=@omarchy-reset-next
|
||||
PROVISIONING_DIR=/var/lib/omarchy/provisioning
|
||||
LOG_FILE="$TOP_MNT/reset.log"
|
||||
|
||||
log() { printf '%s\n' "$1" >>"$LOG_FILE"; }
|
||||
fail() { log "$1"; exit 1; }
|
||||
|
||||
# Account tools are real. Only snapshots, boot rebuilding, and system services
|
||||
# are replaced: all writes stay inside this test's disposable directory.
|
||||
btrfs() {
|
||||
if [[ $1 == "subvolume" && $2 == "snapshot" ]]; then
|
||||
mkdir -p "$4"
|
||||
cp -a "$3/." "$4/"
|
||||
elif [[ $1 == "property" ]]; then
|
||||
printf '%s\n' "$6" >"$4/read-only"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
systemd-id128() { printf '%032d\n' 1; }
|
||||
install_provisioning_units() { :; }
|
||||
encrypted_install() { return 1; }
|
||||
rebuild_next_boot() { touch "$TOP_MNT/rebuilt"; }
|
||||
sync() { :; }
|
||||
|
||||
userdel() {
|
||||
[[ ${FAIL_COMMAND:-} == "userdel" && $2 == "$FAIL_ROOT" ]] && return 42
|
||||
command userdel "$@"
|
||||
}
|
||||
usermod() {
|
||||
[[ ${FAIL_COMMAND:-} == "usermod" && $2 == "$FAIL_ROOT" ]] && return 42
|
||||
command usermod "$@"
|
||||
}
|
||||
rm() {
|
||||
[[ ${FAIL_COMMAND:-} == "rm" && $* == *"$FAIL_ROOT/etc/shadow-"* ]] && return 42
|
||||
command rm "$@"
|
||||
}
|
||||
|
||||
stage_full_reset
|
||||
SH
|
||||
|
||||
make_fixture() {
|
||||
local top="$1" root_hash="${2:-original-root-hash}"
|
||||
local factory="$top/@factory"
|
||||
mkdir -p "$top/@" "$factory/etc" "$factory/home/seller" \
|
||||
"$factory/usr/bin" "$factory/usr/share/omarchy/install/provisioning" \
|
||||
"$factory/var/lib/omarchy/provisioning/packages"
|
||||
touch "$top/@/old-system" "$factory/home/seller/private-file" \
|
||||
"$factory/usr/share/omarchy/install/provisioning/omarchy-provision-owner.service" \
|
||||
"$factory/var/lib/omarchy/provisioning/packages/node-v0.tar.gz"
|
||||
printf '#!/bin/bash\n' >"$factory/usr/bin/omarchy-provision-owner"
|
||||
chmod +x "$factory/usr/bin/omarchy-provision-owner"
|
||||
printf 'true\n' >"$factory/read-only"
|
||||
cat >"$factory/etc/passwd" <<'EOF'
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
daemon:x:1:1:daemon:/:/usr/bin/nologin
|
||||
seller:x:1000:1000:Seller:/home/seller:/bin/bash
|
||||
EOF
|
||||
printf 'root:%s:20000:0:99999:7:::\ndaemon:*:20000:0:99999:7:::\nseller:original-user-hash:20000:0:99999:7:::\n' \
|
||||
"$root_hash" >"$factory/etc/shadow"
|
||||
printf 'root:x:0:\ndaemon:x:1:\nseller:x:1000:\nwheel:x:998:seller\n' >"$factory/etc/group"
|
||||
printf 'root:!::\ndaemon:!::\nseller:!::\nwheel:!::seller\n' >"$factory/etc/gshadow"
|
||||
printf 'USERGROUPS_ENAB yes\n' >"$factory/etc/login.defs"
|
||||
printf 'seller:100000:65536\n' >"$factory/etc/subuid"
|
||||
printf 'seller:100000:65536\n' >"$factory/etc/subgid"
|
||||
chmod 600 "$factory/etc/"{shadow,gshadow}
|
||||
for file in passwd shadow group gshadow subuid subgid; do
|
||||
cp "$factory/etc/$file" "$factory/etc/$file-"
|
||||
done
|
||||
}
|
||||
|
||||
assert_scrubbed() {
|
||||
local root="$1" file
|
||||
[[ $(awk -F: '$1 == "root" { print $2 }' "$root/etc/shadow") == "!" ]] ||
|
||||
fail "reset erases the root hash while keeping the account locked"
|
||||
! grep -q 'original-.*-hash\|seller' "$root/etc/"{passwd,shadow,group,gshadow} ||
|
||||
fail "reset removes seller account credentials and group membership"
|
||||
[[ ! -e $root/home/seller ]] || fail "reset removes the seller's baseline home"
|
||||
grep -q '^daemon:\*:' "$root/etc/shadow" || fail "reset preserves service accounts"
|
||||
[[ $(stat -c '%a' "$root/etc/shadow") == "600" ]] || fail "shadow stays private"
|
||||
for file in passwd shadow group gshadow subuid subgid; do
|
||||
[[ ! -e $root/etc/$file- ]] || fail "reset removes the $file backup"
|
||||
done
|
||||
}
|
||||
|
||||
for scenario in normal locked; do
|
||||
top="$test_tmp/$scenario"
|
||||
if [[ $scenario == "locked" ]]; then
|
||||
make_fixture "$top" '!'
|
||||
else
|
||||
make_fixture "$top"
|
||||
fi
|
||||
bash "$test_tmp/reset" "$test_tmp" "$top" || fail "$scenario reset stages successfully"
|
||||
assert_scrubbed "$top/@factory"
|
||||
assert_scrubbed "$top/@"
|
||||
[[ $(cat "$top/@factory/read-only") == "true" ]] || fail "baseline returns to read-only"
|
||||
[[ -f $top/@/var/lib/omarchy/provisioning/pending && -f $top/rebuilt ]] ||
|
||||
fail "reset reaches provisioning after cleanup"
|
||||
|
||||
bash "$test_tmp/reset" "$test_tmp" "$top" || fail "$scenario reset can be repeated"
|
||||
assert_scrubbed "$top/@factory"
|
||||
assert_scrubbed "$top/@"
|
||||
pass "$scenario reset scrubs both roots, preserves service accounts, and can be repeated"
|
||||
done
|
||||
|
||||
for target in @omarchy-reset-next @factory; do
|
||||
for command in userdel usermod rm; do
|
||||
top="$test_tmp/fail-$target-$command"
|
||||
make_fixture "$top"
|
||||
if FAIL_COMMAND="$command" FAIL_ROOT="$top/$target" bash "$test_tmp/reset" "$test_tmp" "$top"; then
|
||||
fail "reset accepted failed $command in $target"
|
||||
fi
|
||||
[[ -f $top/@/old-system && ! -e $top/rebuilt ]] ||
|
||||
fail "failed cleanup must not activate or rebuild the reset system"
|
||||
[[ $(cat "$top/@factory/read-only") == "true" ]] ||
|
||||
fail "failed cleanup must leave the baseline read-only"
|
||||
pass "failed $command in $target aborts reset before activation"
|
||||
done
|
||||
done
|
||||
Reference in new issue
Block a user