diff --git a/build/dpkg-deps/depends b/build/dpkg-deps/depends
index da2012ae2..3e527e4d9 100644
--- a/build/dpkg-deps/depends
+++ b/build/dpkg-deps/depends
@@ -11,6 +11,7 @@ cifs-utils
conntrack
cryptsetup
curl
+dkms
dmidecode
dnsutils
dosfstools
@@ -36,6 +37,7 @@ lvm2
lxc
magic-wormhole
man-db
+mokutil
ncdu
net-tools
network-manager
diff --git a/build/image-recipe/build.sh b/build/image-recipe/build.sh
index 787f71844..0b3024286 100755
--- a/build/image-recipe/build.sh
+++ b/build/image-recipe/build.sh
@@ -299,6 +299,16 @@ if [ "${NVIDIA}" = "1" ]; then
echo "[nvidia-hook] Removed build dependencies." >&2
fi
+# Install linux-kbuild for sign-file (Secure Boot module signing)
+KVER_ALL="\$(ls -1t /boot/vmlinuz-* 2>/dev/null | head -n1 | sed 's|.*/vmlinuz-||')"
+if [ -n "\${KVER_ALL}" ]; then
+ KBUILD_VER="\$(echo "\${KVER_ALL}" | grep -oP '^\d+\.\d+')"
+ if [ -n "\${KBUILD_VER}" ]; then
+ echo "[build] Installing linux-kbuild-\${KBUILD_VER} for Secure Boot support" >&2
+ apt-get install -y "linux-kbuild-\${KBUILD_VER}" || echo "[build] WARNING: linux-kbuild-\${KBUILD_VER} not available" >&2
+ fi
+fi
+
cp /etc/resolv.conf /etc/resolv.conf.bak
if [ "${IB_SUITE}" = trixie ] && [ "${IB_TARGET_ARCH}" != riscv64 ]; then
diff --git a/build/lib/scripts/sign-unsigned-modules b/build/lib/scripts/sign-unsigned-modules
new file mode 100755
index 000000000..fdaf11e88
--- /dev/null
+++ b/build/lib/scripts/sign-unsigned-modules
@@ -0,0 +1,76 @@
+#!/bin/bash
+
+# sign-unsigned-modules [--source
--dest ] [--sign-file ]
+# [--mok-key ] [--mok-pub ]
+#
+# Signs all unsigned kernel modules using the DKMS MOK key.
+#
+# Default (install) mode:
+# Run inside a chroot. Finds and signs unsigned modules in /lib/modules in-place.
+# sign-file and MOK key are auto-detected from standard paths.
+#
+# Overlay mode (--source/--dest):
+# Finds unsigned modules in , copies to , signs the copies.
+# Clears old signed modules in first. Used during upgrades where the
+# overlay upper is tmpfs and writes would be lost.
+
+set -e
+
+SOURCE=""
+DEST=""
+SIGN_FILE=""
+MOK_KEY="/var/lib/dkms/mok.key"
+MOK_PUB="/var/lib/dkms/mok.pub"
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --source) SOURCE="$2"; shift 2;;
+ --dest) DEST="$2"; shift 2;;
+ --sign-file) SIGN_FILE="$2"; shift 2;;
+ --mok-key) MOK_KEY="$2"; shift 2;;
+ --mok-pub) MOK_PUB="$2"; shift 2;;
+ *) echo "Unknown option: $1" >&2; exit 1;;
+ esac
+done
+
+# Auto-detect sign-file if not specified
+if [ -z "$SIGN_FILE" ]; then
+ SIGN_FILE="$(ls -1 /usr/lib/linux-kbuild-*/scripts/sign-file 2>/dev/null | head -1)"
+fi
+
+if [ -z "$SIGN_FILE" ] || [ ! -x "$SIGN_FILE" ]; then
+ exit 0
+fi
+
+if [ ! -f "$MOK_KEY" ] || [ ! -f "$MOK_PUB" ]; then
+ exit 0
+fi
+
+COUNT=0
+
+if [ -n "$SOURCE" ] && [ -n "$DEST" ]; then
+ # Overlay mode: find unsigned in source, copy to dest, sign in dest
+ rm -rf "${DEST}"/lib/modules
+
+ for ko in $(find "${SOURCE}"/lib/modules -name '*.ko' 2>/dev/null); do
+ if ! modinfo "$ko" 2>/dev/null | grep -q '^sig_id:'; then
+ rel_path="${ko#${SOURCE}}"
+ mkdir -p "${DEST}$(dirname "$rel_path")"
+ cp "$ko" "${DEST}${rel_path}"
+ "$SIGN_FILE" sha256 "$MOK_KEY" "$MOK_PUB" "${DEST}${rel_path}"
+ COUNT=$((COUNT + 1))
+ fi
+ done
+else
+ # In-place mode: sign modules directly
+ for ko in $(find /lib/modules -name '*.ko' 2>/dev/null); do
+ if ! modinfo "$ko" 2>/dev/null | grep -q '^sig_id:'; then
+ "$SIGN_FILE" sha256 "$MOK_KEY" "$MOK_PUB" "$ko"
+ COUNT=$((COUNT + 1))
+ fi
+ done
+fi
+
+if [ $COUNT -gt 0 ]; then
+ echo "[sign-modules] Signed $COUNT unsigned kernel modules"
+fi
diff --git a/build/lib/scripts/upgrade b/build/lib/scripts/upgrade
index 35230eb0a..6945c7586 100755
--- a/build/lib/scripts/upgrade
+++ b/build/lib/scripts/upgrade
@@ -83,6 +83,15 @@ if [ -d /sys/firmware/efi ] && [ -f /media/startos/config/efi-installer-entry ];
fi
fi
+# Sign unsigned kernel modules for Secure Boot
+SIGN_FILE="$(ls -1 /media/startos/next/usr/lib/linux-kbuild-*/scripts/sign-file 2>/dev/null | head -1)"
+/media/startos/next/usr/lib/startos/scripts/sign-unsigned-modules \
+ --source /media/startos/lower \
+ --dest /media/startos/config/overlay \
+ --sign-file "$SIGN_FILE" \
+ --mok-key /media/startos/config/overlay/var/lib/dkms/mok.key \
+ --mok-pub /media/startos/config/overlay/var/lib/dkms/mok.pub
+
sync
umount -Rl /media/startos/next
diff --git a/core/src/error.rs b/core/src/error.rs
index 55b4494b1..88f664394 100644
--- a/core/src/error.rs
+++ b/core/src/error.rs
@@ -101,6 +101,7 @@ pub enum ErrorKind {
UpdateFailed = 77,
Smtp = 78,
SetSysInfo = 79,
+ Bios = 80,
}
impl ErrorKind {
pub fn as_str(&self) -> String {
@@ -185,6 +186,7 @@ impl ErrorKind {
UpdateFailed => t!("error.update-failed"),
Smtp => t!("error.smtp"),
SetSysInfo => t!("error.set-sys-info"),
+ Bios => t!("error.bios"),
}
.to_string()
}
diff --git a/core/src/init.rs b/core/src/init.rs
index e5792adea..fe5f334f2 100644
--- a/core/src/init.rs
+++ b/core/src/init.rs
@@ -173,6 +173,11 @@ pub async fn init(
RpcContext::init_auth_cookie().await?;
local_auth.complete();
+ // Re-enroll MOK on every boot if Secure Boot key exists but isn't enrolled yet
+ if let Err(e) = crate::util::mok::enroll_mok(std::path::Path::new(crate::util::mok::DKMS_MOK_PUB)).await {
+ tracing::warn!("MOK enrollment failed: {e}");
+ }
+
load_database.start();
let db = cfg.db().await?;
crate::version::Current::default().pre_init(&db).await?;
diff --git a/core/src/os_install/mod.rs b/core/src/os_install/mod.rs
index d04491acd..6a1c00f35 100644
--- a/core/src/os_install/mod.rs
+++ b/core/src/os_install/mod.rs
@@ -21,7 +21,7 @@ use crate::prelude::*;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::setup::SetupInfo;
use crate::util::Invoke;
-use crate::util::io::{TmpDir, delete_file, open_file, write_file_atomic};
+use crate::util::io::{TmpDir, delete_dir, delete_file, open_file, write_file_atomic};
use crate::util::serde::IoFormat;
mod gpt;
@@ -30,12 +30,7 @@ mod mbr;
/// Get the EFI BootCurrent entry number (the entry firmware used to boot).
/// Returns None on non-EFI systems or if BootCurrent is not set.
async fn get_efi_boot_current() -> Result