Skip to content

Build the lockdown LSM into the FOS kernel, without activating it - #131

Merged
darksidemilk merged 1 commit into
masterfrom
claude/secure-boot-shim-fos-ssnjdp-hardening
Aug 3, 2026
Merged

Build the lockdown LSM into the FOS kernel, without activating it#131
darksidemilk merged 1 commit into
masterfrom
claude/secure-boot-shim-fos-ssnjdp-hardening

Conversation

@darksidemilk

Copy link
Copy Markdown
Member

Independent of #130 — branched from master, no shared commits, lands in either order. Kept separate because #130 is safe and immediately useful, while this changes how every FOS kernel is built and deserves its own review and its own bisect point.

Why

A signed kernel that does not enforce lockdown is a Secure Boot bypass: anyone can boot it on a machine that trusts the signer and then reach the kernel through /dev/mem, iopl() or kexec_load(). rhboot/shim-review asks about this directly:

How does your signed kernel enforce lockdown when your system runs with Secure Boot enabled?

It is the largest gap between FOS today and a kernel anyone would sign for general use.

One thing makes this easier than usual: FOS has no modules. # CONFIG_MODULES is not set, 1829 =y and zero =m, and the out-of-tree Realtek drivers are built in-tree by addKernelPackages() rather than as external modules. That answers shim-review's other recurring question — ephemeral per-build module-signing keys — with "we build no modules". Nothing to do there.

What changed

CONFIG_SECURITY was not set at all, so the lockdown LSM could not even be selected. Enabled across all three architectures, along with the platform keyring machinery — LOAD_UEFI_KEYS being the one that imports the firmware's db and MokList, without which the kernel cannot see the key the shim just validated against.

CONFIG_LSM is set explicitly because an LSM absent from the ordered list never initialises, and oldconfig's default string names LSMs this kernel does not build. CONFIG_KEXEC goes away — lockdown blocks kexec_load() unconditionally and FOS never used it.

Lockdown is built in but left inactive (FORCE_NONE), so nothing changes for existing users. FORCE_INTEGRITY would have activated it in one line and is the wrong default: it locks down every boot, including the overwhelming majority that never enable Secure Boot, and lockdown blocks the /dev/mem fallback that hardware inventory can still depend on.

What is deliberately missing

Distributions all carry a patch that activates lockdown only when the firmware reports Secure Boot on. That patch is not here, and ADR 0009 records why in detail. Both halves of it turn out to be downstream-only in 6.18:

  • security_lock_kernel_down() — the function the Fedora/Ubuntu patches call — is not in mainline. include/linux/security.h exports only security_locked_down(), the query.
  • efi_enabled(EFI_SECURE_BOOT) is also not in mainline. Upstream x86 keeps the state in boot_params.secure_boot and, in setup_arch(), only prints it.

So it is a multi-file, arch-specific patch. build.sh applies patch/kernel/linux.patch with patch -p1 and exits non-zero on failure, so shipping one that has never been compiled would break every build and take the Intel VMD patch down with it. The ADR records the exact hook point found in setup.c and a smaller single-file alternative in security/lockdown/lockdown.c, so whoever picks it up does not repeat the search.

The ADR also flags the question worth settling first: FOS boots as bzImage + a network-supplied command line + an unsigned ext2 init.xz. A reviewer will push hard on that, and if FOG cannot move to a UKI, a shim application probably is not winnable — in which case the lockdown patch buys nothing on its own.

Verified

make oldconfig silently drops symbols whose dependencies are unmet, so a config can look right in git and still produce a kernel with no lockdown in it. tests/checks/secureboot-config.sh asserts the symbols are present across all three configs, and with -b re-checks the .config Kconfig actually produced after a build. It passes, and it was negative-tested — flipping CONFIG_SECURITY_LOCKDOWN_LSM off makes it fail. Run it with -b after your first build.

Existing harnesses re-run: wipe.sh, sector-size.sh, lvm.sh and the golden fixture all pass. fill-engine.sh fails, but it fails identically on unmodified masterprocsfdisk.awk uses gawk syntax the test host's awk rejects. Pre-existing, untouched.

Not verified

Warning

No kernel was compiled for this change. Before merging: build all three architectures, boot with Secure Boot off and image a machine end to end, and confirm the Realtek NICs and Intel VMD NVMe still work. Those two are the entire reason FOS carries a custom kernel, and CONFIG_SECURITY/keyring churn is exactly the kind of change that could disturb them.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HZ2jZ7qgen6pXrbtmxShaw


Generated by Claude Code

A signed kernel that does not enforce lockdown is a Secure Boot bypass:
anyone can boot it on a machine that trusts the signer and then reach the
kernel through /dev/mem, iopl() or kexec_load(). shim-review asks about
this directly, and it is the largest gap between FOS today and a kernel
anyone would sign for general use.

CONFIG_SECURITY was not even set, so the lockdown LSM could not be
selected at all. This enables it and the platform keyring machinery
across all three architectures -- LOAD_UEFI_KEYS being the one that
imports the firmware's db and MokList, without which the kernel cannot
see the key the shim just validated against. CONFIG_LSM is set explicitly
because an LSM absent from the ordered list never initialises, and
oldconfig's default string names LSMs this kernel does not build.
CONFIG_KEXEC goes away: lockdown blocks kexec_load() unconditionally and
FOS never used it.

