Compare commits

..
23 Commits
Author SHA1 Message Date
Doug BurksandGitHub 6f61e7c901 Merge pull request #16075 from Security-Onion-Solutions/dougburks-patch-1
Fix typos in CPU affinity descriptions
2026-07-16 15:15:38 -04:00
Doug BurksandGitHub cc2bfc26e2 Fix typos in CPU affinity descriptions 2026-07-16 15:13:51 -04:00
Jorge ReyesandGitHub 073e32520b Merge pull request #16074 from Security-Onion-Solutions/reyesj2-patch-5
include so-yaml.py in get_soup_script_hashes() so we ensure its at la…
2026-07-16 13:00:16 -05:00
reyesj2 5867b50720 include so-yaml.py in get_soup_script_hashes() so we ensure its at latest version before using it later on in soup 2026-07-16 12:28:23 -05:00
Mike ReevesandGitHub 3503d0c33d Merge pull request #16054 from Security-Onion-Solutions/kernel
Install UEK8 in so-kernel-upgrade when no UEK kernel is present
2026-07-15 16:29:51 -04:00
Mike ReevesandGitHub 02318f065c Merge pull request #16069 from Security-Onion-Solutions/mreeves/soup-resumable-upgrade
soup: make failed upgrades and hotfixes resumable
2026-07-15 15:13:57 -04:00
Mike Reeves 186bf86e99 soup: require green Elasticsearch cluster before upgrading
Change the pre-flight cluster-health gate to wait_for_status=green instead of
yellow, so soup only proceeds when the cluster is fully green.
2026-07-15 12:14:18 -04:00
Mike Reeves bd70dd53fb soup: add cluster-health and Fleet Server pre-flight checks
Before making any changes, verify the grid is in a good state:
 - check_cluster_health: waits for Elasticsearch to reach at least 'yellow'
   (blocks only on red/unreachable, since yellow is normal), modeled on the
   wait in so-elasticsearch-roles-load.
 - check_fleet_server: confirms the Fleet Server status API returns HTTP 200,
   modeled on the wait_for_so-elastic-fleet state in elasticfleet/enabled.sls.

Both run alongside the existing check_pillar_items (manager pillar render) and
verify_es_version_compatibility, before soup modifies anything, so a failure
exits cleanly with an actionable message and no partial changes. Valid on all
manager roles soup runs on (eval/standalone/manager/managerhype/managersearch/
import), which all run Elasticsearch and the Fleet Server.
2026-07-15 12:00:11 -04:00
Mike Reeves be7d8a2aa7 soup: make partial-upgrade state clear and avoid re-running completed upgrades
After a partial upgrade, /etc/soversion already reads the target version, so
soup's startup line "Found that Security Onion X is currently installed" made
it look finished even as soup resumed. When a resume marker is present and
differs from the installed version, print an explicit NOTE that the grid is only
partially upgraded and this run will resume and complete it.

Also clear any stale resume marker in the already-latest path so a successfully
completed upgrade is never mistaken for a partial one and re-run on a later
invocation (the marker is normally removed at the end of postupgrade_changes;
this is a belt-and-suspenders guard).
2026-07-15 11:35:04 -04:00
Mike Reeves 618712469e soup: clearly report incomplete upgrades on trap exit
When soup fails via the EXIT trap after it has begun modifying the system, print
a prominent UPGRADE INCOMPLETE banner instructing the user to run soup again to
resume and complete the update. Gated on a new SOUP_UPGRADE_STARTED flag set at
the start of the hotfix and upgrade branches, so pre-flight gate failures (ES
compatibility, disk, network) that abort before any changes are made do not show
it.
2026-07-15 09:42:59 -04:00
Mike Reeves 8b488f9226 soup: make failed upgrades and hotfixes resumable
A failed highstate mid-upgrade left /etc/soversion already advanced to the
target version (the highstate stamps it from the pillar via the soversionfile
state), so a re-run of soup saw INSTALLEDVERSION == NEWVERSION and reported
"already running the latest version", stranding the box with post-upgrade
steps never run.

Introduce /etc/sopostversion, a soup-owned marker (no salt state manages it)
that records post-upgrade walk progress. It is seeded from the pre-upgrade
version before the highstate, advanced after each post_to_* step, and removed
on successful completion. upgrade_check treats a leftover marker as "upgrade
not finished" and resumes the remaining post steps instead of bailing.

