Compare commits

..
Author SHA1 Message Date
Josh Brower c4555a5514 Add another pcap job fp 2026-08-14 13:54:14 -04:00
Josh Brower dcb931b97c Update excluded errors in so-log-check script 2026-08-14 11:23:25 -04:00
Josh Brower 8e6b16bde0 Add fp check 2026-08-14 11:22:12 -04:00
Josh Patterson 63692aa1a0 Merge pull request #16159 from Security-Onion-Solutions/fix/zeekctl-cron
Run zeekctl cron so LogExpireInterval and the other expire settings take effect
2026-08-14 09:49:28 -04:00
Josh Patterson 2663ca87a2 Mark the mail-only zeekctl settings read only
MailTo, MailConnectionSummary and MailHostUpDown do nothing but send mail, and
the Zeek container has no mail program, so nothing they control can happen. Mark
them read only rather than offering knobs in SOC that cannot take effect.

MailConnectionSummary only gates the emailed copy; the connection summary is
generated and archived either way. MailTo also feeds Notice::mail_dest, but
Security Onion never enables the notice email action, so that half is inert too.
MailHostUpDown gates only the notification text - host status detection, the
plugin hook and the stored state all run regardless.

MinDiskSpace stays editable. It is not mail only: setting it to 0 skips the disk
space check entirely, and the warning it produces is not emailed but does appear
in the output of "zeekctl cron". Correct its description, and MailHostUpDown's,
which both claimed these settings have no visible effect.
2026-08-14 09:19:11 -04:00
Josh Patterson a337a3e4f6 Run zeekctl cron so the expire settings take effect
LogExpireInterval, StatsLogExpireInterval and CrashExpireInterval are only acted
on by "zeekctl cron", which nothing in the grid ran, so setting them in SOC did
nothing. Add so-zeek-cron and run it every 5 minutes, the interval upstream
recommends.

This also restarts a node that died unexpectedly and marks it crashed so a crash
report is written, which is what CrashExpireInterval then reaps.

The crontab runs as root because the script needs the docker socket; it drops to
the zeek user inside the container so the stats logs and zeekctl-config.sh it
writes stay owned by uid 937.

Annotate the five zeekctl settings that were previously undocumented. The regex
on LogExpireInterval matters: a bare number means days, and a value shorter than
LogRotationInterval raises ConfigurationError, which fails the zeekctl deploy in
the container entrypoint. Zeek then never starts while Salt still reports success
and the container still reports healthy. Excluding the min unit keeps that
unreachable at the default 3600 second rotation interval. MinDiskSpace and
MailHostUpDown only send mail and the image has no sendmail, so their
descriptions say they currently have no effect.
2026-08-13 16:18:24 -04:00
Josh Patterson af222eed08 Merge pull request #16153 from Security-Onion-Solutions/asasoup
Lower default auto_apply batch size to 10%
2026-08-12 10:27:35 -04:00
Josh Patterson 83e55ab0f3 Fix heavynode wording in soup message 2026-08-12 10:16:30 -04:00
Josh Patterson ff82cc32a0 Lower default auto_apply batch size to 10% 2026-08-12 10:14:30 -04:00
Jorge Reyes 721d6483f0 Merge pull request #16152 from Security-Onion-Solutions/jertel/wip
more exclusions due to preproc refactor
2026-08-11 21:54:07 -05:00
Jason Ertel a88562a348 more exclusions due to preproc refactor 2026-08-11 22:02:38 -04:00
Josh Patterson 64d7383233 Merge pull request #16151 from Security-Onion-Solutions/asasoup
soup: push a grid-wide highstate to remote nodes after upgrade
2026-08-11 10:46:22 -04:00
Josh Patterson 7400e3dffa Rename so-grid-highstate to so-soup-grid-highstate 2026-08-11 10:12:40 -04:00
Mike Reeves 792b801086 Merge pull request #16143 from Security-Onion-Solutions/TOoSmOotH/telegraf-partman-maintenance
Fix Telegraf metrics falling into pg_partman default partitions
2026-08-11 09:57:55 -04:00
Jason Ertel 3991e485c0 Merge pull request #16150 from Security-Onion-Solutions/jertel/wip
exclude test log WARNs
2026-08-11 08:49:11 -04:00
Josh Patterson a87a910585 Merge remote-tracking branch 'origin/3/dev' into asasoup 2026-08-11 08:36:37 -04:00
Jason Ertel ba0dd38f4e comments 2026-08-10 22:16:16 -04:00
Jason Ertel b3467854a8 exclude preproc warns 2026-08-10 22:14:08 -04:00
Jorge Reyes 539389c78e Merge pull request #16148 from Security-Onion-Solutions/reyesj2-patch-495
allow airgap installs to reuse previously extracted docker images
2026-08-10 14:01:07 -05:00
Jorge Reyes 65e81d3b3a Merge pull request #16146 from Security-Onion-Solutions/reyesj2-patch-1
remove thehive scripted field from kibana saved objects
2026-08-10 14:00:59 -05:00
Jason Ertel 546462c77f Merge pull request #16149 from Security-Onion-Solutions/jertel/wip
Use newer /api/joblookup endpoint
2026-08-10 13:46:08 -04:00
Jason Ertel a7ddb7a975 Use newer /api/joblookup endpoint 2026-08-10 13:38:41 -04:00
Josh Patterson ee1d2167e8 logrotate: rotate /opt/so/log/salt/so-grid-highstate
Add a logrotate entry for the grid-highstate driver's log, matching the existing
/opt/so/log/salt/{minion,master,so-salt-minion-check} entries (daily, rotate 14,
copytruncate, compress). Registered in both logrotate/defaults.yaml and the SOC
config schema logrotate/soc_logrotate.yaml.
2026-08-10 08:30:40 -04:00
Josh Patterson 2d0ea48c39 so-grid-highstate: drop the .log extension from the log path
Rename the grid-highstate log from /opt/so/log/salt/so-grid-highstate.log to
/opt/so/log/salt/so-grid-highstate. Updates the LOG_FILE var in so-grid-highstate
and the two /opt/so/log/salt/so-grid-highstate.log references in soup (the progress
echo and the distributed-deployment message).
2026-08-10 08:27:40 -04:00
reyesj2 23d92316c1 allow airgap installs to reuse previously extracted docker images 2026-08-09 16:37:21 -05:00
Jorge Reyes e5346af068 Merge pull request #16147 from Security-Onion-Solutions/reyesj2-patch-3
add some extra logging to so-minion log
2026-08-07 12:55:56 -05:00
reyesj2 9762523849 add some extra logging to so-minion log 2026-08-07 11:52:51 -05:00
reyesj2 e8ab6433ab remove thehive scripted field from kibana saved objects 2026-08-06 14:03:27 -05:00
Josh Patterson 9c20ef60f4 Merge remote-tracking branch 'origin/3/dev' into asasoup 2026-08-06 13:56:08 -04:00
Josh Patterson 6abf382ea8 so-grid-highstate: wait for fleet recovery and quiet the expected salt-upgrade warning
Two refinements to the --salt-upgraded path, surfaced testing a salt downgrade+soup
on a manager+heavynode grid:

