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() {
# Keep groups + packages: omarchy-system-factory-reset's degraded path (machines
# without @factory) re-arms first-boot setup from the live copies.
# Keep groups + packages: omarchy-system-factory-reset stages the bundled Node
# 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 /etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service
+22 -60
View File
@@ -8,11 +8,11 @@
# 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.
#
# On machines installed with an ISO that created the @factory snapshot, the
# running root (@) is swapped for a fresh writable clone of @factory, so
# user-installed packages and /etc drift are gone too. Machines without
# @factory get a degraded reset: the current system is kept and only user
# accounts, home data, and machine identity are wiped.
# The running root (@) is swapped for a fresh writable clone of the @factory
# snapshot, so user-installed packages and /etc drift are gone too. That
# snapshot is taken by the Omarchy Quattro installer, so only machines
# installed from that ISO can be reset; a machine upgraded to Quattro from an
# 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
# here. On encrypted installs the LUKS volume is re-keyed to a throwaway
@@ -99,9 +99,6 @@ generate_passphrase() {
}
cleanup() {
if (( ${swap_done:-0} == 0 )); then
rollback_degraded_rekey
fi
if mountpoint -q "$TOP_MNT" 2>/dev/null; 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
@@ -113,23 +110,13 @@ cleanup() {
trap cleanup EXIT
confirm_reset() {
local degraded="$1"
echo
gum style --bold --foreground 1 "Reset this computer to factory state?"
echo
gum style "This will permanently erase:"
gum style " • All user accounts and everything in /home"
if [[ $degraded == true ]]; then
gum style --foreground 3 " • 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
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."
@@ -141,8 +128,7 @@ confirm_reset() {
}
# 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
# running root in degraded mode).
# provisioning window. $1 is the root of the staged factory system.
stage_luks_rekey() {
local next="$1" device passphrase
@@ -396,40 +382,21 @@ stage_full_reset() {
sync
}
stage_degraded_reset() {
local unit_src="$OMARCHY_PATH/install/provisioning"
[[ -f $unit_src/omarchy-provision-owner.service && -x /usr/bin/omarchy-provision-owner ]] ||
fail "this Omarchy version does not ship provisioning; update first"
# A reset is a clone of @factory, so a machine without one has nothing to
# return to. Only the Quattro installer takes that snapshot.
require_factory_snapshot() {
[[ -d $TOP_MNT/@factory ]] && return 0
# Fallible staging (re-key, boot rebuild) happens before the wipe is armed:
# a failure must leave the machine untouched, not schedule a wipe for a
# reset that never finished staging. The degraded path stages into the LIVE
# root, so a mid-staging failure must also undo the auto-unlock material it
# already wrote — otherwise a later boot rebuild would silently embed it.
install -d -m 755 "$PROVISIONING_DIR"
if encrypted_install; then
degraded_rekey_staged=1
stage_luks_rekey ""
log "Rebuilding boot files with the provisioning unlock key"
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
echo
gum style --bold --foreground 3 "This machine has no factory snapshot to reset to."
echo
gum style "A reset restores the @factory snapshot the Omarchy Quattro installer takes"
gum style "right after installing. Machines upgraded to Quattro from an earlier version,"
gum style "or installed some other way, never got one, so there is no baseline here to"
gum style "return to."
echo
gum style --foreground 8 "Reinstall from the Omarchy ISO to make this machine resettable."
exit 1
}
main() {
@@ -448,10 +415,9 @@ main() {
mkdir -p "$TOP_MNT"
mountpoint -q "$TOP_MNT" || mount -o subvolid=5 "$device" "$TOP_MNT"
local degraded=false
[[ -d $TOP_MNT/@factory ]] || degraded=true
require_factory_snapshot
confirm_reset "$degraded"
confirm_reset
# The running system's limine-snapper-sync must not rewrite the ESP's
# 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.path >/dev/null 2>&1 || true
if [[ $degraded == true ]]; then
stage_degraded_reset
else
stage_full_reset
fi
umount -R "$TOP_MNT" 2>/dev/null || true
+21 -23
View File
@@ -9,13 +9,11 @@
# 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.
#
# Two modes:
# - full reset: the root is already a fresh clone of @factory (staged by
# The root is already a fresh clone of @factory (staged by
# omarchy-system-factory-reset); this deletes the previous root (@omarchy-old-*),
# recreates @home/@log, and repairs /.snapshots.
# - degraded (wipe-degraded marker): machines installed before @factory
# existed keep their current root; user accounts and identity state are
# scrubbed here instead.
# recreates @home/@log, and repairs /.snapshots. A reset staged by an older
# Omarchy on a machine without @factory (wipe-degraded) kept its root instead,
# and is scrubbed in place here.
set -uo pipefail
@@ -76,7 +74,13 @@ recreate_subvolume() {
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"
local user
@@ -93,6 +97,15 @@ scrub_degraded_state() {
# Fresh machine identity from the next boot on.
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() {
@@ -108,18 +121,6 @@ repair_snapshots_dir() {
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() {
local device
device=$(root_device)
@@ -134,10 +135,7 @@ main() {
exit 1
fi
if [[ -f $PROVISIONING_DIR/wipe-degraded ]]; then
scrub_degraded_state
wipe_snapper_snapshots
fi
[[ -f $PROVISIONING_DIR/wipe-degraded ]] && scrub_legacy_degraded_state
local old
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)
if [[ -z $NODE_TARBALL ]]; 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.
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