Also fix the hotfix path: /etc/sohotfix was written before the hotfix
highstate, so a failed hotfix highstate looked already-applied on re-run.
Since no salt state manages /etc/sohotfix, defer its write (update_version)
until after the highstate succeeds so it is an honest completion marker.
2026-07-14 17:30:58 -04:00
Jason ErtelandGitHub 1657480d31 Merge pull request #16064 from Security-Onion-Solutions/jertel/wip
prevent login redirect to any API url
2026-07-14 13:55:44 -04:00
Jason Ertel 63d4061500 prevent login redirect to any API url 2026-07-14 13:52:32 -04:00
Jorge ReyesandGitHub 8167ae3282 Merge pull request #16062 from Security-Onion-Solutions/reyesj2/es937
ES 9.3.7
2026-07-13 14:35:35 -05:00
Jorge ReyesandGitHub e5de499bcc Merge pull request #16059 from Security-Onion-Solutions/reyesj2-patch-4 2026-07-10 13:02:18 -05:00
reyesj2 7d17784e96 status messages for so-elastic-fleet-setup 2026-07-10 12:50:21 -05:00
Jason ErtelandGitHub f0bbbf37d8 Merge pull request #16058 from Security-Onion-Solutions/jertel/wip
toggle pg metrics
2026-07-10 10:48:07 -04:00
Jason Ertel 6fc0fd954c clarify output annotation 2026-07-10 10:35:37 -04:00
Jason Ertel 566f90a0c0 toggle pg metrics 2026-07-10 10:32:25 -04:00
Mike Reeves 52885e28c5 Name the reposync-side kernel repo securityonionkernelsync
The reposync section in repodownload.conf and the client repo assigned in
repo/client/oracle.sls both used the bare name securityonionkernel, colliding
across the two roles. Rename the reposync-side section (and its --repoid, the
so-repo-sync guard, and the so-kernel-upgrade presence check) to
securityonionkernelsync, mirroring the existing securityonion/securityonionsync
split for the main repo. The client repo stays securityonionkernel. Also give
the section its own name=Security Onion Kernel Repo repo.
2026-07-09 17:02:47 -04:00
Mike Reeves 9a71f64a35 Branch so-kernel-upgrade on the running kernel flavor
Only the RHCK->UEK flavor cross needs grubby --set-default; a UEK7->UEK8
update stays in the kernel-uek lineage and auto-promotes on its own. Detect
the running kernel and act accordingly:

- UEK8: already on target, no-op.
- UEK7: populate the repo and install UEK8, then verify it auto-promoted
  (warn with the manual grubby command if it did not) -- no grubby change.
- RHCK: install UEK8 and set the boot default explicitly, as before.

Also make an already-installed UEK8 skip the repo entirely so a disabled or
empty kernel repo can't block flipping the default, and correct the header
comment that claimed every transition needs grubby.
2026-07-09 15:10:13 -04:00
Mike Reeves 40c02b3149 Make so-kernel-upgrade populate the kernel repo and fail loudly
Three stages of the UEK8 path fail silently, and the script only handled
the last one:

1. Populate. so-repo-sync runs before the highstate deploys the
   [securityonionkernel] section into repodownload.conf, so the first
   kernel-aware soup skips the kernel sync. kernelrepo_init_empty then
   seeds valid-but-empty repodata, leaving an enabled repo with zero
   packages. dnf resolves it happily and installs nothing, no error.

2. Install. `dnf install kernel-uek` on a UEK7 node sees kernel-uek 5.15
   already installed, prints "Nothing to do" and exits 0 -- so the script
   sailed past the install and died later with a misleading grubby error.

3. Boot. Already handled: grubby only auto-promotes within the running
   kernel's flavor lineage, so 5.x -> 6.x UEK never promotes on its own.

Add ensure_kernel_repo(), which verifies the repo is enabled (necessary
because skip_if_unavailable=1 hides a broken repo) and that it can serve a
6.x kernel-uek. When it cannot, a manager runs so-repo-sync to populate
/nsm/kernelrepo and re-checks; a minion cannot fix it and exits non-zero
pointing the admin at the manager. Airgap managers bail, since their repo
comes from the ISO rather than a sync.

Install the explicit UEK8 NEVRA instead of the bare package name so the
"Nothing to do" exit-0 case cannot mask a no-op, and pin the repoquery to
securityonionkernel so a UEK7 kernel-uek in the main repo is never picked.

Still idempotent and still never reboots.
2026-07-09 14:21:08 -04:00
Mike Reeves 5fd5df54b4 Install UEK8 in so-kernel-upgrade when no UEK kernel is present
The script assumed the UEK8 kernel was already installed and only switched
the boot default to it. On a node running the EL9 stock kernel (RHCK 5.14)
there is no kernel-uek* package at all, so `dnf update` has nothing to
upgrade and UEK8 never lands -- the script just logged "nothing to do" and
exited 0.

When no 6.x UEK boot entry exists, install the kernel-uek metapackage (it
pulls kernel-uek-core plus the module subpackages, including
kernel-uek-modules-extra-netfilter) and then proceed with the grubby
switch. Fail loudly if securityonionkernel is not an enabled repo, since
that assignment is gated on the NIC-pin marker and the salt version match
and a silent no-op there is hard to diagnose.

Also point DEFAULTKERNEL at kernel-uek-core so later kernel updates stay on
the UEK line rather than falling back to RHCK.