- The post-upgrade settle loop settled on any stable reachable count >0, so when a
  target was briefly down for its salt-minion restart it could settle on the
  not-yet-restarted subset (observed: 'settled at 1' with 2 accepted) and release the
  tiered pass before nodes reconnected. Capture the reachable count just before the
  pass and wait for it to recover to that count (up >= pre-upgrade target) and hold
  steady, with an initial grace so the delayed restart dip is observed rather than
  skipped. Still compares against the pre-upgrade reachable set, not accepted keys, so
  an intentionally powered-off node never stalls past the backstop.

- The salt-upgrade pass returns non-zero by design (targets restart salt-minion
  mid-run), but it logged the generic 'nodes it missed will converge on the scheduled
  highstate' warning, which reads like a real failure. Mark that dispatch as
  expect_restart so it logs a benign, explanatory line instead.

Verified live: with the heavynode's salt-minion bounced during the settle window, the
loop logged 'fleet recovered to 2 minions up (>= pre-upgrade 2)' and only then ran the
tiered pass (heavynode highstate 427 succeeded, 0 failed).
2026-08-06 09:46:12 -04:00
Josh Patterson 36833fdad1 so-grid-highstate: emit single-line pillar JSON and skip empty tiers
Two defects surfaced testing BRANCH=asasoup soup on a manager+heavynode grid:

1. The tiered-pass actions JSON was built from a multi-line heredoc. salt parses
   'pillar=<value>' kwargs with a non-DOTALL regex, so the embedded newlines made
   salt-run treat the whole token as a positional saltenv -- 'No matching salt
   environment for environment pillar={...}' -- and the highstate never ran, leaving
   the heavynode on the old version. Emit the actions JSON on a single line (matching
   how so-push-drainer's json.dumps payload already works).

2. orch.push_batch's salt.state step reports 'No minions returned' (a failure) for a
   tier whose compound target matches nothing, so any grid lacking a role (no receiver,
   small grids) always logged a warning and returned rc=1 even when every present node
   converged. Pre-check each tier with 'salt -C <tgt> --preview-target' and include only
   tiers that match >=1 minion; exit cleanly if none match.

Verified live: heavynode highstated 3.2.0 -> 3.3.0 (427 states, 0 failed) and a
follow-up run skips the empty receiver/remainder tiers with rc=0.
2026-08-05 15:05:24 -04:00
Josh Patterson a92d10a1e3 Merge remote-tracking branch 'origin/3/dev' into asasoup 2026-08-05 14:19:39 -04:00
Josh Patterson d3da6b3939 soup: actively push a grid-wide highstate to remote minions after upgrade
The per-minion highstate schedule moved from 15 minutes to 120 minutes
(salt:schedule:highstate_interval_minutes), so after soup upgrades the manager,
non-manager minions could otherwise sit on the old version for up to ~2.5 hours
(interval + splay) before their scheduled highstate applies the new code.

Add so-grid-highstate, a detached best-effort driver soup fires at the end of an
upgrade. It uses the existing orch.push_batch runner to highstate the grid in
role tiers (searchnodes/heavynodes -> receivers -> the rest), skips single-node
grids, and when Salt itself was upgraded first runs an untiered pass and waits
for minions to reconnect on the new salt-minion before the tiered pass.

soup gains a push_grid_highstate() helper (guarded, launched via setsid nohup so
an SSH drop can't kill it) called on both the hotfix and full-upgrade paths,
wires the previously-dead -b flag through as --batch (now accepting N or N%),
and updates the distributed-deployment message to reflect the active push.
2026-08-05 14:06:21 -04:00
14 changed files with 420 additions and 18 deletions
+7
View File
@@ -154,6 +154,8 @@ if [[ $EXCLUDE_FALSE_POSITIVE_ERRORS == 'Y' ]]; then
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|id.orig_h" # false positive (zeek test data)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|emerging-all.rules" # false positive (error in rulename)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|invalid query input" # false positive (Invalid user input in hunt query)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|no data available for the requested dates" # false positive (pcap cypress test submits a job with an empty time frame)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|no job processor" # false positive (same empty-time-frame job on import nodes, where no pcap processor runs)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|example" # false positive (example test data)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|status 200" # false positive (request successful, contained error string in content)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|app_layer.error" # false positive (suricata 7) in stats.log e.g. app_layer.error.imap.parser | Total | 0
@@ -169,6 +171,11 @@ if [[ $EXCLUDE_FALSE_POSITIVE_ERRORS == 'Y' ]]; then
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Error while parsing document for index \[.ds-logs-kratos-so-.*object mapping for \[file\]" # false positive (mapping error occuring BEFORE kratos index has rolled over in 2.4.210)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|No such container" # false positive (telegraf trying to run stats on an old container)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|passwords do not match" # false positive (automated hydra test)
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Request did not pass preprocessing" # expected WARN log lines indicating invalid auth header
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Missing or invalid authorization header for bearer token" # expected WARN log lines indicating invalid auth header
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Unexpected authorization header" # expected WARN log lines indicating invalid auth header
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Missing ory_kratos_session cookie" # expected WARN log lines indicating invalid auth header
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Static assets preprocessor only supports GET and HEAD requests" # expected WARN log lines indicating invalid auth header
fi
if [[ $EXCLUDE_KNOWN_ERRORS == 'Y' ]]; then
File diff suppressed because one or more lines are too long
+10
View File
@@ -220,6 +220,16 @@ logrotate:
- extension .log
- dateext
- dateyesterday
/opt/so/log/salt/so-soup-grid-highstate:
- daily
- rotate 14
- missingok
- copytruncate
- compress
- create
- extension .log
- dateext
- dateyesterday
/nsm/idh/*_x_log:
- daily
- rotate 14
+7
View File
@@ -140,6 +140,13 @@ logrotate:
multiline: True
global: True
forcedType: "[]string"
"/opt/so/log/salt/so-soup-grid-highstate":
description: List of logrotate options for this file.
title: /opt/so/log/salt/so-soup-grid-highstate
advanced: True
multiline: True
global: True
forcedType: "[]string"
"/nsm/idh/*_x_log":
description: List of logrotate options for this file.
title: /nsm/idh/*.log
+18 -7
View File
@@ -138,6 +138,8 @@ function getinstallinfo() {
log "ERROR" "Failed to source install variables"
return 1
fi
log "INFO" "Fetched install info for $MINION_ID (node type: ${NODETYPE:-unset})"
}
function pcapspace() {
@@ -483,6 +485,7 @@ function add_sensoroni_with_analyze_to_minion() {
# Sensor settings for the minion pillar
function add_sensor_to_minion() {
log "INFO" "Writing sensor configuration for $MINION_ID (interface: ${INTERFACE:-unset})"
{
echo "sensor:"
echo " interface: '$INTERFACE'"
@@ -509,6 +512,8 @@ function add_sensor_to_minion() {
log "ERROR" "Failed to add sensor configuration to $PILLARFILE"
return 1
fi
log "INFO" "Wrote sensor configuration for $MINION_ID"
}
function add_elastalert_to_minion() {
@@ -581,11 +586,14 @@ function add_telegraf_to_minion() {
# generates a password on first add and is a no-op on re-add so the cred
# is stable across repeated so-minion runs. postgres.telegraf_users on the
# manager creates/updates the DB role from the same pillar.
so-telegraf-cred add "$MINION_ID"
if [ $? -ne 0 ]; then
log "ERROR" "Failed to provision postgres telegraf cred for $MINION_ID"
return 1
fi
log "INFO" "Provisioning postgres telegraf credential for $MINION_ID"
so-telegraf-cred add "$MINION_ID"
local result=$?
if [ $result -ne 0 ]; then
log "ERROR" "Failed to provision postgres telegraf cred for $MINION_ID (exit code: $result)"
return 1
fi
log "INFO" "Provisioned postgres telegraf credential for $MINION_ID"
}
function add_influxdb_to_minion() {
@@ -1043,7 +1051,7 @@ function updateMineAndApplyStates() {
}
function setupMinionFiles() {
log "INFO" "Setting up minion files for $MINION_ID"
log "INFO" "Setting up minion files for $MINION_ID (pillar: $PILLARFILE)"
# Check to see if nodetype is set
if [ -z $NODETYPE ]; then
@@ -1069,7 +1077,10 @@ function setupMinionFiles() {
fi
# Create node-specific configuration
create$NODETYPE || return 1
create$NODETYPE || {
log "ERROR" "Failed to create $NODETYPE configuration for $MINION_ID"
return 1
}
# Ensure proper ownership after all content is written
ensure_socore_ownership || return 1
@@ -0,0 +1,219 @@
#!/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-soup-grid-highstate
# ======================
# Drives a batched, role-tiered highstate across every non-manager minion in the
# grid. soup fires this (detached) after it finishes upgrading the manager so the
# rest of the grid converges immediately instead of waiting for its own scheduled
# highstate -- which, since the schedule moved from 15 minutes to 120 minutes
# (salt:schedule:highstate_interval_minutes), could otherwise leave nodes on the
# old version for up to ~2.5 hours (interval + splay) while the manager runs new code.
#
# Work is done by the existing orch.push_batch orchestration (salt/orch/push_batch.sls),
# the same runner the active-push drainer uses, so batching/queueing behavior matches.
# Tiers are dispatched in declaration order: searchnodes/heavynodes (Elasticsearch data
# nodes) first, then receivers, then everything else -- so the data tier converges before
# the ingest tier before sensors/fleet/idh/etc.
#
# When soup also upgraded Salt itself, remote minions must first highstate onto the new
# salt-minion package (top.sls gates every real state on G@saltversion, so a stale-version
# minion only gets salt.minion until it upgrades and reconnects). --salt-upgraded runs that
# preliminary pass and waits for the fleet to settle before the tiered pass.
#
# This is best-effort: soup has already completed by the time this runs, and the 120-minute
# scheduled highstate remains the backstop for any node that is offline or missed a batch.
LOG_FILE=/opt/so/log/salt/so-soup-grid-highstate
LOCK_FILE=/opt/so/state/so-soup-grid-highstate.lock
SETTLE_MAX_WAIT=${GRID_HIGHSTATE_SETTLE_WAIT:-900} # backstop for the post-salt-upgrade settle loop
SETTLE_INTERVAL=15
SETTLE_STABLE_CHECKS=3
# salt-minion on an upgraded node restarts ~30s after the upgrade state runs
# (salt/salt/minion/init.sls start_minion_post_upgrade); wait past that before sampling
# so the settle loop sees the drop-off instead of settling on the pre-restart set.
SETTLE_INITIAL_WAIT=${GRID_HIGHSTATE_SETTLE_INITIAL_WAIT:-45}
BATCH=""
BATCH_WAIT=""
SALT_UPGRADED=false
REASON="manual"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') | $*" | tee -a "$LOG_FILE"
}
usage() {
echo "Usage: so-soup-grid-highstate [--batch <spec>] [--batch-wait <sec>] [--salt-upgraded] [--reason <text>]"
exit 1
}
while [ $# -gt 0 ]; do
case "$1" in
--batch) BATCH="$2"; shift 2 ;;
--batch-wait) BATCH_WAIT="$2"; shift 2 ;;
--salt-upgraded) SALT_UPGRADED=true; shift ;;
--reason) REASON="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$LOCK_FILE")"
# Serialize: a second invocation (e.g. two soups, or a manual run overlapping soup's)
# should not dispatch a competing set of batches.
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
log "another so-soup-grid-highstate is already running (lock $LOCK_FILE held); exiting"
exit 0
fi
# Resolve batch settings from the salt:auto_apply pillar when not overridden on the
# command line, falling back to the same defaults orch.push_batch/salt.defaults use.
if [ -z "$BATCH" ]; then
BATCH=$(salt-call --out=newline_values_only pillar.get salt:auto_apply:batch 2>/dev/null)
[ -z "$BATCH" ] && BATCH='10%'
fi
if [ -z "$BATCH_WAIT" ]; then
BATCH_WAIT=$(salt-call --out=newline_values_only pillar.get salt:auto_apply:batch_wait 2>/dev/null)
[ -z "$BATCH_WAIT" ] && BATCH_WAIT=15
fi
MINIONID=$(salt-call --local --out=newline_values_only grains.get id 2>/dev/null)
[ -z "$MINIONID" ] && MINIONID=$(cat /etc/salt/minion_id 2>/dev/null)
if [ -z "$MINIONID" ]; then
log "could not determine this minion's id; aborting"
exit 1
fi
# Single-node grids (eval/standalone/import with no other accepted keys) have nothing
# remote to push -- the manager already highstated during soup.
NUM_ACCEPTED=$(salt-key --out=json --list=accepted 2>/dev/null | jq -r '.minions | length' 2>/dev/null)
NUM_ACCEPTED=${NUM_ACCEPTED:-0}
if [ "$NUM_ACCEPTED" -le 1 ]; then
log "single node grid ($NUM_ACCEPTED accepted minion(s)); nothing to push (reason=$REASON)"
exit 0
fi
log "starting grid highstate: reason=$REASON minion=$MINIONID accepted=$NUM_ACCEPTED batch=$BATCH batch_wait=$BATCH_WAIT salt_upgraded=$SALT_UPGRADED"
# Count minions currently responsive on the bus (includes this manager).
count_up() {
salt-run manage.up --out=json 2>/dev/null \
| python3 -c 'import sys,json; print(len(json.load(sys.stdin)))' 2>/dev/null
}
# Dispatch a single synchronous orch.push_batch run for the given actions JSON.
# Synchronous is fine: soup launched us detached, so blocking here does not hold soup up.
# expect_restart=true marks a dispatch (the salt-upgrade pass) where a non-zero rc is normal
# because targets restart salt-minion mid-run -- so we don't log a misleading failure warning.
dispatch() {
local desc="$1"
local actions="$2"
local expect_restart="${3:-false}"
local rc
log "dispatching $desc"
salt-run state.orchestrate orch.push_batch pillar="{\"actions\": $actions}" >>"$LOG_FILE" 2>&1
rc=$?
if [ "$rc" -eq 0 ]; then
log "$desc dispatch completed (rc=0)"
elif [ "$expect_restart" = "true" ]; then
log "$desc returned rc=$rc; this is expected during a salt upgrade (targets restart salt-minion mid-run). Waiting for them to reconnect before the tiered pass."
else
log "WARNING: $desc dispatch returned rc=$rc; nodes it missed will converge on the scheduled highstate"
fi
}
# Wait for the reachable minion set to recover to its pre-upgrade size and hold steady.
# Used after the salt-upgrade pass, where targets restart salt-minion (~30s delayed, see
# salt/salt/minion/init.sls) and drop off the bus before reconnecting on the new version.
# target = how many minions were reachable just before the pass; requiring up >= target keeps
# us from releasing the tiered pass while nodes are still down for their restart (settling on
# the not-yet-restarted subset). We deliberately compare against the pre-upgrade reachable
# count, not accepted keys, so a node an operator intentionally powered off never stalls us.
# Bounded by SETTLE_MAX_WAIT.
wait_for_settle() {
local target="$1"
local elapsed=0 prev=-1 stable=0 up=0
# Let the delayed salt-minion restart begin before we start counting stability, otherwise
# we could see the pre-restart set as "stable" and settle before the drop-off even happens.
sleep "$SETTLE_INITIAL_WAIT"
elapsed=$SETTLE_INITIAL_WAIT
while [ "$elapsed" -lt "$SETTLE_MAX_WAIT" ]; do
up=$(count_up); up=${up:-0}
if [ "$up" -ge "$target" ] && [ "$up" -eq "$prev" ]; then
stable=$((stable + 1))
[ "$stable" -ge "$SETTLE_STABLE_CHECKS" ] && break
else
stable=0
fi
prev=$up
sleep "$SETTLE_INTERVAL"
elapsed=$((elapsed + SETTLE_INTERVAL))
done
if [ "$up" -ge "$target" ]; then
log "fleet recovered to ${up} minions up (>= pre-upgrade ${target}) after ${elapsed}s"
else
log "WARNING: ${SETTLE_MAX_WAIT}s settle backstop hit; only ${up}/${target} pre-upgrade minions back up; proceeding (stragglers converge on the scheduled highstate)"
fi
}
# Pass 0: when Salt itself was upgraded, remote minions still on the old version only match
# top.sls's 'not G@saltversion' block (salt.minion, which performs the package upgrade). Push
# an untiered highstate so they upgrade+reconnect, then wait for them to come back before the
# real tiered pass applies the new version's states.
if [ "$SALT_UPGRADED" = "true" ]; then
PRE_UP=$(count_up); PRE_UP=${PRE_UP:-1}
log "pre-upgrade reachable minions (incl. this manager): $PRE_UP"
dispatch "salt-upgrade pass (all remote minions)" \
"[{\"highstate\": true, \"tgt\": \"not $MINIONID\", \"tgt_type\": \"compound\", \"batch\": \"$BATCH\", \"batch_wait\": $BATCH_WAIT}]" \
true
log "waiting for minions to reconnect on the new salt version"
wait_for_settle "$PRE_UP"
fi
# Tiered pass: Elasticsearch data nodes first, then receivers, then the remainder. The last
# tier is defined as the complement of the earlier tiers (and of this manager) so coverage is
# exhaustive -- sensors, fleet, idh, desktop, hypervisor, and any future role are all included.
TIER_TGTS=(
"( *_searchnode or *_heavynode ) and not $MINIONID"
"*_receiver and not $MINIONID"
"not $MINIONID and not *_searchnode and not *_heavynode and not *_receiver"
)
# Count minions a compound target matches, using the master's key/cache data (no execution).
tier_count() {
salt --out=json -C "$1" --preview-target 2>/dev/null | jq 'length' 2>/dev/null
}
# Build the actions JSON, including only tiers that actually match minions. An empty target
# would make orch.push_batch's salt.state step return "No minions returned" -- a failure --
# even though nothing needed to run, and grids commonly lack a tier (no receiver, etc.).
# Keep the JSON on a single line: salt parses `pillar=<value>` kwargs with a non-DOTALL
# regex, so an embedded newline makes it treat the whole token as a positional saltenv
# instead ("No matching salt environment for environment 'pillar=...'").
actions=""
for tgt in "${TIER_TGTS[@]}"; do
n=$(tier_count "$tgt"); n=${n:-0}
if [ "$n" -ge 1 ]; then
[ -n "$actions" ] && actions="$actions, "
actions="$actions{\"highstate\": true, \"tgt\": \"$tgt\", \"tgt_type\": \"compound\", \"batch\": \"$BATCH\", \"batch_wait\": $BATCH_WAIT}"
log "tier matched $n minion(s): $tgt"
else
log "tier matched 0 minions, skipping: $tgt"
fi
done
if [ -z "$actions" ]; then
log "no remote minions matched any tier; nothing to push (reason=$REASON)"
exit 0
fi
dispatch "tiered pass (searchnodes/heavynodes -> receivers -> remainder)" "[$actions]"
log "grid highstate complete (reason=$REASON)"
exit 0
+43 -5
View File
@@ -24,7 +24,10 @@ else
POSTVERSION=$INSTALLEDVERSION
fi
INSTALLEDSALTVERSION=$(salt --versions-report | grep Salt: | awk '{print $2}')
BATCHSIZE=5
# Optional -b override for the grid highstate batch size (a count like "5" or a
# percentage like "25%"). Empty means so-soup-grid-highstate uses the salt:auto_apply:batch
# pillar default.
BATCHSIZE=
SOUP_LOG=/root/soup.log
SOUP_DEBUG_LOG=/root/soup-debug.log
WHATWOULDYOUSAYYAHDOHERE=soup
@@ -452,6 +455,31 @@ highstate() {
salt-call state.highstate -l info queue=True
}
push_grid_highstate() {
# Drive a batched, role-tiered highstate across the rest of the grid so remote minions
# pick up this upgrade now instead of waiting up to ~2.5 hours for their own scheduled
# highstate (the schedule moved from 15 to 120 minutes). so-soup-grid-highstate does the work
# via orch.push_batch; it only exists once the manager highstate has deployed this
# version's sbin files, so guard on it. Launch fully detached (setsid) so it survives an
# SSH drop, and never let it affect soup's exit status -- it is best-effort with the
# scheduled highstate as backstop.
if [[ ! -x /usr/sbin/so-soup-grid-highstate ]]; then
echo "so-soup-grid-highstate not present; remote nodes will converge on their scheduled highstate."
return 0
fi
local extra_args=()
if [[ $SALTUPGRADED == true || $UPGRADESALT -eq 1 ]]; then
extra_args+=(--salt-upgraded)
fi
if [[ -n "$BATCHSIZE" ]]; then
extra_args+=(--batch "$BATCHSIZE")
fi
echo "Dispatching a grid-wide highstate to remote nodes. Progress: /opt/so/log/salt/so-soup-grid-highstate"
setsid nohup /usr/sbin/so-soup-grid-highstate --reason soup "${extra_args[@]}" >/dev/null 2>&1 &
}
masterlock() {
echo "Locking Salt Master"
mv -v $TOPFILE $BACKUPTOPFILE
@@ -1969,6 +1997,9 @@ main() {
# 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
# Push the hotfix out to the rest of the grid rather than waiting for the scheduled
# highstate. Hotfixes never upgrade Salt, so no --salt-upgraded pass is needed.
push_grid_highstate
else
SOUP_UPGRADE_STARTED=true
echo ""
@@ -2135,13 +2166,18 @@ main() {
if [[ $NUM_MINIONS -gt 1 ]]; then
# Actively drive the rest of the grid to this version now. The scheduled highstate
# runs only every 120 minutes (salt:schedule:highstate_interval_minutes), so without
# this remote nodes could sit on the old version for a couple of hours after soup finishes.
push_grid_highstate
cat << EOF
This appears to be a distributed deployment. Other nodes should update themselves at the next Salt highstate (typically within 15 minutes). Do not manually restart anything until you know that all the search/heavy nodes in your deployment are updated. This is especially important if you are using true clustering for Elasticsearch.
This appears to be a distributed deployment. soup has dispatched a batched, grid-wide highstate to update the other nodes now: Elasticsearch data nodes (search/heavynodes) first, then receivers, then sensors and the remaining nodes. Progress is logged to /opt/so/log/salt/so-soup-grid-highstate, and you can watch nodes update from the Grid section of SOC. Do not manually restart anything until you know that all the search/heavynodes in your deployment are updated. This is especially important if you are using true clustering for Elasticsearch.
Each minion is on a random 15 minute check-in period and things like network bandwidth can be a factor in how long the actual upgrade takes. If you have a heavy node on a slow link, it is going to take a while to get the containers to it. Depending on what changes happened between the versions, Elasticsearch might not be able to talk to said heavy node until the update is complete.
Nodes are updated in batches, and things like network bandwidth can be a factor in how long the actual upgrade takes. If you have a heavy node on a slow link, it is going to take a while to get the containers to it. Depending on what changes happened between the versions, Elasticsearch might not be able to talk to said heavy node until the update is complete. Any node that is offline or missed a batch will converge on its own scheduled highstate (every 120 minutes by default).
If it looks like youre missing data after the upgrade, please avoid restarting services and instead make sure at least one search node has completed its upgrade. The best way to do this is to run 'sudo salt-call state.highstate' from a search node and make sure there are no errors. Typically if it works on one node it will work on the rest. Sensor nodes are less complex and will update as they check in so you can monitor those from the Grid section of SOC.
@@ -2181,8 +2217,10 @@ while getopts ":b:f:y" opt; do
case ${opt} in
b )
BATCHSIZE="$OPTARG"
if ! [[ "$BATCHSIZE" =~ ^[1-9][0-9]*$ ]]; then
echo "Batch size must be a number greater than 0."
# Accept either a plain count (e.g. 5) or a percentage (e.g. 25%); passed through
# to so-soup-grid-highstate --batch, which salt's batch/batch_wait accepts in both forms.
if ! [[ "$BATCHSIZE" =~ ^[1-9][0-9]*%?$ ]]; then
echo "Batch size must be a number greater than 0, optionally with a trailing % (e.g. 5 or 25%)."
exit 1
fi
;;
+1 -1
View File
@@ -3,7 +3,7 @@ salt:
enabled: true
debounce_seconds: 30
drain_interval: 15
batch: '25%'
batch: '10%'
batch_wait: 15
schedule:
highstate_interval_minutes: 120
+2 -2
View File
@@ -36,8 +36,8 @@ soc:
icon: fa-stream
target: ''
links:
- '/joblookup?esid={:soc_id}&time={:@timestamp}&gridId={gridId}'
- '/joblookup?ncid={:network.community_id}&time={:@timestamp}&gridId={gridId}'
- '/api/joblookup?esid={:soc_id}&time={:@timestamp}&gridId={gridId}'
- '/api/joblookup?ncid={:network.community_id}&time={:@timestamp}&gridId={gridId}'
categories:
- hunt
- alerts
+5
View File
@@ -23,6 +23,11 @@ zeekpacketlosscron:
- identifier: zeekpacketlosscron
- user: root
zeekctlcron:
cron.absent:
- identifier: zeekctlcron
- user: root
{% else %}
{{sls}}_state_not_allowed:
+15
View File
@@ -87,6 +87,21 @@ zeekpacketlosscron:
- month: '*'
- dayweek: '*'
# LogExpireInterval, StatsLogExpireInterval and CrashExpireInterval are only acted on by
# 'zeekctl cron', so run it on the interval upstream recommends. This also restarts any
# node that died unexpectedly. Runs as root because the script needs the docker socket;
# it drops to the zeek user inside the container.
zeekctlcron:
cron.present:
- name: /usr/sbin/so-zeek-cron > /dev/null 2>&1
- identifier: zeekctlcron
- user: root
- minute: '*/5'
- hour: '*'
- daymonth: '*'
- month: '*'
- dayweek: '*'
{% else %}
{{sls}}_state_not_allowed:
+66
View File
@@ -58,6 +58,72 @@ zeek:
CompressLogs:
description: This setting enables compression of Zeek logs. If you are seeing packet loss at the top of the hour in Zeek or PCAP you might need to disable this by seting it to 0. This will use more disk space but save IO and CPU.
helpLink: zeek
LogExpireInterval:
description: >-
How long to keep rotated Zeek logs in /nsm/zeek/logs. A bare number means DAYS, so 7 means 7 days.
You may also give an explicit unit, such as "7 days" or "12 hr". Use 0 to keep logs forever.
This value must not be shorter than LogRotationInterval (3600 seconds by default), so the smallest
usable value is 1 hr - Zeek will fail to start if it is shorter. Expiry is applied by "zeekctl cron",
which runs every 5 minutes, and removes log files older than this based on their modification time.
regex: ^(0|[1-9][0-9]*( ?(day|hr)s?)?)$
regexFailureMessage: Enter 0, or a positive number optionally followed by "day" or "hr" (for example 7, "7 days", or "12 hr"). Minutes are not accepted because a log expire interval shorter than the log rotation interval prevents Zeek from starting.
helpLink: zeek
advanced: True
StatsLogExpireInterval:
description: >-
Number of days to keep entries in the Zeek stats log, or 0 to keep them forever.
Applied by "zeekctl cron", which runs every 5 minutes.
regex: ^[0-9]+$
regexFailureMessage: You must enter a whole number of days, or 0 to keep entries forever.
helpLink: zeek
advanced: True
CrashExpireInterval:
description: >-
Number of days to keep Zeek crash directories, or 0 to keep them forever.
Applied by "zeekctl cron", which runs every 5 minutes.
regex: ^[0-9]+$
regexFailureMessage: You must enter a whole number of days, or 0 to keep crash directories forever.
helpLink: zeek
advanced: True
MinDiskSpace:
description: >-
Percentage of free disk space below which ZeekControl reports a warning, or 0 to disable the check
entirely. The Zeek container does not include a mail program, so the warning is not emailed - it
appears in the output of "zeekctl cron" instead. This setting never deletes anything - cleanup based
on disk usage is handled separately by so-sensor-clean.
regex: ^([0-9]|[1-9][0-9]|100)$
regexFailureMessage: You must enter a percentage between 0 and 100.
helpLink: zeek
advanced: True
MailTo:
description: >-
Address that ZeekControl would send mail to, covering cron output and crash reports, and the address
Zeek's notice framework would use. The Zeek container does not include a mail program, and Security
Onion never enables the notice email action, so no mail is sent and this address is unused. It is
read only for that reason.
helpLink: zeek
advanced: True
readonly: True
MailConnectionSummary:
description: >-
Set to 1 to email the hourly connection summary. This only controls the emailed copy - the summary is
generated and archived with the other Zeek logs either way. The Zeek container does not include a mail
program, so no mail is sent and this setting has no effect. It is read only for that reason.
regex: ^[01]$
regexFailureMessage: You must enter 0 or 1.
helpLink: zeek
advanced: True
readonly: True
MailHostUpDown:
description: >-
Set to 1 to report when a Zeek node changes between the up and down states. The Zeek container does
not include a mail program, so this notification cannot be emailed. It is read only for that reason.
Host status detection still runs regardless of this setting - only the notification is affected.
regex: ^[01]$
regexFailureMessage: You must enter 0 or 1.
helpLink: zeek
advanced: True
readonly: True
policy:
custom:
filters:
+24
View File
@@ -0,0 +1,24 @@
#!/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.
# Run zeekctl's periodic maintenance tasks. This is what actually enforces
# LogExpireInterval, StatsLogExpireInterval and CrashExpireInterval - without a periodic
# 'zeekctl cron' those settings are inert no matter what they are set to.
# This also restarts any node that died unexpectedly, and marks it crashed so a crash report
# is produced. That is upstream's default cron behavior and it recovers a single node in
# place. The beacon in salt/_beacons/zeek.py is the only other recovery path, it is disabled
# by default (healthcheck:enabled), and it removes and recreates the whole container, so
# letting zeekctl handle a single dead worker avoids the heavier restart.
if ! docker ps --filter name=so-zeek --format '{{.Names}}' | grep -q '^so-zeek$'; then
exit 0
fi
# Run as the zeek user so the stats logs and zeekctl-config.sh this writes stay owned by
# uid 937 rather than root.
docker exec so-zeek runuser -l zeek -c '/opt/zeek/bin/zeekctl cron'
+2 -2
View File
@@ -971,8 +971,8 @@ docker_seed_registry() {
if [ -f /nsm/docker-registry/docker/registry.tar ]; then
logCmd "tar xvf /nsm/docker-registry/docker/registry.tar -C /nsm/docker-registry/docker"
logCmd "rm /nsm/docker-registry/docker/registry.tar"
elif [ -d /nsm/docker-registry/docker/registry ] && [ -f /etc/SOCLOUD ]; then
echo "Using existing docker registry content for cloud install"
elif [[ -d /nsm/docker-registry/docker/registry && ( -f /etc/SOCLOUD || "$is_airgap" == true ) ]]; then
echo "Using existing docker registry content"
else
if [ "$install_type" == 'IMPORT' ]; then
container_list 'so-import'