Handbook sections

Hardening the bootloader (GRUB)

Last reviewed

GRUB runs before the OS and before any OS-level control. Protect it with a password, lock down grub.cfg, and pass hardening flags on the kernel command line.

The threat: an editable boot prompt is unauthenticated root

The bootloader runs before the operating system: before logins, before sudo, before SELinux or AppArmor, before any control you configured. Anyone at the console or with physical access can interrupt GRUB, press e to edit a boot entry, and rewrite the kernel command line. The classic moves are appending single (or 1) to drop into single-user mode, or init=/bin/bash to replace init with a shell: both yield an unauthenticated root prompt that bypasses every OS-level safeguard. From there an attacker resets the root password, reads or rewrites disks, or plants persistence. A world-readable grub.cfg leaks the layout and any hashes it carries, and a writable one lets a local user inject boot parameters that survive reboots.

Why harden it

The bootloader is the root of the boot trust chain: it decides what kernel runs and with which parameters. If it can be edited without authentication, none of your downstream hardening matters, the attacker simply boots around it. Hardening GRUB closes the physical/console attack path and makes the kernel command line itself a layer of defense, enabling mitigations that can only be set at boot time (CPU side-channel fixes, memory hygiene, mandatory access control).

Defense principles applied

  • Authentication before privilege: set a GRUB superuser with a password_pbkdf2 hash so editing entries or reaching the GRUB shell requires a password. This is what stops the single / init=/bin/bash edit.
  • The --unrestricted caveat: a superuser password without --unrestricted on the normal menu entries blocks every boot and will hang a headless machine at the GRUB prompt. Add --unrestricted so the password gates editing and the shell, not routine booting. This is the single most common way to brick a server while hardening it.
  • Least privilege on files: grub.cfg should be owned by root:root and not readable, writable or executable by group/other (mode 0600), so its contents can't leak and no local user can tamper with boot parameters (CIS 1.4.2, ANSSI BP-028 R29).
  • Defense in depth on the kernel command line: turn on boot-time mitigations such as pti=on (Meltdown), the spectre/MDS/L1TF flags, init_on_alloc=1/init_on_free=1 and page_poison=1 (zero memory to kill use-after-free leaks), slab_nomerge, vsyscall=none, and keep selinux=0 absent.

What Pavois audits: effective at runtime, not the template

Hardening only counts if it is effective at runtime, not merely written in /etc/default/grub:

  • GRUB password (grub-password, severity high): Pavois greps the rendered grub.cfg across BIOS (/boot/grub, /boot/grub2) and EFI (/boot/efi/EFI/*/grub.cfg) layouts, plus /etc/grub.d/, for set superusers and a password_pbkdf2/password directive. It validates the generated config that GRUB actually loads, not a template fragment.
  • Kernel command line: each rule reads /proc/cmdline, the line the running kernel actually booted with, and matches the required flag (pti=on, init_on_alloc=1, slab_nomerge, the absence of selinux=0). Reading /proc/cmdline instead of /etc/default/grub catches a flag edited but never regenerated, or regenerated but not yet rebooted.
  • grub.cfg permissions: root:root, no group/other read/write/execute on the real grub.cfg and its EFI variant, guarded by an only_if existence test.

When pavois harden apply sets the password it uses the grub_password resource (PBKDF2 hash, snippet in grub.d, config regenerated, reboot_required) and appends --unrestricted automatically, so applying the control never locks you out of a normal boot.

How do you set it and verify?

  1. Generate a PBKDF2 hash: grub-mkpasswd-pbkdf2 (RHEL: grub2-mkpasswd-pbkdf2) and copy the grub.pbkdf2.sha512... string.
  2. Declare the superuser in a grub.d snippet (e.g. /etc/grub.d/01_users): set superusers="admin" then password_pbkdf2 admin <hash>.
  3. Keep normal boot unattended: add --unrestricted to the CLASS in /etc/grub.d/10_linux so menu entries boot without the password (only editing and the shell are gated).
  4. Regenerate: update-grub (Debian/Ubuntu) or grub2-mkconfig -o /boot/grub2/grub.cfg (RHEL), then chmod 0600 the rendered grub.cfg.
  5. Verify: cat /proc/cmdline for the live flags, grep -E 'superusers|password_pbkdf2|unrestricted' /boot/grub*/grub.cfg, then reboot to confirm the machine boots unattended and that pressing e prompts for the password.

Pitfalls

  • Password without --unrestricted bricks a headless boot: the machine hangs at the GRUB password prompt. Always pair them.
  • Editing /etc/default/grub is not enough: nothing changes until you regenerate grub.cfg and reboot. Pavois reads /proc/cmdline, so it tells you the truth.
  • EFI vs BIOS paths differ: the real grub.cfg may be under /boot/efi/EFI/<distro>/. Edit and check the one actually in use.
  • Recovery: if you lock yourself out, boot the install media in rescue mode to fix the snippet.

FAQ

Does a GRUB password stop the machine from booting on its own? Only if you forget --unrestricted. With it, normal entries boot unattended and the password is required only to edit an entry or open the GRUB shell.

Why does Pavois read /proc/cmdline and the rendered grub.cfg instead of /etc/default/grub? Because /etc/default/grub is a template; the kernel boots with /proc/cmdline and GRUB loads the generated grub.cfg. A flag edited but never regenerated, or not yet rebooted, is a false pass on the template.

Where do I put the password? In a /etc/grub.d/ snippet (so grub-mkconfig renders it into grub.cfg), never by editing the generated grub.cfg directly.