1. 项目概述为什么要在Fedora上折腾英伟达驱动签名如果你在Fedora上用过英伟达的官方闭源驱动同时又开启了UEFI安全启动Secure Boot那你大概率遇到过这个经典的“拦路虎”系统更新内核后重启电脑屏幕一片黑或者直接卡在某个启动阶段提示“无法加载NVIDIA内核模块”。这背后的核心矛盾就是安全启动机制与第三方内核模块之间的信任问题。安全启动是现代UEFI固件的一项安全功能旨在确保只有经过操作系统厂商比如微软、红帽数字签名的引导加载程序和内核模块才能被加载从而防止恶意软件在系统启动的早期阶段植入。Fedora作为主流Linux发行版其内核和内核模块都使用了红帽的密钥进行了签名。然而英伟达的专有驱动nvidia.ko、nvidia-drm.ko等是由英伟达自己编译和发布的它们没有、也不可能使用红帽的私钥进行签名。当安全启动开启时系统固件和内核会拒绝加载这些“未授权”的模块导致驱动失效。传统的“解决方案”是进入BIOS关闭安全启动。但这在很多场景下并不可取比如企业环境有安全合规要求或者你希望保留这一层安全防护。另一种临时方法是每次内核更新后手动为模块签名但这无疑是个重复且容易遗忘的体力活。因此这个项目的目标就非常明确了在Fedora系统上建立一套全自动的机制。每当系统更新内核从而触发了英伟达驱动DKMS的重建时系统能自动使用我们自己生成的、且被机器固件信任的密钥为重新编译生成的英伟达内核模块进行签名并完成后续的注册流程。整个过程无需人工干预实现“一次配置永久生效”在享受安全启动保护的同时无缝使用英伟达显卡驱动。2. 核心原理与准备工作拆解在动手之前我们必须搞清楚整个信任链是如何建立的。这不仅仅是执行几条命令理解其背后的逻辑能让你在遇到问题时快速定位。2.1 安全启动与内核模块签名机制简单来说安全启动建立了一个从固件到操作系统的信任链固件信任平台密钥PK这是信任链的根。平台密钥信任密钥交换密钥KEK。密钥交换密钥信任数据库密钥db这里存放着被允许加载的签名密钥通常是操作系统厂商如Microsoft、Red Hat的证书。内核信任db中的密钥Linux内核在加载模块时会检查模块的签名是否由db中记录的密钥所签发。Fedora发行版的内核是用红帽存放在db中的密钥签名的。我们的自制模块要想被加载有两个途径途径A让我们的密钥也被加入到系统的db中获得和红帽密钥同等的“信任地位”。途径B将我们的模块签名并让这个签名被已经受信任的红帽内核“二次认可”。我们采用的是途径A这也是最直接、最标准的方法。这意味着我们需要生成自己的密钥对公钥和私钥然后将公钥证书注册到UEFI固件的db中。此后任何用我们私钥签名的内核模块都会被系统视为可信任。2.2 工具链与核心组件实现自动化主要依赖以下几个核心工具openssl用于生成我们自己的RSA密钥对和X.509证书。这是整个信任体系的密码学基础。mokutilMachine Owner Key工具它是用户空间与UEFI固件安全启动数据库交互的桥梁。我们通过它来“预约”将公钥注册到固件。keyctlLinux密钥环工具用于将签名密钥加载到内核的密钥环中供内核在模块加载时进行验证。sign-file内核源代码中提供的脚本专门用于为内核模块.ko文件进行签名。它会使用指定的私钥对模块生成数字签名并将签名附加到模块文件中。dkms动态内核模块支持。它负责在系统安装新内核时自动为英伟达这类第三方驱动重新编译对应版本的内核模块。我们将挂钩hook它的构建后事件触发自动签名。opensslmokutilkeyctl通常系统已预装或可在仓库轻松安装。sign-file脚本需要安装kernel-devel包来获取。2.3 准备工作清单在开始自动化脚本编写前请确保完成以下一次性准备工作系统更新执行sudo dnf update确保系统和内核是最新的避免后续操作因版本问题出错。安装英伟达驱动确保你的英伟达驱动是通过RPM Fusion或英伟达官方.run文件正确安装的。通常使用RPM Fusion是最省事的方式启用RPM Fusion仓库后安装akmod-nvidia和kmod-nvidia包。akmod会自动处理DKMS编译。安装必要工具sudo dnf install openssl mokutil keyutils kernel-devel备份重要数据操作UEFI密钥有一定风险虽然很低且过程中会重启。建议对重要文件进行备份。确认安全启动状态运行sudo mokutil --sb-state。如果显示SecureBoot enabled则说明你正面临此问题可以继续。如果已禁用本项目对你而言是预防性措施。3. 全自动签名方案设计与实现我们的自动化方案将分为三个核心阶段生成并注册自有密钥、创建自动签名脚本、配置DKMS构建后钩子。下面我们分步详解。3.1 生成密钥与注册至UEFI固件这是建立信任基础的第一步只需操作一次。步骤1生成密钥对和证书我们将在/root目录下创建一个安全的位置来存放密钥。sudo mkdir -p /root/module-signing sudo cd /root/module-signing生成私钥MOK.priv和证书MOK.der。这里使用4096位RSA密钥安全性更高。sudo openssl req -new -x509 -newkey rsa:4096 -keyout MOK.priv -outform DER -out MOK.der -nodes -days 36500 -subj /CNMy Secure Boot Module Signing Key/-days 36500设置证书有效期约100年基本可视为永久。-subj /CN...设置证书主题这里的CN通用名称可以自定义例如你的名字或主机名它会在后续MOK管理界面显示。-nodes生成无密码保护的私钥。这是实现自动化的关键因为脚本需要非交互式地使用私钥。这意味着你必须绝对保证/root/module-signing目录的权限安全仅root可读。步骤2调整密钥文件权限为了保护私钥必须严格限制其访问权限。sudo chmod 600 /root/module-signing/MOK.priv sudo chmod 644 /root/module-signing/MOK.der步骤3将公钥证书注册到UEFI固件使用mokutil工具将公钥证书MOK.der的注册请求提交给固件。sudo mokutil --import /root/module-signing/MOK.der执行此命令后你会被要求输入并确认一个密码。请务必牢记这个密码它将在下一次重启时使用。这个密码是防止恶意软件随意注册密钥的一道屏障。步骤4重启并完成密钥注册输入sudo reboot重启系统。在系统启动初期你会看到一个蓝底白字的界面标题通常是“MOK Manager”部分厂商可能是“Select Key Management”或类似。这是UEFI固件提供的交互界面。选择“Enroll MOK”注册MOK。选择“View key”可以查看你证书的信息即之前设置的CN。选择“Continue”继续。系统会要求你输入在步骤3中设置的密码。输入后选择“Yes”确认注册。完成后选择“Reboot”重启电脑。重启后运行sudo mokutil --list-enrolled你应该能看到你刚刚注册的证书信息通过CN识别。这表明你的公钥已经成功加入UEFI的db数据库系统现在信任用对应私钥签名的内容。重要提示MOK.priv私钥必须妥善保管在本地系统绝不能泄露。任何获得此私钥的人都可以为你系统签名恶意模块。3.2 创建内核自动签名脚本现在我们需要一个脚本它能在需要的时候自动找到新编译的英伟达模块并用我们的私钥为其签名。创建脚本文件/usr/local/bin/sign-nvidia-modulessudo vim /usr/local/bin/sign-nvidia-modules将以下内容粘贴进去#!/bin/bash # 自动签名英伟达内核模块脚本 set -e # 遇到任何错误立即退出 # 1. 定义密钥和工具路径 PRIV_KEY/root/module-signing/MOK.priv PUB_CERT/root/module-signing/MOK.der SIGN_FILE/usr/src/kernels/$(uname -r)/scripts/sign-file # 动态获取当前内核的sign-file # 如果上述路径不存在尝试通用路径适用于大多数Fedora安装 if [ ! -f $SIGN_FILE ]; then SIGN_FILE/usr/lib/modules/$(uname -r)/build/scripts/sign-file fi # 2. 检查必要文件是否存在 if [ ! -f $PRIV_KEY ]; then echo 错误未找到私钥文件 $PRIV_KEY exit 1 fi if [ ! -f $SIGN_FILE ]; then echo 错误未找到 sign-file 工具。请确保 kernel-devel 包已安装。 exit 1 fi # 3. 定义需要签名的模块名称模式 MODULE_PATTERNS(nvidia nvidia-drm nvidia-modeset nvidia-uvm) # 查找当前内核版本下的模块安装目录 MODULE_DIR/lib/modules/$(uname -r)/updates # 4. 签名函数 sign_module() { local module_path$1 local module_name$(basename $module_path) echo 正在为模块 $module_name 签名... # 使用 sha512 哈希算法和我们的私钥进行签名 sudo $SIGN_FILE sha512 $PRIV_KEY $PUB_CERT $module_path if [ $? -eq 0 ]; then echo - $module_name 签名成功。 else echo - 错误$module_name 签名失败 exit 1 fi } # 5. 遍历并签名所有匹配的模块 echo 开始自动签名英伟达内核模块... for pattern in ${MODULE_PATTERNS[]}; do # 使用 find 命令查找所有 .ko 文件 find $MODULE_DIR -name ${pattern}*.ko -type f | while read -r module; do sign_module $module done done echo 所有模块签名完成。 echo 建议运行 sudo depmod -a 重新生成模块依赖关系然后重启系统。保存并退出然后赋予脚本执行权限sudo chmod x /usr/local/bin/sign-nvidia-modules脚本解析与注意事项set -e这是一个重要的安全设置确保脚本中任何命令失败返回非零值时整个脚本会立即停止防止在错误状态下继续执行。动态路径脚本尝试动态定位当前运行内核的sign-file工具和模块目录这比写死路径更健壮能适应内核更新。模块模式MODULE_PATTERNS数组列出了需要签名的核心英伟达模块。你可以根据实际安装的驱动版本进行调整。使用find命令能确保找到所有匹配的模块文件。权限脚本中调用sign-file时使用了sudo因为签名操作需要root权限并且要写入模块文件。确保执行此脚本的用户有sudo权限。3.3 配置DKMS构建后钩子实现全自动这是实现“全自动”的灵魂。我们需要配置DKMS使其在成功为英伟达驱动编译模块后自动调用我们的签名脚本。创建DKMS钩子配置文件/etc/dkms/nvidia-signing.confsudo vim /etc/dkms/nvidia-signing.conf输入以下内容# DKMS 构建后钩子自动为英伟达模块签名 POST_BUILDsign-nvidia-modules这个配置非常简单POST_BUILD是一个特殊的DKMS变量它指定了在dkms build和dkms install步骤之间即模块编译完成后安装到系统前要执行的命令。这里我们直接调用刚才创建的签名脚本。但是为了让这个钩子只对英伟达驱动生效我们需要将其关联到特定的DKMS模块名。英伟达驱动的DKMS模块名通常是nvidia后跟版本号如nvidia-550.90.07。我们可以通过一个包装脚本更精确地控制。创建一个更智能的钩子脚本/etc/dkms/post-build.d/sign-nvidiasudo mkdir -p /etc/dkms/post-build.d sudo vim /etc/dkms/post-build.d/sign-nvidia输入以下内容#!/bin/bash # DKMS post-build hook for NVIDIA driver signing # DKMS会传递以下环境变量给钩子脚本 # $1: dkms 操作 (例如build, install, add, remove) # $2: 内核版本 (例如6.8.9-300.fc40.x86_64) # $3: 模块名称 (例如nvidia) # $4: 模块版本 (例如550.90.07) # 只对模块名称为 nvidia 的构建操作执行签名 if [ $1 build ] [[ $3 nvidia* ]]; then echo DKMS hook: 检测到英伟达驱动 ($3-$4) 针对内核 $2 构建完成。 echo DKMS hook: 开始自动签名内核模块... # 调用我们的主签名脚本 /usr/local/bin/sign-nvidia-modules if [ $? -eq 0 ]; then echo DKMS hook: 模块签名流程结束。 else echo DKMS hook: 签名过程出错 2 exit 1 fi fi赋予执行权限sudo chmod x /etc/dkms/post-build.d/sign-nvidia为什么需要钩子脚本直接使用POST_BUILD虽然简单但它是全局的可能会影响其他DKMS模块。而将脚本放在/etc/dkms/post-build.d/目录下DKMS会在构建后自动执行该目录下所有可执行脚本。我们通过脚本内的判断[[ $3 nvidia* ]]确保了只有英伟达驱动的构建才会触发签名更加精准和安全。4. 测试与验证全流程配置完成后我们需要模拟或触发一次内核更新来测试整个自动化流程是否工作。步骤1手动触发DKMS重建最直接的测试方法是手动移除并重新添加当前内核的英伟达DKMS模块这会触发完整的构建和安装流程从而调用我们的钩子。# 获取当前内核版本和英伟达驱动版本 CURRENT_KERNEL$(uname -r) NVIDIA_VERSION$(dkms status | grep nvidia | head -1 | awk -F, {print $2} | awk -F: {print $1}) # 示例输出nvidia, 550.90.07, 6.8.9-300.fc40.x86_64, x86_64: installed # 那么 NVIDIA_VERSION 就是 “550.90.07” if [ -z $NVIDIA_VERSION ]; then echo 未找到已安装的英伟达DKMS模块。 exit 1 fi echo 正在测试针对内核 $CURRENT_KERNEL 和驱动 $NVIDIA_VERSION 的自动签名... # 先移除模块 sudo dkms remove nvidia/$NVIDIA_VERSION -k $CURRENT_KERNEL # 再重新构建和安装这会触发 post-build 钩子 sudo dkms build nvidia/$NVIDIA_VERSION -k $CURRENT_KERNEL sudo dkms install nvidia/$NVIDIA_VERSION -k $CURRENT_KERNEL观察命令输出。在dkms build步骤成功后你应该能看到来自/etc/dkms/post-build.d/sign-nvidia脚本的输出信息例如“检测到英伟达驱动...开始自动签名内核模块...”以及主签名脚本的详细签名过程。步骤2验证模块签名构建安装完成后验证模块是否已被正确签名。# 使用 modinfo 检查单个模块的签名者信息 modinfo /lib/modules/$(uname -r)/updates/nvidia.ko | grep -E signer|sig_key如果签名成功你会看到类似signer: My Secure Boot Module Signing Key的输出其中的CN就是你生成证书时设置的名称。 更全面的检查可以使用keyctl查看内核密钥环或者直接尝试加载模块# 将模块加载到内核如果已经加载会先卸载 sudo modprobe -r nvidia_drm nvidia_modeset nvidia_uvm nvidia 2/dev/null; echo 旧模块已卸载如已加载。 sudo modprobe nvidia echo 模块加载命令已执行。检查dmesg日志 sudo dmesg | tail -20在dmesg日志中你不应该再看到关于nvidia模块“签名无效”或“被拒绝”的安全启动错误信息。步骤3模拟内核更新后的完整流程最真实的测试是进行一次实际的内核更新。sudo dnf upgrade kernel kernel-devel kernel-headers更新完成后不要立即重启。先检查DKMS是否已为新内核编译了模块dkms status | grep nvidia你应该能看到针对新旧两个内核版本的记录。针对新内核的状态可能是“built”或“installed”。如果状态正常现在可以重启。sudo reboot重启后系统会引导至新内核。如果自动化流程一切正常英伟达驱动应该能正常加载你可以通过nvidia-smi命令验证并且系统日志 (journalctl -k --grepnvidia) 中不应有安全启动相关的错误。5. 故障排查与经验心得即使按照步骤操作也可能遇到各种问题。这里记录一些常见坑点及解决方案。5.1 常见问题速查表问题现象可能原因排查步骤与解决方案MOK Manager界面未出现1.mokutil --import未成功提交请求。2. 固件MOK功能被禁用或异常。3. 快速启动干扰。1. 运行sudo mokutil --list-new确认是否有待处理的注册请求。2. 重启进入BIOS/UEFI设置确认安全启动和MOK相关选项已开启。3. 尝试在BIOS中关闭“快速启动”Fast Boot确保完整执行UEFI启动流程。模块签名后仍加载失败1. 公钥未成功注册到固件db。2. 签名使用的私钥与注册的公钥不匹配。3. 模块路径或版本不对。1. 运行sudo mokutil --list-enrolled确认你的证书在列表中。2. 运行sudo modinfo -F sig_key 模块路径与sudo mokutil --list-enrolled输出的密钥哈希对比是否一致。3. 检查脚本中的MODULE_DIR是否指向了正确的内核版本模块目录。DKMS钩子脚本未执行1. 脚本没有执行权限。2. 脚本路径不在DKMS搜索范围。3. 脚本逻辑判断条件错误。1. 确认/etc/dkms/post-build.d/sign-nvidia有chmod x权限。2. 查看/etc/dkms/dkms.conf或man dkms确认钩子目录。3. 在脚本开头加set -x调试或手动传递参数测试脚本逻辑。sign-file命令找不到kernel-devel包未安装或安装的内核开发包版本与当前运行内核不匹配。1. 运行sudo dnf install kernel-devel-$(uname -r)安装对应版本。2. 使用find /usr/src -name sign-file或find /lib/modules -name sign-file定位工具并更新脚本中的SIGN_FILE路径。系统更新后黑屏/卡住自动化流程在某一步失败导致新内核的模块未正确签名。1. 重启时在GRUB菜单选择旧内核启动。2. 登录后检查/var/lib/dkms/nvidia/.../build/make.log查看构建日志。3. 检查/var/log/dkms相关日志以及系统日志journalctl -u dkms。5.2 实操心得与进阶技巧密钥管理是重中之重私钥MOK.priv相当于你系统的“信任印章”。务必将其权限设置为600仅root可读并考虑将其备份到加密的离线存储中如密码管理器或加密U盘。永远不要将其上传到云端或共享。密码复杂度与保管mokutil --import时设置的密码是保护密钥注册的最后一道手动防线。建议使用强密码并妥善记录。如果忘记此密码将无法在MOK Manager中完成注册唯一的办法是进入UEFI设置清除安全启动密钥数据库这会导致所有自定义密钥失效需要重新注册。脚本的健壮性我们的主签名脚本包含了错误检查set -e 文件存在性检查。在实际生产环境中你还可以增加日志功能将每次签名的模块列表、时间戳记录到/var/log/nvidia-signing.log便于后期审计和排查。处理多版本内核我们的方案已经通过$(uname -r)动态处理当前运行内核。但如果你需要为一个尚未启动的新内核签名例如在系统更新后但重启前可以修改脚本使其接受一个内核版本作为参数例如sign-nvidia-modules 6.8.10-300.fc40.x86_64并在DKMS钩子中传递$2内核版本给脚本。与第三方仓库的兼容性如果你使用的是RPM Fusion的akmod-nvidia它本身已经是一个高度自动化的包装。我们的DKMS钩子仍然有效因为akmod底层也是调用dkms。两者协同工作akmod负责在后台编译我们的钩子负责在编译后插入签名步骤。恢复与回滚如果自动化签名导致问题最快的恢复方式是1) 用旧内核启动。2) 删除或重命名钩子脚本 (sudo mv /etc/dkms/post-build.d/sign-nvidia /etc/dkms/post-build.d/sign-nvidia.bak)。3) 手动卸载并重新安装英伟达驱动不签名。4) 重启进入新内核此时驱动将无法加载但系统应能正常启动进入图形界面使用开源驱动nouveau给你机会修复脚本。这个全自动方案将原本繁琐、易出错的手动操作转化为系统后台静默完成的可靠流程。一旦配置成功你几乎可以忘记安全启动和英伟达驱动之间的冲突像使用开源驱动一样平滑地进行系统更新和重启同时保留了安全启动带来的安全益处。