Require a factory snapshot to reset a computer (#6680)

Machines without @factory fell back to a degraded reset that kept the
current system and only wiped user state. Turn them away with an
explanation instead, and drop the degraded staging path.

The first-boot worker still honors a wipe-degraded marker so a reset
staged by an older version finishes its scrub rather than handing the
machine over with the seller's accounts intact.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-10 17:41:05 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 0b670cd13e
commit 4ab51df2b0
4 changed files with 50 additions and 90 deletions
+2 -2
View File
@@ -939,8 +939,8 @@ reset_limine_config() {
} }
cleanup_oem_state() { cleanup_oem_state() {
# Keep groups + packages: omarchy-system-factory-reset's degraded path (machines # Keep groups + packages: omarchy-system-factory-reset stages the bundled Node
# without @factory) re-arms first-boot setup from the live copies. # tarball from these live copies when the factory snapshot predates it.
rm -f "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/authorized_keys" "$PROVISIONING_DIR/setup-user" rm -f "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/authorized_keys" "$PROVISIONING_DIR/setup-user"
rm -f /etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service rm -f /etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service
+25 -63
View File
@@ -8,11 +8,11 @@
# Returns the machine to provisioning state — fully installed, no user, first-boot # Returns the machine to provisioning state — fully installed, no user, first-boot
# setup pending — the state you want before selling or handing the machine on. # setup pending — the state you want before selling or handing the machine on.
# #
# On machines installed with an ISO that created the @factory snapshot, the # The running root (@) is swapped for a fresh writable clone of the @factory
# running root (@) is swapped for a fresh writable clone of @factory, so # snapshot, so user-installed packages and /etc drift are gone too. That
# user-installed packages and /etc drift are gone too. Machines without # snapshot is taken by the Omarchy Quattro installer, so only machines
# @factory get a degraded reset: the current system is kept and only user # installed from that ISO can be reset; a machine upgraded to Quattro from an
# accounts, home data, and machine identity are wiped. # earlier version has no baseline to return to and is turned away.
# #
# The heavy lifting happens on the next boot via omarchy-system-factory-reset-finish, staged # The heavy lifting happens on the next boot via omarchy-system-factory-reset-finish, staged
# here. On encrypted installs the LUKS volume is re-keyed to a throwaway # here. On encrypted installs the LUKS volume is re-keyed to a throwaway
@@ -99,9 +99,6 @@ generate_passphrase() {
} }
cleanup() { cleanup() {
if (( ${swap_done:-0} == 0 )); then
rollback_degraded_rekey
fi
if mountpoint -q "$TOP_MNT" 2>/dev/null; then if mountpoint -q "$TOP_MNT" 2>/dev/null; then
if [[ -d $TOP_MNT/$NEXT_NAME && ${swap_done:-0} == 0 ]]; then if [[ -d $TOP_MNT/$NEXT_NAME && ${swap_done:-0} == 0 ]]; then
btrfs subvolume delete --recursive "$TOP_MNT/$NEXT_NAME" >/dev/null 2>&1 || true btrfs subvolume delete --recursive "$TOP_MNT/$NEXT_NAME" >/dev/null 2>&1 || true
@@ -113,23 +110,13 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
confirm_reset() { confirm_reset() {
local degraded="$1"
echo echo
gum style --bold --foreground 1 "Reset this computer to factory state?" gum style --bold --foreground 1 "Reset this computer to factory state?"
echo echo
gum style "This will permanently erase:" gum style "This will permanently erase:"
gum style " • All user accounts and everything in /home" gum style " • All user accounts and everything in /home"
if [[ $degraded == true ]]; then gum style " • All packages and system changes made since installation"
gum style --foreground 3 " • Machine identity (network connections, host keys, machine-id)" gum style " • Machine identity (network connections, host keys, machine-id)"
echo
gum style --foreground 3 "This machine was installed before factory snapshots existed, so the"
gum style --foreground 3 "current system (installed packages, /etc changes) is kept — only user"
gum style --foreground 3 "accounts and machine state are wiped."
else
gum style " • All packages and system changes made since installation"
gum style " • Machine identity (network connections, host keys, machine-id)"
fi
echo echo
gum style "The next boot asks for a new user, exactly like a fresh install." gum style "The next boot asks for a new user, exactly like a fresh install."
gum style --foreground 8 "Note: on disks without encryption this is deletion, not secure erasure." gum style --foreground 8 "Note: on disks without encryption this is deletion, not secure erasure."
@@ -141,8 +128,7 @@ confirm_reset() {
} }
# Re-key to a throwaway passphrase whose keyfile auto-unlocks boot during the # Re-key to a throwaway passphrase whose keyfile auto-unlocks boot during the
# provisioning window. $1 is the root of the system the staging applies to ("" for the # provisioning window. $1 is the root of the staged factory system.
# running root in degraded mode).
stage_luks_rekey() { stage_luks_rekey() {
local next="$1" device passphrase local next="$1" device passphrase
@@ -396,40 +382,21 @@ stage_full_reset() {
sync sync
} }
stage_degraded_reset() { # A reset is a clone of @factory, so a machine without one has nothing to
local unit_src="$OMARCHY_PATH/install/provisioning" # return to. Only the Quattro installer takes that snapshot.
[[ -f $unit_src/omarchy-provision-owner.service && -x /usr/bin/omarchy-provision-owner ]] || require_factory_snapshot() {
fail "this Omarchy version does not ship provisioning; update first" [[ -d $TOP_MNT/@factory ]] && return 0
# Fallible staging (re-key, boot rebuild) happens before the wipe is armed: echo
# a failure must leave the machine untouched, not schedule a wipe for a gum style --bold --foreground 3 "This machine has no factory snapshot to reset to."
# reset that never finished staging. The degraded path stages into the LIVE echo
# root, so a mid-staging failure must also undo the auto-unlock material it gum style "A reset restores the @factory snapshot the Omarchy Quattro installer takes"
# already wrote — otherwise a later boot rebuild would silently embed it. gum style "right after installing. Machines upgraded to Quattro from an earlier version,"
install -d -m 755 "$PROVISIONING_DIR" gum style "or installed some other way, never got one, so there is no baseline here to"
if encrypted_install; then gum style "return to."
degraded_rekey_staged=1 echo
stage_luks_rekey "" gum style --foreground 8 "Reinstall from the Omarchy ISO to make this machine resettable."
log "Rebuilding boot files with the provisioning unlock key" exit 1
if ! limine-update >>"$LOG_FILE" 2>&1; then
fail "limine-update failed (see $LOG_FILE)"
fi
degraded_rekey_staged=0
fi
install_provisioning_units "" "$unit_src"
touch "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/wipe-pending" "$PROVISIONING_DIR/wipe-degraded"
swap_done=1
}
# Undo the live-root auto-unlock material a failed degraded rekey left behind.
rollback_degraded_rekey() {
(( ${degraded_rekey_staged:-0} )) || return 0
rm -f /etc/omarchy/provisioning.key "$PROVISIONING_DIR/luks-key" \
/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf \
/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf
limine-update >>"$LOG_FILE" 2>&1 || true
} }
main() { main() {
@@ -448,10 +415,9 @@ main() {
mkdir -p "$TOP_MNT" mkdir -p "$TOP_MNT"
mountpoint -q "$TOP_MNT" || mount -o subvolid=5 "$device" "$TOP_MNT" mountpoint -q "$TOP_MNT" || mount -o subvolid=5 "$device" "$TOP_MNT"
local degraded=false require_factory_snapshot
[[ -d $TOP_MNT/@factory ]] || degraded=true
confirm_reset "$degraded" confirm_reset
# The running system's limine-snapper-sync must not rewrite the ESP's # The running system's limine-snapper-sync must not rewrite the ESP's
# limine.conf behind the staged rebuild (subvolume changes below can # limine.conf behind the staged rebuild (subvolume changes below can
@@ -459,11 +425,7 @@ main() {
systemctl mask --runtime --now limine-snapper-sync.service >/dev/null 2>&1 || true systemctl mask --runtime --now limine-snapper-sync.service >/dev/null 2>&1 || true
systemctl mask --runtime --now limine-snapper-sync.path >/dev/null 2>&1 || true systemctl mask --runtime --now limine-snapper-sync.path >/dev/null 2>&1 || true
if [[ $degraded == true ]]; then stage_full_reset
stage_degraded_reset
else
stage_full_reset
fi
umount -R "$TOP_MNT" 2>/dev/null || true umount -R "$TOP_MNT" 2>/dev/null || true
+22 -24
View File
@@ -9,13 +9,11 @@
# Ordered before home.mount/var-log.mount so @home and @log can be dropped and # Ordered before home.mount/var-log.mount so @home and @log can be dropped and
# recreated as empty subvolumes instead of rm -rf'd file by file. # recreated as empty subvolumes instead of rm -rf'd file by file.
# #
# Two modes: # The root is already a fresh clone of @factory (staged by
# - full reset: the root is already a fresh clone of @factory (staged by # omarchy-system-factory-reset); this deletes the previous root (@omarchy-old-*),
# omarchy-system-factory-reset); this deletes the previous root (@omarchy-old-*), # recreates @home/@log, and repairs /.snapshots. A reset staged by an older
# recreates @home/@log, and repairs /.snapshots. # Omarchy on a machine without @factory (wipe-degraded) kept its root instead,
# - degraded (wipe-degraded marker): machines installed before @factory # and is scrubbed in place here.
# existed keep their current root; user accounts and identity state are
# scrubbed here instead.
set -uo pipefail set -uo pipefail
@@ -76,7 +74,13 @@ recreate_subvolume() {
btrfs subvolume create "$TOP_MNT/$name" btrfs subvolume create "$TOP_MNT/$name"
} }
scrub_degraded_state() { # Only for a reset staged by an Omarchy that still had the degraded path: the
# current system was kept, so accounts and identity are scrubbed in place here
# instead of arriving pre-scrubbed in the factory clone. Reachable when a
# degraded reset was staged, the reboot deferred, and this worker updated in
# between — the reset must still finish, or provisioning hands the machine over
# with the seller's accounts intact.
scrub_legacy_degraded_state() {
log "degraded reset: scrubbing user accounts and machine identity in place" log "degraded reset: scrubbing user accounts and machine identity in place"
local user local user
@@ -93,6 +97,15 @@ scrub_degraded_state() {
# Fresh machine identity from the next boot on. # Fresh machine identity from the next boot on.
systemd-id128 new >/etc/machine-id 2>/dev/null || :>/etc/machine-id systemd-id128 new >/etc/machine-id 2>/dev/null || :>/etc/machine-id
# The kept root still carries its accumulated snapper snapshots (nested under
# /.snapshots); a factory clone loses them with the old root.
local snapshot
for snapshot in /.snapshots/*/snapshot; do
[[ -d $snapshot ]] || continue
delete_subvolume "$snapshot"
rm -rf "$(dirname "$snapshot")"
done
} }
repair_snapshots_dir() { repair_snapshots_dir() {
@@ -108,18 +121,6 @@ repair_snapshots_dir() {
fi fi
} }
wipe_snapper_snapshots() {
# Degraded resets keep the current root, so its accumulated snapper
# snapshots (nested under /.snapshots) still exist. Full resets already lost
# them with the old root.
local snapshot
for snapshot in /.snapshots/*/snapshot; do
[[ -d $snapshot ]] || continue
delete_subvolume "$snapshot"
rm -rf "$(dirname "$snapshot")"
done
}
main() { main() {
local device local device
device=$(root_device) device=$(root_device)
@@ -134,10 +135,7 @@ main() {
exit 1 exit 1
fi fi
if [[ -f $PROVISIONING_DIR/wipe-degraded ]]; then [[ -f $PROVISIONING_DIR/wipe-degraded ]] && scrub_legacy_degraded_state
scrub_degraded_state
wipe_snapper_snapshots
fi
local old local old
for old in "$TOP_MNT"/@omarchy-old-*; do for old in "$TOP_MNT"/@omarchy-old-*; do
+1 -1
View File
@@ -22,7 +22,7 @@ if [[ -n $NODE_PACKAGE_DIR ]]; then
NODE_TARBALL=$(find "$NODE_PACKAGE_DIR" -name "node-v*-linux-x64.tar.gz" -type f 2>/dev/null | head -n1) NODE_TARBALL=$(find "$NODE_PACKAGE_DIR" -name "node-v*-linux-x64.tar.gz" -type f 2>/dev/null | head -n1)
if [[ -z $NODE_TARBALL ]]; then if [[ -z $NODE_TARBALL ]]; then
if [[ ${OMARCHY_SETUP_CONTEXT:-} == "provision-owner" ]]; then if [[ ${OMARCHY_SETUP_CONTEXT:-} == "provision-owner" ]]; then
# A degraded reset on an old install may not have the tarball staged. # A factory snapshot predating the bundled tarball may not have it staged.
# Leave Node to the network rather than failing the whole first boot. # Leave Node to the network rather than failing the whole first boot.
echo "Warning: no bundled Node.js tarball in $NODE_PACKAGE_DIR; trying the network" >&2 echo "Warning: no bundled Node.js tarball in $NODE_PACKAGE_DIR; trying the network" >&2
mise use -g node@latest || echo "Warning: Node.js install deferred (no network)" >&2 mise use -g node@latest || echo "Warning: Node.js install deferred (no network)" >&2