From 23f04e28665af7569a7e837f5c66db87547a0c21 Mon Sep 17 00:00:00 2001 From: Matthew Wright Date: Mon, 15 Jun 2026 11:40:01 -0400 Subject: [PATCH 01/44] maxSubSessionTokens and maxDelegationDepth config settings --- salt/soc/defaults.yaml | 2 ++ salt/soc/soc_soc.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/salt/soc/defaults.yaml b/salt/soc/defaults.yaml index 7e8e76094..afb4c37d1 100644 --- a/salt/soc/defaults.yaml +++ b/salt/soc/defaults.yaml @@ -1509,6 +1509,8 @@ soc: assistant: systemPromptAddendum: "" systemPromptAddendumMaxLength: 50000 + maxSubSessionTokens: 0 + maxDelegationDepth: 0 adapters: - name: SOAI protocol: securityonion_ai_cloud diff --git a/salt/soc/soc_soc.yaml b/salt/soc/soc_soc.yaml index 19853196a..cf483c406 100644 --- a/salt/soc/soc_soc.yaml +++ b/salt/soc/soc_soc.yaml @@ -719,6 +719,16 @@ soc: description: Maximum length of the system prompt addendum. Longer prompts will be truncated. global: True advanced: True + maxSubSessionTokens: + description: Maximum number of output tokens a delegated sub-session may generate across all of its turns. When the budget is reached, the sub-agent is halted and its result is returned to the parent agent. Set to 0 to disable the limit. + global: True + advanced: True + forcedType: int + maxDelegationDepth: + description: Maximum delegation nesting depth for sub-agents. For example, a value of 2 lets the main agent delegate to a sub-agent that may itself delegate one level deeper. Any deeper delegation is refused and the requesting agent continues without it. Set to 0 to disable the limit. + global: True + advanced: True + forcedType: int adapters: description: Configuration for AI adapters used by the Onion AI assistant. Please see documentation for help on which fields are required for which protocols. global: True From 8675296393aa3dd9b5f4f64177602546ec18cf00 Mon Sep 17 00:00:00 2001 From: Corey Ogburn Date: Wed, 1 Jul 2026 15:04:42 -0600 Subject: [PATCH 02/44] More Agentic Fields The big agentic switch, a specific maxDelegationDepth, and the agentMapping dict --- salt/soc/defaults.yaml | 6 +++++- salt/soc/soc_soc.yaml | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/salt/soc/defaults.yaml b/salt/soc/defaults.yaml index afb4c37d1..c751b25f4 100644 --- a/salt/soc/defaults.yaml +++ b/salt/soc/defaults.yaml @@ -1510,7 +1510,7 @@ soc: systemPromptAddendum: "" systemPromptAddendumMaxLength: 50000 maxSubSessionTokens: 0 - maxDelegationDepth: 0 + maxDelegationDepth: 5 adapters: - name: SOAI protocol: securityonion_ai_cloud @@ -1522,6 +1522,10 @@ soc: serviceAccountJSON: "" serviceAccountLocation: "" healthTimeoutSeconds: 5 + agentic: false + agentMapping: + Orchestrator: sonnet + Hunter: sonnet onionconfig: saltstackDir: /opt/so/saltstack bypassEnabled: false diff --git a/salt/soc/soc_soc.yaml b/salt/soc/soc_soc.yaml index cf483c406..a11f8e0e4 100644 --- a/salt/soc/soc_soc.yaml +++ b/salt/soc/soc_soc.yaml @@ -767,6 +767,17 @@ soc: label: Health Timeout Seconds required: False forcedType: int + agentic: + description: Indicates if the Assistant Module should operate in agentic mode or not. If true, agents can work together to solve tasks. + global: True + forcedType: bool + agentMapping: + Orchestrator: + description: The initial agent in most agentic conversations. This agent will delegate requests to specialized agents. + global: True + Hunter: + description: This agent is specialized in querying events. + global: True client: assistant: enabled: From 83cf1f0793efaf8ee446e216698eebb37bd842cb Mon Sep 17 00:00:00 2001 From: Corey Ogburn Date: Thu, 2 Jul 2026 10:11:52 -0600 Subject: [PATCH 03/44] New Client Params for Tool Retries --- salt/soc/defaults.yaml | 2 ++ salt/soc/soc_soc.yaml | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/salt/soc/defaults.yaml b/salt/soc/defaults.yaml index c751b25f4..490a09e3c 100644 --- a/salt/soc/defaults.yaml +++ b/salt/soc/defaults.yaml @@ -2695,6 +2695,8 @@ soc: thresholdColorRatioLow: 0.5 thresholdColorRatioMed: 0.75 thresholdColorRatioMax: 1 + toolBusyMaxRetries: 30 + toolBusyRetryDelayMs: 1000 availableModels: - id: sonnet displayName: Claude Sonnet diff --git a/salt/soc/soc_soc.yaml b/salt/soc/soc_soc.yaml index a11f8e0e4..5e171cd43 100644 --- a/salt/soc/soc_soc.yaml +++ b/salt/soc/soc_soc.yaml @@ -784,6 +784,12 @@ soc: description: Set to true to enable the Onion AI assistant in SOC. global: True forcedType: bool + toolBusyMaxRetries: + description: How many times to retry auto approving a tool while a tool is already running. + global: True + toolBusyRetryDelayMs: + description: How long in milliseconds to wait between each retry when auto approving a tool. + global: True investigationPrompt: description: Prompt given to Onion AI when beginning an investigation. global: True From ef83450107ce82935eb6fa46339e36a348160615 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Tue, 7 Jul 2026 16:54:06 -0400 Subject: [PATCH 04/44] recollate --- salt/manager/tools/sbin/soup | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index 6725cc95c..f0be61546 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -789,7 +789,7 @@ bootstrap_so_soc_database() { # and runs automatically only on a fresh data directory. Hosts upgrading from # 3.1.0 already have /nsm/postgres populated, so the so_soc bootstrap block # added in 3.2 never fires. Re-run the script explicitly; it's idempotent. - echo "Bootstrapping so_soc database via init-db.sh." + echo "Bootstrapping database via init-db.sh." # The postgres image has no USER directive, so `docker exec` defaults to # root, and the container env intentionally omits POSTGRES_USER (the upstream # entrypoint defaults it transiently during first-init only). Recreate both @@ -800,10 +800,10 @@ bootstrap_so_soc_database() { return 0 fi if ! $exec_cmd; then - FINAL_MESSAGE_QUEUE+=("WARNING: init-db.sh failed inside so-postgres during the 3.2.0 upgrade; the so_soc database may not have been bootstrapped. Re-run manually: $exec_cmd") + FINAL_MESSAGE_QUEUE+=("WARNING: init-db.sh failed inside so-postgres during the 3.2.0 upgrade; the database may not have been bootstrapped. Re-run manually: $exec_cmd") return 0 fi - echo "so_soc bootstrap complete." + echo "Database bootstrap complete." } # Existing grids should keep ILM unless an admin explicitly opts in to DLM. @@ -873,6 +873,16 @@ update_kafka_metadata() { fi } +recollate_postgres() { + for db in postgres securityonion so-telegraf; do + exec_cmd="docker exec so-postgres psql -U postgres $db -c \"reindex database $db; alter database $db refresh collation version;\"" + if ! $exec_cmd; then + FINAL_MESSAGE_QUEUE+=("WARNING: recollating $db database failed. Re-run manually: $exec_cmd") + return 0 + fi + done +} + up_to_3.2.0() { fix_logstash_0013_lumberjack_pipeline_name @@ -884,6 +894,9 @@ up_to_3.2.0() { post_to_3.2.0() { bootstrap_so_soc_database + # Recollate due to image OS rebase + recollate_postgres + # Including agent regen script here since it was missed in post_to_3.1.0 echo "Regenerating Elastic Agent Installers" /sbin/so-elastic-agent-gen-installers From 57b7d59387269ab6a26c45bf7fc5e35cdad57d26 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:42:47 -0500 Subject: [PATCH 05/44] verify elastic-agent reports healthy status before completing installation --- salt/elasticfleet/install_agent_grid.sls | 8 +- .../tools/sbin/so-elastic-agent-install | 75 +++++++++++++++++++ 2 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 salt/elasticfleet/tools/sbin/so-elastic-agent-install diff --git a/salt/elasticfleet/install_agent_grid.sls b/salt/elasticfleet/install_agent_grid.sls index 482af2e1e..431aa6f97 100644 --- a/salt/elasticfleet/install_agent_grid.sls +++ b/salt/elasticfleet/install_agent_grid.sls @@ -21,11 +21,9 @@ pull_agent_installer: run_installer: cmd.run: - - name: ./so-elastic-agent_linux_amd64 -token={{ GRIDNODETOKEN }} -force - - cwd: /opt/so - - retry: - attempts: 3 - interval: 20 + - name: /usr/sbin/so-elastic-agent-install "{{ GRIDNODETOKEN }}" + - require: + - file: pull_agent_installer cleanup_agent_installer: file.absent: diff --git a/salt/elasticfleet/tools/sbin/so-elastic-agent-install b/salt/elasticfleet/tools/sbin/so-elastic-agent-install new file mode 100644 index 000000000..dda3f1672 --- /dev/null +++ b/salt/elasticfleet/tools/sbin/so-elastic-agent-install @@ -0,0 +1,75 @@ +#!/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. + +. /usr/sbin/so-elastic-fleet-common + + +# passed in as arg from elasticfleet/install_agent_grid.sls, else pulled from pillar later +GRIDNODETOKEN="$1" + +check_agent_health() { + timeout=180 + interval=10 + start=$SECONDS + + while (( SECONDS - start < timeout )); do + if elastic-agent status 2>/dev/null | grep -A1 'elastic-agent$' | grep -q 'status: (HEALTHY)'; then + return 0 + fi + echo "elastic-agent is not yet healthy. Waiting for ${interval} seconds before checking again..." + sleep "$interval" + done + + echo "elastic-agent did not become healthy within ${timeout} seconds" + return 1 +} + +if command -v elastic-agent >/dev/null 2>&1; then + elastic-agent uninstall -f +fi + +if [[ -z "$GRIDNODETOKEN" ]]; then + noderole=$(so-yaml.py get -r /etc/salt/grains role) + if [[ "$noderole" == "so-heavynode" ]]; then + GRIDNODETOKEN=$(salt-call pillar.get global:fleet_grid_enrollment_token_heavy --out=newline_values_only) + else + GRIDNODETOKEN=$(salt-call pillar.get global:fleet_grid_enrollment_token_general --out=newline_values_only) + fi +fi + +if [[ ! -x /opt/so/so-elastic-agent_linux_amd64 ]]; then + echo "Downloading elastic-agent installer... This could take a while if another Salt job is running." + salt-call state.sls_id pull_agent_installer elasticfleet.install_agent_grid queue=True +fi + +if [[ -x /opt/so/so-elastic-agent_linux_amd64 ]]; then + attempts=0 + cd /opt/so/ || exit 1 + + while [[ $attempts -lt 3 ]]; do + if ./so-elastic-agent_linux_amd64 -token="$GRIDNODETOKEN" -force; echo "Verifying elastic-agent health..." && check_agent_health; then + rm -f /opt/so/so-elastic-agent_linux_amd64 + elastic-agent status + + exit 0 + fi + + attempts=$((attempts + 1)) + + if [[ $attempts -lt 3 ]]; then + echo "so-elastic-agent installer failed. Retrying in 20 seconds..." + sleep 20 + fi + done + + echo "so-elastic-agent installer failed after 3 attempts. Exiting." + + exit 1 +else + echo "Unable to locate so-elastic-agent installer. Exiting." + + exit 1 +fi From 70af3cec534dfa2c5122893ce73ed1c9b29632d4 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:48:10 -0500 Subject: [PATCH 06/44] avoid using 'failure' until all loops are done so so-verify doesn't flag it --- .../elasticfleet/tools/sbin/so-elastic-agent-install | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/salt/elasticfleet/tools/sbin/so-elastic-agent-install b/salt/elasticfleet/tools/sbin/so-elastic-agent-install index dda3f1672..fd6bceeff 100644 --- a/salt/elasticfleet/tools/sbin/so-elastic-agent-install +++ b/salt/elasticfleet/tools/sbin/so-elastic-agent-install @@ -19,11 +19,11 @@ check_agent_health() { if elastic-agent status 2>/dev/null | grep -A1 'elastic-agent$' | grep -q 'status: (HEALTHY)'; then return 0 fi - echo "elastic-agent is not yet healthy. Waiting for ${interval} seconds before checking again..." + echo "The Elastic Agent is not yet healthy. Waiting for ${interval} seconds before checking again..." sleep "$interval" done - echo "elastic-agent did not become healthy within ${timeout} seconds" + echo "The Elastic Agent did not become healthy within ${timeout} seconds" return 1 } @@ -41,7 +41,7 @@ if [[ -z "$GRIDNODETOKEN" ]]; then fi if [[ ! -x /opt/so/so-elastic-agent_linux_amd64 ]]; then - echo "Downloading elastic-agent installer... This could take a while if another Salt job is running." + echo "Downloading so-elastic-agent installer... This could take a while if another Salt job is running." salt-call state.sls_id pull_agent_installer elasticfleet.install_agent_grid queue=True fi @@ -50,7 +50,7 @@ if [[ -x /opt/so/so-elastic-agent_linux_amd64 ]]; then cd /opt/so/ || exit 1 while [[ $attempts -lt 3 ]]; do - if ./so-elastic-agent_linux_amd64 -token="$GRIDNODETOKEN" -force; echo "Verifying elastic-agent health..." && check_agent_health; then + if ./so-elastic-agent_linux_amd64 -token="$GRIDNODETOKEN" -force; echo "Verifying Elastic Agent health..." && check_agent_health; then rm -f /opt/so/so-elastic-agent_linux_amd64 elastic-agent status @@ -60,12 +60,12 @@ if [[ -x /opt/so/so-elastic-agent_linux_amd64 ]]; then attempts=$((attempts + 1)) if [[ $attempts -lt 3 ]]; then - echo "so-elastic-agent installer failed. Retrying in 20 seconds..." + echo "Unable to verify Elastic Agent health... Retrying in 20 seconds..." sleep 20 fi done - echo "so-elastic-agent installer failed after 3 attempts. Exiting." + echo "The so-elastic-agent installer failed after 3 attempts. Exiting." exit 1 else From 7f6014096b199e4aa38bc36fa17e4433ee9aa20e Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Tue, 7 Jul 2026 22:01:27 -0400 Subject: [PATCH 07/44] recollate db --- salt/manager/tools/sbin/soup | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index f0be61546..9264d9dca 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -874,12 +874,9 @@ update_kafka_metadata() { } recollate_postgres() { - for db in postgres securityonion so-telegraf; do - exec_cmd="docker exec so-postgres psql -U postgres $db -c \"reindex database $db; alter database $db refresh collation version;\"" - if ! $exec_cmd; then - FINAL_MESSAGE_QUEUE+=("WARNING: recollating $db database failed. Re-run manually: $exec_cmd") - return 0 - fi + for db in postgres securityonion so_telegraf; do + docker exec so-postgres psql -U postgres $db -c "reindex database $db" + docker exec so-postgres psql -U postgres $db -c "alter database $db refresh collation version" done } From 8a8f2c4a336a103ea90449414b0fa16cb8b2d962 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Tue, 7 Jul 2026 22:22:00 -0400 Subject: [PATCH 08/44] change order to recollate first --- salt/manager/tools/sbin/soup | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index 9264d9dca..56f4c66dd 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -889,11 +889,11 @@ up_to_3.2.0() { } post_to_3.2.0() { - bootstrap_so_soc_database - # Recollate due to image OS rebase recollate_postgres + bootstrap_so_soc_database + # Including agent regen script here since it was missed in post_to_3.1.0 echo "Regenerating Elastic Agent Installers" /sbin/so-elastic-agent-gen-installers From d131d167de8e2b5633b84076a8f9116497185dcd Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Wed, 8 Jul 2026 07:00:38 -0400 Subject: [PATCH 09/44] provide explanation text --- salt/manager/tools/sbin/soup | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index 56f4c66dd..aee767db7 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -874,10 +874,14 @@ update_kafka_metadata() { } recollate_postgres() { + echo "" + echo "Recollating PostgreSQL databases. The following output may contain warnings about a version mismatch, followed by a note indicating that the collation version has been changed." for db in postgres securityonion so_telegraf; do docker exec so-postgres psql -U postgres $db -c "reindex database $db" docker exec so-postgres psql -U postgres $db -c "alter database $db refresh collation version" done + echo "Recollating PostgreSQL databases complete." + echo "" } up_to_3.2.0() { From f8de176f4bbbde8b8fd5f6aae06b48c93d1709b1 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Wed, 8 Jul 2026 07:47:47 -0500 Subject: [PATCH 10/44] uninstall agent on final failed attempt --- .../tools/sbin/so-elastic-agent-install | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/salt/elasticfleet/tools/sbin/so-elastic-agent-install b/salt/elasticfleet/tools/sbin/so-elastic-agent-install index fd6bceeff..920b17137 100644 --- a/salt/elasticfleet/tools/sbin/so-elastic-agent-install +++ b/salt/elasticfleet/tools/sbin/so-elastic-agent-install @@ -27,9 +27,13 @@ check_agent_health() { return 1 } -if command -v elastic-agent >/dev/null 2>&1; then - elastic-agent uninstall -f -fi +uninstall_agent() { + if command -v elastic-agent >/dev/null 2>&1; then + elastic-agent uninstall -f + fi +} + +uninstall_agent if [[ -z "$GRIDNODETOKEN" ]]; then noderole=$(so-yaml.py get -r /etc/salt/grains role) @@ -40,6 +44,12 @@ if [[ -z "$GRIDNODETOKEN" ]]; then fi fi +if [[ -z "$GRIDNODETOKEN" ]]; then + echo "Unable to determine Elastic Fleet enrollment token. Exiting." + + exit 1 +fi + if [[ ! -x /opt/so/so-elastic-agent_linux_amd64 ]]; then echo "Downloading so-elastic-agent installer... This could take a while if another Salt job is running." salt-call state.sls_id pull_agent_installer elasticfleet.install_agent_grid queue=True @@ -50,7 +60,7 @@ if [[ -x /opt/so/so-elastic-agent_linux_amd64 ]]; then cd /opt/so/ || exit 1 while [[ $attempts -lt 3 ]]; do - if ./so-elastic-agent_linux_amd64 -token="$GRIDNODETOKEN" -force; echo "Verifying Elastic Agent health..." && check_agent_health; then + if ./so-elastic-agent_linux_amd64 -token="$GRIDNODETOKEN" -force && echo "Verifying Elastic Agent health..." && check_agent_health; then rm -f /opt/so/so-elastic-agent_linux_amd64 elastic-agent status @@ -65,6 +75,8 @@ if [[ -x /opt/so/so-elastic-agent_linux_amd64 ]]; then fi done + uninstall_agent + rm -f /opt/so/so-elastic-agent_linux_amd64 echo "The so-elastic-agent installer failed after 3 attempts. Exiting." exit 1 From 2a4a7307f70d2b39229b3abb545d80554999a51f Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Wed, 8 Jul 2026 07:51:13 -0500 Subject: [PATCH 11/44] uninstall agent after downloading new one and getting gridtoken --- salt/elasticfleet/tools/sbin/so-elastic-agent-install | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/elasticfleet/tools/sbin/so-elastic-agent-install b/salt/elasticfleet/tools/sbin/so-elastic-agent-install index 920b17137..bda8306f0 100644 --- a/salt/elasticfleet/tools/sbin/so-elastic-agent-install +++ b/salt/elasticfleet/tools/sbin/so-elastic-agent-install @@ -33,8 +33,6 @@ uninstall_agent() { fi } -uninstall_agent - if [[ -z "$GRIDNODETOKEN" ]]; then noderole=$(so-yaml.py get -r /etc/salt/grains role) if [[ "$noderole" == "so-heavynode" ]]; then @@ -55,6 +53,8 @@ if [[ ! -x /opt/so/so-elastic-agent_linux_amd64 ]]; then salt-call state.sls_id pull_agent_installer elasticfleet.install_agent_grid queue=True fi +uninstall_agent + if [[ -x /opt/so/so-elastic-agent_linux_amd64 ]]; then attempts=0 cd /opt/so/ || exit 1 From 85d7f6bebcf406fde9425171b12a1b5f182ac076 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:19:15 -0500 Subject: [PATCH 12/44] independently download so-elastic-agent installer --- .../tools/sbin/so-elastic-agent-install | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/salt/elasticfleet/tools/sbin/so-elastic-agent-install b/salt/elasticfleet/tools/sbin/so-elastic-agent-install index bda8306f0..c6a499e94 100644 --- a/salt/elasticfleet/tools/sbin/so-elastic-agent-install +++ b/salt/elasticfleet/tools/sbin/so-elastic-agent-install @@ -50,15 +50,23 @@ fi if [[ ! -x /opt/so/so-elastic-agent_linux_amd64 ]]; then echo "Downloading so-elastic-agent installer... This could take a while if another Salt job is running." - salt-call state.sls_id pull_agent_installer elasticfleet.install_agent_grid queue=True -fi -uninstall_agent + # When running outside of elasticfleet/install_agent_grid.sls we need to download the installer independently. + # PYTHONWARNINGS="ignore" to avoid messages like the following when running salt-call: + # '/opt/saltstack/salt/lib/python3.10/site-packages/salt/transport/base.py:129: TransportWarning: Unclosed transport! + # File "/bin/salt-call", line 12, in + # sys.exit(salt_call())' + + PYTHONWARNINGS="ignore" salt-call state.single file.managed name=/opt/so/so-elastic-agent_linux_amd64 source=salt://elasticfleet/files/so_agent-installers/so-elastic-agent_linux_amd64 mode=755 makedirs=True queue=True + +fi if [[ -x /opt/so/so-elastic-agent_linux_amd64 ]]; then attempts=0 cd /opt/so/ || exit 1 + uninstall_agent + while [[ $attempts -lt 3 ]]; do if ./so-elastic-agent_linux_amd64 -token="$GRIDNODETOKEN" -force && echo "Verifying Elastic Agent health..." && check_agent_health; then rm -f /opt/so/so-elastic-agent_linux_amd64 From 1f44e9868102ddabf5fee09d73c45615c53df90a Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Wed, 8 Jul 2026 14:34:27 -0400 Subject: [PATCH 13/44] restart soc after re-initing db --- salt/manager/tools/sbin/soup | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index aee767db7..e33c27f6f 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -784,6 +784,17 @@ post_to_3.1.0() { ### 3.2.0 Scripts ### +recollate_postgres() { + echo "" + echo "Recollating PostgreSQL databases. The following output may contain warnings about a version mismatch, followed by a note indicating that the collation version has been changed." + for db in postgres securityonion so_telegraf; do + docker exec so-postgres psql -U postgres $db -c "reindex database $db" + docker exec so-postgres psql -U postgres $db -c "alter database $db refresh collation version" + done + echo "Recollating PostgreSQL databases complete." + echo "" +} + bootstrap_so_soc_database() { # init-db.sh is mounted into so-postgres at /docker-entrypoint-initdb.d/init-db.sh # and runs automatically only on a fresh data directory. Hosts upgrading from @@ -804,6 +815,9 @@ bootstrap_so_soc_database() { return 0 fi echo "Database bootstrap complete." + + echo "Restarting so-soc container to pick up database changes" + docker restart so-soc } # Existing grids should keep ILM unless an admin explicitly opts in to DLM. @@ -873,17 +887,6 @@ update_kafka_metadata() { fi } -recollate_postgres() { - echo "" - echo "Recollating PostgreSQL databases. The following output may contain warnings about a version mismatch, followed by a note indicating that the collation version has been changed." - for db in postgres securityonion so_telegraf; do - docker exec so-postgres psql -U postgres $db -c "reindex database $db" - docker exec so-postgres psql -U postgres $db -c "alter database $db refresh collation version" - done - echo "Recollating PostgreSQL databases complete." - echo "" -} - up_to_3.2.0() { fix_logstash_0013_lumberjack_pipeline_name From 5a57bbe4de6bb60e7c55ebfa3b44cdad5bd2c308 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:03:31 -0500 Subject: [PATCH 14/44] add missing so-logs-soc annotation --- salt/elasticsearch/soc_elasticsearch.yaml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/salt/elasticsearch/soc_elasticsearch.yaml b/salt/elasticsearch/soc_elasticsearch.yaml index 46e836c9c..95880bb9d 100644 --- a/salt/elasticsearch/soc_elasticsearch.yaml +++ b/salt/elasticsearch/soc_elasticsearch.yaml @@ -28,14 +28,14 @@ elasticsearch: description: The maximum number of memory map areas a process may use. Elasticsearch uses a mmapfs directory by default to store its indices. The default operating system limits on mmap counts could be too low, which may result in out of memory exceptions. forcedType: int helpLink: elasticsearch - retention: + retention: retention_pct: decription: Total percentage of space used by Elasticsearch for multi node clusters helpLink: elasticsearch global: True config: cluster: - name: + name: description: The name of the Security Onion Elasticsearch cluster, for identification purposes. readonly: True global: True @@ -55,13 +55,13 @@ elasticsearch: forcedType: bool helpLink: elasticsearch watermark: - low: + low: description: The lower percentage of used disk space representing a healthy node. helpLink: elasticsearch - high: + high: description: The higher percentage of used disk space representing an unhealthy node. helpLink: elasticsearch - flood_stage: + flood_stage: description: The max percentage of used disk space that will cause the node to take protective actions, such as blocking incoming events. helpLink: elasticsearch action: @@ -172,11 +172,11 @@ elasticsearch: forcedType: int global: True helpLink: elasticsearch - refresh_interval: + refresh_interval: description: Seconds between index refreshes. Shorter intervals can cause query performance to suffer since this is a synchronous and resource-intensive operation. global: True helpLink: elasticsearch - number_of_shards: + number_of_shards: description: Number of shards required for this index. Using multiple shards increases fault tolerance, but also increases storage and network costs. global: True helpLink: elasticsearch @@ -269,7 +269,7 @@ elasticsearch: global: True advanced: True warm: - min_age: + min_age: description: Minimum age of index. ex. 30d - This determines when the index should be moved to the warm tier. Nodes in the warm tier generally don’t need to be as fast as those in the hot tier. It’s important to note that this is calculated relative to the rollover date (NOT the original creation date of the index). For example, if you have an index that is set to rollover after 30 days and warm min_age set to 30 then there will be 30 days from index creation to rollover and then an additional 30 days before moving to warm tier. regex: ^[0-9]{1,5}d$ forcedType: string @@ -370,7 +370,7 @@ elasticsearch: template: settings: index: - number_of_replicas: + number_of_replicas: description: Number of replicas required for this index. Multiple replicas protects against data loss, but also increases storage costs. forcedType: int global: True @@ -391,12 +391,12 @@ elasticsearch: global: True advanced: True helpLink: elasticsearch - refresh_interval: + refresh_interval: description: Seconds between index refreshes. Shorter intervals can cause query performance to suffer since this is a synchronous and resource-intensive operation. global: True advanced: True helpLink: elasticsearch - number_of_shards: + number_of_shards: description: Number of shards required for this index. Using multiple shards increases fault tolerance, but also increases storage and network costs. global: True advanced: True @@ -645,6 +645,7 @@ elasticsearch: global: True advanced: True helpLink: elasticsearch + so-logs-soc: *dataStreamSettings so-logs-system_x_auth: *dataStreamSettings so-logs-system_x_syslog: *dataStreamSettings so-logs-system_x_system: *dataStreamSettings From c04a30785f06218510b3d37a917ab9e02e21b977 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:15:02 -0500 Subject: [PATCH 15/44] make elastic agent state persistent to prevent re-enrollment during soup / reboots --- salt/common/tools/sbin/so-restart | 3 +++ salt/common/tools/sbin/so-stop | 2 ++ salt/elasticfleet/enabled.sls | 10 ++++++++++ 3 files changed, 15 insertions(+) diff --git a/salt/common/tools/sbin/so-restart b/salt/common/tools/sbin/so-restart index 14747d134..b3864d2d1 100755 --- a/salt/common/tools/sbin/so-restart +++ b/salt/common/tools/sbin/so-restart @@ -35,6 +35,9 @@ case $1 in "elastic-fleet"|"elasticfleet") docker_check_running "elastic-fleet" "--stop" docker rm "so-elastic-fleet" 2> /dev/null + # Removing the elastic fleet state directory, so that the next startup re-enrolls with a fresh policy + rm -rf /opt/so/conf/elastic-fleet/state + salt-call state.apply elasticfleet queue=True ;; *) diff --git a/salt/common/tools/sbin/so-stop b/salt/common/tools/sbin/so-stop index d036a7b63..9218b44f7 100755 --- a/salt/common/tools/sbin/so-stop +++ b/salt/common/tools/sbin/so-stop @@ -29,6 +29,8 @@ case $1 in "elasticfleet"|"elastic-fleet") docker_check_running "elastic-fleet" "--stop" docker rm "so-elastic-fleet" 2> /dev/null + # Removing the elastic fleet state directory, so that the next startup re-enrolls with a fresh policy + rm -rf /opt/so/conf/elastic-fleet/state ;; *) docker_check_running "$1" "--stop" diff --git a/salt/elasticfleet/enabled.sls b/salt/elasticfleet/enabled.sls index 60cf27deb..ea64a4cb1 100644 --- a/salt/elasticfleet/enabled.sls +++ b/salt/elasticfleet/enabled.sls @@ -11,6 +11,10 @@ {# This value is generated during node install and stored in minion pillar #} {% set SERVICETOKEN = salt['pillar.get']('elasticfleet:config:server:es_token','') %} +{# Prevent Elastic Agent from re-enrolling with a new agent.id everytime the container starts up. + - if a fresh enrollment is needed use 'so-stop elasticfleet' + #} +{% set ENROLLED = salt['file.file_exists']('/opt/so/conf/elastic-fleet/state/fleet.enc') %} include: - ca @@ -65,6 +69,7 @@ so-elastic-fleet: - /etc/pki/elasticfleet-server.crt:/etc/pki/elasticfleet-server.crt:ro - /etc/pki/elasticfleet-server.key:/etc/pki/elasticfleet-server.key:ro - /etc/pki/tls/certs/intca.crt:/etc/pki/tls/certs/intca.crt:ro + - /opt/so/conf/elastic-fleet/state:/usr/share/elastic-agent/state - /opt/so/log/elasticfleet:/usr/share/elastic-agent/logs {% if DOCKERMERGED.containers['so-elastic-fleet'].custom_bind_mounts %} {% for BIND in DOCKERMERGED.containers['so-elastic-fleet'].custom_bind_mounts %} @@ -72,6 +77,7 @@ so-elastic-fleet: {% endfor %} {% endif %} - environment: + {% if not ENROLLED %} - FLEET_SERVER_ENABLE=true - FLEET_URL=https://{{ GLOBALS.hostname }}:8220 - FLEET_SERVER_ELASTICSEARCH_HOST=https://{{ GLOBALS.manager }}:9200 @@ -81,6 +87,9 @@ so-elastic-fleet: - FLEET_SERVER_CERT_KEY=/etc/pki/elasticfleet-server.key - FLEET_CA=/etc/pki/tls/certs/intca.crt - FLEET_SERVER_ELASTICSEARCH_CA=/etc/pki/tls/certs/intca.crt + {% endif %} + - STATE_PATH=/usr/share/elastic-agent/state + - CONFIG_PATH=/usr/share/elastic-agent/state - LOGS_PATH=logs {% if DOCKERMERGED.containers['so-elastic-fleet'].extra_env %} {% for XTRAENV in DOCKERMERGED.containers['so-elastic-fleet'].extra_env %} @@ -99,6 +108,7 @@ so-elastic-fleet: - x509: etc_elasticfleet_crt - require: - file: trusttheca + - file: eastatedir - x509: etc_elasticfleet_key - x509: etc_elasticfleet_crt From 3394e9aab78bb832392682593b1a05c2c45190cf Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:29:43 -0500 Subject: [PATCH 16/44] increase agent health timeout and add logging for elastic agent status output --- salt/elasticfleet/tools/sbin/so-elastic-agent-install | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/salt/elasticfleet/tools/sbin/so-elastic-agent-install b/salt/elasticfleet/tools/sbin/so-elastic-agent-install index c6a499e94..93889c002 100644 --- a/salt/elasticfleet/tools/sbin/so-elastic-agent-install +++ b/salt/elasticfleet/tools/sbin/so-elastic-agent-install @@ -9,14 +9,19 @@ # passed in as arg from elasticfleet/install_agent_grid.sls, else pulled from pillar later GRIDNODETOKEN="$1" +LOGFILE="/opt/so/SO-Elastic-Agent_Installer_Health.log" check_agent_health() { - timeout=180 + timeout=300 interval=10 start=$SECONDS + truncate -s 0 "$LOGFILE" + while (( SECONDS - start < timeout )); do - if elastic-agent status 2>/dev/null | grep -A1 'elastic-agent$' | grep -q 'status: (HEALTHY)'; then + agent_status=$(elastic-agent status 2>&1) + echo -e "\n$(date)\n$agent_status\n" >> "$LOGFILE" + if echo "$agent_status" | grep -A1 'elastic-agent$' | grep -q 'status: (HEALTHY)'; then return 0 fi echo "The Elastic Agent is not yet healthy. Waiting for ${interval} seconds before checking again..." From 6fa0d327cbafd9a18d38a4f3bb51b1f145e74cfe Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:14:58 -0500 Subject: [PATCH 17/44] cause so-setup to fail if there are issues setting up fleet --- .../tools/sbin_jinja/so-elastic-fleet-setup | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup b/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup index b62310375..2b677606a 100755 --- a/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup +++ b/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup @@ -244,11 +244,27 @@ printf '%s\n'\ "" >> "$global_pillar_file" # Call Elastic-Fleet Salt State -printf "\nApplying elasticfleet state" -salt-call state.apply elasticfleet queue=True +printf "\nApplying elasticfleet state\n" +if ! salt-call state.apply elasticfleet queue=True; then + printf "\nFailure(s) in elasticfleet state... Exiting...\n" + exit 1 +fi # Generate installers & install Elastic Agent on the node -so-elastic-agent-gen-installers -printf "\nApplying elasticfleet.install_agent_grid state" -salt-call state.apply elasticfleet.install_agent_grid queue=True -exit 0 +for agent_gen_attempt in {1..3}; do + if so-elastic-agent-gen-installers; then + break + elif [[ $agent_gen_attempt -lt 3 ]]; then + printf "\nUnable to generate Elastic Agent installers... Attempt (%s/3). Retrying...\n" "$agent_gen_attempt" + sleep 10 + else + printf "\nFailed to generate Elastic Agent installers after 3 attempts. Exiting...\n" + exit 1 + fi +done + +printf "\nApplying elasticfleet.install_agent_grid state\n" +if ! salt-call state.apply elasticfleet.install_agent_grid queue=True; then + printf "\nFailure(s) in elasticfleet.install_agent_grid state... Exiting...\n" + exit 1 +fi \ No newline at end of file From 8b0759866ea8d2c121c0eb32b9bdf581fed6c7e8 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:15:22 -0500 Subject: [PATCH 18/44] verify installers are generated --- .../so-elastic-agent-gen-installers | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/salt/elasticfleet/tools/sbin_jinja/so-elastic-agent-gen-installers b/salt/elasticfleet/tools/sbin_jinja/so-elastic-agent-gen-installers index d25c18e29..de342657f 100755 --- a/salt/elasticfleet/tools/sbin_jinja/so-elastic-agent-gen-installers +++ b/salt/elasticfleet/tools/sbin_jinja/so-elastic-agent-gen-installers @@ -30,7 +30,7 @@ done if [[ -z $FLEETHOST ]] || [[ -z $ENROLLMENTOKEN ]]; then printf "\nFleet Host URL, Enrollment Token or Elastic Version empty - exiting..." printf "\nFleet Host: $FLEETHOST, Enrollment Token: $ENROLLMENTOKEN\n" - exit + exit 1 fi OSARCH=( "linux-x86_64" "windows-x86_64" "darwin-x86_64" "darwin-aarch64" ) @@ -62,31 +62,54 @@ do done GOTARGETOS=( "linux" "windows" "darwin" "darwin/arm64" ) -GOARCH="amd64" printf "\n### Generating OS packages using the cleaned up tarballs" -for GOOS in "${GOTARGETOS[@]}" -do +for GOOS in "${GOTARGETOS[@]}"; do + GOARCH="amd64" if [[ $GOOS == 'darwin/arm64' ]]; then GOOS="darwin" && GOARCH="arm64"; fi printf "\n\n### Generating $GOOS/$GOARCH Installer...\n" docker run -e CGO_ENABLED=0 -e GOOS=$GOOS -e GOARCH=$GOARCH \ --mount type=bind,source=/etc/pki/tls/certs/,target=/workspace/files/cert/ \ --mount type=bind,source=/nsm/elastic-agent-workspace/,target=/workspace/files/elastic-agent/ \ - --mount type=bind,source=/opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/,target=/output/ \ + --mount type=bind,source=/opt/so/saltstack/local/salt/elasticfleet/files/,target=/output/ \ {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent-builder:{{ GLOBALS.so_version }} go build -ldflags "-X main.fleetHostURLsList=$FLEETHOST -X main.enrollmentToken=$ENROLLMENTOKEN" -o /output/so-elastic-agent_${GOOS}_${GOARCH} printf "\n### $GOOS/$GOARCH Installer Generated...\n" done printf "\n\n### Generating MSI...\n" -cp /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/so-elastic-agent_windows_amd64 /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/so-elastic-agent_windows_amd64.exe +cp /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_windows_amd64 /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_windows_amd64.exe docker run \ ---mount type=bind,source=/opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/,target=/output/ -w /output \ +--mount type=bind,source=/opt/so/saltstack/local/salt/elasticfleet/files/,target=/output/ -w /output \ {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent-builder:{{ GLOBALS.so_version }} wixl -o so-elastic-agent_windows_amd64_msi --arch x64 /workspace/so-elastic-agent.wxs printf "\n### MSI Generated...\n" +# Verify installers were created +for GOOS in "${GOTARGETOS[@]}"; do + GOARCH="amd64" + if [[ $GOOS == 'darwin/arm64' ]]; then GOOS="darwin"; GOARCH="arm64"; fi + if [[ ! -f /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_${GOOS}_${GOARCH} ]]; then + printf "\n### ERROR: Installer for %s/%s was not generated. Exiting...\n" "$GOOS" "$GOARCH" + exit 1 + fi + # After verifying new installer was generated, move it to so_agent-installers directory + mv /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_${GOOS}_${GOARCH} /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/ +done + +# Verify MSI installer +if [[ ! -f /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_windows_amd64_msi ]]; then + printf "\n### ERROR: Installer MSI was not generated. Exiting...\n" + exit 1 +else + # After verifying new installer MSI was generated, move it to so_agent-installers directory + mv /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_windows_amd64_msi /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/ +fi + printf "\n### Cleaning up temp files \n" rm -rf /nsm/elastic-agent-workspace -rm -rf /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/so-elastic-agent_windows_amd64.exe +rm -rf /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_windows_amd64.exe printf "\n### Copying so_agent-installers to /nsm/elastic-fleet/ for nginx.\n" \cp -vr /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/ /nsm/elastic-fleet/ chmod 644 /nsm/elastic-fleet/so_agent-installers/* + +# if we got here all installers have been generated successfully +exit 0 From 2959dc95644a4372dadd425fd72929d32e093177 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:18:08 -0500 Subject: [PATCH 19/44] keep logs for all 3 attempts --- salt/elasticfleet/tools/sbin/so-elastic-agent-install | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/elasticfleet/tools/sbin/so-elastic-agent-install b/salt/elasticfleet/tools/sbin/so-elastic-agent-install index 93889c002..8f57d217c 100644 --- a/salt/elasticfleet/tools/sbin/so-elastic-agent-install +++ b/salt/elasticfleet/tools/sbin/so-elastic-agent-install @@ -16,8 +16,6 @@ check_agent_health() { interval=10 start=$SECONDS - truncate -s 0 "$LOGFILE" - while (( SECONDS - start < timeout )); do agent_status=$(elastic-agent status 2>&1) echo -e "\n$(date)\n$agent_status\n" >> "$LOGFILE" @@ -70,6 +68,8 @@ if [[ -x /opt/so/so-elastic-agent_linux_amd64 ]]; then attempts=0 cd /opt/so/ || exit 1 + truncate -s 0 "$LOGFILE" + uninstall_agent while [[ $attempts -lt 3 ]]; do From 0b078c48041a30947efbb6dc8bf4945e26272339 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:23:45 -0500 Subject: [PATCH 20/44] give the elasticfleet state a few chances to complete successfully before exiting 1 causing so-setup to fail --- .../tools/sbin_jinja/so-elastic-fleet-setup | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup b/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup index 2b677606a..21d74cf6f 100755 --- a/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup +++ b/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup @@ -245,10 +245,17 @@ printf '%s\n'\ # Call Elastic-Fleet Salt State printf "\nApplying elasticfleet state\n" -if ! salt-call state.apply elasticfleet queue=True; then - printf "\nFailure(s) in elasticfleet state... Exiting...\n" - exit 1 -fi +for state_attempt in {1..3}; do + if salt-call state.apply elasticfleet queue=True; then + break + elif [[ $state_attempt -lt 3 ]]; then + printf "\nElasticfleet state did not complete successfully... Attempt (%s/3). Retrying...\n" "$state_attempt" + sleep 10 + else + printf "\nFailure(s) in elasticfleet state... Exiting...\n" + exit 1 + fi +done # Generate installers & install Elastic Agent on the node for agent_gen_attempt in {1..3}; do From f6a2758321891c9e12f0d4efe29a76ee446067e5 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:44:42 -0500 Subject: [PATCH 21/44] add so-elastic-agent-install script to minions --- salt/elasticfleet/install_agent_grid.sls | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/salt/elasticfleet/install_agent_grid.sls b/salt/elasticfleet/install_agent_grid.sls index 431aa6f97..7ed727b48 100644 --- a/salt/elasticfleet/install_agent_grid.sls +++ b/salt/elasticfleet/install_agent_grid.sls @@ -10,6 +10,15 @@ {% set AGENT_STATUS = salt['service.available']('elastic-agent') %} {% set AGENT_EXISTS = salt['file.file_exists']('/opt/Elastic/Agent/elastic-agent') %} +so-elastic-agent-install: + file.managed: + - name: /usr/sbin/so-elastic-agent-install + - source: salt://elasticfleet/tools/sbin/so-elastic-agent-install + - user: 947 + - group: 939 + - mode: 755 + - show_changes: False + {% if not AGENT_STATUS or not AGENT_EXISTS %} pull_agent_installer: From 8a3f5d0f819ebd2b2059aa71a446e0af9335a31a Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Thu, 9 Jul 2026 16:55:30 -0400 Subject: [PATCH 22/44] Gate so-salt-minion-wait on real minion readiness The previous gate did not detect whether the restarted minion was back: systemctl is-active --quiet salt-minion \ && salt-call --local --timeout=5 --out=quiet test.ping Both halves are near-vacuous. `--local` sets file_client=local, so test.ping runs in a throwaway minion that never contacts the master and never inspects the running daemon; it only proves python and the module loader work. And the shipped unit is Type=notify with notify_systemd() called before the daemon imports salt.cli.daemons, so is-active goes true at process launch, not at connection. The script could return ready while the minion was still authenticating, which is the race it exists to prevent. Gate instead on the condition salt itself uses to log "Minion is ready to receive requests!", requiring both signals of the current daemon instance: 1. the pid-tagged ready line in the minion log. tune_in() emits it only after sync_connect_master() returns, i.e. the pub channel authenticated, the req channel connected, and _post_master_init() finished loading modules and compiling pillar. 2. that same pid holding an ESTABLISHED req connection to a master on 4506 plus a second (publish) connection to the same master IP. The publish port is absent from minion config -- the minion learns it from the master's auth reply -- so it is derived from the connection. Resolve the daemon pid from systemd (MainPID -> pgrep -P), never from /var/run/salt-minion.pid. salt_minion() runs the minion in a multiprocessing child; that child writes the pidfile, owns the sockets and logs the ready line, while MainPID is the parent. During a restart the pidfile still names the old child, whose own ready line is already in the log, so keying off it reports ready instantly. Children of the current MainPID exclude the old instance structurally, with no timing assumptions. Degrade deterministically rather than spinning to the timeout: if log_level_logfile does not emit INFO records the ready line can never appear, so detect that up front from the merged config and fall back to the socket check. log_level_logfile defaults to None (inherit log_level), so resolve the inheritance before deciding. If ss is unavailable, fall back to the log gate. If neither signal is usable, fail immediately with a clear message. Requiring master connectivity adds no new dependency: every path that applies salt.minion or a highstate does so without --local, so file_client=remote already required a reachable master to fetch salt:// files. No salt-call master round-trip is added; the daemon's own successful auth already proves the key is accepted. Also fix the comment above wait_for_salt_minion_ready, which attributed the script to common_sbin/common/tools/sbin (it is deployed by salt_sbin from salt/tools/sbin) and asserted a --no-block restart that appears nowhere in the repo. No state logic changed. --- salt/salt/minion/init.sls | 13 ++- salt/salt/tools/sbin/so-salt-minion-wait | 142 +++++++++++++++++++++-- 2 files changed, 138 insertions(+), 17 deletions(-) diff --git a/salt/salt/minion/init.sls b/salt/salt/minion/init.sls index a251aa633..035d9e7f8 100644 --- a/salt/salt/minion/init.sls +++ b/salt/salt/minion/init.sls @@ -131,11 +131,14 @@ salt_minion_service: {% endif %} - order: last -# block until the just-restarted salt-minion is back and can execute modules locally, so -# follow-on jobs and the next highstate iteration do not race the restart. onchanges + -# require on salt_minion_service catches every restart trigger uniformly because watch -# mod_watch results replace the service state's running entry. wait logic lives in -# /usr/sbin/so-salt-minion-wait (deployed by common_sbin from common/tools/sbin/). +# block until the just-restarted salt-minion daemon logs "Minion is ready to receive requests!" +# for the current instance, so follow-on jobs and the next highstate iteration do not race the +# restart. onchanges + require on salt_minion_service catches every restart trigger uniformly +# because watch mod_watch results replace the service state's running entry. wait logic lives in +# /usr/sbin/so-salt-minion-wait (deployed by salt_sbin from salt/tools/sbin/); it keys the ready +# line to the current daemon pid (resolved via systemd, not the pidfile) and corroborates with the +# master req/publish sockets. set_log_levels above enforces the log_level_logfile: info that the +# ready line depends on. wait_for_salt_minion_ready: cmd.run: - name: /usr/sbin/so-salt-minion-wait diff --git a/salt/salt/tools/sbin/so-salt-minion-wait b/salt/salt/tools/sbin/so-salt-minion-wait index a30c67e80..9fb2a41c7 100644 --- a/salt/salt/tools/sbin/so-salt-minion-wait +++ b/salt/salt/tools/sbin/so-salt-minion-wait @@ -5,31 +5,149 @@ # https://securityonion.net/license; you may not use this file except in compliance with the # Elastic License 2.0. -# Block until the local salt-minion service is back up and can execute modules locally. -# Invoked from the wait_for_salt_minion_ready state in salt/minion/init.sls after -# salt_minion_service fires its watch-driven mod_watch (a non-blocking systemctl restart), -# so follow-on jobs and the next highstate iteration do not race the in-flight restart. +# Block until the just-restarted salt-minion daemon reaches the point where salt itself logs +# "Minion is ready to receive requests!". Invoked from the wait_for_salt_minion_ready state in +# salt/minion/init.sls after salt_minion_service fires its watch-driven restart, so follow-on jobs +# and the next highstate iteration do not race it. +# +# Salt logs that line from Minion.tune_in() only after sync_connect_master() returns, which means +# the pub channel authenticated, the long-running req channel connected, and _post_master_init() +# finished loading modules and compiling pillar. Two signals reproduce that: +# +# 1. Primary the pid-tagged ready line in the minion log. Salt's log_fmt_logfile embeds +# [%(process)d] just before the message, so this is keyed to one daemon instance. +# 2. Corroborating that same pid holds an ESTABLISHED req connection to a master on 4506 plus a +# second (publish) connection to that same master IP on another port. The publish +# port is learned from the master's auth reply and is absent from minion config, +# so it is derived from the connection rather than read from config. +# +# The daemon pid is resolved from systemd, never from /var/run/salt-minion.pid. salt_minion() runs +# the real minion in a multiprocessing child; that child writes the pidfile, owns the sockets and +# logs the ready line, while systemd's MainPID is the parent. During a restart the pidfile can still +# name the OLD child, whose own ready line is already in the log -- matching it would report ready +# instantly. Children of the current MainPID structurally exclude the old instance. . /usr/sbin/so-common -# Initial sleep gives the systemctl restart (--no-block by default for salt-minion on -# >=3006.15) time to begin tearing down the old process before we probe for readiness. +set -u + INITIAL_SLEEP=3 TIMEOUT=120 -PING_TIMEOUT=5 +MASTER_PORT=4506 +LOG_TAIL_LINES=10000 +DEFAULT_LOG_FILE="/opt/so/log/salt/minion" +LOG_FILE="$DEFAULT_LOG_FILE" + +# Decide whether the ready line can ever appear. salt-call --local sets file_client=local, so this +# reads the merged config (honoring minion.d overrides) without contacting the master. salt defaults +# log_level_logfile to None, meaning it inherits log_level, so resolve that before deciding. +LOG_LEVEL_LOGFILE=$(salt-call --local --out=newline_values_only config.get log_level_logfile 2>/dev/null | head -n1) +case "${LOG_LEVEL_LOGFILE,,}" in + ""|none) LOG_LEVEL_LOGFILE=$(salt-call --local --out=newline_values_only config.get log_level 2>/dev/null | head -n1) ;; +esac + +case "${LOG_LEVEL_LOGFILE,,}" in + all|garbage|trace|debug|profile|info) USE_LOG_GATE=1 ;; + *) USE_LOG_GATE=0 ;; +esac + +if [ "$USE_LOG_GATE" -eq 1 ]; then + LOG_FILE=$(salt-call --local --out=newline_values_only config.get log_file 2>/dev/null | head -n1) + [ -z "$LOG_FILE" ] && LOG_FILE="$DEFAULT_LOG_FILE" + [ -d "$(dirname "$LOG_FILE")" ] || USE_LOG_GATE=0 +fi + +if command -v ss >/dev/null 2>&1; then + USE_SOCKET_GATE=1 +else + USE_SOCKET_GATE=0 +fi + +if [ "$USE_LOG_GATE" -eq 0 ] && [ "$USE_SOCKET_GATE" -eq 0 ]; then + echo "so-salt-minion-wait: no usable readiness signal (log_level_logfile='${LOG_LEVEL_LOGFILE:-unset}', ss not found)" >&2 + exit 1 +fi + +if [ "$USE_LOG_GATE" -eq 1 ] && [ "$USE_SOCKET_GATE" -eq 1 ]; then + echo "so-salt-minion-wait: gating on pid-tagged ready line in ${LOG_FILE} plus master sockets" +elif [ "$USE_LOG_GATE" -eq 1 ]; then + echo "so-salt-minion-wait: ss not found; gating on pid-tagged ready line in ${LOG_FILE} only" +else + echo "so-salt-minion-wait: INFO file logging unavailable (log_level_logfile='${LOG_LEVEL_LOGFILE:-unset}'); gating on master sockets only" +fi + +# Emit the pid(s) of the current daemon instance. systemd's MainPID is the parent keepalive process; +# its child runs tune_in. Fall back to MainPID when there is no child (--disable-keepalive path). +resolve_daemon_pids() { + local mainpid children + mainpid=$(systemctl show -p MainPID --value salt-minion 2>/dev/null) + if [ -z "$mainpid" ] || [ "$mainpid" = "0" ]; then + return 1 + fi + children=$(pgrep -P "$mainpid" 2>/dev/null) + printf '%s\n' "${children:-$mainpid}" +} + +# True iff the ready line tagged with this pid is in the current or most recently rotated log. +ready_logged() { + local pid=$1 f + for f in "$LOG_FILE" "$LOG_FILE.1"; do + [ -r "$f" ] || continue + if tail -n "$LOG_TAIL_LINES" "$f" 2>/dev/null | grep -Fq "[$pid] Minion is ready to receive requests!"; then + return 0 + fi + done + return 1 +} + +# True iff this pid holds an ESTABLISHED req connection to a master on MASTER_PORT and a second +# ESTABLISHED connection to that same master IP on another port. The trailing comma in "pid=N," +# keeps pid=123 from matching pid=1234. Grid comms are IPv4 (the unit's ExecStartPre gates on ip -4). +socket_ready() { + local pid=$1 mip master_ips + master_ips=$(ss -tnp state established "dport = :${MASTER_PORT}" 2>/dev/null \ + | grep -F "pid=${pid}," \ + | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:${MASTER_PORT}" \ + | sed "s/:${MASTER_PORT}\$//" \ + | sort -u) + [ -z "$master_ips" ] && return 1 + for mip in $master_ips; do + if ss -tnp state established "dst ${mip} and dport != :${MASTER_PORT}" 2>/dev/null | grep -qF "pid=${pid},"; then + return 0 + fi + done + return 1 +} + +instance_ready() { + local pid=$1 + if [ "$USE_LOG_GATE" -eq 1 ] && ! ready_logged "$pid"; then + return 1 + fi + if [ "$USE_SOCKET_GATE" -eq 1 ] && ! socket_ready "$pid"; then + return 1 + fi + return 0 +} sleep "$INITIAL_SLEEP" elapsed="$INITIAL_SLEEP" +pids="" while [ "$elapsed" -lt "$TIMEOUT" ]; do - if systemctl is-active --quiet salt-minion \ - && salt-call --local --timeout="$PING_TIMEOUT" --out=quiet test.ping >/dev/null 2>&1; then - echo "salt-minion ready after ${elapsed}s" - exit 0 + if pids=$(resolve_daemon_pids); then + # shellcheck disable=SC2086 + for pid in $pids; do + if instance_ready "$pid"; then + echo "salt-minion (pid ${pid}) ready after ${elapsed}s" + exit 0 + fi + done fi sleep 1 elapsed=$((elapsed + 1)) done -echo "salt-minion did not become ready within ${TIMEOUT}s" >&2 +mainpid=$(systemctl show -p MainPID --value salt-minion 2>/dev/null) +echo "salt-minion did not become ready within ${TIMEOUT}s (MainPID=${mainpid:-unknown}, candidate pids='${pids:-none}', log_gate=${USE_LOG_GATE}, socket_gate=${USE_SOCKET_GATE}, log_file=${LOG_FILE})" >&2 exit 1 From fbeac25ee9ca07341335e1551fe035303c958120 Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Thu, 9 Jul 2026 16:57:19 -0400 Subject: [PATCH 23/44] so-salt-minion-check: highstate after minion restart When the minion is deemed hung and restarted, wait for it to become ready via so-salt-minion-wait, then kick off salt-call state.highstate (queued, backgrounded) so the box re-applies its states and recovers on its own rather than waiting for the next scheduled highstate. --- salt/common/tools/sbin_jinja/so-salt-minion-check | 2 ++ 1 file changed, 2 insertions(+) diff --git a/salt/common/tools/sbin_jinja/so-salt-minion-check b/salt/common/tools/sbin_jinja/so-salt-minion-check index 1376193cc..281efbcb7 100755 --- a/salt/common/tools/sbin_jinja/so-salt-minion-check +++ b/salt/common/tools/sbin_jinja/so-salt-minion-check @@ -91,6 +91,8 @@ if [ $CURRENT_TIME -ge $((SYSTEM_START_TIME+$UPTIME_REQ)) ]; then logCmd "pkill -9 -ef /usr/bin/salt-minion" I log "starting salt-minion service" I logCmd "systemctl start salt-minion" I + log "waiting for salt-minion to become ready, then applying highstate in the background (queued)" I + nohup bash -c '/usr/sbin/so-salt-minion-wait; salt-call state.highstate queue=True' >> "/opt/so/log/salt/so-salt-minion-check" 2>&1 & else log "/opt/so/log/salt/healthcheck-state-apply last touched: `date -d @$LAST_HEALTHCHECK_STATE_APPLY` must be touched by `date -d @$THRESHOLD_DATE` to avoid salt-minion restart" I fi From 89e6a746c8e730b4b5a51bf4c16365ca486657a4 Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Fri, 10 Jul 2026 09:35:21 -0400 Subject: [PATCH 24/44] so-salt-minion-wait: wait for the restart job before reading MainPID Live testing on a standalone node found the previous commit still reported ready on the OUTGOING daemon. Reproduced by running the production sequence: systemctl restart --no-block salt-minion # what service.restart issues /usr/sbin/so-salt-minion-wait # what cmd.run then runs so-salt-minion-wait: gating on pid-tagged ready line ... plus master sockets salt-minion (pid 2750297) ready after 3s # 2750297 is the OLD child salt restarts this unit with --no-block -- _no_block_default() in salt/modules/systemd_service.py returns True when the unit is the salt-minion service -- so service.restart returns as soon as the job is enqueued. Measured on the host, systemd does not swap MainPID until ~7.3s later. Throughout that window the old daemon is still running, still holds its master sockets, and its own ready line is already in the log, so every gate passes on the instance that is about to be torn down. INITIAL_SLEEP=3 expired inside that window. Wait for systemd's job queue for the unit to drain before resolving MainPID. That is deterministic rather than a timing guess: the job exists from the moment --no-block returns until the new instance signals READY, and MainPID is new by the time it clears. Measured transition: t=0.0s job pending, child=OLD, sockets up, ready line present t=7.9s job drained, child=NEW, sockets up, ready line ABSENT t=10.7s ready line for NEW child appears <- script returns here The same run also confirms empirically why the log line is required in addition to the sockets: for 2.8s the new child has both master connections while _post_master_init() is still loading modules and compiling pillar, so a socket-only gate would return that much too early. Correct the comment claim from the previous commit. The --no-block restart is real; it lives in salt's systemd_service module, not in this repo, which is why searching the repo for it turned up nothing. --- salt/salt/minion/init.sls | 3 ++- salt/salt/tools/sbin/so-salt-minion-wait | 28 ++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/salt/salt/minion/init.sls b/salt/salt/minion/init.sls index 035d9e7f8..1f1ec8305 100644 --- a/salt/salt/minion/init.sls +++ b/salt/salt/minion/init.sls @@ -138,7 +138,8 @@ salt_minion_service: # /usr/sbin/so-salt-minion-wait (deployed by salt_sbin from salt/tools/sbin/); it keys the ready # line to the current daemon pid (resolved via systemd, not the pidfile) and corroborates with the # master req/publish sockets. set_log_levels above enforces the log_level_logfile: info that the -# ready line depends on. +# ready line depends on. salt restarts this unit with --no-block, so mod_watch returns while the old +# daemon is still up; the script waits for systemd's restart job to drain before it reads MainPID. wait_for_salt_minion_ready: cmd.run: - name: /usr/sbin/so-salt-minion-wait diff --git a/salt/salt/tools/sbin/so-salt-minion-wait b/salt/salt/tools/sbin/so-salt-minion-wait index 9fb2a41c7..984f144f3 100644 --- a/salt/salt/tools/sbin/so-salt-minion-wait +++ b/salt/salt/tools/sbin/so-salt-minion-wait @@ -26,6 +26,14 @@ # logs the ready line, while systemd's MainPID is the parent. During a restart the pidfile can still # name the OLD child, whose own ready line is already in the log -- matching it would report ready # instantly. Children of the current MainPID structurally exclude the old instance. +# +# That is only true once systemd has actually swapped MainPID. Salt restarts this unit with +# --no-block (salt/modules/systemd_service.py:_no_block_default returns True for salt-minion), so +# service.restart returns as soon as the job is enqueued and the state proceeds to run this script +# while the OLD daemon is still up -- observed at ~7s before MainPID flips. Reading MainPID in that +# window names the outgoing instance, which is still fully connected and has its own ready line, so +# every gate below would pass on the daemon that is about to die. Wait for systemd's job queue for +# the unit to drain first; that is the deterministic "the swap has happened" signal. . /usr/sbin/so-common @@ -76,6 +84,13 @@ else echo "so-salt-minion-wait: INFO file logging unavailable (log_level_logfile='${LOG_LEVEL_LOGFILE:-unset}'); gating on master sockets only" fi +# True while systemd still has a queued or running job for the unit, i.e. an in-flight --no-block +# restart. MainPID still names the outgoing daemon until this drains. The unit name is passed as a +# filter and grepped as well, so this stays correct if an older systemctl ignores the filter. +restart_pending() { + systemctl list-jobs --no-legend salt-minion.service 2>/dev/null | grep -q 'salt-minion\.service' +} + # Emit the pid(s) of the current daemon instance. systemd's MainPID is the parent keepalive process; # its child runs tune_in. Fall back to MainPID when there is no child (--disable-keepalive path). resolve_daemon_pids() { @@ -134,8 +149,16 @@ sleep "$INITIAL_SLEEP" elapsed="$INITIAL_SLEEP" pids="" +announced_pending=0 while [ "$elapsed" -lt "$TIMEOUT" ]; do - if pids=$(resolve_daemon_pids); then + if restart_pending; then + # An in-flight --no-block restart: MainPID still names the outgoing daemon. Evaluating now + # would bless the instance that is about to be torn down. + if [ "$announced_pending" -eq 0 ]; then + echo "so-salt-minion-wait: systemd restart job in flight; waiting for it to drain" + announced_pending=1 + fi + elif pids=$(resolve_daemon_pids); then # shellcheck disable=SC2086 for pid in $pids; do if instance_ready "$pid"; then @@ -149,5 +172,6 @@ while [ "$elapsed" -lt "$TIMEOUT" ]; do done mainpid=$(systemctl show -p MainPID --value salt-minion 2>/dev/null) -echo "salt-minion did not become ready within ${TIMEOUT}s (MainPID=${mainpid:-unknown}, candidate pids='${pids:-none}', log_gate=${USE_LOG_GATE}, socket_gate=${USE_SOCKET_GATE}, log_file=${LOG_FILE})" >&2 +restart_pending && pending=yes || pending=no +echo "salt-minion did not become ready within ${TIMEOUT}s (MainPID=${mainpid:-unknown}, candidate pids='${pids:-none}', restart_job_pending=${pending}, log_gate=${USE_LOG_GATE}, socket_gate=${USE_SOCKET_GATE}, log_file=${LOG_FILE})" >&2 exit 1 From 566f90a0c0e544260ce94349553099a90c357411 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Fri, 10 Jul 2026 10:32:25 -0400 Subject: [PATCH 25/44] toggle pg metrics --- salt/soc/defaults.map.jinja | 24 +++++++++++++++++------- salt/soc/merged.map.jinja | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/salt/soc/defaults.map.jinja b/salt/soc/defaults.map.jinja index 2821bb8e5..93fb63efe 100644 --- a/salt/soc/defaults.map.jinja +++ b/salt/soc/defaults.map.jinja @@ -8,6 +8,7 @@ {% from 'docker/docker.map.jinja' import DOCKERMERGED -%} {% set INFLUXDB_TOKEN = salt['pillar.get']('influxdb:token') %} {% import_text 'influxdb/metrics_link.txt' as METRICS_LINK %} +{% from 'telegraf/map.jinja' import TELEGRAFMERGED %} {% for module, application_url in GLOBALS.application_urls.items() %} {% 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.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 %} +{% if TELEGRAFMERGED.output == 'POSTGRES' %} +{% for tool in SOCDEFAULTS.soc.config.server.client.tools %} +{% if tool.name == "toolInfluxDb" %} +{% do SOCDEFAULTS.soc.config.server.client.tools.remove(tool) %} +{% endif %} +{% 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}) %} diff --git a/salt/soc/merged.map.jinja b/salt/soc/merged.map.jinja index cfc0fafbd..a421711e2 100644 --- a/salt/soc/merged.map.jinja +++ b/salt/soc/merged.map.jinja @@ -7,6 +7,11 @@ {% from 'soc/defaults.map.jinja' import SOCDEFAULTS with context %} {% from 'elasticsearch/config.map.jinja' import ELASTICSEARCH_NODES %} {% 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 %} {% 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}) %} {% 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 #} {% set CUSTOM_RULESET = { From 6fc0fd954c7562e94fbf751dbea8bab98564ad9b Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Fri, 10 Jul 2026 10:35:37 -0400 Subject: [PATCH 26/44] clarify output annotation --- salt/telegraf/soc_telegraf.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/telegraf/soc_telegraf.yaml b/salt/telegraf/soc_telegraf.yaml index 4b9a2e3d1..0decdd06a 100644 --- a/salt/telegraf/soc_telegraf.yaml +++ b/salt/telegraf/soc_telegraf.yaml @@ -5,7 +5,7 @@ telegraf: advanced: True helpLink: influxdb 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: - INFLUXDB - POSTGRES From 7d17784e96857e6ea6620364231eba2dcdff4751 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:50:21 -0500 Subject: [PATCH 27/44] status messages for so-elastic-fleet-setup --- salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup | 5 ++++- setup/so-setup | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup b/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup index 2b677606a..0d7b99e48 100755 --- a/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup +++ b/salt/elasticfleet/tools/sbin_jinja/so-elastic-fleet-setup @@ -250,6 +250,7 @@ if ! salt-call state.apply elasticfleet queue=True; then exit 1 fi +printf "\nRunning so-elastic-agent-gen-installers\n" # Generate installers & install Elastic Agent on the node for agent_gen_attempt in {1..3}; do if so-elastic-agent-gen-installers; then @@ -267,4 +268,6 @@ printf "\nApplying elasticfleet.install_agent_grid state\n" if ! salt-call state.apply elasticfleet.install_agent_grid queue=True; then printf "\nFailure(s) in elasticfleet.install_agent_grid state... Exiting...\n" exit 1 -fi \ No newline at end of file +fi + +printf "\nElastic Fleet setup completed successfully\n" \ No newline at end of file diff --git a/setup/so-setup b/setup/so-setup index a44934088..896505ba5 100755 --- a/setup/so-setup +++ b/setup/so-setup @@ -793,7 +793,7 @@ if ! [[ -f $install_opt_file ]]; then logCmd "so-soc-restart" title "Setting up Elastic Fleet" 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" fi mark_setup_complete From 99e9fc1c3b6be005ebbb31bc053393a61271a240 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:50:55 -0500 Subject: [PATCH 28/44] ES 9.3.7 --- salt/elasticfleet/defaults.yaml | 1 - salt/elasticsearch/defaults.yaml | 2 +- salt/kibana/defaults.yaml | 2 +- salt/manager/tools/sbin/soup | 9 ++++++--- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/salt/elasticfleet/defaults.yaml b/salt/elasticfleet/defaults.yaml index 022600083..a3132d3f4 100644 --- a/salt/elasticfleet/defaults.yaml +++ b/salt/elasticfleet/defaults.yaml @@ -1,6 +1,5 @@ elasticfleet: enabled: False - patch_version: 9.3.3+build202604082258 # Elastic Agent specific patch release. enable_manager_output: True config: server: diff --git a/salt/elasticsearch/defaults.yaml b/salt/elasticsearch/defaults.yaml index f0b01b3ca..14d753c21 100644 --- a/salt/elasticsearch/defaults.yaml +++ b/salt/elasticsearch/defaults.yaml @@ -1,6 +1,6 @@ elasticsearch: enabled: false - version: 9.3.3 + version: 9.3.7 index_clean: true data_retention_method: DLM vm: diff --git a/salt/kibana/defaults.yaml b/salt/kibana/defaults.yaml index ecf56756b..2cf6fe92f 100644 --- a/salt/kibana/defaults.yaml +++ b/salt/kibana/defaults.yaml @@ -22,7 +22,7 @@ kibana: - default - file migrations: - discardCorruptObjects: "9.3.3" + discardCorruptObjects: "9.3.7" telemetry: enabled: False xpack: diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index e33c27f6f..79ea3967b 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -740,7 +740,6 @@ fix_logstash_0013_lumberjack_pipeline_name() { up_to_3.1.0() { ensure_postgres_local_pillar ensure_postgres_secret - determine_elastic_agent_upgrade elasticsearch_backup_index_templates # Clear existing component template state file. rm -f /opt/so/state/esfleet_component_templates.json @@ -888,6 +887,9 @@ update_kafka_metadata() { } up_to_3.2.0() { + # download 9.3.7 elastic agent packages + determine_elastic_agent_upgrade + fix_logstash_0013_lumberjack_pipeline_name pin_elasticsearch_data_retention_method @@ -901,7 +903,7 @@ post_to_3.2.0() { bootstrap_so_soc_database - # Including agent regen script here since it was missed in post_to_3.1.0 + # Generate 9.3.7 elastic agent installers echo "Regenerating Elastic Agent Installers" /sbin/so-elastic-agent-gen-installers @@ -1174,7 +1176,8 @@ verify_es_version_compatibility() { ["8.18.4"]="8.18.6 8.18.8 9.0.8" ["8.18.6"]="8.18.8 9.0.8" ["8.18.8"]="9.0.8" - ["9.0.8"]="9.3.3" + ["9.0.8"]="9.3.3 9.3.7" + ) # Elasticsearch MUST upgrade through these versions From 5af6c56996ac5287e892ac9780dd972ee9558355 Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Fri, 10 Jul 2026 14:54:20 -0400 Subject: [PATCH 29/44] so-salt-minion-check: force highstate if none has completed since boot Add a second, independent trigger to the every-5-minute health check: if the host has been up >= 15 minutes (HIGHSTATE_UPTIME_REQ) and no highstate has completed since this boot (lasthighstate mtime older than boot time), run salt-call state.highstate. This recovers a host whose boot highstate (so-boot-highstate.service) failed or was skipped, even while the minion is otherwise healthy and touching state-apply-test. The new path deliberately does not enable highstate, so a soup-disabled highstate is respected and never forced mid-upgrade. A saltutil.running guard plus queue=True prevents stacking across successive cron runs, and a RESTARTED flag suppresses the new block when the existing minion-restart path already queued a highstate. --- .../tools/sbin_jinja/so-salt-minion-check | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/salt/common/tools/sbin_jinja/so-salt-minion-check b/salt/common/tools/sbin_jinja/so-salt-minion-check index 281efbcb7..936ecda9a 100755 --- a/salt/common/tools/sbin_jinja/so-salt-minion-check +++ b/salt/common/tools/sbin_jinja/so-salt-minion-check @@ -18,6 +18,7 @@ QUIET=false UPTIME_REQ=1800 #in seconds, how long the box has to be up before considering restarting salt-minion due to /opt/so/log/salt/state-apply-test not being touched +HIGHSTATE_UPTIME_REQ=900 #in seconds; if the box has been up this long and no highstate has completed since boot, force one CURRENT_TIME=$(date +%s) SYSTEM_START_TIME=$(date -d "$(> "/opt/so/log/salt/so-salt-minion-check" 2>&1 & + RESTARTED=true else log "/opt/so/log/salt/healthcheck-state-apply last touched: `date -d @$LAST_HEALTHCHECK_STATE_APPLY` must be touched by `date -d @$THRESHOLD_DATE` to avoid salt-minion restart" I fi else log "system uptime only $((CURRENT_TIME-SYSTEM_START_TIME)) seconds does not meet $UPTIME_REQ second requirement." I fi + +# If the host has been up long enough but no highstate has completed since this boot, +# force one. This recovers a host whose boot highstate (so-boot-highstate.service) failed +# or was skipped, even while the minion is otherwise healthy (touching state-apply-test). +# We deliberately do NOT enable highstate here: if soup has disabled it during an upgrade, +# Salt will refuse the highstate and we avoid forcing one mid-upgrade. +if ! $RESTARTED && [ $CURRENT_TIME -ge $((SYSTEM_START_TIME+HIGHSTATE_UPTIME_REQ)) ] && [ $LAST_HIGHSTATE_END -lt $SYSTEM_START_TIME ]; then + if salt-call --local saltutil.running 2>/dev/null | grep -q 'state.highstate'; then + log "no highstate has completed since boot, but one is already running; skipping" I + else + log "no highstate has completed since boot after $((CURRENT_TIME-SYSTEM_START_TIME))s uptime; applying highstate" E + nohup bash -c 'salt-call state.highstate -l info queue=True' >> "/opt/so/log/salt/so-salt-minion-check" 2>&1 & + fi +fi From ed533efb7b52bd3b6fadf38277124f3c811896eb Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Fri, 10 Jul 2026 15:53:49 -0400 Subject: [PATCH 30/44] so-salt-minion-check: tag and clarify log lines per check With two independent checks now writing to the same log, messages like "system uptime only N seconds does not meet 1800 second requirement" were ambiguous about which check they came from. Prefix every line with a [minion-restart-check] or [boot-highstate-check] tag and reword the uptime, threshold, and healthy messages to say what was evaluated and why it was skipped. Restructure the boot-highstate check from a nested if into an if/elif chain so each outcome (restart already queued, uptime too low, healthy, already running, forcing) logs its own reason instead of silently doing nothing. --- .../tools/sbin_jinja/so-salt-minion-check | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/salt/common/tools/sbin_jinja/so-salt-minion-check b/salt/common/tools/sbin_jinja/so-salt-minion-check index 936ecda9a..c78846a5f 100755 --- a/salt/common/tools/sbin_jinja/so-salt-minion-check +++ b/salt/common/tools/sbin_jinja/so-salt-minion-check @@ -79,41 +79,48 @@ log "running so-salt-minion-check" RESTARTED=false +# Check 1 (minion-restart-check): if the minion has stopped applying states (the +# state-apply-test healthcheck file has gone stale), restart the salt-minion service. if [ $CURRENT_TIME -ge $((SYSTEM_START_TIME+$UPTIME_REQ)) ]; then if [ $THRESHOLD_DATE -le $CURRENT_TIME ]; then - log "salt-minion is unable to apply states" E - log "/opt/so/log/salt/healthcheck-state-apply not touched by required date: `date -d @$THRESHOLD_DATE`, last touched: `date -d @$LAST_HEALTHCHECK_STATE_APPLY`" I - log "last highstate completed at `date -d @$LAST_HIGHSTATE_END`" I - log "checking if any jobs are running" I + log "[minion-restart-check] salt-minion is unable to apply states; restarting salt-minion" E + log "[minion-restart-check] state-apply-test not touched by required date `date -d @$THRESHOLD_DATE`, last touched `date -d @$LAST_HEALTHCHECK_STATE_APPLY`" I + log "[minion-restart-check] last highstate completed at `date -d @$LAST_HIGHSTATE_END`" I + log "[minion-restart-check] checking if any jobs are running" I logCmd "salt-call --local saltutil.running" I - log "ensure salt.minion-state-apply-test is enabled" I + log "[minion-restart-check] ensure salt.minion-state-apply-test is enabled" I logCmd "salt-call state.enable salt.minion-state-apply-test" I - log "ensure highstate is enabled" I + log "[minion-restart-check] ensure highstate is enabled" I logCmd "salt-call state.enable highstate" I - log "killing all salt-minion processes" I + log "[minion-restart-check] killing all salt-minion processes" I logCmd "pkill -9 -ef /usr/bin/salt-minion" I - log "starting salt-minion service" I + log "[minion-restart-check] starting salt-minion service" I logCmd "systemctl start salt-minion" I - log "waiting for salt-minion to become ready, then applying highstate in the background (queued)" I + log "[minion-restart-check] waiting for salt-minion to become ready, then applying highstate in the background (queued)" I nohup bash -c '/usr/sbin/so-salt-minion-wait; salt-call state.highstate queue=True' >> "/opt/so/log/salt/so-salt-minion-check" 2>&1 & RESTARTED=true else - log "/opt/so/log/salt/healthcheck-state-apply last touched: `date -d @$LAST_HEALTHCHECK_STATE_APPLY` must be touched by `date -d @$THRESHOLD_DATE` to avoid salt-minion restart" I + log "[minion-restart-check] healthy: state-apply-test last touched `date -d @$LAST_HEALTHCHECK_STATE_APPLY`, must go stale past `date -d @$THRESHOLD_DATE` to trigger a salt-minion restart" I fi else - log "system uptime only $((CURRENT_TIME-SYSTEM_START_TIME)) seconds does not meet $UPTIME_REQ second requirement." I + log "[minion-restart-check] skipped: system uptime $((CURRENT_TIME-SYSTEM_START_TIME))s is below the ${UPTIME_REQ}s minimum required before a salt-minion restart" I fi -# If the host has been up long enough but no highstate has completed since this boot, -# force one. This recovers a host whose boot highstate (so-boot-highstate.service) failed -# or was skipped, even while the minion is otherwise healthy (touching state-apply-test). -# We deliberately do NOT enable highstate here: if soup has disabled it during an upgrade, -# Salt will refuse the highstate and we avoid forcing one mid-upgrade. -if ! $RESTARTED && [ $CURRENT_TIME -ge $((SYSTEM_START_TIME+HIGHSTATE_UPTIME_REQ)) ] && [ $LAST_HIGHSTATE_END -lt $SYSTEM_START_TIME ]; then - if salt-call --local saltutil.running 2>/dev/null | grep -q 'state.highstate'; then - log "no highstate has completed since boot, but one is already running; skipping" I - else - log "no highstate has completed since boot after $((CURRENT_TIME-SYSTEM_START_TIME))s uptime; applying highstate" E - nohup bash -c 'salt-call state.highstate -l info queue=True' >> "/opt/so/log/salt/so-salt-minion-check" 2>&1 & - fi +# Check 2 (boot-highstate-check): if the host has been up long enough but no highstate +# has completed since this boot, force one. This recovers a host whose boot highstate +# (so-boot-highstate.service) failed or was skipped, even while the minion is otherwise +# healthy (touching state-apply-test). We deliberately do NOT enable highstate here: if +# soup has disabled it during an upgrade, Salt will refuse the highstate and we avoid +# forcing one mid-upgrade. +if $RESTARTED; then + log "[boot-highstate-check] skipped: minion-restart-check already queued a highstate this run" I +elif [ $CURRENT_TIME -lt $((SYSTEM_START_TIME+HIGHSTATE_UPTIME_REQ)) ]; then + log "[boot-highstate-check] skipped: system uptime $((CURRENT_TIME-SYSTEM_START_TIME))s is below the ${HIGHSTATE_UPTIME_REQ}s minimum required before forcing a highstate" I +elif [ $LAST_HIGHSTATE_END -ge $SYSTEM_START_TIME ]; then + log "[boot-highstate-check] healthy: a highstate completed at `date -d @$LAST_HIGHSTATE_END`, after this boot at `date -d @$SYSTEM_START_TIME`" I +elif salt-call --local saltutil.running 2>/dev/null | grep -q 'state.highstate'; then + log "[boot-highstate-check] no highstate has completed since boot, but one is already running; skipping" I +else + log "[boot-highstate-check] no highstate has completed since boot after $((CURRENT_TIME-SYSTEM_START_TIME))s uptime; applying highstate" E + nohup bash -c 'salt-call state.highstate -l info queue=True' >> "/opt/so/log/salt/so-salt-minion-check" 2>&1 & fi From 87a56396437f6aacc2a97251de0410fceeca9d07 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:05:25 -0500 Subject: [PATCH 31/44] pipeline updates --- .../elastic-defend-endpoints.json | 2 +- .../grid-nodes_general/import-evtx-logs.json | 2 +- ...nse.log-1.25.2 => logs-pfsense.log-1.25.4} | 20 +++++++++---------- ...icata => logs-pfsense.log-1.25.4-suricata} | 0 4 files changed, 12 insertions(+), 12 deletions(-) rename salt/elasticsearch/files/ingest/{logs-pfsense.log-1.25.2 => logs-pfsense.log-1.25.4} (96%) rename salt/elasticsearch/files/ingest/{logs-pfsense.log-1.25.2-suricata => logs-pfsense.log-1.25.4-suricata} (100%) diff --git a/salt/elasticfleet/files/integrations/elastic-defend/elastic-defend-endpoints.json b/salt/elasticfleet/files/integrations/elastic-defend/elastic-defend-endpoints.json index c27da26f7..7d64fd1ab 100644 --- a/salt/elasticfleet/files/integrations/elastic-defend/elastic-defend-endpoints.json +++ b/salt/elasticfleet/files/integrations/elastic-defend/elastic-defend-endpoints.json @@ -5,7 +5,7 @@ "package": { "name": "endpoint", "title": "Elastic Defend", - "version": "9.3.0", + "version": "9.3.1", "requires_root": true }, "enabled": true, diff --git a/salt/elasticfleet/files/integrations/grid-nodes_general/import-evtx-logs.json b/salt/elasticfleet/files/integrations/grid-nodes_general/import-evtx-logs.json index 32d210172..be05d59e0 100644 --- a/salt/elasticfleet/files/integrations/grid-nodes_general/import-evtx-logs.json +++ b/salt/elasticfleet/files/integrations/grid-nodes_general/import-evtx-logs.json @@ -29,7 +29,7 @@ "\\.gz$" ], "include_files": [], - "processors": "- dissect:\n tokenizer: \"/nsm/import/%{import.id}/evtx/%{import.file}\"\n field: \"log.file.path\"\n target_prefix: \"\"\n- decode_json_fields:\n fields: [\"message\"]\n target: \"\"\n- drop_fields:\n fields: [\"host\"]\n ignore_missing: true\n- add_fields:\n target: data_stream\n fields:\n type: logs\n dataset: system.security\n- add_fields:\n target: event\n fields:\n dataset: system.security\n module: system\n imported: true\n- add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.security-2.15.0\n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-Sysmon/Operational'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: windows.sysmon_operational\n - add_fields:\n target: event\n fields:\n dataset: windows.sysmon_operational\n module: windows\n imported: true\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-windows.sysmon_operational-3.8.0\n- if:\n equals:\n winlog.channel: 'Application'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: system.application\n - add_fields:\n target: event\n fields:\n dataset: system.application\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.application-2.15.0\n- if:\n equals:\n winlog.channel: 'System'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: system.system\n - add_fields:\n target: event\n fields:\n dataset: system.system\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.system-2.15.0\n \n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-PowerShell/Operational'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: windows.powershell_operational\n - add_fields:\n target: event\n fields:\n dataset: windows.powershell_operational\n module: windows\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-windows.powershell_operational-3.8.0\n- add_fields:\n target: data_stream\n fields:\n dataset: import", + "processors": "- dissect:\n tokenizer: \"/nsm/import/%{import.id}/evtx/%{import.file}\"\n field: \"log.file.path\"\n target_prefix: \"\"\n- decode_json_fields:\n fields: [\"message\"]\n target: \"\"\n- drop_fields:\n fields: [\"host\"]\n ignore_missing: true\n- add_fields:\n target: data_stream\n fields:\n type: logs\n dataset: system.security\n- add_fields:\n target: event\n fields:\n dataset: system.security\n module: system\n imported: true\n- add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.security-2.20.0\n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-Sysmon/Operational'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: windows.sysmon_operational\n - add_fields:\n target: event\n fields:\n dataset: windows.sysmon_operational\n module: windows\n imported: true\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-windows.sysmon_operational-3.8.3\n- if:\n equals:\n winlog.channel: 'Application'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: system.application\n - add_fields:\n target: event\n fields:\n dataset: system.application\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.application-2.20.0\n- if:\n equals:\n winlog.channel: 'System'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: system.system\n - add_fields:\n target: event\n fields:\n dataset: system.system\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.system-2.20.0\n \n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-PowerShell/Operational'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: windows.powershell_operational\n - add_fields:\n target: event\n fields:\n dataset: windows.powershell_operational\n module: windows\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-windows.powershell_operational-3.8.3\n- add_fields:\n target: data_stream\n fields:\n dataset: import", "tags": [ "import" ], diff --git a/salt/elasticsearch/files/ingest/logs-pfsense.log-1.25.2 b/salt/elasticsearch/files/ingest/logs-pfsense.log-1.25.4 similarity index 96% rename from salt/elasticsearch/files/ingest/logs-pfsense.log-1.25.2 rename to salt/elasticsearch/files/ingest/logs-pfsense.log-1.25.4 index 1ea828514..c01383fde 100644 --- a/salt/elasticsearch/files/ingest/logs-pfsense.log-1.25.2 +++ b/salt/elasticsearch/files/ingest/logs-pfsense.log-1.25.4 @@ -118,70 +118,70 @@ { "pipeline": { "tag": "pipeline_e16851a7", - "name": "logs-pfsense.log-1.25.2-firewall", + "name": "logs-pfsense.log-1.25.4-firewall", "if": "ctx.event.provider == 'filterlog'" } }, { "pipeline": { "tag": "pipeline_828590b5", - "name": "logs-pfsense.log-1.25.2-openvpn", + "name": "logs-pfsense.log-1.25.4-openvpn", "if": "ctx.event.provider == 'openvpn'" } }, { "pipeline": { "tag": "pipeline_9d37039c", - "name": "logs-pfsense.log-1.25.2-ipsec", + "name": "logs-pfsense.log-1.25.4-ipsec", "if": "ctx.event.provider == 'charon'" } }, { "pipeline": { "tag": "pipeline_ad56bbca", - "name": "logs-pfsense.log-1.25.2-dhcp", + "name": "logs-pfsense.log-1.25.4-dhcp", "if": "[\"dhcpd\", \"dhclient\", \"dhcp6c\", \"dnsmasq-dhcp\"].contains(ctx.event.provider)" } }, { "pipeline": { "tag": "pipeline_dd85553d", - "name": "logs-pfsense.log-1.25.2-unbound", + "name": "logs-pfsense.log-1.25.4-unbound", "if": "ctx.event.provider == 'unbound'" } }, { "pipeline": { "tag": "pipeline_720ed255", - "name": "logs-pfsense.log-1.25.2-haproxy", + "name": "logs-pfsense.log-1.25.4-haproxy", "if": "ctx.event.provider == 'haproxy'" } }, { "pipeline": { "tag": "pipeline_456beba5", - "name": "logs-pfsense.log-1.25.2-php-fpm", + "name": "logs-pfsense.log-1.25.4-php-fpm", "if": "ctx.event.provider == 'php-fpm'" } }, { "pipeline": { "tag": "pipeline_a0d89375", - "name": "logs-pfsense.log-1.25.2-squid", + "name": "logs-pfsense.log-1.25.4-squid", "if": "ctx.event.provider == 'squid'" } }, { "pipeline": { "tag": "pipeline_c2f1ed55", - "name": "logs-pfsense.log-1.25.2-snort", + "name": "logs-pfsense.log-1.25.4-snort", "if": "ctx.event.provider == 'snort'" } }, { "pipeline": { "tag":"pipeline_33db1c9e", - "name": "logs-pfsense.log-1.25.2-suricata", + "name": "logs-pfsense.log-1.25.4-suricata", "if": "ctx.event.provider == 'suricata'" } }, diff --git a/salt/elasticsearch/files/ingest/logs-pfsense.log-1.25.2-suricata b/salt/elasticsearch/files/ingest/logs-pfsense.log-1.25.4-suricata similarity index 100% rename from salt/elasticsearch/files/ingest/logs-pfsense.log-1.25.2-suricata rename to salt/elasticsearch/files/ingest/logs-pfsense.log-1.25.4-suricata From 2cd889782db10210f463bc6aa30586a0f7b83bf9 Mon Sep 17 00:00:00 2001 From: reyesj2 <94730068+reyesj2@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:16:12 -0500 Subject: [PATCH 32/44] soup es check for 9.3.7 --- salt/manager/tools/sbin/soup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index 79ea3967b..a87dfecfc 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -1177,7 +1177,7 @@ verify_es_version_compatibility() { ["8.18.6"]="8.18.8 9.0.8" ["8.18.8"]="9.0.8" ["9.0.8"]="9.3.3 9.3.7" - + ["9.3.3"]="9.3.7" ) # Elasticsearch MUST upgrade through these versions From e42f7cd6fca72c23a46fe2940622c36d5e176ebb Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Tue, 14 Jul 2026 13:33:57 -0400 Subject: [PATCH 33/44] add restart policy to so-postgres --- salt/postgres/enabled.sls | 1 + 1 file changed, 1 insertion(+) diff --git a/salt/postgres/enabled.sls b/salt/postgres/enabled.sls index 20d256ae8..b0eadd205 100644 --- a/salt/postgres/enabled.sls +++ b/salt/postgres/enabled.sls @@ -19,6 +19,7 @@ include: so-postgres: docker_container.running: - image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-postgres:{{ GLOBALS.so_version }} + - restart_policy: unless-stopped - hostname: so-postgres - networks: - sobridge: From 405dc52587fe417c767e7005cfc177a444cf8296 Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Tue, 14 Jul 2026 13:34:19 -0400 Subject: [PATCH 34/44] move up restart policy for so-kratos --- salt/kratos/enabled.sls | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/salt/kratos/enabled.sls b/salt/kratos/enabled.sls index 7dd13732e..ab13d759f 100644 --- a/salt/kratos/enabled.sls +++ b/salt/kratos/enabled.sls @@ -15,6 +15,7 @@ include: so-kratos: docker_container.running: - image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-kratos:{{ GLOBALS.so_version }} + - restart_policy: unless-stopped - hostname: kratos - name: so-kratos - networks: @@ -51,8 +52,6 @@ so-kratos: - {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }} {% endfor %} {% endif %} - # Intentionally unless-stopped -- matches the fleet default. - - restart_policy: unless-stopped - watch: - file: kratosschema - file: kratosconfig From 63d4061500cd7c4576d64a1b0c0d5cf837d92cb4 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Tue, 14 Jul 2026 13:52:32 -0400 Subject: [PATCH 35/44] prevent login redirect to any API url --- salt/nginx/etc/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/nginx/etc/nginx.conf b/salt/nginx/etc/nginx.conf index b7a70da2b..3f74c411c 100644 --- a/salt/nginx/etc/nginx.conf +++ b/salt/nginx/etc/nginx.conf @@ -399,7 +399,7 @@ http { error_page 429 = @error429; location @error401 { - if ($request_uri ~* (^/api/.*|^/connect/.*|^/oauth2/.*|^/.*\.map$)) { + if ($request_uri ~* (^.*/api/.*|^/connect/.*|^/oauth2/.*|^/.*\.map$)) { return 401; } From 8b488f92267fcb319f8ee7e2f34c3245270d110e Mon Sep 17 00:00:00 2001 From: Mike Reeves Date: Tue, 14 Jul 2026 17:30:58 -0400 Subject: [PATCH 36/44] 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. --- salt/manager/tools/sbin/soup | 46 ++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index a87dfecfc..18cf6a732 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -12,7 +12,17 @@ UPDATE_DIR=/tmp/sogh/securityonion DEFAULT_SALT_DIR=/opt/so/saltstack/default 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}') BATCHSIZE=5 SOUP_LOG=/root/soup.log @@ -414,6 +424,13 @@ preupgrade_changes() { 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() { # This function is to add any new pillar items if needed. echo "Running post upgrade processes." @@ -421,6 +438,8 @@ postupgrade_changes() { [[ "$POSTVERSION" =~ ^2\.4\.21[0-9]+$ ]] && post_to_3.0.0 [[ "$POSTVERSION" == "3.0.0" ]] && post_to_3.1.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 } @@ -513,7 +532,7 @@ post_to_3.0.0() { # convert yes/no in suricata pillars to true/false convert_suricata_yes_no - POSTVERSION=3.0.0 + set_postversion 3.0.0 } ### 3.0.0 End ### @@ -776,7 +795,7 @@ post_to_3.1.0() { # Check for unhealthy / unauthorized integration transform jobs and attempt reauthorizations check_transform_health_and_reauthorize || true - POSTVERSION=3.1.0 + set_postversion 3.1.0 } ### 3.1.0 End ### @@ -911,7 +930,7 @@ post_to_3.2.0() { update_kafka_metadata "4.3" - POSTVERSION=3.2.0 + set_postversion 3.2.0 } ### 3.2.0 End ### @@ -1063,6 +1082,14 @@ upgrade_check() { fi [[ -f /etc/sohotfix ]] && CURRENTHOTFIX=$(cat /etc/sohotfix) 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" if [ "$HOTFIXVERSION" == "$CURRENTHOTFIX" ]; then echo "You are already running the latest version of Security Onion." @@ -1814,9 +1841,14 @@ main() { create_local_directories "/opt/so/saltstack/default" apply_hotfix echo "Hotfix applied" - update_version enable_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 echo "" echo "Performing upgrade from Security Onion $INSTALLEDVERSION to Security Onion $NEWVERSION." @@ -1873,6 +1905,10 @@ main() { copy_new_files echo "" 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 echo "" From 618712469e55e6e2d55025de095fa5106ef4b6f1 Mon Sep 17 00:00:00 2001 From: Mike Reeves Date: Wed, 15 Jul 2026 09:42:59 -0400 Subject: [PATCH 37/44] 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. --- salt/manager/tools/sbin/soup | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index 18cf6a732..4fcdfebf9 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -33,6 +33,10 @@ NOTIFYCUSTOMELASTICCONFIG=false TOPFILE=/opt/so/saltstack/default/salt/top.sls BACKUPTOPFILE=/opt/so/saltstack/default/salt/top.sls.backup 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_CONFIGURED=false # Check if salt-cloud is installed @@ -133,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" + # 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 fi @@ -1831,6 +1857,7 @@ main() { fi if [ "$is_hotfix" == "true" ]; then + SOUP_UPGRADE_STARTED=true echo "Applying $HOTFIXVERSION hotfix" # 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 @@ -1850,6 +1877,7 @@ main() { # update_version are no-ops here since the version is unchanged for a hotfix. update_version else + SOUP_UPGRADE_STARTED=true echo "" echo "Performing upgrade from Security Onion $INSTALLEDVERSION to Security Onion $NEWVERSION." echo "" From fee62ab976cfabe96390d752bcf4b42dd6aed337 Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Wed, 15 Jul 2026 10:49:27 -0400 Subject: [PATCH 38/44] change restart_policy to unless-stopped --- salt/strelka/backend/enabled.sls | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/salt/strelka/backend/enabled.sls b/salt/strelka/backend/enabled.sls index 8c71bdf68..e62349f70 100644 --- a/salt/strelka/backend/enabled.sls +++ b/salt/strelka/backend/enabled.sls @@ -15,6 +15,7 @@ include: strelka_backend: docker_container.running: - image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-strelka-backend:{{ GLOBALS.so_version }} + - restart_policy: unless-stopped - binds: - /opt/so/conf/strelka/backend/:/etc/strelka/:ro - /opt/so/conf/strelka/rules/compiled/:/etc/yara/:ro @@ -47,11 +48,6 @@ strelka_backend: - {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }} {% endfor %} {% endif %} - # Intentionally `on-failure` (not unless-stopped) -- strelka backend shuts - # down cleanly during rule reloads and we do not want those clean exits to - # trigger an auto-restart. Do not homogenize; see the container - # auto-restart section of the plan. - - restart_policy: on-failure - watch: - file: strelkasensorcompiledrules - file: backend_backend_config From 5178d5fd0e91ec9a0cd48a4361313fb4c03fa9f8 Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Wed, 15 Jul 2026 11:04:51 -0400 Subject: [PATCH 39/44] ensure restart_policy is below image --- salt/hydra/enabled.sls | 3 +-- salt/registry/enabled.sls | 7 +++---- salt/suricata/enabled.sls | 2 +- salt/zeek/enabled.sls | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/salt/hydra/enabled.sls b/salt/hydra/enabled.sls index b19f9a80d..74106f550 100644 --- a/salt/hydra/enabled.sls +++ b/salt/hydra/enabled.sls @@ -22,6 +22,7 @@ include: so-hydra: docker_container.running: - image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-hydra:{{ GLOBALS.so_version }} + - restart_policy: unless-stopped - hostname: hydra - name: so-hydra - networks: @@ -58,8 +59,6 @@ so-hydra: - {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }} {% endfor %} {% endif %} - # Intentionally unless-stopped -- matches the fleet default. - - restart_policy: unless-stopped - watch: - file: hydraconfig - require: diff --git a/salt/registry/enabled.sls b/salt/registry/enabled.sls index 7b039b313..b2bfbc56d 100644 --- a/salt/registry/enabled.sls +++ b/salt/registry/enabled.sls @@ -17,14 +17,13 @@ include: so-dockerregistry: docker_container.running: - image: ghcr.io/security-onion-solutions/registry:3.1.1 + # Intentionally `always`-- registry is critical and must + # come back up even if it was manually stopped. + - restart_policy: always - hostname: so-registry - networks: - sobridge: - ipv4_address: {{ DOCKERMERGED.containers['so-dockerregistry'].ip }} - # Intentionally `always` (not unless-stopped) -- registry is critical infra - # and must come back up even if it was manually stopped. Do not homogenize - # to unless-stopped; see the container auto-restart section of the plan. - - restart_policy: always - port_bindings: {% for BINDING in DOCKERMERGED.containers['so-dockerregistry'].port_bindings %} - {{ BINDING }} diff --git a/salt/suricata/enabled.sls b/salt/suricata/enabled.sls index 53f367971..d9206798e 100644 --- a/salt/suricata/enabled.sls +++ b/salt/suricata/enabled.sls @@ -17,8 +17,8 @@ include: so-suricata: docker_container.running: - image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-suricata:{{ GLOBALS.so_version }} - - privileged: True - restart_policy: unless-stopped + - privileged: True - environment: - INTERFACE={{ GLOBALS.sensor.interface }} {% if DOCKERMERGED.containers['so-suricata'].extra_env %} diff --git a/salt/zeek/enabled.sls b/salt/zeek/enabled.sls index 355e555b3..ec01693d7 100644 --- a/salt/zeek/enabled.sls +++ b/salt/zeek/enabled.sls @@ -16,9 +16,9 @@ include: so-zeek: docker_container.running: - image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-zeek:{{ GLOBALS.so_version }} + - restart_policy: unless-stopped - start: True - privileged: True - - restart_policy: unless-stopped {% if DOCKERMERGED.containers['so-zeek'].ulimits %} - ulimits: {% for ULIMIT in DOCKERMERGED.containers['so-zeek'].ulimits %} From be7d8a2aa77d06ef5773b27ac2fba3fe05addaf9 Mon Sep 17 00:00:00 2001 From: Mike Reeves Date: Wed, 15 Jul 2026 11:35:04 -0400 Subject: [PATCH 40/44] 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). --- salt/manager/tools/sbin/soup | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index 4fcdfebf9..1200fc423 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -1118,6 +1118,10 @@ upgrade_check() { fi echo "Checking to see if there are hotfixes needed" 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." exit 0 else @@ -1813,6 +1817,15 @@ main() { set_minionid MINION_ROLE=$(lookup_role) 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 "" check_minimum_version From bd70dd53fb8e9e3acdd68bd7c023327b57f5e270 Mon Sep 17 00:00:00 2001 From: Mike Reeves Date: Wed, 15 Jul 2026 12:00:11 -0400 Subject: [PATCH 41/44] 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. --- salt/manager/tools/sbin/soup | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index 1200fc423..37e550db2 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -327,6 +327,31 @@ check_pillar_items() { fi } +check_cluster_health() { + echo "Checking Elasticsearch cluster health." + # Block only if the cluster cannot reach at least 'yellow' status (i.e. it is red or + # unreachable); 'yellow' is normal for many Security Onion deployments. Modeled on the + # wait used in so-elasticsearch-roles-load. + if so-elasticsearch-query "_cluster/health?wait_for_status=yellow&timeout=120s" --fail > /dev/null 2>&1; then + printf "\nThe Elasticsearch cluster is healthy. We can proceed with SOUP.\n\n" + else + printf "\nThe Elasticsearch cluster is not healthy (it did not reach at least 'yellow' status). Please resolve the cluster health issue 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() { set +e echo "Waiting on the Salt Master service to be ready." @@ -1854,6 +1879,12 @@ main() { echo "Verifying Elasticsearch version compatibility across the grid before upgrading." 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." upgrade_check_salt set -e From 186bf86e996f17f00d4af0eb7292888881f7cba3 Mon Sep 17 00:00:00 2001 From: Mike Reeves Date: Wed, 15 Jul 2026 12:14:18 -0400 Subject: [PATCH 42/44] 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. --- salt/manager/tools/sbin/soup | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index 37e550db2..ec2dcaed5 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -329,13 +329,12 @@ check_pillar_items() { check_cluster_health() { echo "Checking Elasticsearch cluster health." - # Block only if the cluster cannot reach at least 'yellow' status (i.e. it is red or - # unreachable); 'yellow' is normal for many Security Onion deployments. Modeled on the - # wait used in so-elasticsearch-roles-load. - if so-elasticsearch-query "_cluster/health?wait_for_status=yellow&timeout=120s" --fail > /dev/null 2>&1; then - printf "\nThe Elasticsearch cluster is healthy. We can proceed with SOUP.\n\n" + # 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 healthy (it did not reach at least 'yellow' status). Please resolve the cluster health issue before running SOUP again.\n\n" + 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 } From 376607d292852ab238a2a6fee1c4dffa6bae571c Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Wed, 15 Jul 2026 14:52:27 -0400 Subject: [PATCH 43/44] so-status: show container status while system is starting Containers now start on boot via restart_policy unless-stopped, so a highstate is no longer required to bring them up. Gather and display the container table even when no highstate has completed since reboot, while still warning the user. The exit code / JSON status_code stays 2 in that state so SOC's Grid continues to show the restarting message unchanged. --- salt/common/tools/sbin/so-status | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/salt/common/tools/sbin/so-status b/salt/common/tools/sbin/so-status index f4abd8aa3..b50cdff14 100755 --- a/salt/common/tools/sbin/so-status +++ b/salt/common/tools/sbin/so-status @@ -74,13 +74,13 @@ def output(options, console, code, data): summary = { "status_code": code, "containers": data } print(json.dumps(summary)) elif "-q" not in options: - if code == 2: - console.print(" [bold yellow]:hourglass: [bold white]System appears to be starting. No highstate has completed since the system was restarted.") - elif code == 99: + if code == 99: console.print(" [bold red]:exclamation: [bold white]Installation does not appear to be complete. A highstate has not fully completed.") elif code == 100: console.print(" [bold red]:exclamation: [bold white]Installation encountered errors.") else: + if code == 2: + console.print(" [bold yellow]:hourglass: [bold white]System appears to be starting. No highstate has completed since the system was restarted. Container status is shown below.") table = Table(title = "Security Onion Status", show_edge = False, safe_box = True, box = box.MINIMAL) table.add_column("Container", justify="right", style="white", no_wrap=True) table.add_column("Status", justify="left", style="green", no_wrap=True) @@ -154,8 +154,14 @@ def check_status(options, console): code = check_installation_status(options, console) if code == 0: code = check_system_status(options, console) - if code == 0: - code, container_list = check_container_status(options, console) + # Containers now start on boot without a highstate, so gather/display their + # status even when the system is still "starting" (code 2). Keep the starting + # code as the exit/status_code so SOC keeps showing the "restarting" message + # on the Grid until a highstate completes. + if code == 0 or code == 2: + container_code, container_list = check_container_status(options, console) + if code == 0: + code = container_code output(options, console, code, container_list) return code @@ -180,4 +186,3 @@ def main(): if __name__ == "__main__": main() - From 23c74f172764758bb2478e50565d0dd9328250fb Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Wed, 15 Jul 2026 15:24:33 -0400 Subject: [PATCH 44/44] remove installation of pyinotify --- salt/salt/master.sls | 1 - salt/salt/master/pyinotify.sls | 20 ------------------ .../pyinotify/pyinotify-0.9.6.tar.gz | Bin 60998 -> 0 bytes 3 files changed, 21 deletions(-) delete mode 100644 salt/salt/master/pyinotify.sls delete mode 100644 salt/salt/module_packages/pyinotify/pyinotify-0.9.6.tar.gz diff --git a/salt/salt/master.sls b/salt/salt/master.sls index bb39f1395..2c47a95c6 100644 --- a/salt/salt/master.sls +++ b/salt/salt/master.sls @@ -15,7 +15,6 @@ include: - salt.minion - - salt.master.pyinotify - salt.master.boot_mine_update {% if 'vrt' in salt['pillar.get']('features', []) %} - salt.cloud diff --git a/salt/salt/master/pyinotify.sls b/salt/salt/master/pyinotify.sls deleted file mode 100644 index 8aa2f1d53..000000000 --- a/salt/salt/master/pyinotify.sls +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - -pyinotify_module_package: - file.recurse: - - name: /opt/so/conf/salt/module_packages/pyinotify - - source: salt://salt/module_packages/pyinotify - - clean: True - - makedirs: True - -pyinotify_python_module_install: - cmd.run: - - name: /opt/saltstack/salt/bin/python3.10 -m pip install pyinotify --no-index --find-links=/opt/so/conf/salt/module_packages/pyinotify/ --upgrade - - onchanges: - - file: pyinotify_module_package - - failhard: True - - watch_in: - - service: salt_minion_service diff --git a/salt/salt/module_packages/pyinotify/pyinotify-0.9.6.tar.gz b/salt/salt/module_packages/pyinotify/pyinotify-0.9.6.tar.gz deleted file mode 100644 index 3150ad360fcd785b11c1d67e734fe906e973a782..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60998 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

y>w-0k;31nP{WrocQke%$Sx(n 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 bM>38