Skip to main content

max / alloy

privilege: name run0 as the way to root, not sudo run0 asks PID 1 for a transient unit running as root and PID 1 asks polkit, so becoming root goes through the same authority every writing console view already goes through. Nothing in the console changes; what changes is what Alloy names. sudo stays in the base and keeps working. The fingerprint promise in HARDWARE-FW12 was the one real defect: it named /etc/pam.d/sudo, and a fprintd stack there would have covered nothing a user is told to type. It is polkit's stack that matters, and configuring it once covers run0 and the console together. A Containerfile guard beside the polkit one, for the same reason that one exists: run0 present, systemd >= 256, manage-units defined in the image's own .policy files. A documented command missing from the image is silent at build time. The build scripts keep their sudo. Their calls are rootful podman on the dev host, not the shipped OS, and fw13 has systemd 255 and no run0 at all. Deferred as GO task dce514e8, with the pty and env gotchas written down. Rationale in wiki alloy-privilege.
Author: Max Johnson <me@maxj.phd> · 2026-07-25 13:09 UTC
Signed with PGP, not checked
Commit: fc7e9e036ac59cc87f8ab533224234cd133c73f3
Parent: 28f80d9
4 files changed, +46 insertions, -9 deletions
@@ -466,6 +466,41 @@
466 466 done; \
467 467 echo "polkit: granted $(echo "$granted" | wc -l) actions, all defined"
468 468
469 + # =====================================================================
470 + # run0 — assert the way to root that Alloy documents is in the image.
471 + #
472 + # Alloy names `run0` and not `sudo` (docs/CONTINUITY.md,
473 + # docs/HARDWARE-FW12.md, the installer's account pane; rationale in wiki
474 + # note `alloy-privilege`). run0 is systemd's, arrived in 256, and is a
475 + # symlink to systemd-run: it asks PID 1 for a transient unit running as
476 + # root, and PID 1 asks polkit. So becoming root goes through the same
477 + # authority the console's writing views already go through, and a
478 + # fingerprint or FIDO2 factor configured once at polkit's PAM stack
479 + # covers both.
480 + #
481 + # sudo is still in the base and still works. Nothing here removes it;
482 + # what is asserted is only that the path the docs name is present, since
483 + # a manual telling a user to type run0 on an image without it is worse
484 + # than no manual. Three ways that can be wrong, all silent on a built
485 + # image:
486 + #
487 + # - the base dropped run0, so every doc naming it is a lie
488 + # - systemd is older than 256, so run0 predates its own existence
489 + # - the action a transient-unit request authenticates against is not
490 + # defined, so run0 has no admin to resolve and cannot authorize
491 + # =====================================================================
492 + RUN set -eux; \
493 + command -v run0 >/dev/null \
494 + || { echo "no run0 in the base image; the docs name it as the way to root" >&2; exit 1; }; \
495 + version=$(systemctl --version | sed -n '1s/^systemd \([0-9]*\).*/\1/p'); \
496 + [ -n "$version" ] || { echo "could not read a systemd version" >&2; exit 1; }; \
497 + [ "$version" -ge 256 ] \
498 + || { echo "systemd $version predates run0, which arrived in 256" >&2; exit 1; }; \
499 + action=org.freedesktop.systemd1.manage-units; \
500 + grep -qr "action id=\"$action\"" /usr/share/polkit-1/actions/ \
501 + || { echo "$action is not defined; run0 cannot ask polkit for an admin" >&2; exit 1; }; \
502 + echo "run0: present, systemd $version, $action defined"
503 +
469 504 # =====================================================================
470 505 # Systemd presets — shipped via etc/systemd/{system,user}-preset/
471 506 # in the config tree above. Split across system-preset (greetd,
@@ -16,7 +16,7 @@
16 16
17 17 - The client (`tailscaled` + `tailscale`) is open-source and works with either the tailscale.com control plane or a self-hosted Headscale server.
18 18 - The control plane choice is exposed at enrollment time as a "Server" field, defaulting to tailscale.com. Users pointing at Headscale set `--login-server=<url>`, which Alloy handles as a first-class field in `alloy mesh` rather than a footnote. The console reads the control plane back and names it in the view title when it is self-hosted, so "which control plane am I on" is answerable without dropping to a shell.
19 - - Enabled via `alloy mesh` (see below) or plain `sudo tailscale up`. Not enrolled by default. An OS that phones home before the user says yes is out of character for Alloy.
19 + - Enabled via `alloy mesh` (see below) or plain `run0 tailscale up`. Not enrolled by default. An OS that phones home before the user says yes is out of character for Alloy.
20 20
21 21 **Not shipping the Headscale server itself.** That is fleet-admin work, not client-OS work. Users self-hosting Headscale run it on their infrastructure of choice; Alloy points at it.
22 22
@@ -68,7 +68,7 @@
68 68
69 69 Framework 12 ships a fingerprint reader on the power button. Upstream Linux support is via `libfprint`; enrollment through `fprintd`. Fedora Silverblue includes the stack.
70 70
71 - - Alloy configures PAM to allow fingerprint at sudo and at the lockscreen.
71 + - Alloy configures PAM to allow fingerprint wherever the system asks who you are: at the lockscreen, and at the polkit prompt, which is the one `run0` and the console's writing views both raise. The file that matters for the second is `/etc/pam.d/polkit-1`, not `/etc/pam.d/sudo` — Alloy documents `run0` rather than `sudo` (wiki note `alloy-privilege`), so a fingerprint stack that only covered sudo would cover nothing a user is told to type.
72 72 - Fingerprint unlock wires through swaylock's PAM stack (the authored egui lockscreen was dropped in the pivot). No Alloy code beyond PAM config.
73 73
74 74 ## Firmware (fwupd / LVFS)
@@ -98,7 +98,7 @@
98 98
99 99 ## Open questions
100 100
101 - - [ ] fprintd unlock through swaylock's PAM stack on the FW12: enroll, then verify unlock at swaylock and at sudo.
101 + - [ ] fprintd unlock through swaylock's PAM stack on the FW12: enroll, then verify unlock at swaylock and at a `run0` prompt (polkit's PAM stack, via `pkttyagent`).
102 102 - [ ] Fractional-scale validation at 1.25x on this panel under sway: do terminal glyph metrics stay crisp?
103 103 - [ ] s2idle drain measurement on FW12 running the Alloy image, baseline vs. tuned. If drain is bad enough to matter, revisit whether Alloy ships any power tweaks or leaves it to Fedora defaults.
104 104 - [ ] (shelved) The `alloy-hinged` fold-to-notes questions (keyboard/trackpad suppression on `SW_TABLET_MODE`, Rnote fullscreen behavior) revive only if the tablet flow comes back.
@@ -782,9 +782,10 @@
782 782 .arg("--force"),
783 783 ),
784 784 // useradd rather than a sysusers entry because this account needs a home
785 - // and a supplementary group. wheel is what sudo grants on Fedora, and an
786 - // install whose only account cannot escalate is one with no way to
787 - // administer itself.
785 + // and a supplementary group. wheel is the group Fedora's polkit resolves
786 + // an administrator to, which is what `run0` and every writing console
787 + // view ask for, and an install whose only account cannot escalate is one
788 + // with no way to administer itself. See wiki note `alloy-privilege`.
788 789 //
789 790 // --no-create-home despite the account needing one: --create-home
790 791 // resolves the path inside the target, which puts the directory in the
@@ -1622,7 +1623,7 @@
1622 1623 let lines = vec![
1623 1624 Line::from(text::muted(
1624 1625 theme,
1625 - "The account you will log in with. It can use sudo.",
1626 + "The account you will log in with. It can become root with run0.",
1626 1627 )),
1627 1628 Line::default(),
1628 1629 self.field_line(theme, "username", &self.username, FIELD_USERNAME, false),
@@ -2877,9 +2878,10 @@
2877 2878 }
2878 2879
2879 2880 // An account that cannot escalate leaves an install with no way to
2880 - // administer itself.
2881 + // administer itself. wheel is the group polkit resolves an administrator
2882 + // to, so it is what `run0` and the console's writing views both ask for.
2881 2883 #[test]
2882 - fn the_account_can_sudo() {
2884 + fn the_account_can_administer_the_machine() {
2883 2885 let useradd = command_starting("useradd");
2884 2886
2885 2887 assert!(useradd.contains("wheel"), "{useradd}");