max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
4 files changed,
+84 insertions,
-41 deletions
| @@ -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 | * |