Skip to main content

max / alloy

Take the secureblue review's adopted set, nine files and a SUID strip The 2026-08-21 review (wiki `alloy-hardening-posture`) measured the built image rather than the docs and found the same shape of gap nine times: a policy Alloy had never stated, so whatever Fedora shipped was the answer by default. This lands every item the review adopted. What the image gains: a sysctl drop-in Alloy owns, the KSPP kernel arguments in 10-alloy.toml, an `install <mod> /bin/false` module policy, a faillock policy and a coredump policy, six SUID bits stripped, Xwayland off in the sway config, flatpak's permissive defaults trimmed, DNS over TLS and NTS time. Three of them are deliberately weaker than secureblue's, and each file says so where a reader will find it rather than in a note nobody opens. `kernel.yama.ptrace_scope` stays at Fedora's 1: scope 2 makes `gdb -p` ask for root and this is the machine its author develops on, so it is the mitigation that would get turned off. `rp_filter` is loose rather than strict, because strict reverse-path filtering is a known way to break a Tailscale subnet router and every machine in this tree is addressed by tailnet name. `DNSOverTLS` is opportunistic rather than yes, because a global yes refuses the MagicDNS stub and `ssh astra` failing to resolve is worse than a plaintext lookup on a coffee-shop network. Upgrading that one is a measurement on a tailnet-joined machine, and the file says which. The SUID strip re-adds no capabilities. secureblue hands `cap_dac_read_search` to `unix_chkpwd` so the stripped tools keep working unprivileged; Kicksecure's objection to that is sound, and on a single-user machine a rarely-used tool that now needs root is the cheaper answer. `pkexec`, `sudo`, `su`, `passwd` and the mount helpers stay setuid, and the Containerfile says why for each. Every drop-in has a way of being present and doing nothing, and all of those ways are silent on a built image: a sysctl file outsorted by a higher number, a modprobe file saying `blacklist` where it means `install`, a faillock file no PAM stack reads, a kargs entry the template dropped, a tmpfiles rule naming a source that is not there. So the Containerfile asserts each one the way it already asserts the polkit grants, and `authselect enable-feature with-faillock` is what makes faillock.conf more than a file. squashfs is not blacklisted and there is an assertion that keeps it that way: the installer ISO boots a live root out of one, so blacklisting it produces a medium that fails on someone else's machine and not on this one. Measured on `localhost/alloy:hardening`, built from this tree: sixteen setuid binaries down to ten, the six named ones stripped, `pam_faillock` in both system-auth and password-auth, the pool line gone from chrony.conf with `confdir /etc/chrony.d` in its place, and all six kernel arguments in the rendered 10-alloy.toml. The manual carries the costs a person actually hits: X11 applications not running and what to do about it (chapter 7), the lockout and its reset, and a new troubleshooting section for the three diagnostics this pass turns off, `dmesg` as a user, core files, and a refused modprobe. Not in this pass, and each is its own task: `lockdown=confidentiality`, which costs hibernation and unsigned modules, the firewall the review found missing entirely, and whether Flatpak is worth what it costs each browser's sandbox.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-22 04:09 UTC
Signed with PGP, not checked
Commit: 55e12dd4ff15e92dc7aa2d444ce9e5503f3ff5c9
Parent: 989af71
16 files changed, +781 insertions, -0 deletions
M Containerfile +170
@@ -2918,6 +2918,176 @@
2918 2918 || { echo "$action is not defined; run0 cannot ask polkit for an admin" >&2; exit 1; }; \
2919 2919 echo "run0: present, systemd $version, $action defined"
2920 2920
2921 + # =====================================================================
2922 + # Hardening, part 1: the SUID set.
2923 + #
2924 + # Filed out of the 2026-08-21 secureblue review; the adopt/reject list and
2925 + # the reasoning are in wiki `alloy-hardening-posture`.
2926 + #
2927 + # A SUID binary is a program any user can run that becomes root, so each one
2928 + # is a promise that its argument parsing, its environment handling and its
2929 + # error paths are all correct. The base image ships sixteen. Six of them have
2930 + # no caller on a machine with one human account and a console that owns
2931 + # account management, and a promise nobody needs is one worth not making.
2932 + #
2933 + # WHAT IS NOT DONE HERE, deliberately: secureblue re-adds capabilities to
2934 + # some of these so they keep working for unprivileged users. Kicksecure's
2935 + # objection to that is sound — `cap_dac_read_search` on `unix_chkpwd` hands
2936 + # out a dangerous capability to preserve a rarely-used convenience — so the
2937 + # tools simply stop working without root, which is the cheaper answer.
2938 + #
2939 + # WHAT STAYS SUID, and why, because a list of removals without the
2940 + # complement reads as an oversight:
2941 + # - pkexec and polkit-agent-helper-1: the console's inline authorize flow
2942 + # depends on the helper (crates/alloy/src/polkit.rs), and the block below
2943 + # already asserts it is setuid.
2944 + # - sudo and su: Alloy names run0 as the way to root (see above) and has
2945 + # never removed sudo. Scripts across this tree call it. Removing it is a
2946 + # separate decision with its own blast radius, not a line in a hardening
2947 + # pass.
2948 + # - passwd and unix_chkpwd: a user changing their own password, and the
2949 + # lock screen checking it.
2950 + # - mount, umount, mount.nfs, fusermount3: `alloy disk` and removable media.
2951 + # - grub2-set-bootflag: the bootloader's one-shot flag.
2952 + #
2953 + # The result is asserted rather than assumed: chmod is silent about a file
2954 + # that was already not setuid, and about one this list misspelled.
2955 + # =====================================================================
2956 + RUN set -eux; \
2957 + stripped=""; \
2958 + for bin in chfn chsh newgrp gpasswd chage pam_timestamp_check; do \
2959 + path=$(command -v "$bin" 2>/dev/null || true); \
2960 + [ -n "$path" ] || { echo "$bin is not in the image; this list names a binary the base no longer ships" >&2; exit 1; }; \
2961 + [ -u "$path" ] || { echo "$path is already not setuid; the base changed and this line is now inert" >&2; exit 1; }; \
2962 + chmod u-s "$path"; \
2963 + [ -u "$path" ] && { echo "chmod u-s did not take on $path" >&2; exit 1; }; \
2964 + stripped="$stripped $bin"; \
2965 + done; \
2966 + for keep in /usr/bin/sudo /usr/bin/passwd /usr/bin/mount /usr/bin/umount; do \
2967 + [ -u "$keep" ] || { echo "$keep lost its setuid bit; the loop above is stripping more than it names" >&2; exit 1; }; \
2968 + done; \
2969 + echo "suid: stripped$stripped; $(find /usr/bin /usr/sbin /usr/libexec -perm -4000 -type f 2>/dev/null | wc -l) setuid binaries remain"
2970 +
2971 + # =====================================================================
2972 + # Hardening, part 2: faillock.
2973 + #
2974 + # etc/security/faillock.conf carries the numbers (50 attempts, 24 hours).
2975 + # That file is inert on its own: what reads it is pam_faillock, and pam_faillock
2976 + # is only in the stack if authselect's `with-faillock` feature is on. Same
2977 + # shape as the fingerprint block above, and asserted the same way, because
2978 + # "the file is present" and "the file is doing something" are different
2979 + # claims and only the second one is worth making.
2980 + #
2981 + # Applies on both profiles. sshd takes keys only, so this is about the
2982 + # console, the greeter and the lock screen — all of which exist on a server
2983 + # too, in the form of whoever is standing at it.
2984 + # =====================================================================
2985 + RUN set -eux; \
2986 + conf=/etc/security/faillock.conf; \
2987 + [ -f "$conf" ] || { echo "$conf did not land from the config tree" >&2; exit 1; }; \
2988 + grep -q '^deny = 50' "$conf" \
2989 + || { echo "$conf does not set the deny count this image documents" >&2; exit 1; }; \
2990 + authselect enable-feature with-faillock; \
2991 + authselect apply-changes; \
2992 + grep -q 'pam_faillock\.so' /etc/pam.d/system-auth \
2993 + || { echo "with-faillock did not reach system-auth; $conf would never be read" >&2; exit 1; }; \
2994 + grep -q 'pam_faillock\.so' /etc/pam.d/password-auth \
2995 + || { echo "with-faillock reached system-auth but not password-auth; half the stack counts failures" >&2; exit 1; }; \
2996 + echo "faillock: $(grep -c pam_faillock /etc/pam.d/system-auth) pam_faillock line(s) in system-auth, deny=50 unlock_time=86400"
2997 +
2998 + # =====================================================================
2999 + # Hardening, part 3: the clock.
3000 + #
3001 + # etc/chrony.d/10-alloy-nts.conf names four NTS servers. Two things have to
3002 + # be true for it to matter, and neither is true of the file on its own:
3003 + # /etc/chrony.conf has to read the directory, and it has to stop preferring
3004 + # the unauthenticated pool it ships with.
3005 + #
3006 + # Both are edits to a file the chrony rpm owns. /etc is writable on a bootc
3007 + # deployment so this is legal, and it is the same arrangement as the Firefox
3008 + # pref file: the package's file, edited in place, with the edit stated here.
3009 + # The pool line is commented rather than deleted so an operator reading the
3010 + # file can see what was turned off and why.
3011 + #
3012 + # `chronyd -p` parses the configuration and exits, which is what makes this
3013 + # an assertion instead of a hope. Without it, a typo in a server name would
3014 + # be discovered by a machine that silently never synchronises.
3015 + # =====================================================================
3016 + RUN set -eux; \
3017 + conf=/etc/chrony.conf; \
3018 + drop=/etc/chrony.d/10-alloy-nts.conf; \
3019 + [ -f "$drop" ] || { echo "$drop did not land from the config tree" >&2; exit 1; }; \
3020 + grep -q '^pool ' "$conf" \
3021 + || { echo "$conf has no pool line; the base changed and this edit no longer describes it" >&2; exit 1; }; \
3022 + sed -i 's|^pool |# Commented by Alloy: unauthenticated NTP. See /etc/chrony.d/10-alloy-nts.conf.\n# pool |' "$conf"; \
3023 + printf '\n# Added by Alloy: read /etc/chrony.d for the NTS sources.\nconfdir /etc/chrony.d\n' >> "$conf"; \
3024 + ! grep -q '^pool ' "$conf" \
3025 + || { echo "the pool line survived the edit; the machine would still take unauthenticated time" >&2; exit 1; }; \
3026 + chronyd -p >/dev/null \
3027 + || { echo "chronyd rejects the configuration after the Alloy edits" >&2; exit 1; }; \
3028 + sources=$(chronyd -p 2>/dev/null | grep -c '^server .* nts$' || true); \
3029 + [ "$sources" -ge 4 ] \
3030 + || { echo "chronyd parsed the config and sees $sources NTS sources; the confdir is not being read" >&2; exit 1; }; \
3031 + echo "chrony: pool commented, confdir /etc/chrony.d, $sources NTS sources"
3032 +
3033 + # =====================================================================
3034 + # Hardening, part 4: assert the drop-in files are not inert.
3035 + #
3036 + # Six files landed from the config tree with nothing else to turn them on.
3037 + # Each one has a way of being present and doing nothing, and every one of
3038 + # those ways is silent on a built image:
3039 + #
3040 + # - a sysctl file that loses to a higher-numbered one, so its values never
3041 + # take. The numbering is the whole defence and it is checked, not trusted.
3042 + # - a modprobe file using `blacklist` where it means `install`, which stops
3043 + # autoload and permits a direct modprobe.
3044 + # - a kargs file whose new entries did not survive template rendering.
3045 + # - a sway directive in a file the compositor does not include.
3046 + # - a tmpfiles line naming a source that is not in the image, so the flatpak
3047 + # overrides never reach the only path flatpak reads.
3048 + # =====================================================================
3049 + RUN set -eux; \
3050 + hard=/usr/lib/sysctl.d/90-alloy-hardening.conf; \
3051 + core=/usr/lib/sysctl.d/95-alloy-coredump.conf; \
3052 + [ -f "$hard" ] && [ -f "$core" ] \
3053 + || { echo "the sysctl drop-ins did not land" >&2; exit 1; }; \
3054 + last=$(ls /usr/lib/sysctl.d/*.conf /etc/sysctl.d/*.conf 2>/dev/null \
3055 + | xargs -n1 basename \
3056 + | grep -v -e '^9[05]-alloy-' -e '^99-sysctl\.conf$' \
3057 + | sort | tail -1); \
3058 + [ "$(printf '%s\n%s\n' "$last" 90-alloy-hardening.conf | sort | tail -1)" = 90-alloy-hardening.conf ] \
3059 + || { echo "$last sorts after the Alloy sysctl drop-ins and would win any key they share" >&2; exit 1; }; \
3060 + grep -q '^kernel.core_pattern=|/bin/false' "$core" \
3061 + || { echo "$core does not disable cores; an empty core_pattern means 'default name', not 'off'" >&2; exit 1; }; \
3062 + mod=/usr/lib/modprobe.d/50-alloy-blacklist.conf; \
3063 + [ -f "$mod" ] || { echo "$mod did not land" >&2; exit 1; }; \
3064 + ! grep -q '^blacklist ' "$mod" \
3065 + || { echo "$mod uses 'blacklist', which a direct modprobe ignores; it must use 'install <mod> /bin/false'" >&2; exit 1; }; \
3066 + grep -q '^install squashfs' "$mod" \
3067 + && { echo "$mod blacklists squashfs; the installer ISO boots a live root out of one and would stop booting" >&2; exit 1; }; \
3068 + echo "modprobe: $(grep -c '^install ' "$mod") modules refused, squashfs left loadable for the ISO"; \
3069 + kargs=/usr/lib/bootc/kargs.d/10-alloy.toml; \
3070 + [ -f "$kargs" ] || { echo "$kargs did not land from the render stage" >&2; exit 1; }; \
3071 + for karg in init_on_free=1 slab_nomerge page_alloc.shuffle=1 randomize_kstack_offset=on vsyscall=none debugfs=off; do \
3072 + grep -q "\"$karg\"" "$kargs" \
3073 + || { echo "$kargs is missing $karg; the render dropped it" >&2; exit 1; }; \
3074 + done; \
3075 + swayconf=/etc/sway/config.d/00-alloy.conf; \
3076 + grep -q '^xwayland disable' "$swayconf" \
3077 + || { echo "$swayconf does not disable Xwayland" >&2; exit 1; }; \
3078 + tmpf=/usr/lib/tmpfiles.d/50-alloy-flatpak.conf; \
3079 + src=/usr/share/alloy/flatpak/global; \
3080 + [ -f "$tmpf" ] && [ -f "$src" ] \
3081 + || { echo "the flatpak override or its tmpfiles rule did not land" >&2; exit 1; }; \
3082 + grep -q "^C .*$src\$" "$tmpf" \
3083 + || { echo "$tmpf does not copy $src; flatpak reads /var/lib/flatpak/overrides/global and nothing else" >&2; exit 1; }; \
3084 + systemd-tmpfiles --dry-run --create "$tmpf" >/dev/null \
3085 + || { echo "systemd-tmpfiles rejects $tmpf" >&2; exit 1; }; \
3086 + dns=/etc/systemd/resolved.conf.d/10-alloy-dns.conf; \
3087 + grep -q '^DNSOverTLS=opportunistic' "$dns" \
3088 + || { echo "$dns does not set DNSOverTLS, or sets 'yes' before anyone measured it against MagicDNS" >&2; exit 1; }; \
3089 + echo "hardening drop-ins: sysctl, modprobe, kargs, sway, flatpak, resolved — all present and non-inert"
3090 +
2921 3091 # =====================================================================
2922 3092 # Systemd presets — shipped via etc/systemd/{system,user}-preset/
2923 3093 # in the config tree above. Split across system-preset (greetd,
@@ -66,6 +66,14 @@
66 66
67 67 Rejected: Niri (Alloy's original pick; its scrolling-column model was the specific thing rejected in the 2026-07-17 pivot, see [MANIFESTO.md](MANIFESTO.md#status)), Hyprland (governance contested, animation-forward against Alloy's understatement), river (tag-based/dwm-shaped, Zig, further from i3 than sway), dwl (tiny but too bare for a curated default).
68 68
69 + ### Xwayland: **disabled**, ruled 2026-08-21
70 +
71 + The session ships `xwayland disable`. X11 has no isolation between clients: any X client can read every other client's keystrokes, window contents and clipboard, because that was the design. Running an X server inside a Wayland session hands that model back to anything that asks, and "anything that asks" includes a Flathub app that Alloy otherwise presents as sandboxed.
72 +
73 + Breaking Steam, most Electron applications and older Java toolkits is the intent rather than a cost to be mitigated. Alloy has been a Wayland-only session since the pivot, and this makes the stack honest about it.
74 +
75 + The package stays in the image. `xwayland enable` in `~/.config/sway/config.d/` restores it, which makes the escape hatch a config line rather than a rebuild, and the manual says so (chapter 7). Rationale for this and the rest of the 2026-08-21 hardening pass: wiki `alloy-hardening-posture`.
76 +
69 77 ## Authored toolkit
70 78
71 79 **ratatui.** Alloy authors one thing, the `alloy` console and its `alloy_tui` design-system crate, and it authors in ratatui. Immediate-mode: every frame the UI code runs top to bottom from current state, no bindings, no observables, no retained UI state to diverge. Picked because **Alloy's principle is to avoid the reactive pattern** (staged appearance, async UI trickling, state-divergence bugs, the "web-shaped feel"), and immediate-mode rules those out by construction. The mountaineer-sysop `sysop-tui` pattern carries over directly as the seed of `alloy_tui`: palette, themed widgets, footer chrome, reserved keys, mock-or-real backend detection. See [CONSOLE.md](CONSOLE.md).
@@ -106,6 +106,48 @@
106 106 Alloy ships no graphical file manager, so this is where one comes from if you
107 107 want one.
108 108
109 + ## What a sandboxed app does not get
110 +
111 + Alloy trims flatpak's default permissions before an app ever asks. The image
112 + ships system-wide overrides that remove three grants: X11 access, raw access to
113 + every device node, and blanket access to your home directory and the host
114 + filesystem. GPU access stays, because a video player or a browser without it is
115 + not usable.
116 +
117 + That is a smaller sandbox than Flathub assumes, so some apps will misbehave. Give
118 + back what one actually needs, per app:
119 +
120 + flatpak override --user org.example.SomeApp --filesystem=~/Projects
121 +
122 + The list of what an app currently has:
123 +
124 + flatpak info --show-permissions org.example.SomeApp
125 +
126 + The defaults live in `/var/lib/flatpak/overrides/global`. It is yours to edit,
127 + and an image update will not overwrite it once it exists.
128 +
129 + ## X11 applications do not run
130 +
131 + There is no X server. Alloy is a Wayland-only session and the sway config ships
132 + `xwayland disable`, so an application that can only speak X11 exits saying it
133 + cannot open a display.
134 +
135 + This affects Steam, most Electron applications, some screen-sharing paths, and
136 + older Java toolkits. Anything Wayland-native is unaffected, and most Electron
137 + apps can be told to use Wayland with `--ozone-platform=wayland`.
138 +
139 + The reason is that X11 gives every client the ability to read every other
140 + client's keystrokes and window contents. A sandboxed app with X11 access is not
141 + sandboxed in any way that matters.
142 +
143 + If you need it back, put this in `~/.config/sway/config.d/` and log in again:
144 +
145 + xwayland enable
146 +
147 + Your file wins over the shipped one. The Xwayland package stays in the image so
148 + that this works without rebuilding. Turning it on is a real cost, not a
149 + formality: it applies to the whole session, not to the one app you wanted.
150 +
109 151 ## Which one, in practice
110 152
111 153 - **A CLI tool you use daily and Fedora has it.** Layer it, or put it in a
@@ -36,6 +36,25 @@
36 36 `Ctrl+Alt+F2` gets you a virtual console where you can edit
37 37 `~/.config/sway/config` and log back in.
38 38
39 + ### An app exits saying it cannot open a display
40 +
41 + It is an X11 application, and there is no X server. See chapter 7: Alloy ships
42 + `xwayland disable`, and the way back is `xwayland enable` in
43 + `~/.config/sway/config.d/`.
44 +
45 + Try the Wayland flag first if it is an Electron app:
46 +
47 + org.example.SomeApp --ozone-platform=wayland
48 +
49 + ### I mistyped my password too many times and now it will not take the right one
50 +
51 + Fifty consecutive failures locks the account for 24 hours. From another session,
52 + or a virtual console logged in as another user:
53 +
54 + run0 faillock --user <name> --reset
55 +
56 + `faillock --user <name>` on its own shows the count without clearing it.
57 +
39 58 ### I edited the sway config and the session will not start
40 59
41 60 Log in on a virtual console and check it:
@@ -268,6 +287,57 @@
268 287
269 288 ---
270 289
290 + ## Diagnostics that used to work
291 +
292 + The 2026-08-21 hardening pass turned three things off that a person debugging
293 + software expects to be on. Each has a way back; two of them are a command, and
294 + the third is a line in your image.
295 +
296 + ### dmesg says operation not permitted
297 +
298 + The kernel ring buffer takes a capability to read now
299 + (`kernel.dmesg_restrict=1`). Two ways to the same text:
300 +
301 + run0 dmesg
302 + journalctl -k
303 +
304 + `journalctl -k` is the better habit: it is the same messages, it needs no
305 + privilege, and it survives a reboot.
306 +
307 + ### A program crashed and left no core file
308 +
309 + Alloy writes no cores. `kernel.core_pattern` is pointed at `/bin/false` and the
310 + hard limit is zero. A core is the process's memory written to disk, including
311 + whatever it had decrypted, which on this machine is keys and tokens.
312 +
313 + Getting one back takes root twice over, which is the intended friction: the
314 + pattern is a system setting, and the per-process hard limit is zero, so raising
315 + it needs `CAP_SYS_RESOURCE`.
316 +
317 + run0 sysctl -w kernel.core_pattern=core
318 + run0 bash -c 'ulimit -c unlimited; ./the-program-that-crashes'
319 +
320 + The core lands in the working directory, owned by root, and the program ran as
321 + root to produce it. Reboot when you are done, or set the pattern back by hand.
322 + `coredumpctl` is not the path here; `systemd-coredump` is not installed.
323 +
324 + ### modprobe refuses a module that exists
325 +
326 + A short list of filesystem drivers, legacy network protocols and the FireWire
327 + stack cannot be loaded at all, rather than merely being blocked from
328 + autoloading. The list and the reason for each entry are in
329 + `/usr/lib/modprobe.d/50-alloy-blacklist.conf`.
330 +
331 + If you need one back, delete its line from that file in your image checkout and
332 + rebuild. Editing it on the running machine does not last: `/usr/lib` is replaced
333 + on every update.
334 +
335 + Attaching a debugger is unaffected. `gdb -p` and `perf record -p` against your
336 + own processes still work; Alloy deliberately did not take the setting that
337 + would have stopped them.
338 +
339 + ---
340 +
271 341 ## Reporting something
272 342
273 343 Alloy is pre-v0 and this manual describes a moving system. If something here is
@@ -16,3 +16,32 @@
16 16 # does, from the panel's own EDID (crates/alloy/src/display.rs, `detect_panel`),
17 17 # so the machine's scale is written where its owner can change it rather than
18 18 # into a root-owned file naming one laptop.
19 +
20 + # ---------------------------------------------------------------------
21 + # Xwayland off. Ruled by Max 2026-08-21.
22 + #
23 + # X11 has no isolation between clients: any X client can read every other
24 + # client's keystrokes, window contents and clipboard, because the protocol
25 + # was designed when that was the point. Running an X server inside a Wayland
26 + # session hands that model back to anything that asks for it, and an X11
27 + # client is a common thing for a browser extension, an Electron app or a
28 + # random AppImage to be.
29 + #
30 + # WHAT BREAKS, plainly: Steam, most Electron applications, some
31 + # screen-sharing paths, older Java toolkits, and anything shipping an X11-only
32 + # binary. Wayland-native software is untouched.
33 + #
34 + # That breakage is the intent rather than a cost to be mitigated. Alloy is a
35 + # Wayland-only session (docs/STACK.md) and has been since the pivot; an
36 + # Electron app declining to run is the stack being honest about what it is.
37 + #
38 + # THE WAY BACK is this line. Delete it, or set `xwayland enable` in
39 + # ~/.config/sway/config.d/, and the next session has an X server again — a
40 + # user's file wins over this directory by the include order documented at the
41 + # top of this file. The package stays in the image precisely so that works;
42 + # removing it would make the escape hatch a rebuild. It is a few megabytes and
43 + # it buys a supported answer for the person who needs Steam.
44 + #
45 + # Rationale and the review this came out of: wiki `alloy-hardening-posture`.
46 + # ---------------------------------------------------------------------
47 + xwayland disable
@@ -33,9 +33,49 @@
33 33 # retyping a byte.
34 34 # ---------------------------------------------------------------------
35 35
36 + # ---------------------------------------------------------------------
37 + # Kernel hardening arguments (KSPP), added 2026-08-21.
38 + #
39 + # The self-protection settings that can only be asked for on the command
40 + # line. Their sysctl-shaped siblings live in
41 + # /usr/lib/sysctl.d/90-alloy-hardening.conf; rationale for the whole set is
42 + # wiki `alloy-hardening-posture`.
43 + #
44 + # `init_on_alloc=1` is deliberately absent: Fedora's kernel is built with
45 + # CONFIG_INIT_ON_ALLOC_DEFAULT_ON, so setting it here would state a default
46 + # twice and imply the other half is off when it is not.
47 + #
48 + # `lockdown=confidentiality` is also absent, and that is an open decision
49 + # rather than an omission (GoingsOn alloy `17489bac`). It costs hibernation
50 + # and unsigned module loading, and the aarch64 NVIDIA work is exactly the
51 + # case that would collide with it.
52 + #
53 + # The two with a measurable throughput cost are `init_on_free` and
54 + # `slab_nomerge`, and this machine is also a build host. If a build gets
55 + # slower after this lands, these are the lines to measure rather than the
56 + # ones to quietly drop.
57 + # ---------------------------------------------------------------------
58 +
36 59 kargs = [
37 60 "quiet",
38 61 "loglevel=3",
62 + # Free memory is wiped on release, so a use-after-free reads zeroes rather
63 + # than the previous occupant's data.
64 + "init_on_free=1",
65 + # Slab caches of the same size stay separate, so an overflow in one object
66 + # type cannot land on another type's object.
67 + "slab_nomerge",
68 + # The free lists start shuffled, so heap layout is not a constant an
69 + # exploit can assume.
70 + "page_alloc.shuffle=1",
71 + # A random offset per syscall on the kernel stack.
72 + "randomize_kstack_offset=on",
73 + # The legacy vsyscall page is a fixed-address, executable mapping in every
74 + # process. Nothing built this decade needs it; a pre-2013 glibc binary does.
75 + "vsyscall=none",
76 + # debugfs exposes kernel internals to root and is a recurring source of
77 + # information leaks. Nothing in this image reads it.
78 + "debugfs=off",
39 79 "vt.default_red=@{vt.red}",
40 80 "vt.default_grn=@{vt.grn}",
41 81 "vt.default_blu=@{vt.blu}",