Still idempotent and still never reboots.
2026-07-09 13:47:50 -04:00
9 changed files with 384 additions and 57 deletions
+223 -37
View File
@@ -5,53 +5,239 @@
# https://securityonion.net/license; you may not use this file except in compliance with the # https://securityonion.net/license; you may not use this file except in compliance with the
# Elastic License 2.0. # Elastic License 2.0.
# #
# so-kernel-upgrade — switch the boot default to the installed UEK8 (6.x) kernel. # so-kernel-upgrade — install the UEK8 (6.x) kernel and make it the boot default.
# #
# Security Onion is moving off the EL9 stock kernel / UEK7 (5.x) onto UEK8 (6.x). # Security Onion is moving off the EL9 stock kernel (RHCK, 5.14) and UEK7 (5.15) onto UEK8
# Installing the kernel-uek-core package adds a UEK8 boot entry but does NOT make it the # (6.x). Three things have to happen, and the tool has to drive each one:
# default: kernel-install/grubby only auto-promote a new kernel within the running
# kernel's flavor lineage, and we're crossing from a 5.x kernel to the new 6.x UEK flavor.
# So even with UPDATEDEFAULT=yes and DEFAULTKERNEL=kernel-uek-core the box keeps booting
# the old kernel. This tool finds the newest installed 6.x UEK kernel and makes it the
# GRUB default via grubby so the next boot comes up on UEK8.
# #
# Idempotent: if the UEK8 kernel is already the default it does nothing. It only sets the # 1. Populate. The manager mirrors the UEK8 packages into /nsm/kernelrepo via so-repo-sync,
# boot default; it does NOT reboot — the admin reboots the node on their own schedule. # and serves them to the grid over https://<manager>/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] $*"; } log() { echo "[so-kernel-upgrade] $*"; }
die() { echo "[so-kernel-upgrade] ERROR: $*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || { log "must run as root"; exit 1; } command -v grubby >/dev/null 2>&1 || die "grubby not found"
command -v grubby >/dev/null 2>&1 || { log "grubby not found"; exit 1; } 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 # Newest installed UEK8 (6.x) kernel known to the bootloader. UEK8 vmlinuz paths look like
# /boot/vmlinuz-6.12.0-203.76.7.5.el9uek.x86_64; the 5.x UEK7 and 5.14 RHCK won't match. # /boot/vmlinuz-6.12.0-204.92.4.2.el9uek.x86_64; UEK7 (5.15) and RHCK (5.14) won't match.
target="$(grubby --info=ALL 2>/dev/null \ find_uek8() {
| sed -n 's/^kernel="\(.*\)"$/\1/p' \ grubby --info=ALL 2>/dev/null \
| grep -E '/vmlinuz-6\.[0-9]+.*uek' \ | sed -n 's/^kernel="\(.*\)"$/\1/p' \
| sort -V | tail -1)" | grep -E '/vmlinuz-6\.[0-9]+.*uek' \
| sort -V | tail -1
}
if [ -z "$target" ]; then # Classify the RUNNING kernel (uname -r) -- this, not what's installed, is what decides whether
log "no installed 6.x UEK (UEK8) kernel found — confirm the kernel repo is assigned and" # a UEK8 install auto-promotes to the boot default:
log "'dnf update' has installed kernel-uek-core. Nothing to do." # 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 exit 0
fi ;;
current="$(grubby --default-kernel 2>/dev/null)" uek7)
if [ "$current" = "$target" ]; then # On a 5.x UEK kernel. Installing UEK8 stays inside the kernel-uek lineage, so dnf/grubby
log "UEK8 kernel is already the boot default: $target" # (UPDATEDEFAULT=yes) auto-promote it and we do NOT touch grubby. A node still on UEK7
exit 0 # usually means the kernel repo was empty when it last updated, so populate it and install.
fi 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
log "current default kernel: ${current:-unknown}" now="$(grubby --default-kernel 2>/dev/null)"
log "switching boot default to UEK8 kernel: $target" if [ "$now" = "$INSTALLED_UEK8" ]; then
grubby --set-default="$target" || { log "ERROR: grubby --set-default failed for $target"; exit 1; } 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"
;;
# Verify the change actually took before claiming success. rhck)
now="$(grubby --default-kernel 2>/dev/null)" # On the stock EL9 kernel (5.14, no UEK installed). Crossing from RHCK into the UEK flavor
if [ "$now" != "$target" ]; then # does NOT auto-promote -- kernel-install/grubby only auto-promote within the running
log "ERROR: default kernel is still '${now:-unknown}' after set-default" # kernel's flavor lineage -- so after installing we must set the boot default explicitly.
exit 1 log "running stock EL9 (RHCK) kernel ($(uname -r)); installing UEK8 and setting it as the"
fi log "boot default explicitly (a RHCK->UEK flavor change does not auto-promote)."
set_default_kernel_conf
ensure_uek8_installed
target="$INSTALLED_UEK8"
log "boot default is now $target" current="$(grubby --default-kernel 2>/dev/null)"
log "REBOOT REQUIRED to start using the UEK8 kernel (currently running $(uname -r))." 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
@@ -257,6 +257,7 @@ for state_attempt in {1..3}; do
fi fi
done done
printf "\nRunning so-elastic-agent-gen-installers\n"
# Generate installers & install Elastic Agent on the node # Generate installers & install Elastic Agent on the node
for agent_gen_attempt in {1..3}; do for agent_gen_attempt in {1..3}; do
if so-elastic-agent-gen-installers; then if so-elastic-agent-gen-installers; then
@@ -274,4 +275,6 @@ printf "\nApplying elasticfleet.install_agent_grid state\n"
if ! salt-call state.apply elasticfleet.install_agent_grid queue=True; then if ! salt-call state.apply elasticfleet.install_agent_grid queue=True; then
printf "\nFailure(s) in elasticfleet.install_agent_grid state... Exiting...\n" printf "\nFailure(s) in elasticfleet.install_agent_grid state... Exiting...\n"
exit 1 exit 1
fi fi
printf "\nElastic Fleet setup completed successfully\n"
+116 -7
View File
@@ -12,7 +12,17 @@
UPDATE_DIR=/tmp/sogh/securityonion UPDATE_DIR=/tmp/sogh/securityonion
DEFAULT_SALT_DIR=/opt/so/saltstack/default DEFAULT_SALT_DIR=/opt/so/saltstack/default
INSTALLEDVERSION=$(cat /etc/soversion) INSTALLEDVERSION=$(cat /etc/soversion)
POSTVERSION=$INSTALLEDVERSION # /etc/sopostversion is a soup-owned marker (no salt state manages it) tracking how
# far the post-upgrade walk has progressed. Its presence means a prior upgrade did
# not finish its post-upgrade steps; its contents are the resume point. It is read
# here before preupgrade_changes mutates INSTALLEDVERSION and before any highstate
# stamps /etc/soversion from the pillar.
POSTVERSION_FILE=/etc/sopostversion
if [ -f "$POSTVERSION_FILE" ]; then
POSTVERSION=$(cat "$POSTVERSION_FILE")
else
POSTVERSION=$INSTALLEDVERSION
fi
INSTALLEDSALTVERSION=$(salt --versions-report | grep Salt: | awk '{print $2}') INSTALLEDSALTVERSION=$(salt --versions-report | grep Salt: | awk '{print $2}')
BATCHSIZE=5 BATCHSIZE=5
SOUP_LOG=/root/soup.log SOUP_LOG=/root/soup.log
@@ -23,6 +33,10 @@ NOTIFYCUSTOMELASTICCONFIG=false
TOPFILE=/opt/so/saltstack/default/salt/top.sls TOPFILE=/opt/so/saltstack/default/salt/top.sls
BACKUPTOPFILE=/opt/so/saltstack/default/salt/top.sls.backup BACKUPTOPFILE=/opt/so/saltstack/default/salt/top.sls.backup
SALTUPGRADED=false SALTUPGRADED=false
# Set true once soup begins modifying the system (past the pre-flight checks), so the
# EXIT trap can tell the user the update did not finish and must be re-run. Only the
# pre-flight gates (ES compatibility, disk, network) fail before this is set.
SOUP_UPGRADE_STARTED=false
SALT_CLOUD_INSTALLED=false SALT_CLOUD_INSTALLED=false
SALT_CLOUD_CONFIGURED=false SALT_CLOUD_CONFIGURED=false
# Check if salt-cloud is installed # Check if salt-cloud is installed
@@ -123,6 +137,28 @@ check_err() {
echo "SOUP XTRACE debug log (if enabled) at $SOUP_DEBUG_LOG. Re-run soup with SOUP_DEBUG=1 to create $SOUP_DEBUG_LOG" echo "SOUP XTRACE debug log (if enabled) at $SOUP_DEBUG_LOG. Re-run soup with SOUP_DEBUG=1 to create $SOUP_DEBUG_LOG"
# If soup had already started modifying the system, make it unmistakable that the
# update is incomplete and must be re-run. soup is resumable: a version upgrade
# picks up from the /etc/sopostversion marker, and a hotfix re-applies because
# /etc/sohotfix is only advanced after a successful highstate.
if [[ "$SOUP_UPGRADE_STARTED" == "true" ]]; then
echo ""
echo "=============================================================================="
echo " UPGRADE INCOMPLETE"
echo "=============================================================================="
echo " This soup run did NOT finish. Your Security Onion installation may be in a"
echo " partially-updated state and is not yet fully upgraded."
echo ""
echo " Review the error above and $SOUP_LOG, resolve the underlying problem, then"
echo " run soup again to resume and complete the update:"
echo ""
echo " sudo soup"
echo ""
echo " soup is resumable -- re-running it continues from where this run stopped."
echo "=============================================================================="
echo ""
fi
exit $exit_code exit $exit_code
fi fi
@@ -291,6 +327,30 @@ check_pillar_items() {
fi fi
} }
check_cluster_health() {
echo "Checking Elasticsearch cluster health."
# Require a 'green' cluster before upgrading; anything less (yellow, red, or
# unreachable) blocks. Modeled on the wait used in so-elasticsearch-roles-load.
if so-elasticsearch-query "_cluster/health?wait_for_status=green&timeout=120s" --fail > /dev/null 2>&1; then
printf "\nThe Elasticsearch cluster is healthy (green). We can proceed with SOUP.\n\n"
else
printf "\nThe Elasticsearch cluster is not green. Please resolve the cluster health issue so the cluster is green before running SOUP again.\n\n"
exit 0
fi
}
check_fleet_server() {
echo "Checking that Elastic Fleet Server is responding."
# Modeled on the wait_for_so-elastic-fleet state check in elasticfleet/enabled.sls,
# which waits for HTTP 200 from the Fleet Server status API.
if curl -sk --fail --retry 3 --retry-delay 10 --max-time 30 "https://localhost:8220/api/status" > /dev/null 2>&1; then
printf "\nElastic Fleet Server is responding. We can proceed with SOUP.\n\n"
else
printf "\nElastic Fleet Server is not responding at https://localhost:8220/api/status. Please ensure Elastic Fleet is healthy before running SOUP again.\n\n"
exit 0
fi
}
check_saltmaster_status() { check_saltmaster_status() {
set +e set +e
echo "Waiting on the Salt Master service to be ready." echo "Waiting on the Salt Master service to be ready."
@@ -377,6 +437,8 @@ get_soup_script_hashes() {
GITIMGCMN=$(md5sum $UPDATE_DIR/salt/common/tools/sbin/so-image-common | awk '{print $1}') GITIMGCMN=$(md5sum $UPDATE_DIR/salt/common/tools/sbin/so-image-common | awk '{print $1}')
CURRENTSOFIREWALL=$(md5sum /usr/sbin/so-firewall | awk '{print $1}') CURRENTSOFIREWALL=$(md5sum /usr/sbin/so-firewall | awk '{print $1}')
GITSOFIREWALL=$(md5sum $UPDATE_DIR/salt/manager/tools/sbin/so-firewall | awk '{print $1}') GITSOFIREWALL=$(md5sum $UPDATE_DIR/salt/manager/tools/sbin/so-firewall | awk '{print $1}')
CURRENTSOYAML=$(md5sum /usr/sbin/so-yaml.py | awk '{print $1}')
GITSOYAML=$(md5sum $UPDATE_DIR/salt/manager/tools/sbin/so-yaml.py | awk '{print $1}')
} }
highstate() { highstate() {
@@ -414,6 +476,13 @@ preupgrade_changes() {
true true
} }
set_postversion() {
# Persist post-upgrade walk progress so an interrupted upgrade can resume the
# remaining steps on the next soup run (see /etc/sopostversion handling).
POSTVERSION="$1"
echo "$POSTVERSION" > "$POSTVERSION_FILE"
}
postupgrade_changes() { postupgrade_changes() {
# This function is to add any new pillar items if needed. # This function is to add any new pillar items if needed.
echo "Running post upgrade processes." echo "Running post upgrade processes."
@@ -421,6 +490,8 @@ postupgrade_changes() {
[[ "$POSTVERSION" =~ ^2\.4\.21[0-9]+$ ]] && post_to_3.0.0 [[ "$POSTVERSION" =~ ^2\.4\.21[0-9]+$ ]] && post_to_3.0.0
[[ "$POSTVERSION" == "3.0.0" ]] && post_to_3.1.0 [[ "$POSTVERSION" == "3.0.0" ]] && post_to_3.1.0
[[ "$POSTVERSION" == "3.1.0" ]] && post_to_3.2.0 [[ "$POSTVERSION" == "3.1.0" ]] && post_to_3.2.0
# All applicable post-upgrade steps completed; clear the resume marker.
rm -f "$POSTVERSION_FILE"
true true
} }
@@ -513,7 +584,7 @@ post_to_3.0.0() {
# convert yes/no in suricata pillars to true/false # convert yes/no in suricata pillars to true/false
convert_suricata_yes_no convert_suricata_yes_no
POSTVERSION=3.0.0 set_postversion 3.0.0
} }
### 3.0.0 End ### ### 3.0.0 End ###
@@ -776,7 +847,7 @@ post_to_3.1.0() {
# Check for unhealthy / unauthorized integration transform jobs and attempt reauthorizations # Check for unhealthy / unauthorized integration transform jobs and attempt reauthorizations
check_transform_health_and_reauthorize || true check_transform_health_and_reauthorize || true
POSTVERSION=3.1.0 set_postversion 3.1.0
} }
### 3.1.0 End ### ### 3.1.0 End ###
@@ -911,7 +982,7 @@ post_to_3.2.0() {
update_kafka_metadata "4.3" update_kafka_metadata "4.3"
POSTVERSION=3.2.0 set_postversion 3.2.0
} }
### 3.2.0 End ### ### 3.2.0 End ###
@@ -1063,8 +1134,20 @@ upgrade_check() {
fi fi
[[ -f /etc/sohotfix ]] && CURRENTHOTFIX=$(cat /etc/sohotfix) [[ -f /etc/sohotfix ]] && CURRENTHOTFIX=$(cat /etc/sohotfix)
if [ "$INSTALLEDVERSION" == "$NEWVERSION" ]; then if [ "$INSTALLEDVERSION" == "$NEWVERSION" ]; then
# A leftover post-version marker means a previous upgrade to this version
# advanced /etc/soversion (the highstate stamps it from the pillar) but did not
# finish its post-upgrade steps. Resume the upgrade instead of reporting "latest".
if [ -f "$POSTVERSION_FILE" ] && [ "$(cat "$POSTVERSION_FILE")" != "$NEWVERSION" ]; then
echo "A previous upgrade to $NEWVERSION did not complete its post-upgrade steps; resuming."
is_hotfix=false
return 0
fi
echo "Checking to see if there are hotfixes needed" echo "Checking to see if there are hotfixes needed"
if [ "$HOTFIXVERSION" == "$CURRENTHOTFIX" ]; then if [ "$HOTFIXVERSION" == "$CURRENTHOTFIX" ]; then
# Reaching here means we are at the target version and NOT resuming (the resume
# check above returned otherwise). Clear any stale resume marker so a completed
# upgrade is never mistaken for a partial one and re-run on a later invocation.
rm -f "$POSTVERSION_FILE"
echo "You are already running the latest version of Security Onion." echo "You are already running the latest version of Security Onion."
exit 0 exit 0
else else
@@ -1143,7 +1226,7 @@ upgrade_salt() {
verify_latest_update_script() { verify_latest_update_script() {
get_soup_script_hashes get_soup_script_hashes
if [[ "$CURRENTSOUP" == "$GITSOUP" && "$CURRENTCMN" == "$GITCMN" && "$CURRENTIMGCMN" == "$GITIMGCMN" && "$CURRENTSOFIREWALL" == "$GITSOFIREWALL" ]]; then if [[ "$CURRENTSOUP" == "$GITSOUP" && "$CURRENTCMN" == "$GITCMN" && "$CURRENTIMGCMN" == "$GITIMGCMN" && "$CURRENTSOFIREWALL" == "$GITSOFIREWALL" && "$CURRENTSOYAML" == "$GITSOYAML" ]]; then
echo "This version of the soup script is up to date. Proceeding." echo "This version of the soup script is up to date. Proceeding."
else else
echo "You are not running the latest soup version. Updating soup and its components. This might take multiple runs to complete." echo "You are not running the latest soup version. Updating soup and its components. This might take multiple runs to complete."
@@ -1152,7 +1235,7 @@ verify_latest_update_script() {
# Verify that soup scripts updated as expected # Verify that soup scripts updated as expected
get_soup_script_hashes get_soup_script_hashes
if [[ "$CURRENTSOUP" == "$GITSOUP" && "$CURRENTCMN" == "$GITCMN" && "$CURRENTIMGCMN" == "$GITIMGCMN" && "$CURRENTSOFIREWALL" == "$GITSOFIREWALL" ]]; then if [[ "$CURRENTSOUP" == "$GITSOUP" && "$CURRENTCMN" == "$GITCMN" && "$CURRENTIMGCMN" == "$GITIMGCMN" && "$CURRENTSOFIREWALL" == "$GITSOFIREWALL" && "$CURRENTSOYAML" == "$GITSOYAML" ]]; then
echo "Succesfully updated soup scripts." echo "Succesfully updated soup scripts."
else else
echo "There was a problem updating soup scripts. Trying to rerun script update." echo "There was a problem updating soup scripts. Trying to rerun script update."
@@ -1760,6 +1843,15 @@ main() {
set_minionid set_minionid
MINION_ROLE=$(lookup_role) MINION_ROLE=$(lookup_role)
echo "Found that Security Onion $INSTALLEDVERSION is currently installed." echo "Found that Security Onion $INSTALLEDVERSION is currently installed."
# /etc/soversion is stamped to the target version before the upgrade fully
# completes, so a lingering resume marker means this grid is only partially
# upgraded even though the line above shows the target version. Make that explicit
# so it is not mistaken for a finished upgrade.
if [ -f "$POSTVERSION_FILE" ] && [ "$(cat "$POSTVERSION_FILE")" != "$INSTALLEDVERSION" ]; then
echo ""
echo "NOTE: A previous upgrade to $INSTALLEDVERSION did not finish. This grid is"
echo " partially upgraded and this soup run will resume and complete it."
fi
echo "" echo ""
check_minimum_version check_minimum_version
@@ -1788,6 +1880,12 @@ main() {
echo "Verifying Elasticsearch version compatibility across the grid before upgrading." echo "Verifying Elasticsearch version compatibility across the grid before upgrading."
verify_es_version_compatibility verify_es_version_compatibility
# Pre-flight health checks: confirm the grid is in a good state before we change
# anything. These run before any modifications, so a failure exits cleanly and the
# operator can fix the issue and re-run soup.
check_cluster_health
check_fleet_server
echo "Checking for Salt Master and Minion updates." echo "Checking for Salt Master and Minion updates."
upgrade_check_salt upgrade_check_salt
set -e set -e
@@ -1804,6 +1902,7 @@ main() {
fi fi
if [ "$is_hotfix" == "true" ]; then if [ "$is_hotfix" == "true" ]; then
SOUP_UPGRADE_STARTED=true
echo "Applying $HOTFIXVERSION hotfix" echo "Applying $HOTFIXVERSION hotfix"
# since we don't run the backup.config_backup state on import we wont snapshot previous version states and pillars # since we don't run the backup.config_backup state on import we wont snapshot previous version states and pillars
if [[ ! "$MINION_ROLE" == "import" ]]; then if [[ ! "$MINION_ROLE" == "import" ]]; then
@@ -1814,10 +1913,16 @@ main() {
create_local_directories "/opt/so/saltstack/default" create_local_directories "/opt/so/saltstack/default"
apply_hotfix apply_hotfix
echo "Hotfix applied" echo "Hotfix applied"
update_version
enable_highstate enable_highstate
highstate highstate
# Record the hotfix only after the highstate succeeds. /etc/sohotfix is written
# solely by soup (no salt state manages it), so deferring the write means a failed
# hotfix highstate leaves the old hotfix value and re-running soup re-applies it,
# rather than reporting "already latest". The soversion/pillar writes in
# update_version are no-ops here since the version is unchanged for a hotfix.
update_version
else else
SOUP_UPGRADE_STARTED=true
echo "" echo ""
echo "Performing upgrade from Security Onion $INSTALLEDVERSION to Security Onion $NEWVERSION." echo "Performing upgrade from Security Onion $INSTALLEDVERSION to Security Onion $NEWVERSION."
echo "" echo ""
@@ -1873,6 +1978,10 @@ main() {
copy_new_files copy_new_files
echo "" echo ""
create_local_directories "/opt/so/saltstack/default" create_local_directories "/opt/so/saltstack/default"
# Seed the resume marker before the highstate stamps /etc/soversion to the new
# version, so an interrupted upgrade is detectable as "not finished" on re-run.
# POSTVERSION still holds the pre-upgrade (or prior resume) version here.
[ -f "$POSTVERSION_FILE" ] || echo "$POSTVERSION" > "$POSTVERSION_FILE"
update_version update_version
echo "" echo ""
+1 -1
View File
@@ -399,7 +399,7 @@ http {
error_page 429 = @error429; error_page 429 = @error429;
location @error401 { location @error401 {
if ($request_uri ~* (^/api/.*|^/connect/.*|^/oauth2/.*|^/.*\.map$)) { if ($request_uri ~* (^.*/api/.*|^/connect/.*|^/oauth2/.*|^/.*\.map$)) {
return 401; return 401;
} }
+17 -7
View File
@@ -8,6 +8,7 @@
{% from 'docker/docker.map.jinja' import DOCKERMERGED -%} {% from 'docker/docker.map.jinja' import DOCKERMERGED -%}
{% set INFLUXDB_TOKEN = salt['pillar.get']('influxdb:token') %} {% set INFLUXDB_TOKEN = salt['pillar.get']('influxdb:token') %}
{% import_text 'influxdb/metrics_link.txt' as METRICS_LINK %} {% import_text 'influxdb/metrics_link.txt' as METRICS_LINK %}
{% from 'telegraf/map.jinja' import TELEGRAFMERGED %}
{% for module, application_url in GLOBALS.application_urls.items() %} {% for module, application_url in GLOBALS.application_urls.items() %}
{% do SOCDEFAULTS.soc.config.server.modules[module].update({'hostUrl': application_url}) %} {% do SOCDEFAULTS.soc.config.server.modules[module].update({'hostUrl': application_url}) %}
@@ -24,13 +25,22 @@
{% do SOCDEFAULTS.soc.config.server.modules.elastic.update({'username': GLOBALS.elasticsearch.auth.users.so_elastic_user.user, 'password': GLOBALS.elasticsearch.auth.users.so_elastic_user.pass}) %} {% do SOCDEFAULTS.soc.config.server.modules.elastic.update({'username': GLOBALS.elasticsearch.auth.users.so_elastic_user.user, 'password': GLOBALS.elasticsearch.auth.users.so_elastic_user.pass}) %}
{% do SOCDEFAULTS.soc.config.server.modules.influxdb.update({'hostUrl': 'https://' ~ GLOBALS.influxdb_host ~ ':8086'}) %} {% if TELEGRAFMERGED.output == 'POSTGRES' %}
{% do SOCDEFAULTS.soc.config.server.modules.influxdb.update({'token': INFLUXDB_TOKEN}) %} {% for tool in SOCDEFAULTS.soc.config.server.client.tools %}
{% for tool in SOCDEFAULTS.soc.config.server.client.tools %} {% if tool.name == "toolInfluxDb" %}
{% if tool.name == "toolInfluxDb" and METRICS_LINK | length > 0 %} {% do SOCDEFAULTS.soc.config.server.client.tools.remove(tool) %}
{% do tool.update({'link': METRICS_LINK}) %} {% endif %}
{% endif %} {% endfor %}
{% endfor %}
{% else %}
{% do SOCDEFAULTS.soc.config.server.modules.influxdb.update({'hostUrl': 'https://' ~ GLOBALS.influxdb_host ~ ':8086'}) %}
{% do SOCDEFAULTS.soc.config.server.modules.influxdb.update({'token': INFLUXDB_TOKEN}) %}
{% for tool in SOCDEFAULTS.soc.config.server.client.tools %}
{% if tool.name == "toolInfluxDb" and METRICS_LINK | length > 0 %}
{% do tool.update({'link': METRICS_LINK}) %}
{% endif %}
{% endfor %}
{% endif %}
{% do SOCDEFAULTS.soc.config.server.modules.statickeyauth.update({'anonymousCidr': DOCKERMERGED.range, 'apiKey': pillar.sensoroni.config.sensoronikey}) %} {% do SOCDEFAULTS.soc.config.server.modules.statickeyauth.update({'anonymousCidr': DOCKERMERGED.range, 'apiKey': pillar.sensoroni.config.sensoronikey}) %}
+19
View File
@@ -7,6 +7,11 @@
{% from 'soc/defaults.map.jinja' import SOCDEFAULTS with context %} {% from 'soc/defaults.map.jinja' import SOCDEFAULTS with context %}
{% from 'elasticsearch/config.map.jinja' import ELASTICSEARCH_NODES %} {% from 'elasticsearch/config.map.jinja' import ELASTICSEARCH_NODES %}
{% from 'manager/map.jinja' import MANAGERMERGED %} {% from 'manager/map.jinja' import MANAGERMERGED %}
{% from 'telegraf/map.jinja' import TELEGRAFMERGED %}
{%- set PG_ENTRY = salt['pillar.get']('telegraf:postgres_creds:' ~ grains.id, {}) %}
{%- set PG_USER = PG_ENTRY.get('user', '') %}
{%- set PG_PASS = PG_ENTRY.get('pass', '') %}
{% set DOCKER_EXTRA_HOSTS = ELASTICSEARCH_NODES %} {% set DOCKER_EXTRA_HOSTS = ELASTICSEARCH_NODES %}
{% do DOCKER_EXTRA_HOSTS.append({GLOBALS.influxdb_host:pillar.node_data[GLOBALS.influxdb_host].ip}) %} {% do DOCKER_EXTRA_HOSTS.append({GLOBALS.influxdb_host:pillar.node_data[GLOBALS.influxdb_host].ip}) %}
@@ -75,6 +80,20 @@
{% do SOCMERGED.config.server.update({'airgapEnabled': false}) %} {% do SOCMERGED.config.server.update({'airgapEnabled': false}) %}
{% endif %} {% endif %}
{# Define the postgresmetrics module if telegraf is setup to only use Postgres #}
{% if TELEGRAFMERGED.output != 'INFLUXDB' and PG_USER and PG_PASS %}
{% do SOCMERGED.config.server.modules.update({
'postgresmetrics': {
'database': 'so_telegraf',
'host': GLOBALS.manager_ip,
'password': PG_PASS,
'port': 5432,
'sslMode': 'allow',
'user': PG_USER,
}
}) %}
{% do SOCMERGED.config.server.modules.pop('influxdb') %}
{% endif %}
{# Define the Detections custom ruleset that should always be present #} {# Define the Detections custom ruleset that should always be present #}
{% set CUSTOM_RULESET = { {% set CUSTOM_RULESET = {
+2 -2
View File
@@ -153,12 +153,12 @@ suricata:
cpu-affinity: cpu-affinity:
management-cpu-set: management-cpu-set:
cpu: cpu:
description: Bind management threads to a core or range of cores. This can be a sigle core, list of cores, or list of range of cores. set-cpu-affinity must be set to true for this to be used. description: Bind management threads to a core or range of cores. This can be a single core, list of cores, or list of range of cores. set-cpu-affinity must be set to true for this to be used.
forcedType: "[]string" forcedType: "[]string"
helpLink: suricata helpLink: suricata
worker-cpu-set: worker-cpu-set:
cpu: cpu:
description: Bind worker threads to a core or range of cores. This can be a sigle core, list of cores, or list of range of cores. set-cpu-affinity must be set to true for this to be used. description: Bind worker threads to a core or range of cores. This can be a single core, list of cores, or list of range of cores. set-cpu-affinity must be set to true for this to be used.
forcedType: "[]string" forcedType: "[]string"
helpLink: suricata helpLink: suricata
vars: vars:
+1 -1
View File
@@ -5,7 +5,7 @@ telegraf:
advanced: True advanced: True
helpLink: influxdb helpLink: influxdb
output: output:
description: Selects the backend(s) Telegraf writes metrics to. INFLUXDB keeps the current behavior; POSTGRES writes to the grid's Postgres instance; BOTH dual-writes for migration validation. description: Selects the backend(s) Telegraf writes metrics to. INFLUXDB keeps the current behavior; POSTGRES writes to the grid's Postgres instance; BOTH dual-writes for migration validation. When set to BOTH, the grid screen's metrics are pulled from Postgres, and the InfluxDB tool link remains visible. When set to POSTGRES, the InfluxDB tool link is removed.
options: options:
- INFLUXDB - INFLUXDB
- POSTGRES - POSTGRES
+1 -1
View File
@@ -793,7 +793,7 @@ if ! [[ -f $install_opt_file ]]; then
logCmd "so-soc-restart" logCmd "so-soc-restart"
title "Setting up Elastic Fleet" title "Setting up Elastic Fleet"
logCmd "salt-call state.apply elasticfleet.config" logCmd "salt-call state.apply elasticfleet.config"
if ! logCmd so-elastic-fleet-setup; then if ! so-elastic-fleet-setup; then
fail_setup "Failed to run so-elastic-fleet-setup" fail_setup "Failed to run so-elastic-fleet-setup"
fi fi
mark_setup_complete mark_setup_complete