#!/bin/bash # # Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one # or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at # https://securityonion.net/license; you may not use this file except in compliance with the # Elastic License 2.0. # # so-kernel-upgrade — install the UEK8 (6.x) kernel and make it the boot default. # # Security Onion is moving off the EL9 stock kernel (RHCK, 5.14) and UEK7 (5.15) onto UEK8 # (6.x). Three things have to happen, and the tool has to drive each one: # # 1. Populate. The manager mirrors the UEK8 packages into /nsm/kernelrepo via so-repo-sync, # and serves them to the grid over https:///kernelrepo. Until that sync runs the # repo is valid but EMPTY -- dnf resolves it happily and installs nothing, with no error. # 2. Install. A node on RHCK has no kernel-uek* package at all, so there is nothing for # 'dnf update' to upgrade. A node on UEK7 does have kernel-uek installed, so # 'dnf install kernel-uek' reports "Nothing to do" and exits 0 without installing 6.x. # Both cases need an explicit install of the UEK8 NEVRA. # 3. Boot it. Whether a newly installed UEK8 kernel becomes the boot default depends on the # RUNNING kernel's flavor. kernel-install/grubby (with UPDATEDEFAULT=yes) only auto-promote # within the running kernel's flavor lineage: # - From UEK7 (5.x, kernel-uek) the install stays in the kernel-uek lineage and IS # auto-promoted, so no grubby change is needed -- just make sure the repo is populated # and install UEK8. # - From the stock EL9 kernel (RHCK, 5.14, no UEK) it is a flavor CROSS that is NOT # auto-promoted, so the box keeps booting RHCK until grubby is told otherwise. # This tool inspects the running kernel and only runs 'grubby --set-default' for RHCK. # # Every one of those failure modes is silent by default. This tool handles each case and fails # loudly when it cannot, rather than reporting success while changing nothing. # # Manager vs minion: only the manager owns /nsm/kernelrepo, so only the manager can populate # it. If the repo is empty here, a manager runs so-repo-sync itself; a minion has no way to # fix it and exits non-zero telling the admin to sync the manager first. # # Idempotent: an already-installed, already-default UEK8 kernel is left alone. It only sets # the boot default; it does NOT reboot -- the admin reboots the node on their own schedule. . /usr/sbin/so-common # Client-side repo id (what dnf enables on this node, from repo/client/oracle.sls) vs the # reposync-side section in repodownload.conf that the manager mirrors from (mirrors the # securityonion/securityonionsync split for the main repo). KERNEL_REPO="securityonionkernel" KERNEL_REPO_SYNC="securityonionkernelsync" KERNEL_PKG="kernel-uek" KERNEL_REPO_DIR="/nsm/kernelrepo" REPOSYNC_CONF="/opt/so/conf/reposync/repodownload.conf" GLOBAL_PILLAR="/opt/so/saltstack/local/pillar/global/soc_global.sls" log() { echo "[so-kernel-upgrade] $*"; } die() { echo "[so-kernel-upgrade] ERROR: $*" >&2; exit 1; } command -v grubby >/dev/null 2>&1 || die "grubby not found" command -v dnf >/dev/null 2>&1 || die "dnf not found" ARCH="$(rpm -E '%{_arch}')" is_airgap() { [ -f "$GLOBAL_PILLAR" ] && grep -q 'airgap: *[Tt]rue' "$GLOBAL_PILLAR" } # Newest installed UEK8 (6.x) kernel known to the bootloader. UEK8 vmlinuz paths look like # /boot/vmlinuz-6.12.0-204.92.4.2.el9uek.x86_64; UEK7 (5.15) and RHCK (5.14) won't match. find_uek8() { grubby --info=ALL 2>/dev/null \ | sed -n 's/^kernel="\(.*\)"$/\1/p' \ | grep -E '/vmlinuz-6\.[0-9]+.*uek' \ | sort -V | tail -1 } # Classify the RUNNING kernel (uname -r) -- this, not what's installed, is what decides whether # a UEK8 install auto-promotes to the boot default: # uek8 6.x UEK already on the target line; nothing to do # uek7 5.x UEK a UEK8 install stays in the kernel-uek lineage and auto-promotes (no grubby) # rhck 5.14 EL9 crossing into the UEK flavor does NOT auto-promote (needs grubby --set-default) running_flavor() { case "$(uname -r)" in 6.*uek*) echo uek8 ;; *uek*) echo uek7 ;; *) echo rhck ;; esac } # Newest UEK8 kernel-uek NEVRA offered by the kernel repo, empty if the repo has none. # Restricted to the kernel repo so a UEK7 kernel-uek in the main repo can't be picked up, # and filtered to 6.x so we never "succeed" by reinstalling the 5.15 we already have. uek8_available() { dnf -q repoquery --disablerepo='*' --enablerepo="$KERNEL_REPO" \ --arch="$ARCH" --latest-limit=1 \ --qf '%{name}-%{evr}.%{arch}\n' "$KERNEL_PKG" 2>/dev/null \ | grep -E "^${KERNEL_PKG}-6\." | tail -1 } kernelrepo_rpm_count() { find "$KERNEL_REPO_DIR" -maxdepth 1 -name '*.rpm' 2>/dev/null | wc -l } # The kernel repo starts life as valid-but-empty (kernelrepo_init_empty in # salt/manager/init.sls) and is filled by so-repo-sync. During a soup, so-repo-sync runs # BEFORE the highstate deploys the [securityonionkernelsync] section into repodownload.conf, so # the first kernel-aware soup leaves the repo empty until the next nightly sync. sync_kernel_repo() { if is_airgap; then log "airgap install: $KERNEL_REPO_DIR is populated from the airgap ISO, not by so-repo-sync." return 1 fi if ! grep -q "^\[${KERNEL_REPO_SYNC}\]" "$REPOSYNC_CONF" 2>/dev/null; then log "$REPOSYNC_CONF has no [${KERNEL_REPO_SYNC}] section -- run a highstate to deploy it." return 1 fi log "populating $KERNEL_REPO_DIR with so-repo-sync (mirrors upstream; can take several minutes)" su socore -c '/usr/sbin/so-repo-sync' || { log "so-repo-sync failed"; return 1; } dnf -q clean expire-cache >/dev/null 2>&1 return 0 } # Make the kernel repo actually able to serve a UEK8 package, or fail trying. ensure_kernel_repo() { # The repo is assigned by the repo.client highstate, and only once NICs are pinned by MAC # (/opt/so/state/nic_names_pinned) so the kernel swap can't renumber interfaces SO binds # by name. skip_if_unavailable=1 means a broken repo is silently ignored, so check first. if ! dnf -q repolist --enabled 2>/dev/null | awk '{print $1}' | grep -qx "$KERNEL_REPO"; then log "repo '$KERNEL_REPO' is not enabled on this node." log "Run a highstate first; the repo is skipped until /opt/so/state/nic_names_pinned" log "exists (run so-nic-pin) and this node's salt matches the version this release ships." die "kernel repo unavailable" fi [ -n "$(uek8_available)" ] && return 0 log "repo '$KERNEL_REPO' is enabled but offers no UEK8 $KERNEL_PKG package" if ! is_manager_node; then log "This is a minion; it consumes the kernel repo from the manager and cannot populate it." log "On the manager, run: su socore -c /usr/sbin/so-repo-sync" log "then re-run this script here." die "manager's kernel repo is empty" fi log "this is a manager and $KERNEL_REPO_DIR holds $(kernelrepo_rpm_count) rpm(s)" sync_kernel_repo || die "could not populate $KERNEL_REPO_DIR" [ -n "$(uek8_available)" ] \ || die "so-repo-sync completed but $KERNEL_REPO still offers no UEK8 $KERNEL_PKG" } reboot_notice() { [ "$(uname -r)" = "$(basename "$1" | sed 's/^vmlinuz-//')" ] \ || log "REBOOT REQUIRED to start using the UEK8 kernel (currently running $(uname -r))." } # Keep future kernel updates on the UEK line rather than falling back to RHCK. Oracle ships # /etc/sysconfig/kernel; only rewrite it when it's actually pointing somewhere else. set_default_kernel_conf() { if [ -f /etc/sysconfig/kernel ] && ! grep -q '^DEFAULTKERNEL=kernel-uek-core$' /etc/sysconfig/kernel; then log "setting DEFAULTKERNEL=kernel-uek-core in /etc/sysconfig/kernel" sed -i 's/^DEFAULTKERNEL=.*/DEFAULTKERNEL=kernel-uek-core/' /etc/sysconfig/kernel fi } # Make sure a UEK8 kernel is installed, leaving its boot entry in INSTALLED_UEK8. If one is # already present we leave the repo alone -- it may be disabled or empty and we don't need it # just to flip the boot default. Otherwise install the explicit NEVRA, not the bare package # name: on a UEK7 node 'dnf install kernel-uek' sees 5.15 already present, prints "Nothing to # do" and exits 0 without installing 6.x. ensure_uek8_installed() { INSTALLED_UEK8="$(find_uek8)" if [ -n "$INSTALLED_UEK8" ]; then log "UEK8 kernel already installed: $INSTALLED_UEK8" return 0 fi ensure_kernel_repo local nevra; nevra="$(uek8_available)" log "installing $nevra from $KERNEL_REPO" dnf -y install "$nevra" || die "failed to install $nevra" INSTALLED_UEK8="$(find_uek8)" [ -n "$INSTALLED_UEK8" ] || die "$nevra installed but no 6.x UEK boot entry appeared -- check 'grubby --info=ALL'" log "installed UEK8 kernel: $INSTALLED_UEK8" } case "$(running_flavor)" in uek8) # Already on the 6.x UEK line. A plain 'dnf update' keeps this node current within the # lineage and auto-promotes newer builds, so there is nothing for this tool to do. log "already running a UEK8 kernel ($(uname -r)); nothing to do." exit 0 ;; uek7) # On a 5.x UEK kernel. Installing UEK8 stays inside the kernel-uek lineage, so dnf/grubby # (UPDATEDEFAULT=yes) auto-promote it and we do NOT touch grubby. A node still on UEK7 # usually means the kernel repo was empty when it last updated, so populate it and install. log "running UEK7 kernel ($(uname -r)); the kernel repo was likely not yet populated when" log "this node last updated. Populating it and installing UEK8 -- the update stays on the" log "kernel-uek line, so it becomes the boot default automatically (no grubby change needed)." set_default_kernel_conf ensure_uek8_installed now="$(grubby --default-kernel 2>/dev/null)" if [ "$now" = "$INSTALLED_UEK8" ]; then log "boot default auto-promoted to UEK8 kernel: $INSTALLED_UEK8" else log "WARNING: expected the UEK8 kernel to auto-promote but the default is still" log "'${now:-unknown}'. Run 'grubby --set-default=$INSTALLED_UEK8' to force it." fi reboot_notice "$INSTALLED_UEK8" ;; rhck) # On the stock EL9 kernel (5.14, no UEK installed). Crossing from RHCK into the UEK flavor # does NOT auto-promote -- kernel-install/grubby only auto-promote within the running # kernel's flavor lineage -- so after installing we must set the boot default explicitly. log "running stock EL9 (RHCK) kernel ($(uname -r)); installing UEK8 and setting it as the" log "boot default explicitly (a RHCK->UEK flavor change does not auto-promote)." set_default_kernel_conf ensure_uek8_installed target="$INSTALLED_UEK8" current="$(grubby --default-kernel 2>/dev/null)" if [ "$current" = "$target" ]; then log "UEK8 kernel is already the boot default: $target" reboot_notice "$target" exit 0 fi log "current default kernel: ${current:-unknown}" log "switching boot default to UEK8 kernel: $target" grubby --set-default="$target" || die "grubby --set-default failed for $target" # Verify the change actually took before claiming success. now="$(grubby --default-kernel 2>/dev/null)" [ "$now" = "$target" ] || die "default kernel is still '${now:-unknown}' after set-default" log "boot default is now $target" reboot_notice "$target" ;; esac