Skip to content

DEVICE_REINSTALL — Linux-native reinstall-and-re-enroll (GRUB one-shot + autoinstall) #491

Description

@PaulDotterer

Problem

"Wipe but keep enrollment" has no built-in mechanism on Linux (no recovery partition, no A/B slots). A factory-reset-equivalent that preserves the device identity requires a fresh OS install. Design discussed and converged 2026-07-03.

Converged design (summary)

Admin provides an ISO (HTTPS URL + SHA-256), autoinstall file(s), and a kernel cmdline. The agent stages everything, boots the distro installer via a GRUB one-shot entry, the autoinstall drives a fresh install, and a one-time reinstall token lets the fresh agent re-enroll as the same device ULID — assignments re-run and re-provision automatically.

Key decisions:

  • Name: ACTION_TYPE_DEVICE_REINSTALL / DeviceReinstallParams — it is a reinstall, not disk erasure (LUKS + CRL already covers lost/stolen).
  • Instant, dispatch-only, single-device-only.
  • GRUB first behind the standard sys/boot Backend interface (kexec later, additive enum field then — no boot_method field in v1). Staging under /boot/pm-reinstall/ (works on LUKS; /boot is readable by GRUB). One-shot via grub-editenv next_entry → falls back to the intact old OS if the installer kernel never comes up.
  • Autoinstall delivery: agent packs templated autoinstall file(s) into a newc cpio and concatenates it onto the installer initrd (/pm-autoinstall/…; {autoinstall_path} cmdline variable). file:// against the old rootfs cannot work post-boot — initrd embedding is the mechanism. repeated AutoinstallFile {path, content} because NoCloud needs user-data + meta-data.
  • Token reaches the fresh OS via agent-side template substitution of {enrollment_token} / {control_url} / {device_id} inside the autoinstall content (documented per-distro late-command snippets).
  • ISO layout is admin-owned: iso_kernel_path / iso_initrd_path params, no probing.
  • stage_only dry-run: full download → verify → extract → template → report, no reboot. Canary story.
  • URL stays opaque HTTPS + sha256 — future S3/artifact storage integrates by minting the URL, zero proto changes (same shape as DEB/RPM/APP_IMAGE/AGENT_UPDATE).

Server-side work items

  • Proto (sdk): ACTION_TYPE_DEVICE_REINSTALL + DeviceReinstallParams (iso_url, iso_sha256, iso_kernel_path, iso_initrd_path, kernel_cmdline, repeated autoinstall_files, stage_only, enrollment_token) with full @gotags validate tags
  • Dispatch-surface lockdown: reject the type in CreateAction, AddActionToSet, definitions/action-sets, DispatchToGroup, DispatchToMultiple — single-device DispatchInstantAction only. Self-discovering deny test across ALL dispatch surfaces (AddActionToSet-bypass lesson, cf. fix(api): reject system actions in AddActionToSet (#477 guard gap) #485)
  • DispatchInstantAction path: reject client-supplied enrollment_token, server generates + injects it, signs the envelope
  • Reinstall token: new registration-token kind, hashed at rest, single-use, bound to existing device ULID, fixed 24 h TTL, janitor expiry
  • Enrollment mapping: reinstall-token enrollment maps to the existing device — supersede + CRL-revoke old cert fingerprint, close open executions, preserve ULID/labels/groups/assignments
  • State machine via events: ReinstallDispatched → ReinstallStaged → ReinstallExecuting (terminal agent event pre-reboot) → DeviceReinstalled | ReinstallExpired; device UI state "reinstalling"
  • New permission device:reinstall (+ scoped variant); UI step-up confirmation (type device name); audit events for dispatch + re-enrollment
  • Redaction: params.enrollmentToken + params.autoinstallFiles[].content in actionRedactionSchemas

Cross-repo (tracked separately when work starts)

  • sdk: sys/boot — Backend enum (GRUB first: Preflight = bootloader detection fail-closed on non-GRUB, grubenv writability incl. btrfs/zfs /boot gotcha, disk space; Stage = /boot/pm-reinstall + cpio append; Execute = next_entry + reboot), newc cpio writer, container tests
  • agent: executor (streaming ISO download w/ incremental SHA-256 + Range resume, checksum-pinned redirect policy, loop mount ro,nodev,nosuid,noexec, templating, stage_only path, flush/stop-scheduler/sync before reboot)
  • docs: per-distro autoinstall cookbook (Kickstart / NoCloud / preseed / AutoYaST) with tested agent-install late-commands; QEMU e2e matrix

Non-goals

  • No disk-erasure / "FULL wipe" mode (LUKS + CRL covers lost/stolen)
  • No autoinstall format parsing/validation — admin owns it (wrong format = interactive installer)
  • No kexec in v1; no artifact storage (separate feature; this action only consumes URL+sha256)

Process

Spec-driven: spec 19 in docs/content/06-specs/ with acceptance criteria + rejection-paths table + test requirements, approved before implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions