// /etc/apt/apt.conf.d/90-kernel-update-tpm.conf // // ════════════════════════════════════════════════════════════════════════════ // WHAT THIS FILE IS // ════════════════════════════════════════════════════════════════════════════ // // This is an APT configuration file. APT (Advanced Package Tool) is the // package manager on Debian and Ubuntu — the program you use when you run // "apt install" or "apt upgrade". APT reads its configuration from files // in /etc/apt/apt.conf.d/ every time it runs. Files in that directory are // loaded in alphabetical order. The "90" prefix in this filename puts it // near the end of the load order, after APT's own defaults have been // established. This avoids accidentally overriding something important. // // Comments in APT config files use C++ style: // for single-line comments. // There is no block comment syntax. Every statement ends with a semicolon. // // ════════════════════════════════════════════════════════════════════════════ // WHY THIS FILE EXISTS — THE PROBLEM IT SOLVES // ════════════════════════════════════════════════════════════════════════════ // // This system uses TPM-sealed full-disk encryption via Clevis. Here is what // that means: // // - The hard disk is encrypted with LUKS2 (Linux Unified Key Setup v2). // - To boot without typing a password, the LUKS decryption key is "sealed" // inside the TPM (Trusted Platform Module) chip on the motherboard. // - The TPM will only release the key if a set of measurements called PCRs // (Platform Configuration Registers) match the values recorded when the // key was sealed. // - PCRs measure boot components: firmware, bootloader, kernel, initramfs. // If any of them change, the PCR values change, and the TPM refuses to // release the key. // // The problem: when you run "apt upgrade" and a new kernel is installed, // the initramfs is rebuilt and the GRUB bootloader configuration changes. // Both of these affect PCR values. If the LUKS key is still sealed to the // OLD PCR values when the new kernel boots for the first time, the TPM will // refuse to unseal it and the disk will not decrypt automatically. The // system will stop at a passphrase prompt. // // The solution: this file causes APT to run a hook script // (kernel-update-tpm-apt-hook) BEFORE packages are installed. That hook // calls kernel-update-tpm.sh to prepare a temporary "loose" Clevis slot // that will still work even after the PCR values change. The strict slot // is then re-enrolled after the new kernel boots successfully. // // ════════════════════════════════════════════════════════════════════════════ // APT HOOK TYPES AND WHY WE USE Pre-Install-Pkgs // ════════════════════════════════════════════════════════════════════════════ // // APT supports several hook points. The most common are: // // Pre-Install-Pkgs — runs BEFORE dpkg installs any package. // The full package list is available on stdin. // If the hook exits non-zero, APT ABORTS the // transaction. This is what we use. // // Post-Invoke — runs AFTER dpkg finishes all package changes. // Used by other tools (e.g., update-notifier). // Cannot abort the transaction. // // DPkg::Pre-Invoke — runs before dpkg runs at all, even before // package lists are read. Too early to be useful // for our purpose. // // We use Pre-Install-Pkgs because: // 1. We need to act BEFORE the new kernel/initramfs files land on disk, // because that is when the PCR values will change on next boot. // 2. We need the package list so we can decide whether this particular // apt transaction touches boot-sensitive packages at all. There is // no point triggering the TPM workflow for "apt install vim". // 3. We want the ability to abort the transaction if preparation fails — // it is safer to refuse a kernel install than to let it proceed // without the loose Clevis slot in place. // // ════════════════════════════════════════════════════════════════════════════ // APT HOOK PROTOCOL VERSIONS — WHY VERSION 3 // ════════════════════════════════════════════════════════════════════════════ // // When APT runs a Pre-Install-Pkgs hook, it can send different amounts of // information to the hook's standard input depending on the protocol version: // // Version 1 — Sends package names only, one per line. // Example: linux-image-6.1.0-30-amd64 // // Version 2 — Sends package names and file paths. // Example: linux-image-6.1.0-30-amd64 /var/cache/apt/... // // Version 3 — Sends package name and ACTION (install/upgrade/remove). // Example: linux-image-6.1.0-30-amd64 install // This is the most informative and is what we request. // // We use Version 3 because the hook needs to: // a) Know the package NAME to match against our list of boot-sensitive // packages (kernel, initramfs-tools, grub, clevis, tpm2, etc.) // b) Know the ACTION to distinguish installs/upgrades from removals, // although currently we trigger on any action involving these packages. // // Without requesting a version, APT would send nothing to stdin and the // hook would have no way to inspect the transaction. // // ════════════════════════════════════════════════════════════════════════════ // INSTALL AND REMOVAL // ════════════════════════════════════════════════════════════════════════════ // // To install: // sudo cp 90-kernel-update-tpm.conf /etc/apt/apt.conf.d/ // sudo chown root:root /etc/apt/apt.conf.d/90-kernel-update-tpm.conf // sudo chmod 644 /etc/apt/apt.conf.d/90-kernel-update-tpm.conf // // To remove (disable the hook entirely): // sudo rm /etc/apt/apt.conf.d/90-kernel-update-tpm.conf // This does NOT remove the hook script itself; it just stops APT from // calling it. To remove the script too: // sudo rm /usr/local/sbin/kernel-update-tpm-apt-hook // // ════════════════════════════════════════════════════════════════════════════ // THE CONFIGURATION STATEMENTS // ════════════════════════════════════════════════════════════════════════════ // DPkg::Pre-Install-Pkgs is a LIST of hook scripts to run before package // installation. APT runs each listed script in order, waits for it to // finish, and checks its exit code. If any hook exits non-zero, APT // aborts the whole transaction and reports an error to the user. // // The value inside the braces is the FULL PATH to the hook executable. // APT does not search PATH — you must give the absolute path. // // Why /usr/local/sbin/? // /usr/local/ is the standard location for software installed manually // by the system administrator (as opposed to software managed by the // package manager itself, which goes in /usr/). The /sbin/ subdirectory // is for system administration commands not intended for normal users. // This is the correct location for a root-only automation script. // // Security consideration: the hook runs AS ROOT. The file must be owned // by root and not writable by other users (chmod 755, chown root:root). // A world-writable hook script would be a privilege escalation hole. DPkg::Pre-Install-Pkgs { "/usr/local/sbin/kernel-update-tpm-apt-hook"; }; // DPkg::Tools::Options::::Version tells APT which protocol // version to use when invoking a specific hook. The :: separators form // a nested key hierarchy in APT's configuration language. // // Breaking down the key: // DPkg::Tools::Options — the parent namespace for per-tool options // ::/usr/local/sbin/... — the specific hook script this applies to // ::Version — the option being set // // The value "3" (a quoted string in APT config, even though it looks like // a number) requests the Version 3 protocol: package name + action on // each stdin line. // // This must EXACTLY match the path given in DPkg::Pre-Install-Pkgs above. // If the paths differ by even one character, APT will use the default // version (Version 1 or no stdin at all) for our hook, and the hook will // see an empty package list and do nothing. DPkg::Tools::Options::/usr/local/sbin/kernel-update-tpm-apt-hook::Version "3";