Lockdown is built in but left inactive (FORCE_NONE), so nothing changes
for existing users. FORCE_INTEGRITY would have activated it in one line
and is the wrong default -- it locks down every boot, including the
overwhelming majority that never enable Secure Boot, and lockdown blocks
the /dev/mem fallback that hardware inventory can still depend on.
Distributions all carry a patch that activates lockdown only when the
firmware reports Secure Boot on. That patch is NOT included here, and
0009 records why in detail: both halves of it turn out to be downstream
only in 6.18 -- security_lock_kernel_down() is absent from mainline, as
is efi_enabled(EFI_SECURE_BOOT) -- so it is a multi-file arch-specific
patch. build.sh applies patch/kernel/linux.patch with `patch -p1` and
exits on failure, so shipping one that has never been compiled would
break every build and take the Intel VMD patch down with it.

Because oldconfig silently drops symbols whose dependencies are unmet, a
config can look right in git and still produce a kernel with no lockdown
in it. tests/checks/secureboot-config.sh asserts the symbols are present,
and with -b re-checks the .config Kconfig actually produced after a build.

Not built or booted -- no kernel was compiled for this change. Build all
three arches and confirm the Realtek NICs and Intel VMD NVMe still work
before merging; those are the whole reason FOS carries a custom kernel.

Copy link
Copy Markdown
Member Author

Verified this doesn't break Secure-Boot-disabled operation, per a question during review.

Grepped the whole repo for kexec: matches only in config files and files this PR itself added (the ADR, the test script) — zero hits in Buildroot/board/FOG/FOS/rootfs_overlay/, where funcs.sh, bin/fog.*, and the init scripts live. BR2_PACKAGE_KEXEC is also already # not set in all three filesystem configs, so Buildroot was never building a userspace kexec tool to begin with. Removing the in-kernel kexec_load() syscall has nothing to break — no script calls it, no binary exists that would.

The rest of the diff is dormant either way: this PR deliberately does not include the patch that would activate lockdown based on the firmware's Secure Boot state (see ADR 0009's "not yet done" section), so CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE=y keeps the lockdown level at "none" whether Secure Boot is on or off. None of lockdown's restrictions engage from this PR alone — it's groundwork, not a behavior change yet. Same for the keyring additions (nothing imports into them yet) and the integrity LSM (inert without CONFIG_IMA/CONFIG_EVM, both still off).

Still stands: none of this has been built or booted (see "Not verified" above). The grep rules out the specific kexec concern, but make oldconfig pulling in CONFIG_SECURITY for the first time could still resolve some other dependent prompt in a way source-reading alone can't fully catch — building all three arches and imaging with Secure Boot off remains the actual gate before merge.

Testing this before merge

This is the PR most worth testing — it explicitly wasn't built or booted while writing it.

git clone https://github.com/FOGProject/fos.git /tmp/fos-hardening-test
cd /tmp/fos-hardening-test
git fetch origin claude/secure-boot-shim-fos-ssnjdp-hardening
git checkout claude/secure-boot-shim-fos-ssnjdp-hardening

./build.sh -nka x64 --install-dep

Confirm the hardening symbols survived oldconfig

tests/checks/secureboot-config.sh -b

This is the check that actually matters here — make oldconfig silently drops symbols whose dependencies aren't met, so the committed config can look right in git and still produce a kernel missing lockdown entirely. -b re-checks the .config your build just produced, not the file in git.

Fast smoke test, no FOG server or network needed

qemu-system-x86_64 -kernel dist/bzImage -initrd dist/init.xz \
  -append "console=ttyS0" -nographic -m 2048

This only proves the kernel boots and mounts its initrd — FOS expects mode=/type= kernel parameters from iPXE for a real imaging flow, so this isn't a substitute for the full test below, just a cheap first check before investing in one.

Full test — Secure Boot OFF (the path that must not regress)

Point a spare/test client (physical or VM, not part of your production fleet) at a throwaway FOG server — the one from the #961 testing steps works well — with dist/bzImage/dist/init.xz copied into its service/ipxe/. Run a normal capture/deploy task end to end.

dmesg | grep -i lockdown

Expect lockdown: none (or no lockdown line at all) — not integrity. This PR deliberately does not activate lockdown regardless of Secure Boot state (see "What is deliberately missing" above); seeing none here is the CORRECT and expected result at this stage, not a sign the PR did nothing.

If your test hardware has the Realtek NICs or Intel VMD NVMe this custom kernel exists for, exercise those specifically — CONFIG_SECURITY/keyring churn is exactly the kind of change that could disturb them, and that's the actual regression risk this PR carries.


Generated by Claude Code

@darksidemilk

Copy link
Copy Markdown
Member Author

I was able to test this with an experimental build. Confirmed working with secure boot on or off.
Ready to be merged

@darksidemilk
darksidemilk merged commit 007e0ee into master Aug 3, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants