How to Upgrade and Rebuild the Linux Kernel: Complete Step-by-Step Guide

How to upgrade and rebuild the linux kernel

The kernel is the one piece of a Linux system I never touch casually. Everything else — a broken package, a bad config file, a corrupted service — I can fix from a rescue shell in ten minutes. A botched kernel upgrade can leave a server that won’t boot at all. And yet, upgrading and occasionally rebuilding the kernel is one of the most useful skills in a sysadmin’s toolkit: it’s how I’ve picked up new hardware support, backported a security fix ahead of a vendor’s schedule, stripped out drivers I’ll never use to shrink boot time, and enabled kernel features that a stock distro kernel ships disabled.

This guide covers both sides of that coin: the routine, low-risk path (upgrading via your distribution’s package manager) and the deep-end path (fetching kernel source, configuring it, and compiling your own).

Why You’d Upgrade or Rebuild a Kernel at All

Three distinct motivations come up in practice:

  1. Security and bug fixes. CVEs land in the kernel constantly — privilege escalation bugs, use-after-free bugs in drivers, netfilter issues. Distro vendors backport fixes into their “stable” kernel branch, and a routine apt upgrade or dnf update picks those up.
  2. Hardware support. New NICs, new NVMe controllers, new GPUs — sometimes the driver only exists in a kernel version newer than what your distro ships, especially on LTS distros like Ubuntu 24.04 LTS or RHEL 9, which intentionally freeze kernel versions for stability.
  3. Custom builds. You need a kernel feature disabled by default, you’re building an embedded or minimal image, you’re doing kernel development, or you want to apply a patch that isn’t in the distro’s tree.

For the first two reasons, package-manager upgrades cover the vast majority of situations. Reason three is where a full source rebuild becomes worthwhile.

Checking Your Current Kernel

Before touching anything, know exactly what you’re running:

uname -r
# 6.8.0-31-generic

uname -a
# Linux myserver 6.8.0-31-generic #31-Ubuntu SMP PREEMPT_DYNAMIC ... x86_64 GNU/Linux

cat /proc/version

On RHEL-family systems, rpm -q kernel lists installed kernel packages, since RHEL keeps several kernels installed side by side:

rpm -q kernel
# kernel-5.14.0-427.13.1.el9_4.x86_64

Method 1: Upgrading the Kernel via Package Manager (Recommended for Most Cases)

This is the path I use unless I have a specific reason not to. It’s tested by the distro, signed, and integrates cleanly with your bootloader and initramfs.

Debian / Ubuntu

sudo apt update
sudo apt list --upgradable | grep linux-image
sudo apt install linux-generic linux-headers-generic

Ubuntu also supports mainline kernel builds for bleeding-edge (non-LTS) kernels through a dedicated helper tool:

sudo add-apt-repository ppa:cappelikan/ppa
sudo apt update
sudo apt install mainline
mainline           # GUI/CLI tool to list and install upstream kernels

I’d caution against installing raw mainline .deb kernels from kernel.ubuntu.com on a production box — they’re unsigned, unsupported, and not integrated with Ubuntu’s Secure Boot chain. Fine for a test VM, risky for anything you can’t afford to lose.

RHEL / CentOS Stream / Fedora / Rocky / AlmaLinux

sudo dnf check-update kernel
sudo dnf update kernel

RHEL-family distros keep multiple kernels installed by default (usually 2-3), controlled by installonly_limit in /etc/dnf/dnf.conf. This is deliberate — if a new kernel fails to boot, you fall back to the previous one from the GRUB menu without any recovery media.

For Fedora specifically, which ships new kernel versions far more aggressively than RHEL:

sudo dnf upgrade --refresh

After the Package-Manager Upgrade

sudo reboot
uname -r   # confirm the new version is running

If it’s not the version you expect, check the default GRUB entry:

sudo grubby --default-kernel        # RHEL family
grep GRUB_DEFAULT /etc/default/grub  # Debian family

Method 2: Building the Kernel from Source

This is the deep-end approach. I’d only recommend it if you actually need something a stock kernel can’t give you, and I’d always do the first attempt on a VM or spare machine, never a production server you can’t get physical/console access to.

Step 1: Install Build Dependencies

Debian/Ubuntu:

sudo apt update
sudo apt install build-essential libncurses-dev bison flex libssl-dev \
    libelf-dev bc dwarves git fakeroot

RHEL/Fedora:

sudo dnf groupinstall "Development Tools"
sudo dnf install ncurses-devel bison flex openssl-devel elfutils-libelf-devel \
    bc dwarves rpm-build

libssl-dev/openssl-devel matters more than people expect — modern kernels sign modules during the build, and that step fails silently-ish without OpenSSL headers.

Step 2: Get the Kernel Source

Two common routes: pull the distro’s source package (safer, matches what you’re already running plus distro patches), or pull vanilla upstream from kernel.org (for the newest features or if you’re doing kernel development).

Vanilla source:

cd /usr/src
sudo git clone --depth 1 --branch v6.9 https://github.com/torvalds/linux.git
cd linux

Or download a release tarball directly:

wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.9.tar.xz
tar xf linux-6.9.tar.xz
cd linux-6.9

Step 3: Configure the Kernel

Don’t start from a blank config. Start from your running kernel’s config so you inherit a sane baseline:

zcat /proc/config.gz > .config       # if your kernel exposes /proc/config.gz
# or, if not available:
cp /boot/config-$(uname -r) .config

Then bring it up to date for the new source tree and choose defaults for anything new:

make olddefconfig

To actually customize options — enabling a driver, disabling something you don’t need, turning on a debug feature — use the interactive menu:

make menuconfig

This opens an ncurses UI organized into categories (Processor type, Device Drivers, Networking support, and so on). Search with / inside the menu to jump straight to an option by name — much faster than digging through submenus by hand.

Other config front-ends exist:

make xconfig     # Qt-based GUI, needs X11 and qt5 dev packages
make nconfig     # newer ncurses UI, arguably nicer than menuconfig
make defconfig   # generate a distro-default config from scratch (loses your customizations)

Step 4: Build

make -j$(nproc)

-j$(nproc) parallelizes the build across all CPU cores — on a modern multi-core machine this is the difference between a 45-minute build and a 6-hour one. On memory-constrained VMs, watch out: too many parallel jobs on too little RAM causes the compiler to OOM. A rough rule I use: don’t exceed 1.5 jobs per GB of RAM for a full kernel build.

Build the modules:

make modules -j$(nproc)

Step 5: Install

sudo make modules_install
sudo make install

make install on most distros will:

  • Copy the new kernel image into /boot
  • Generate a new initramfs (via dracut on RHEL-family, update-initramfs on Debian-family)
  • Update the GRUB configuration automatically

If it doesn’t call these hooks automatically (common when building raw upstream source rather than a distro’s kernel package), do it manually:

Debian/Ubuntu:

sudo update-initramfs -c -k 6.9.0
sudo update-grub

RHEL/Fedora:

sudo dracut --force /boot/initramfs-6.9.0.img 6.9.0
sudo grub2-mkconfig -o /boot/grub2/grub.cfg

Step 6: Building a Distro Package Instead of Raw Install (Recommended for RHEL)

Rather than a raw make install, I generally prefer building an actual .rpm or .deb so the kernel is tracked by the package manager, uninstalls cleanly, and coexists properly with other installed kernels.

RHEL/Fedora:

make rpm-pkg
sudo rpm -ivh ~/rpmbuild/RPMS/x86_64/kernel-6.9.0*.rpm

Debian/Ubuntu:

make bindeb-pkg -j$(nproc)
sudo dpkg -i linux-image-6.9.0*.deb linux-headers-6.9.0*.deb

Step 7: Reboot and Verify

sudo reboot
uname -r
dmesg | less     # check for driver/module errors on the new kernel

Kernel Internals: What Actually Happens During a Build

Understanding the pipeline helps when a build fails halfway through:

  1. Configuration produces .config, which drives conditional compilation via preprocessor macros (CONFIG_* symbols) throughout the source tree.
  2. Kbuild, the kernel’s own build system layered on GNU Make, walks the tree and compiles each subsystem — process scheduler, memory management, VFS, networking stack, and every driver marked =y or =m in the config.
  3. Anything marked =m becomes a loadable kernel module (a .ko file), compiled separately and installed under /lib/modules/$(uname -r)/.
  4. The final image is linked and, on most modern distros, compressed (bzImage on x86_64) and optionally signed for Secure Boot.
  5. depmod builds the module dependency map so modprobe can resolve dependencies at load time.
  6. The bootloader config is regenerated so the new kernel entry appears in the GRUB menu, normally as the new default while the old kernel remains as a fallback entry.

Managing Multiple Installed Kernels

RHEL-family systems handle this natively:

sudo grubby --default-kernel                 # show current default
sudo grubby --set-default /boot/vmlinuz-5.14.0-427.13.1.el9_4.x86_64
sudo dnf remove kernel-5.14.0-284.el9.x86_64  # remove an old one explicitly

On Debian/Ubuntu, old kernels accumulate under /boot and can be cleaned up with:

sudo apt autoremove --purge
dpkg -l | grep linux-image      # see what's installed

I always keep at least one known-good older kernel around until I’ve confirmed the new one is stable under real load for at least a few days.

Troubleshooting

System won’t boot after upgrade — at the GRUB menu, select “Advanced options” and boot the previous kernel. Then investigate before trying again.

Missing module after custom build — usually means make modules_install wasn’t run, or the initramfs wasn’t regenerated to include a module needed at boot (like a storage controller driver). Check with:

lsinitrd /boot/initramfs-$(uname -r).img | grep module_name   # RHEL/dracut
lsinitramfs /boot/initrd.img-$(uname -r) | grep module_name   # Debian

Build fails on missing OpenSSL/cert error — modern kernels attempt to sign modules by default. Either install the OpenSSL dev headers (see Step 1) or disable module signing in menuconfig under Cryptographic API -> Certificates for signature checking.

Build fails with “No rule to make target” on menuconfig — usually a missing libncurses-dev/ncurses-devel, since the config UI is ncurses-based.

dmesg full of “unknown symbol” for a module — the module was built against a different kernel ABI than what’s currently running; rebuild the module against the exact running kernel’s headers.

Performance and Security Considerations

A custom-built kernel gives you levers a stock kernel doesn’t:

  • Stripping unused drivers reduces attack surface and shrinks the compiled image, which matters for embedded/container-host use cases.
  • You can disable debug options (CONFIG_DEBUG_*) that carry a measurable performance cost on production hardware.
  • You can enable hardening options — CONFIG_STACKPROTECTOR_STRONG, CONFIG_FORTIFY_SOURCE, KASLR — some of which distro kernels already ship enabled, but not all.
  • Custom builds mean you’re now responsible for tracking upstream CVEs yourself instead of relying on your distro’s security team. That’s a real ongoing cost, not a one-time decision.

Kernel Versioning and Release Cadence

Understanding how upstream kernel releases and distro release cycles relate helps explain why the “right” upgrade approach differs so much between distros. Upstream kernel.org follows roughly a 9-10 week release cycle for mainline versions, with a subset designated “longterm” (LTS) and maintained with backported fixes for several years. Distros pick one of these versions and then diverge:

  • Ubuntu LTS (like 24.04) ships with a specific kernel at release, then periodically rebases to a newer HWE (Hardware Enablement) kernel partway through its lifecycle for newer hardware support, while still backporting security fixes to the original kernel branch throughout.
  • RHEL takes a single upstream kernel version at major release time (e.g., RHEL 9 started on a 5.14 base) and then backports fixes and features into that exact version for the entire product lifecycle — meaning uname -r on RHEL can look “old” by upstream numbering while actually containing years of backported fixes invisible from the version string alone.
  • Fedora tracks upstream far more aggressively, often within one or two mainline releases of the very latest kernel.

This is worth internalizing because comparing “kernel versions” across distros by number alone is misleading — a RHEL 5.14-based kernel from 2026 may contain security fixes that a naive comparison against a vanilla 5.14 release from years earlier would miss entirely. Check a distro’s actual changelog rather than assuming version number equals patch level.

rpm -q --changelog kernel | head -40

Understanding Kernel Module Signing and Secure Boot

If Secure Boot is enabled (common on modern hardware by default), the kernel and any modules it loads must be signed by a key the firmware trusts, or the system will refuse to boot the kernel or refuse to load the unsigned module.

Check Secure Boot status:

mokutil --sb-state

