Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Protect GRUB with a password (boot loader hardening)

By updated

Improve this page

Without protection, anyone with physical access to your server can, at the GRUB menu, press e to edit the kernel command line, add init=/bin/bash or single, and get a root shell without a password. All your system hardening is then bypassed in ten seconds. This guide shows how to protect the editing of GRUB entries with a password, while letting the server boot normally (no prompt at each boot), on Debian/Ubuntu and RHEL/AlmaLinux/Rocky. It also covers kernel parameters, where the classic method fails silently on RHEL. All commands were validated in a VM on Debian 12, AlmaLinux 9.8 and AlmaLinux 10.2. This page covers the ANSSI-BP-028 R5 recommendation and the CIS 1.4.1 / 1.4.2 controls.

What you will learn

  • Generate a hashed GRUB password (PBKDF2)
  • Protect entry editing and the GRUB console
  • Let the system boot without a password (avoid the lockout trap)
  • Set a persistent kernel parameter with the tool that really works on your distribution
  • Check the configuration and restore it in case of error

Why protect the boot loader?

The root password, SELinux, auditd: all of this applies after the kernel starts. Yet GRUB runs before, and by default it lets you change the boot parameters. An attacker with console access (physical, KVM, or a VM) edits an entry, forces an emergency shell, and reads or changes the system outside any control. Protecting GRUB closes this door.

The goal is not to ask for a password at every boot (unmanageable on a server that must restart on its own), but to require authentication only to edit an entry or open the GRUB console. Normal boot stays automatic.

Prerequisites

  • root access (or sudo).
  • A test VM with a snapshot before any change.
  • GRUB 2 (the standard on every modern distribution).

Generate a hashed GRUB password

GRUB never stores the password in clear: it uses a PBKDF2 hash (SHA-512). Generate it with grub-mkpasswd-pbkdf2 (Debian/Ubuntu) or grub2-mkpasswd-pbkdf2 (RHEL):

Terminal window
grub-mkpasswd-pbkdf2

The tool asks for the password twice, then displays the hash:

PBKDF2 hash of your password is grub.pbkdf2.sha512.10000.809AB137...38817EB8.5E09FF7B...70D4AC03

Copy the full string starting with grub.pbkdf2.sha512: it is the one you will put in the configuration. The clear password appears nowhere, neither in the file nor in the output.

Declare the superuser and the password

The declaration differs between the two distribution families. Never edit grub.cfg directly: this file is regenerated (and therefore overwritten) at every kernel update.

Create a file in /etc/grub.d/. 40_custom is the right place (CIS advises against 00_header, overwritten when the package is updated):

Terminal window
cat >> /etc/grub.d/40_custom <<'EOF'
set superusers="bootadmin"
password_pbkdf2 bootadmin grub.pbkdf2.sha512.10000.YOUR.HASH.HERE
EOF
  • set superusers defines who can edit entries and open the console.
  • password_pbkdf2 associates the GRUB user bootadmin with its hash.

Choose a superuser name different from root (defence in depth).

The trap: boot without a password, protect editing

This is the most important step, and the one many people miss. As soon as superusers is defined, GRUB protects all entries by default: the server then asks for the password at every boot. On a server, that is unacceptable.

The solution is the --unrestricted flag: an entry marked this way boots without a password, but its editing (e) and the GRUB console (c) stay protected.

Entries are generated by /etc/grub.d/10_linux. First read the CLASS= line actually installed (it varies by version), then add --unrestricted:

Terminal window
grep '^CLASS=' /etc/grub.d/10_linux
# CLASS="--class gnu-linux --class gnu --class os"
sed -i '/^CLASS=/ s/"$/ --unrestricted"/' /etc/grub.d/10_linux

All normal entries will inherit --unrestricted when the file is regenerated.

Regenerate and check

Apply the configuration by regenerating grub.cfg, with the command of your distribution, then check that the directives made it into the generated file:

Terminal window
update-grub
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.1.0-49-amd64
done

Check that the directives and the option are written:

Terminal window
grep -E 'set superusers|password_pbkdf2' /boot/grub/grub.cfg
grep -c 'menuentry.*--unrestricted' /boot/grub/grub.cfg

The output must show set superusers="bootadmin", the password_pbkdf2 line, and a non-zero count of --unrestricted entries.

On UEFI, locate grub.cfg instead of assuming it

The location of the active grub.cfg on UEFI has moved from one version to another, and it is a classic source of mistakes. On AlmaLinux 9.8, /boot/grub2/grub.cfg did not exist: the active file was /boot/efi/EFI/almalinux/grub.cfg. On AlmaLinux 10.2, it is unified: /boot/grub2/grub.cfg exists, /etc/grub2.cfg and /etc/grub2-efi.cfg are two symbolic links that both point to it, and the EFI file is now only a four-line stub that delegates:

search --no-floppy --root-dev-only --fs-uuid --set=dev cea79fb2-...
set prefix=($dev)/grub2
export $prefix
configfile $prefix/grub.cfg

In both cases, the user.cfg written by grub2-setpassword is indeed taken into account, since it is loaded through ${prefix}/user.cfg. The rule to remember is therefore the same whatever the version: locate your active file instead of assuming its path, then target it for grub2-mkconfig.

Terminal window
find /boot -name grub.cfg

Restrict the permissions (CIS 1.4.2, ANSSI R29)

The grub.cfg file contains the password hash and the boot parameters. If it is readable by everyone, these elements leak. Restrict it to root only:

