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):
grub-mkpasswd-pbkdf2The tool asks for the password twice, then displays the hash:
PBKDF2 hash of your password is grub.pbkdf2.sha512.10000.809AB137...38817EB8.5E09FF7B...70D4AC03Copy 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):
cat >> /etc/grub.d/40_custom <<'EOF'set superusers="bootadmin"password_pbkdf2 bootadmin grub.pbkdf2.sha512.10000.YOUR.HASH.HEREEOFset superusersdefines who can edit entries and open the console.password_pbkdf2associates the GRUB userbootadminwith its hash.
Choose a superuser name different from root (defence in depth).
Red Hat provides a dedicated utility, grub2-setpassword (interactive,
requires a real terminal):
grub2-setpasswordIt writes the hash into /boot/grub2/user.cfg as
GRUB2_PASSWORD=grub.pbkdf2..., and automatically creates the root
superuser (no need to define set superusers). By default, it only
protects editing and the console, not booting: exactly the behaviour you
want, delivered without any extra configuration.
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:
grep '^CLASS=' /etc/grub.d/10_linux# CLASS="--class gnu-linux --class gnu --class os"
sed -i '/^CLASS=/ s/"$/ --unrestricted"/' /etc/grub.d/10_linuxAll normal entries will inherit --unrestricted when the file is regenerated.
Nothing to do: grub2-setpassword already sets up the protection so that
it only applies to editing, not booting. If, on the contrary, you wanted
to require the password at every boot, you would have to remove
--unrestricted from the entries (through 10_linux) before regenerating.
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:
update-grubGenerating grub configuration file ...Found linux image: /boot/vmlinuz-6.1.0-49-amd64doneCheck that the directives and the option are written:
grep -E 'set superusers|password_pbkdf2' /boot/grub/grub.cfggrep -c 'menuentry.*--unrestricted' /boot/grub/grub.cfgThe output must show set superusers="bootadmin", the password_pbkdf2
line, and a non-zero count of --unrestricted entries.
grub2-mkconfig -o /boot/grub2/grub.cfgCheck the hash and its loading:
grep GRUB2_PASSWORD /boot/grub2/user.cfgOn 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)/grub2export $prefixconfigfile $prefix/grub.cfgIn 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.
find /boot -name grub.cfgRestrict 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:
# Debian/Ubuntuchown root:root /boot/grub/grub.cfgchmod og-rwx /boot/grub/grub.cfg
# RHEL/AlmaLinux/Rockychown root:root /boot/grub2/grub.cfg /boot/grub2/user.cfgchmod og-rwx /boot/grub2/grub.cfg /boot/grub2/user.cfgCheck:
stat -c '%a %U:%G' /boot/grub/grub.cfg600 root:rootThe 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.
-
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 -
Reboot the machine. The system must boot normally, without asking for a password (thanks to
--unrestricted). -
At the GRUB menu, press
e(edit) orc(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.
| Symptom | Probable cause | Solution |
|---|---|---|
| Password asked at every boot | superusers defined without --unrestricted | Add --unrestricted to CLASS in 10_linux, then regenerate |
| System unbootable after configuration | Syntax error or truncated hash | Restore the snapshot; otherwise boot a live USB and fix /etc/grub.d/ |
| Changes lost after an update | Direct editing of grub.cfg | Always 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 distro | find /boot -name grub.cfg then target that file |
Kernel parameter missing from /proc/cmdline after editing /etc/default/grub and regenerating, on RHEL | BLS 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.
| Location | After grub2-mkconfig | After grubby --update-kernel=ALL |
|---|---|---|
/boot/loader/entries/*.conf (read at boot) | unchanged | modified |
/boot/grub2/grub.cfg (kernelopts fallback, ignored) | modified | unchanged |
/etc/default/grub | modified by hand | modified too |
/etc/kernel/cmdline (template for future kernels) | unchanged | modified |
/proc/cmdline at the next reboot | unchanged | modified |
The tool that acts on already installed kernels is therefore
grubby:
sudo grubby --update-kernel=ALL --args="audit=1"sudo grubby --info=ALL | grep ^argsEditing /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:
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_pbkdf2in/etc/grub.d/40_custom, thenupdate-grub - RHEL:
grub2-setpassworddoes everything, and only protects editing by default --unrestrictedis the key: the system boots without a password, only editing is protected- Never touch
grub.cfgdirectly (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-mkconfigdoes 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
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
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: NAT and port forwarding on a Linux gateway with nftables
A new full quiz with random questions
Work again only on the questions you missed