Distro-provided kernels are signed against a key chain that’s already enrolled in most firmware (via Microsoft’s UEFI signing service, for distros that go through that process, like Ubuntu and Fedora). A custom-built kernel is not signed against any key your firmware already trusts, which means either:

  1. Disable Secure Boot in firmware settings (simplest, but weakens a real security boundary), or
  2. Generate your own signing key, sign the kernel and any out-of-tree modules with it, and enroll that key into the firmware’s Machine Owner Key (MOK) database.
# Generate a signing key
openssl req -new -x509 -newkey rsa:2048 -keyout MOK.priv -outform DER -out MOK.der -nodes -days 36500 -subj "/CN=My Custom Kernel Signing Key/"

# Sign the kernel image
sudo /usr/src/linux-6.9/scripts/sign-file sha256 MOK.priv MOK.der /boot/vmlinuz-6.9.0

# Enroll the key with the firmware (prompts for a password used during the next boot's MOK enrollment)
sudo mokutil --import MOK.der
sudo reboot

On reboot, the firmware’s MOK management screen appears automatically, where you confirm enrollment using the password you set. Skipping this step on a Secure Boot system is the single most common reason a freshly built custom kernel simply refuses to boot with no obvious error message beyond a generic “not trusted” screen.

Kernel Command Line Parameters

Beyond the kernel image itself, boot-time parameters (passed via GRUB) control significant runtime behavior without requiring a rebuild:

cat /proc/cmdline

Common parameters worth knowing:

quiet splash                  # suppress boot messages, show splash screen
nomodeset                     # disable kernel mode setting (useful for graphics driver troubleshooting)
mem=4G                        # artificially cap visible memory (testing/debugging)
nosmp                         # disable multi-core support (extreme troubleshooting only)
systemd.unified_cgroup_hierarchy=1   # force cgroup v2

Edit persistently via GRUB config:

sudo nano /etc/default/grub
# Modify GRUB_CMDLINE_LINUX_DEFAULT="quiet splash nomodeset"
sudo update-grub          # Debian/Ubuntu
sudo grub2-mkconfig -o /boot/grub2/grub.cfg   # RHEL/Fedora

Real-World Example: Enabling a Specific Feature via Custom Build

A concrete scenario where a custom build genuinely earns its complexity: enabling CONFIG_USER_NS combined with specific unprivileged namespace restrictions for a container-hosting use case where the distro’s default kernel ships a restrictive default that blocks what you need.

# Check current setting
grep CONFIG_USER_NS /boot/config-$(uname -r)

# In menuconfig, this lives under:
# General setup -> Namespaces support -> UTS namespace / User namespace / ...
make menuconfig
# Navigate: General setup ---> [*] Namespaces support ---> [*] User namespace

This is a genuinely narrow use case — most people running containers do so on a stock distro kernel without any custom build at all, since mainstream distro kernels already enable the namespace features container runtimes need. Custom builds for this reason usually only make sense in specialized embedded, security-research, or kernel-development contexts.

Comparing Kernel Upgrade Approaches Across Distros

AspectUbuntu/DebianRHEL/Fedora
Default upgrade commandapt install linux-genericdnf update kernel
Multiple kernels keptYes, via apt autoremove cleanup policyYes, via installonly_limit
Rollback mechanismGRUB “Advanced options” menugrubby --set-default or GRUB menu
Package format.deb.rpm
Custom build package toolmake bindeb-pkgmake rpm-pkg
Secure Boot signingAutomatic for official kernelsAutomatic for official kernels
Mainline/bleeding-edge optionmainline PPA toolFedora itself tracks upstream closely; RHEL discourages this entirely

Summary

Package-manager kernel upgrades are the right default for nearly everyone — they’re tested, signed, and safe to roll back. Building from source is a specialized tool for specific situations: custom hardware support, kernel development, stripped-down images, or features a distro kernel disables. Either way, the fundamentals are the same: know your current version, keep a fallback kernel available, regenerate your initramfs and bootloader config, and verify with dmesg after reboot before you call it done.

References

Total
14
Shares

Leave a Reply

Previous Post
GNU C++ Compiler in Linux and explore gcc options

GNU C++ Compiler in Linux: Complete Guide to GCC Options and Usage

Next Post
Complex Adaptive Systems: Understanding the Intricacies of Emergent Behavior

Complex Adaptive Systems: Understanding the Intricacies of Emergent Behavior

Related Posts