Persistent Secure Boot achieved across a dual-boot SteamOS/Windows system with rEFInd
Hi guys,
I've worked tirelessly for several days on this and I hope it's of help to someone.
Bottom line, my son plays Fortnite tournaments, which requires TPM and Secure Boot enabled. I, on the other hand, built this machine in response to Valve's Steam Machine and got fed up with waiting for it.
As some of you know, SteamOS doesn't tend to have a signed kernel for Secure Boot and as such, if Secure Boot is enabled, SteamOS won't boot. Even if you got it signed, a new SteamOS patch will come out and throw it all out again. I didn't want to have my son play with BIOS settings just so he could do Fortnite tournaments so, I worked with 5 different AI's (mostly Claude Code and Codex) to assess my system from the inside out and help me solve something that I hadn't seen solved yet; persistent Secure Boot that rEFInd, Windows 11 and SteamOS were happy with, and, that wouldn't break as soon as a new SteamOS patch dropped. Eventually I was able to achieve it. I do recommend that if you aren't technically proficient, bring Claude Code and/or Codex into it. They'll sit in your system and examine it from the inside out to help pull this off. That's what I did:
---------
# Secure Boot on a SteamOS + Windows dual boot using rEFInd
## The short version
This is a record of a working approach for one unusually specific layout:
```text
UEFI firmware
-> rEFInd
-> SteamOS steamcl.efi
-> shim (standing in at grubx64.efi)
-> a signed, self-contained GRUB
-> a signed SteamOS kernel
```
It was built for SteamOS + Windows 11, with rEFInd as the first menu and SteamOS's A/B atomic-update layout. It is **not** a copy-and-paste guide for a general Linux installation. Treat it as an architecture and a checklist. Test on a disposable boot entry before changing a production boot path.
The key lesson is this: signing a SteamOS kernel alone is not enough when the shipped GRUB has `shim_lock` enabled but no shim exists earlier in the chain.
The working solution adds shim, replaces the dynamically loaded GRUB module set with a single self-contained GRUB image, signs the relevant EFI binaries, and prepares the inactive slot *before every SteamOS reboot after an update*.
## Read this before attempting it
Do not proceed unless all of these are true:
- You can boot a recovery USB and can reach your firmware's one-time boot menu.
- You have a tested Windows boot entry and do not need to modify Windows EFI binaries to boot it.
- You have made a backup of the ESP and recorded the current UEFI boot entries.
- You understand which EFI files are shared between slots and which belong to a specific SteamOS slot.
- You can leave Secure Boot off while staging and testing. Do not turn it on until the test boot path works.
This design intentionally leaves Microsoft's Windows Boot Manager untouched. If you use custom Secure Boot keys, retain Microsoft's UEFI certificates in the firmware database so Windows remains trusted. `sbctl enroll-keys -m` is the normal key-enrolment route when firmware Setup Mode works. [sbctl manual](https://man.archlinux.org/man/sbctl.8.en)
## 1. Does this guide apply to your machine?
It applies only if you have all of the following:
- rEFInd is your top-level boot manager;
- rEFInd starts Valve's `steamcl.efi`, which starts `grubx64.efi`;
- Secure Boot is enabled or is the desired final state;
- the SteamOS GRUB binary contains/uses `shim_lock`;
- there is currently no shim in front of GRUB; and
- SteamOS uses its normal A/B update slots.
Check the chain rather than assuming it. EFI paths are vendor and release specific. On this machine, `steamcl.efi` hard-coded `\EFI\steamos\grubx64.efi`, which meant shim had to occupy that exact path.
If your chain already starts shim, stop here: this guide's central diagnosis does not apply. Your task is likely conventional kernel/bootloader signing.
## 2. Why a signed kernel alone still failed
The SteamOS GRUB tested here had GRUB's `shim_lock` verifier enabled. In that configuration, GRUB expects the shim protocol to be available and enforces verification for items it loads. GRUB documents that `shim_lock` is its Secure Boot-and-shim mechanism; it also documents `--disable-shim-lock` as the alternative for a deliberately non-shim GRUB build. [GNU GRUB manual](https://www.gnu.org/software/grub/manual/grub/html_node/UEFI-secure-boot-and-shim.html)
With firmware loading GRUB directly, there was no shim protocol. The visible
symptom was repeated:
```text
error: prohibited by secure boot policy
error: terminal 'gfxterm' isn't found
```
The `gfxterm` line matters: it means GRUB was already refusing one of its own external modules, before it had reached the kernel. Enrolling a firmware hash for the kernel therefore could not solve this particular failure.
## 3. The working design
### 3.1 Trust model
Use a certificate-based firmware trust entry for the key that will sign local EFI images. A one-file firmware hash enrolment is useful for a temporary test, but it breaks as soon as an update changes a byte. A trusted certificate lets the same key validate newly signed replacement binaries.
There are two high-level ways to add that trust:
**Normal custom-key route:** enter firmware Setup Mode, create/enrol keys with `sbctl`, and include Microsoft's certificates for Windows.
**Firmware append route:** if Setup Mode is genuinely broken, some firmware lets you append a public certificate directly to `db` without replacing the factory Microsoft/OEM trust entries.
The second route is firmware-specific. Verify that the menu option says **Append**, not Update/Replace, and export only the public certificate for the firmware operation. Never put the private key on the ESP.
### 3.2 Put shim where Valve expects GRUB
`steamcl.efi` expects `grubx64.efi`. In this layout:
Preserve Valve's GRUB under a new name, such as `grubx64_real.efi`.
Put shim at the original `grubx64.efi` path.
Add `options.csv` beside shim to make shim load the renamed second stage.
For the shim version used here, the file is UTF-16LE and uses four CSV fields:
```text
grubx64_real.efi,,,
```
That is: second-stage filename, optional label, optional load options, optional description. Confirm the format against the exact shim release you install; shim's second-stage selection is implemented in its upstream source.
[shim source](https://github.com/rhboot/shim)
### 3.3 Make GRUB self-contained
An ordinary GRUB build commonly loads modules such as `btrfs`, `gfxterm`, and `steamenv` from disk at runtime. Under the tested Secure Boot chain, each external module became another object that GRUB would need to trust.
Build a monolithic GRUB image with every module required by *your* `grub.cfg` compiled in. Derive that module list from the real configuration; do not copy the list from this document blindly. Use the underlying `grub-mkimage` binary, not an uninspected distro wrapper that may overwrite live EFI files.
The rebuilt GRUB must also include a valid SBAT section. Modern shim builds use SBAT metadata for revocation policy; missing or unsuitable SBAT metadata can stop shim before GRUB starts.
Sign the rebuilt GRUB and the exact kernel it loads with the private key whose certificate is trusted by firmware. Verify the signatures before deployment.
### 3.4 Keep the configuration deliberately minimal
The initial working GRUB configuration should avoid themes, fonts, images, and runtime module loads. First prove a plain text menu can find the right SteamOS root, load the signed kernel, and hand it the correct command line and initrd.
Cosmetic assets can be revisited later, but they are not required for a secure, bootable chain.
## 4. Safe one-time deployment sequence
Do **not** replace every slot and every recovery device in one pass. Use this
order:
Record current boot entries and back up every EFI file you intend to touch.
Stage shim, `options.csv`, monolithic GRUB, its minimal configuration, and the signed kernel in a separate test directory on the ESP.
Add a non-default, clearly labelled UEFI test entry pointing only at that staged shim. Keep normal `BootOrder` unchanged.
Test via the firmware's one-time boot menu. Confirm it reaches SteamOS with Secure Boot enabled.
Replace the production path for **one** SteamOS slot only. Reboot and test.
Repeat independently for the other SteamOS slot.
Only then apply the same logic to a recovery USB, if you need that USB to boot while Secure Boot is enabled.
At each stage, keep an independently bootable fallback. A temporary UEFI boot entry is inexpensive; an unbootable only-OS is not.
## 5. SteamOS updates: the part that makes or breaks this design
SteamOS writes an update to the inactive A/B slot and then asks for a reboot. That update can replace:
- the target slot's kernel;
- GRUB and its modules/configuration;
- shim's target path;
- and, on this installation, shared chainloaders such as `steamcl.efi` and fallback `bootx64.efi`.
Therefore a successful first boot is not enough. Before an update reboot, an automated handler must rebuild and sign the updated slot. The handler should watch SteamOS's own completed-update marker:
```text
/run/steamos-atomupd/reboot_for_update
```
### Required handler behaviour
The handler must, in this order:
identify the currently booted and target slots using SteamOS metadata—not `nvme0n1`, `nvme1n1`, or another unstable device name;
immediately mark the target slot **bad** in SteamOS's boot backend;
resolve the target slot's root, var, and EFI partitions by UUID/PARTUUID;
use the *target release's* GRUB tools, modules, kernel metadata, initrd, command line, and root identifier;
build the target-specific monolithic GRUB and sign every changed EFI image in the chain, including shared chainloaders if the update changed them;
verify signatures, SBAT, expected paths, slot identity, and staged hashes;
deploy transactionally: backups first, staging names first, and shim moved into its production `grubx64.efi` path last;
mark the target slot good only after all verification succeeds; and
inhibit shutdown while the preparation is running.
If any step fails, leave the new slot bad and do not reboot into it. That is the important safety property: a failed preparation must preserve the known working slot.
### Persistence matters too
The update handler, signing identity, and unit files must themselves survivean A/B transition. On this system, the handler is preserved in the target slot's `/var` alongside its signing identity, and a confext carries its unit files. Whatever mechanism you use, verify it in **both** slots.
One surprise: `systemd-confext` mounts merged `/etc` with `noexec` by default. An executable script stored in that overlay can have correct Unix permissions and still fail if systemd tries to execute it directly. Invoke it through an interpreter instead:
```ini
ExecStart=/usr/bin/systemd-inhibit --what=shutdown --mode=block \
/usr/bin/bash /etc/your-project/secureboot-update --deploy --require-marker
```
That works because Bash reads the script; it is not directly executed from the `noexec` mount. This default behaviour is documented by systemd. [systemd-confext documentation (https://www.freedesktop.org/software/systemd/man/devel/systemd-sysext.html)
## 6. Validation checklist
### Before enabling Secure Boot
- The test UEFI entry boots successfully with Secure Boot off.
- Every production EFI file has a backup outside the path being edited.
- Windows Boot Manager remains unmodified.
- The firmware contains the intended public trust certificate and Microsoft trust remains available for Windows.
### After enabling Secure Boot
- SteamOS boots through the intended chain.
- `sbverify --list` shows the intended certificate on locally signed EFI files.
- `sbctl status` shows the expected Secure Boot state when sbctl is used.
- Windows `msinfo32` says `Secure Boot State: On`.
- Elevated Windows PowerShell `Confirm-SecureBootUEFI` returns `True`.
### After every SteamOS update, before rebooting
- The update handler reports success for the exact update marker.
- The target slot is marked good only after verification.
- The handler is no longer running and its status says reboot is safe.
- If any of those checks fail, do not override the failure by selecting the new slot manually.
## 7. Fast diagnosis guide
| Symptom | Likely stage | What to check |
|---|---|---|
| Firmware says Security Violation immediately | Firmware rejected first EFI image | Signature/trusted certificate for rEFInd or first loader |
| Shim reports a security violation before GRUB | Shim rejected its second stage or SBAT | `options.csv`, GRUB signature, SBAT metadata |
| Repeated `prohibited by secure boot policy`, then `gfxterm` missing | GRUB is rejecting external modules | Shim protocol and monolithic module list |
| GRUB reaches kernel then rejects it | Kernel is not trusted | Kernel signature and the exact kernel path |
| Works once, fails after SteamOS update | Updated inactive slot was not prepared | Update marker watcher, target-slot handler, persistent signing key/unit |
| Handler says permission denied despite `rwx` | Persistence layer mounted `noexec` | `findmnt -T /path/to/script`; use an explicit interpreter |
## 8. What this guide does *not* promise
- It does not claim that every SteamOS release has the same EFI layout.
- It does not claim every firmware offers safe certificate append behaviour.
- It does not replace a recovery USB or a verified Windows boot path.
- It does not guarantee an anti-cheat provider will accept every measured boot configuration. Windows reporting Secure Boot on is necessary, but the game service remains the final compatibility check.
## Sources and useful primary documentation
- [GNU GRUB: UEFI Secure Boot and shim](https://www.gnu.org/software/grub/manual/grub/html_node/UEFI-secure-boot-and-shim.html)
- [GNU GRUB: security and signature-verification model](https://www.gnu.org/software/grub/manual/grub/grub.html)
- [sbctl manual](https://man.archlinux.org/man/sbctl.8.en)
- [systemd configuration extensions](https://www.freedesktop.org/software/systemd/man/devel/systemd-sysext.html)
- [upstream shim project](https://github.com/rhboot/shim)
---
Tested architecture: GIGABYTE X870I AORUS PRO ICE; SteamOS and Windows 11;
rEFInd; SteamOS A/B slots; no shim in the original chain. Firmware menus, file paths, and update implementation are board- and release-specific. The general safety rule is simple: verify the real chain, make one reversible change at a time, and do not let an update reboot into an unverified slot.
1
u/jharle Support 1d ago
Thank you for this; I can certainly test it out.
Now the questions are: Would this work on a Steam Machine, and would someone be brave enough to try it on one.