GRUB 2 is the bootloader that Ubuntu uses to load the Linux kernel and its initramfs. You rarely touch it on a server, but when you need to add a kernel parameter, boot an older kernel or repair a server that stops at a grub rescue> prompt, you need to know how its configuration is built. In this tutorial you will change GRUB settings the supported way on Ubuntu 24.04, pass kernel parameters, choose which kernel boots, protect the menu with a password and recover from a broken bootloader.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • Access to the server console, since the GRUB menu is not visible over SSH. On a CubePath VPS use the VNC console in the control panel.
  • For the recovery section, a way to boot a rescue environment or an Ubuntu live ISO.

Step 1 - Understanding where GRUB's configuration comes from

The file GRUB reads at boot is /boot/grub/grub.cfg, but you never edit it by hand. It is generated by update-grub from three sources:

LocationPurpose
/etc/default/grubMain settings: timeout, default entry, kernel parameters
/etc/default/grub.d/*.cfgExtra settings files, read after /etc/default/grub and overriding it
/etc/grub.d/Executable scripts that write the menu entries, run in numeric order

The scripts in /etc/grub.d/ include 10_linux, which creates an entry for every installed kernel, 30_os-prober, which detects other operating systems, and 40_custom, where you can add your own entries.

Show the active settings, without comments:

grep -hv '^\s*#' /etc/default/grub /etc/default/grub.d/*.cfg | grep -v '^\s*$'
GRUB_DEFAULT=0
GRUB_TIMEOUT_STYLE=hidden
GRUB_TIMEOUT=0
GRUB_DISTRIBUTOR=`( . /etc/os-release; echo ${NAME:-Ubuntu} ) 2>/dev/null || echo Ubuntu`
GRUB_CMDLINE_LINUX_DEFAULT=""
GRUB_CMDLINE_LINUX=""
GRUB_CMDLINE_LINUX_DEFAULT="console=tty1 console=ttyS0"
GRUB_TERMINAL=console
GRUB_TIMEOUT=0

The last three lines come from /etc/default/grub.d/50-cloudimg-settings.cfg, which Ubuntu cloud images install. Because files in grub.d are read last, a change in /etc/default/grub to a key that also appears there has no effect. Check that directory first whenever a setting seems to be ignored.

Step 2 - Showing the menu and changing the timeout

On cloud images the menu is hidden and the timeout is 0, which makes boots fast but leaves no time to pick another kernel from the console. Give yourself a few seconds by creating a settings file that is read after the cloud image defaults:

sudo nano /etc/default/grub.d/99-local.cfg

Add the following lines:

GRUB_TIMEOUT_STYLE=menu
GRUB_TIMEOUT=5

GRUB_TIMEOUT_STYLE=menu always shows the menu, and GRUB_TIMEOUT=5 waits five seconds before booting the default entry. Using your own file instead of editing the existing ones keeps your changes separate and survives package updates.

Regenerate grub.cfg:

sudo update-grub
Sourcing file `/etc/default/grub'
Sourcing file `/etc/default/grub.d/50-cloudimg-settings.cfg'
Sourcing file `/etc/default/grub.d/99-local.cfg'
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.8.0-83-generic
Found initrd image: /boot/initrd.img-6.8.0-83-generic
Found linux image: /boot/vmlinuz-6.8.0-79-generic
Found initrd image: /boot/initrd.img-6.8.0-79-generic
done

The output confirms which settings files were read and which kernels were found. Confirm the new timeout made it into the generated file:

grep -m1 'set timeout=' /boot/grub/grub.cfg
  set timeout=5

Open the console and reboot. The menu now stays on screen for five seconds.

Step 3 - Adding kernel parameters

Kernel parameters are set in two variables:

  • GRUB_CMDLINE_LINUX is added to every Linux entry, including recovery mode.
  • GRUB_CMDLINE_LINUX_DEFAULT is added only to normal entries, not to recovery mode.

Use GRUB_CMDLINE_LINUX for parameters the system needs to work, such as a serial console or IOMMU settings. As an example, the following enables the IOMMU in passthrough mode, which is a typical requirement on a physical host for PCI passthrough to virtual machines. Add it to your settings file:

sudo nano /etc/default/grub.d/99-local.cfg
GRUB_TIMEOUT_STYLE=menu
GRUB_TIMEOUT=5
GRUB_CMDLINE_LINUX="$GRUB_CMDLINE_LINUX intel_iommu=on iommu=pt"

Referencing $GRUB_CMDLINE_LINUX appends to the value set in earlier files instead of replacing it. Regenerate the configuration and reboot:

sudo update-grub
sudo reboot

After the reboot, check the command line the kernel actually received:

cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-6.8.0-83-generic root=LABEL=cloudimg-rootfs ro intel_iommu=on iommu=pt console=tty1 console=ttyS0

If your parameter is missing from /proc/cmdline, it was not in the generated grub.cfg: rerun sudo update-grub and read its output.

To test a parameter for a single boot without saving anything, press e on the entry in the GRUB menu, append the parameter to the end of the line that starts with linux, and press Ctrl+X or F10. The next normal reboot uses the saved configuration again, which makes this the safest way to try parameters that might prevent booting.

Step 4 - Choosing which kernel boots

By default GRUB boots the first entry, which is the newest kernel. List the menu entries and submenus that update-grub generated:

awk -F"'" '/^[ \t]*(menuentry|submenu) / {print $2}' /boot/grub/grub.cfg
Ubuntu
Advanced options for Ubuntu
Ubuntu, with Linux 6.8.0-83-generic
Ubuntu, with Linux 6.8.0-83-generic (recovery mode)
Ubuntu, with Linux 6.8.0-79-generic
Ubuntu, with Linux 6.8.0-79-generic (recovery mode)

Entries under Advanced options for Ubuntu are addressed as submenu title>entry title. To boot an older kernel only once, for example to test whether a problem started with the new kernel, first let GRUB remember a saved default. Add this line to /etc/default/grub.d/99-local.cfg:

GRUB_DEFAULT=saved

Regenerate the configuration so the setting takes effect:

sudo update-grub

Then select the older kernel for the next boot only:

sudo grub-reboot 'Advanced options for Ubuntu>Ubuntu, with Linux 6.8.0-79-generic'
sudo reboot

After the reboot, confirm the running kernel:

uname -r
6.8.0-79-generic

The following boot goes back to the saved default. To make the older kernel the permanent default instead, use grub-set-default with the same title:

sudo grub-set-default 'Advanced options for Ubuntu>Ubuntu, with Linux 6.8.0-79-generic'

Check what is stored with sudo grub-editenv list. To return to the newest kernel, run sudo grub-set-default 0.

Step 5 - Protecting GRUB with a password

Anyone with console access can edit a GRUB entry and add init=/bin/bash to get a root shell. A GRUB password prevents editing entries and using the GRUB command line, while still letting the server boot unattended.

Generate a PBKDF2 hash of the password you want to use:

grub-mkpasswd-pbkdf2
Enter password:
Reenter password:
PBKDF2 hash of your password is grub.pbkdf2.sha512.10000.8C1E...4F2A.9D07...B36E

Copy the full string that starts with grub.pbkdf2. Open the custom entries script:

sudo nano /etc/grub.d/40_custom

Add these lines at the end of the file, below the existing header, replacing your_hash with the string you copied and admin with the user name you want to use at the GRUB prompt:

set superusers="admin"
password_pbkdf2 admin your_hash

Once superusers is set, GRUB asks for the password to boot any entry that is not marked --unrestricted, which would stop the server from rebooting on its own. Mark the Ubuntu entries as unrestricted by editing the CLASS line in the script that generates them:

sudo nano /etc/grub.d/10_linux

Find this line:

CLASS="--class gnu-linux --class gnu --class os"

Change it to:

CLASS="--class gnu-linux --class gnu --class os --unrestricted"

Regenerate the configuration:

sudo update-grub

Verify that the password and the unrestricted entries are in grub.cfg:

sudo grep -E '^(set superusers|password_pbkdf2)' /boot/grub/grub.cfg
set superusers="admin"
password_pbkdf2 admin grub.pbkdf2.sha512.10000.8C1E...4F2A.9D07...B36E

Then confirm that the Ubuntu entries carry the --unrestricted flag:

sudo grep -c -- '--unrestricted' /boot/grub/grub.cfg

The number should match the Ubuntu entries you listed in Step 4, not counting the "Advanced options for Ubuntu" submenu line. If it is 0, update-grub did not pick up your change to 10_linux.

Reboot with the console open. The server boots normally, but pressing e or c in the menu now asks for the user name and password.

Step 6 - Recovering from a GRUB prompt

If the configuration or the files in /boot/grub are missing or broken, GRUB stops at one of two prompts:

  • grub rescue>: GRUB could not load its modules. Only a few commands work.
  • grub> with "Minimal BASH-like line editing is supported": GRUB loaded, but did not find or could not read grub.cfg.

In both cases, first find the partition that contains /boot/grub. ls lists the disks and partitions, and ls (hd0,gpt1)/ lists the files in one of them:

ls
ls (hd0,gpt1)/
ls (hd0,gpt1)/boot/grub/

When you find the partition that has a grub directory with a grub.cfg inside, point GRUB at it. If /boot is a separate partition, the path is /grub instead of /boot/grub:

set root=(hd0,gpt1)
set prefix=(hd0,gpt1)/boot/grub
insmod normal
normal

normal loads the regular menu. If grub.cfg itself is missing, boot the kernel by hand from the grub> prompt instead. Ubuntu keeps /boot/vmlinuz and /boot/initrd.img as links to the newest kernel; replace /dev/vda1 with your root partition:

linux /boot/vmlinuz root=/dev/vda1 ro
initrd /boot/initrd.img
boot

Once the system is running, repair the bootloader permanently with sudo update-grub and, if the prompt was grub rescue>, reinstall GRUB as described in the next step.

Step 7 - Reinstalling GRUB from a rescue system

When GRUB cannot start at all, boot a rescue environment or Ubuntu live ISO and reinstall it from a chroot of the installed system. Identify the partitions first:

lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT

Mount the root filesystem, replacing /dev/vda1 with your root partition:

sudo mount /dev/vda1 /mnt

Open /mnt/etc/fstab and check whether the system has a separate /boot or an EFI System Partition at /boot/efi. Mount those too, in that order, for example:

sudo mount /dev/vda16 /mnt/boot
sudo mount /dev/vda15 /mnt/boot/efi

Bind the virtual filesystems and enter the chroot:

for d in dev proc sys run; do sudo mount --rbind "/$d" "/mnt/$d"; done
sudo chroot /mnt

Inside the chroot, reinstall GRUB for the firmware type the server uses. For legacy BIOS, install it to the disk (not a partition):

grub-install /dev/vda

For UEFI, install it to the EFI System Partition:

grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ubuntu

On UEFI systems with Secure Boot enabled, reinstall the signed packages instead, which also runs grub-install with the signed binaries: apt install --reinstall grub-efi-amd64-signed shim-signed.

Both variants should end with:

Installation finished. No error reported.

Regenerate the configuration, then leave the chroot and unmount everything:

update-grub
exit
sudo umount -R /mnt

Remove the rescue media or switch the server back to booting from its disk, and reboot.

Troubleshooting

Package upgrades fail with "The GRUB boot loader was previously installed to a disk that is no longer present". The grub-pc package remembers the install disk by its ID, which changes when a disk is replaced or a VM is migrated. Choose the correct disk again with sudo dpkg-reconfigure grub-pc.

A setting in /etc/default/grub is ignored. A file in /etc/default/grub.d/ sets the same key and is read later. Put your change in a file such as 99-local.cfg in that directory, then run sudo update-grub.

error: no such device followed by a UUID. The grub.cfg refers to a filesystem UUID that no longer exists, usually after restoring or cloning a disk. Boot manually from the grub> prompt as shown in Step 6, check sudo blkid and /etc/fstab, then run sudo update-grub so the configuration uses the current UUIDs.

The server boots another OS or loader first on UEFI. Check the firmware boot order with sudo efibootmgr. Move the ubuntu entry first with sudo efibootmgr -o XXXX,YYYY, using the four-digit numbers from the BootOrder list.

Conclusion

You learned how update-grub builds grub.cfg from /etc/default/grub, the grub.d settings files and the /etc/grub.d scripts, changed the timeout and kernel parameters, selected a kernel with grub-reboot and grub-set-default, protected the menu with a password and recovered from both GRUB prompts. As next steps, test kernel parameters one boot at a time before saving them, keep at least two kernels installed, and read the guide on the Linux boot process to troubleshoot problems that happen after GRUB hands over to the kernel.