Skip to main content

max / alloy

Correct the NetworkManager policy row, and assert the table at build settings.modify.system reads allow_active=yes on the image we ship, not the auth_admin_keep recorded in three places. That value was read on fw13, which runs Pop!_OS. The other fifteen rows match, so the tier 1 grant rationale is unchanged, but the rules file justified not granting it with a prompt that does not happen. The Containerfile block asserted only that the action ids were defined, which is why the drift went unseen. It now reads allow_active out of the policy XML against the same table check-installed.sh carries, single sourced so the two cannot diverge again. Also: the seed cache README claimed the mirror hosting decision had not been made while documenting the mirror two sections above it, and the seed contract comment narrated its own edits.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-01 15:16 UTC
Signed with PGP, not checked
Commit: 33027e848d61e9397abd15ce2c463e37b55f303f
Parent: fe01327
4 files changed, +84 insertions, -41 deletions
M Containerfile +43 -14
@@ -344,12 +344,12 @@
344 344 # pin, and it would go stale the first time QUASI_TYPE_REV moved to a base with
345 345 # a new version in its file names.
346 346 #
347 - # THE COPY ON INFRASTRUCTURE WE OWN EXISTS NOW, and it is the piece this comment
348 - # used to say was missing. The four pinned files live in the MNW server's
349 - # `static/bases/` named by their sha256, and `QUASI_TYPE_MIRROR` near the top of
350 - # this stage points quasi-type at them. It is an ENV over the whole stage rather
351 - # than a flag here because the fetch that took the build down was shop's, not
352 - # these two, and shop reads the same variable through the same crate.
347 + # THE COPY ON INFRASTRUCTURE WE OWN is the mirror. The four pinned files live
348 + # in the MNW server's `static/bases/` named by their sha256, and
349 + # `QUASI_TYPE_MIRROR` near the top of this stage points quasi-type at them. It
350 + # is an ENV over the whole stage rather than a flag here because the fetch that
351 + # took the build down was shop's, not these two, and shop reads the same
352 + # variable through the same crate.
353 353 #
354 354 # It adds a source and does not remove one: every base still carries its
355 355 # upstream url and falls back to it, so a build with no access to makenot.work
@@ -3131,24 +3131,36 @@
3131 3131 || { echo "udisks2 ships no service unit; nothing would answer udisksctl" >&2; exit 1; }; \
3132 3132 echo "udisks: present, D-Bus activated, drives alloy disk"
3133 3133
3134 - # polkit rules — assert the grant is not inert.
3134 + # polkit rules — assert the grant is not inert, and that the vendor
3135 + # defaults it rests on are the ones it was written against.
3135 3136 #
3136 3137 # usr/share/polkit-1/rules.d/50-alloy-settings.rules turns five actions
3137 3138 # into silent yeses so `alloy settings` does not raise a password prompt
3138 - # to change a timezone. Two ways that file can be shipped and do
3139 - # nothing at all, both silent:
3139 + # to change a timezone. Three ways that file can be shipped and mean
3140 + # something other than what it says, all silent:
3140 3141 #
3141 3142 # - polkit is not in the base image, so nothing reads rules.d
3142 3143 # - an action id was renamed upstream, so the grant names something
3143 3144 # that no longer exists and the console prompts anyway
3145 + # - a vendor default moved: one that became `yes` makes the rule a
3146 + # security artifact shipped for nothing, and one that became
3147 + # `auth_admin` takes away the burst-caching the settings tab counts
3148 + # on
3144 3149 #
3145 - # Both are checked here rather than discovered on a booted machine,
3150 + # All three are checked here rather than discovered on a booted machine,
3146 3151 # where the symptom is a password prompt nobody can explain. The action
3147 - # ids are read back out of the shipped rule so this cannot drift from
3148 - # it. Grant rationale is in the rule's own header and in wiki note
3149 - # `alloy-privilege`.
3152 + # ids are read back out of the shipped rule and the defaults out of the
3153 + # image's own `.policy` files, so neither reading can drift from what it
3154 + # is checked against. Grant rationale is in the rule's own header and in
3155 + # wiki note `alloy-privilege`.
3156 + #
3157 + # The expected defaults are POLICY_TABLE in build/check-installed.sh,
3158 + # bind-mounted for the length of this step rather than copied, so the
3159 + # table has one home and the image gains no file for it. That script
3160 + # asks the same question of a running machine with `--policy`.
3150 3161 # =====================================================================
3151 - RUN set -eux; \
3162 + RUN --mount=type=bind,source=build/check-installed.sh,target=/run/check-installed.sh \
3163 + set -eux; \
3152 3164 rules=/usr/share/polkit-1/rules.d/50-alloy-settings.rules; \
3153 3165 [ -d /usr/share/polkit-1/actions ] \
3154 3166 || { echo "no polkit in the base image; $rules would never be read" >&2; exit 1; }; \
@@ -3159,6 +3171,23 @@
3159 3171 grep -qr "action id=\"$action\"" /usr/share/polkit-1/actions/ \
3160 3172 || { echo "granted action $action is not one this image defines" >&2; exit 1; }; \
3161 3173 done; \
3174 + awk '/^POLICY_TABLE=/ { inside = 1; next } \
3175 + inside && $0 !~ /^org\./ { inside = 0 } \
3176 + inside && NF == 2 { print $1, $2 }' \
3177 + /run/check-installed.sh > /tmp/policy-expected; \
3178 + [ -s /tmp/policy-expected ] \
3179 + || { echo "read no POLICY_TABLE out of build/check-installed.sh" >&2; exit 1; }; \
3180 + awk 'match($0, /<action id="[^"]*"/) { id = substr($0, RSTART + 12, RLENGTH - 13) } \
3181 + match($0, /<allow_active>[^<]*/) { print id, substr($0, RSTART + 14, RLENGTH - 14) }' \
3182 + /usr/share/polkit-1/actions/*.policy > /tmp/policy-actual; \
3183 + awk 'NR == FNR { actual[$1] = $2; next } \
3184 + { rows++ } \
3185 + !($1 in actual) { printf "%s is defined by no .policy file in this image\n", $1 > "/dev/stderr"; bad++; next } \
3186 + actual[$1] != $2 { printf "%s reads %s, POLICY_TABLE says %s\n", $1, actual[$1], $2 > "/dev/stderr"; bad++ } \
3187 + END { if (bad) { printf "%d polkit default(s) drifted from build/check-installed.sh; the ladder is built on that table, so correct it before the rules file\n", bad > "/dev/stderr"; exit 1 } \
3188 + printf "polkit: %d implicit defaults match build/check-installed.sh\n", rows }' \
3189 + /tmp/policy-actual /tmp/policy-expected; \
3190 + rm -f /tmp/policy-expected /tmp/policy-actual; \
3162 3191 echo "polkit: granted $(echo "$granted" | wc -l) actions, all defined"
3163 3192
3164 3193 # =====================================================================
@@ -38,14 +38,16 @@
38 38 # policy what polkit's *implicit* defaults actually are on this machine,
39 39 # for every action the console depends on.
40 40 #
41 - # Wiki `alloy-privilege` records that table, and every reading in it was taken
42 - # on fw13, which is Pop!_OS. The note says so and asks for it to be re-read on
43 - # a booted Alloy image, because the whole privilege ladder is built on those
44 - # values: tier 1 grants five actions precisely because their upstream default
45 - # is `auth_admin_keep`, and a Fedora override or an upstream change would make
46 - # a shipped rules file either redundant or, worse, the only thing standing
47 - # between a user and a prompt nobody can explain. `--policy` is that re-read,
48 - # reduced to one command someone can run on fw12 and paste back.
41 + # Wiki `alloy-privilege` records that table, read off a built Alloy image
42 + # (NetworkManager 1.54.3, polkit 126). The whole privilege ladder is built on
43 + # those values: tier 1 grants five actions precisely because their default is
44 + # `auth_admin_keep`, and a Fedora override or an upstream change would make a
45 + # shipped rules file either redundant or, worse, the only thing standing
46 + # between a user and a prompt nobody can explain. The Containerfile asserts
47 + # every row of POLICY_TABLE against the image's own `.policy` files on each
48 + # build, reading the table out of this file. `--policy` asks the same question
49 + # of a running machine, where a layered package or an `/etc` drop-in can differ
50 + # from what the image shipped.
49 51 #
50 52 # Usage:
51 53 # sudo build/check-installed.sh # check this machine
@@ -151,12 +153,19 @@
151 153 # answer on an Alloy machine includes
152 154 # `usr/share/polkit-1/rules.d/50-alloy-settings.rules`, which is asserted
153 155 # separately by `crates/alloy/tests/polkit_rules.rs` and by a Containerfile
154 - # block. What is unasserted anywhere, and what this reads, is the vendor
155 - # default underneath: the five granted actions are granted *because* their
156 - # default is `auth_admin_keep`, and a default that quietly became `yes` would
157 - # make the rules file a security artifact shipped for no reason, while one that
158 - # became `auth_admin` would mean the burst-caching the settings tab counts on
159 - # is gone.
156 + # block. What this reads is the vendor default underneath: the five granted
157 + # actions are granted *because* their default is `auth_admin_keep`, and a
158 + # default that quietly became `yes` would make the rules file a security
159 + # artifact shipped for no reason, while one that became `auth_admin` would mean
160 + # the burst-caching the settings tab counts on is gone.
161 + #
162 + # This is the only copy of the table. The Containerfile's build assertion
163 + # parses it out of this file rather than restating it, so the two cannot
164 + # disagree.
165 + #
166 + # `NetworkManager.settings.modify.system` reads `yes` on the image, so saving a
167 + # system connection needs neither a grant nor a prompt. It is in the table
168 + # because the net views depend on that value holding.
160 169 #
161 170 # rpm-ostree and systemd1.manage-units are in the list without being granted
162 171 # anything: the first is what `alloy update` and `alloy pkg` go through and the
@@ -166,7 +175,7 @@
166 175 org.freedesktop.NetworkManager.network-control yes
167 176 org.freedesktop.NetworkManager.enable-disable-wifi yes
168 177 org.freedesktop.NetworkManager.enable-disable-network yes
169 - org.freedesktop.NetworkManager.settings.modify.system auth_admin_keep
178 + org.freedesktop.NetworkManager.settings.modify.system yes
170 179 org.freedesktop.timedate1.set-timezone auth_admin_keep
171 180 org.freedesktop.timedate1.set-ntp auth_admin_keep
172 181 org.freedesktop.timedate1.set-time auth_admin_keep
@@ -15,11 +15,11 @@
15 15
16 16 The two cuts the image runs, and nothing else. A build makes three requests to
17 17 that host: these two, and a third from shop's own build script, which cuts the
18 - terminal's bundled face while `cargo install shop` runs. This directory does not
19 - cover that third one: shop's build script passes its own `OUT_DIR` as the cache
20 - directory, so there is no path for the image build to point at a carried copy. Covering it with a seed
21 - needs a change in shop (read the cache directory and the offline flag from the
22 - environment).
18 + terminal's bundled face while `cargo install shop` runs. This directory
19 + does not cover that third one: shop's build script passes its own `OUT_DIR` as
20 + the cache directory, so there is no path for the image build to point at a
21 + carried copy. Covering it with a seed needs a change in shop (read the cache
22 + directory and the offline flag from the environment).
23 23
24 24 What covers all three without that change is the mirror. `QUASI_TYPE_MIRROR`,
25 25 set in the Containerfile to `https://makenot.work/static/bases`, is read inside
@@ -78,8 +78,12 @@
78 78
79 79 ## What none of this is
80 80
81 - A fix. The durable answer is a copy of the bases on infrastructure we own,
82 - addressed by the same sha256, so that an unseeded build has somewhere to reach
83 - that is not somebody else's rate limiter. That is a hosting decision and it has
84 - not been made. It is also the only one of these that would cover shop's fetch
85 - without shop changing.
81 + A fix. It is a carried copy of two files and it goes stale the first time
82 + `QUASI_TYPE_REV` moves. The durable answer is the mirror above: the same files
83 + on infrastructure we own, addressed by the same sha256, so an unseeded build has
84 + somewhere to reach that is not somebody else's rate limiter, and reached from
85 + inside `quasi-type` so it covers shop's fetch too.
86 +
87 + What the seed buys on top of the mirror is `sealed`: a cut that reaches no
88 + network at all, which is the only way to prove the two faces came from the bytes
89 + the pins name and nothing else.
@@ -32,8 +32,9 @@
32 32 * location. Location in particular is not a thing
33 33 * to hand over for a screen that sets a hostname
34 34 * hostname1.get-product-uuid, .get-hardware-serial identifiers, not settings
35 - * NetworkManager.settings.modify.system saving a new connection is a real
36 - * administrative act and keeps its prompt
35 + * NetworkManager.settings.modify.system nothing to grant: it already reads
36 + * allow_active=yes on this image, so saving a
37 + * system connection is answered without asking
37 38 * rpmostree1.* changing the system image should be asked about;
38 39 * the prompt there is the point
39 40 *