Terminal window
# Debian/Ubuntu
chown root:root /boot/grub/grub.cfg
chmod og-rwx /boot/grub/grub.cfg
# RHEL/AlmaLinux/Rocky
chown root:root /boot/grub2/grub.cfg /boot/grub2/user.cfg
chmod og-rwx /boot/grub2/grub.cfg /boot/grub2/user.cfg

Check:

Terminal window
stat -c '%a %U:%G' /boot/grub/grub.cfg
600 root:root

The exact expected value varies by framework (0400 for CIS Ubuntu 22.04, 0600 for CIS CentOS 7); og-rwx removes access for other accounts in every case, which meets the intent. Also remember to restrict /boot itself (ANSSI R29).

Check that the protection works

The proof comes at reboot: the machine must start on its own, and only editing an entry must trigger the password prompt.

  1. Confirm that the directives are present in the active configuration:

    Terminal window
    grep -E 'password_pbkdf2|GRUB2_PASSWORD' /boot/grub/grub.cfg /boot/grub2/user.cfg 2>/dev/null
  2. Reboot the machine. The system must boot normally, without asking for a password (thanks to --unrestricted).

  3. At the GRUB menu, press e (edit) or c (console): GRUB must then ask for a user name and a password. This is the protection in action.

In the lab, the reboot confirmed there was no lockout: the VM came back in less than ten seconds without a prompt, while keeping the password_pbkdf2 directive active in grub.cfg.

Troubleshooting

The table starts from the symptom you observe, gives the most likely cause, then the solution; the last row concerns kernel parameters on the RHEL family.

SymptomProbable causeSolution
Password asked at every bootsuperusers defined without --unrestrictedAdd --unrestricted to CLASS in 10_linux, then regenerate
System unbootable after configurationSyntax error or truncated hashRestore the snapshot; otherwise boot a live USB and fix /etc/grub.d/
Changes lost after an updateDirect editing of grub.cfgAlways go through /etc/grub.d/ + /etc/default/grub, never grub.cfg
grub2-setpassword fails (Inappropriate ioctl)No real terminal (script, pipe)Run it interactively; or generate the hash with grub2-mkpasswd-pbkdf2 and write user.cfg by hand
grub.cfg not found at the expected path (UEFI)EFI location varies by distrofind /boot -name grub.cfg then target that file
Kernel parameter missing from /proc/cmdline after editing /etc/default/grub and regenerating, on RHELBLS is active: grub2-mkconfig does not rewrite /boot/loader/entries/sudo grubby --update-kernel=ALL --args="…"

Add a kernel parameter: grub2-mkconfig is not enough on RHEL

Hardening often leads to setting a persistent kernel parameter (audit=1, slab_nomerge, init_on_alloc=1). The method you read everywhere, "edit /etc/default/grub then regenerate", does not work on RHEL 8 and later, nor on AlmaLinux and Rocky. It fails silently, which is worse than an error.

The cause is BLS (Boot Loader Specification), active by default (GRUB_ENABLE_BLSCFG=true in /etc/default/grub). Each installed kernel has its own entry file under /boot/loader/entries/, and the options line of that file is what is really read at boot.

Measured on AlmaLinux 10.2: after editing GRUB_CMDLINE_LINUX then grub2-mkconfig -o /boot/grub2/grub.cfg, the BLS entry file is identical bit for bit. The parameter only appears in grub.cfg, at a place GRUB never reads when BLS is active.

LocationAfter grub2-mkconfigAfter grubby --update-kernel=ALL
/boot/loader/entries/*.conf (read at boot)unchangedmodified
/boot/grub2/grub.cfg (kernelopts fallback, ignored)modifiedunchanged
/etc/default/grubmodified by handmodified too
/etc/kernel/cmdline (template for future kernels)unchangedmodified
/proc/cmdline at the next rebootunchangedmodified

The tool that acts on already installed kernels is therefore grubby:

Terminal window
sudo grubby --update-kernel=ALL --args="audit=1"
Terminal window
sudo grubby --info=ALL | grep ^args

Editing /etc/default/grub remains useful, but for another reason: this file feeds /etc/kernel/cmdline, the template applied to future kernels when they are installed. The two gestures do not target the same population of kernels, which is why you often need both.

To remove a parameter, grubby can also undo:

Terminal window
sudo grubby --update-kernel=ALL --remove-args="audit=1"

The topic has its own page, with the details of the four possible locations, the Debian method, the check after reboot and troubleshooting: Pass parameters to the kernel at boot.

Put it into practice

Handling the GRUB configuration stays abstract until you have regenerated a grub.cfg yourself and seen the effect at reboot. This lab has you add a persistent kernel parameter, apply it with the tool suited to your distribution (grubby on RHEL, update-grub on Debian), then prove the result in /proc/cmdline after a reboot, on a disposable VM where a mistake costs nothing.

Key points

  • GRUB runs before the kernel: without a password, editing an entry gives root without authentication
  • The PBKDF2 hash is generated with grub-mkpasswd-pbkdf2; the clear password is never stored
  • Debian: set superusers + password_pbkdf2 in /etc/grub.d/40_custom, then update-grub
  • RHEL: grub2-setpassword does everything, and only protects editing by default
  • --unrestricted is the key: the system boots without a password, only editing is protected
  • Never touch grub.cfg directly (regenerated); a snapshot is mandatory before any test
  • On RHEL and derivatives, a kernel parameter is set with grubby --update-kernel=ALL: because of BLS, /etc/default/grub + grub2-mkconfig does not touch already installed kernels

Knowledge check

Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn