Merge pull request #10379 from AksharP5/fix/factory-reset-password-hashes

Erase old password hashes during factory reset
This commit is contained in:
Erik Melton authored and GitHub committed 2026-09-16 18:07:22 +02:00
commit f6ce7c554f
2 files changed
+173 -14

No files matched your search

+25 -14
View File
@@ -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
+148
View File
@@ -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