Compare commits

...
Author SHA1 Message Date
Mike Reeves 387781c629 Trim verbose comments in postgres provisioning changes 2026-07-20 15:19:02 -04:00
Mike Reeves 011749ad09 Self-heal PostgreSQL SOC database bootstrap
init-db.sh only runs on a fresh PGDATA (docker-entrypoint-initdb.d), so a
cluster left partially initialized -- e.g. the container restarted mid first-init
by a watch trigger -- never recovered: the so_postgres role existed but its
schema grants and the so_telegraf database were missing, breaking SOC with
"permission denied for schema public".

- init-db.sh: make the role upsert race-safe. Try CREATE ROLE and fall back to
  ALTER on duplicate_object/unique_violation instead of IF NOT EXISTS, so a
  concurrent creator no longer aborts the script (set -e + ON_ERROR_STOP=1)
  before the grants and so_telegraf creation run. The whole script is now
  idempotent and safe to re-run.
- postgres/enabled.sls: move postgres_wait_ready here (was telegraf-gated in
  telegraf_users.sls) and add postgres_bootstrap_soc_db, which re-runs init-db.sh
  every highstate so a partially-initialized cluster self-heals.
- telegraf_users.sls: drop its now-duplicate postgres_wait_ready definition; it
  resolves from postgres.enabled via the existing include.
- soup: remove bootstrap_so_soc_database and its post_to_3.2.0 call. The
  highstates soup runs before postupgrade now reconcile the SOC DB, making the
  one-shot bootstrap redundant.
2026-07-20 15:04:58 -04:00
Mike Reeves ad78e84ccd Prevent PostgreSQL from leaking plaintext passwords to postgres.log
PostgreSQL's log_min_error_statement defaults to 'error', so whenever a
CREATE/ALTER ROLE ... PASSWORD statement errored, the full statement text --
including the plaintext password -- was written to /opt/so/log/postgres/postgres.log.
The role-provisioning paths (init-db.sh for so_postgres, so-telegraf-postgres
for per-minion telegraf roles) both dispatch such DDL, the latter on every
state.apply.

- init-db.sh / so-telegraf-postgres: SET log_min_error_statement = panic before
  the password-bearing DDL so an error no longer emits the STATEMENT line. The
  ERROR message itself (no password) still logs, preserving debuggability.
- logrotate: add a postgres stanza (daily, keep 14, copytruncate, compress) so
  postgres.log is rotated like every other service and leaked content can't
  persist indefinitely. copytruncate is required because the container holds the
  log open via redirected stderr.
- soup: scrub any already-logged PASSWORD lines from postgres.log during
  post_to_3.2.0, rewriting in place to preserve the inode postgres is writing to.
2026-07-20 12:41:14 -04:00
Josh PattersonandGitHub c4295b4e0a Merge pull request #16071 from Security-Onion-Solutions/saltthangs
auto state apply
2026-07-20 08:56:08 -04:00
Josh BrowerandGitHub c2075ddafb Merge pull request #16081 from Security-Onion-Solutions/agentic-tweaks
Update baseline agents
2026-07-17 21:54:20 +02:00
Josh Brower 48a7d66964 Update baseline agents 2026-07-17 15:20:20 -04:00
9 changed files with 68 additions and 40 deletions
+10
View File
@@ -150,6 +150,16 @@ logrotate:
- extension .log - extension .log
- dateext - dateext
- dateyesterday - dateyesterday
/opt/so/log/postgres/*_x_log:
- daily
- rotate 14
- missingok
- copytruncate
- compress
- create
- extension .log
- dateext
- dateyesterday
/opt/so/log/telegraf/*_x_log: /opt/so/log/telegraf/*_x_log:
- daily - daily
- rotate 14 - rotate 14
+7
View File
@@ -91,6 +91,13 @@ logrotate:
multiline: True multiline: True
global: True global: True
forcedType: "[]string" forcedType: "[]string"
"/opt/so/log/postgres/*_x_log":
description: List of logrotate options for this file.
title: /opt/so/log/postgres/*.log
advanced: True
multiline: True
global: True
forcedType: "[]string"
"/opt/so/log/telegraf/*_x_log": "/opt/so/log/telegraf/*_x_log":
description: List of logrotate options for this file. description: List of logrotate options for this file.
title: /opt/so/log/telegraf/*.log title: /opt/so/log/telegraf/*.log
+15 -22
View File
@@ -880,29 +880,21 @@ recollate_postgres() {
echo "" echo ""
} }
bootstrap_so_soc_database() { scrub_postgres_log_passwords() {
# init-db.sh is mounted into so-postgres at /docker-entrypoint-initdb.d/init-db.sh # Purge plaintext passwords a pre-3.2 postgres could log on DDL errors.
# and runs automatically only on a fresh data directory. Hosts upgrading from local log=/opt/so/log/postgres/postgres.log
# 3.1.0 already have /nsm/postgres populated, so the so_soc bootstrap block [[ -f "$log" ]] || return 0
# added in 3.2 never fires. Re-run the script explicitly; it's idempotent. if ! grep -qai "PASSWORD '" "$log" 2>/dev/null; then
echo "Bootstrapping database via init-db.sh." echo "No leaked passwords found in $log."
# 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
# so psql inside init-db.sh resolves the connect user correctly.
local exec_cmd="docker exec -u postgres -e POSTGRES_USER=postgres so-postgres bash /docker-entrypoint-initdb.d/init-db.sh"
if ! /usr/sbin/so-postgres-wait; then
FINAL_MESSAGE_QUEUE+=("WARNING: so-postgres was not ready during the 3.2.0 upgrade; the so_soc database may not have been bootstrapped. Re-run manually: $exec_cmd")
return 0 return 0
fi fi
if ! $exec_cmd; then echo "Removing leaked password statements from $log."
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") local tmp
return 0 tmp=$(mktemp)
fi # Rewrite in place (cat >) to keep the inode postgres is writing to.
echo "Database bootstrap complete." grep -avi "PASSWORD '" "$log" > "$tmp" 2>/dev/null || true
cat "$tmp" > "$log"
echo "Restarting so-soc container to pick up database changes" rm -f "$tmp"
docker restart so-soc
} }
# Existing grids should keep ILM unless an admin explicitly opts in to DLM. # Existing grids should keep ILM unless an admin explicitly opts in to DLM.
@@ -989,7 +981,8 @@ post_to_3.2.0() {
# Recollate due to image OS rebase # Recollate due to image OS rebase
recollate_postgres recollate_postgres
bootstrap_so_soc_database # SOC database bootstrap is handled by the postgres.enabled highstate.
scrub_postgres_log_passwords
# Generate 9.3.7 elastic agent installers # Generate 9.3.7 elastic agent installers
echo "Regenerating Elastic Agent Installers" echo "Regenerating Elastic Agent Installers"
+17
View File
@@ -85,6 +85,23 @@ so-postgres:
- x509: postgres_crt - x509: postgres_crt
- x509: postgres_key - x509: postgres_key
postgres_wait_ready:
cmd.run:
- name: /usr/sbin/so-postgres-wait
- require:
- docker_container: so-postgres
- file: postgres_sbin
# Reconcile the SOC database (role, grants, so_telegraf) every highstate so a
# partially-initialized cluster self-heals. POSTGRES_USER is injected because
# the container env omits it.
postgres_bootstrap_soc_db:
cmd.run:
- name: docker exec -u postgres -e POSTGRES_USER=postgres so-postgres bash /docker-entrypoint-initdb.d/init-db.sh
- require:
- cmd: postgres_wait_ready
- file: postgresinitdb
delete_so-postgres_so-status.disabled: delete_so-postgres_so-status.disabled:
file.uncomment: file.uncomment:
- name: /opt/so/conf/so-status/so-status.conf - name: /opt/so/conf/so-status/so-status.conf
+8 -4
View File
@@ -8,13 +8,17 @@ if [ -z "${SO_POSTGRES_PASS:-}" ] && [ -n "${SO_POSTGRES_PASS_FILE:-}" ] && [ -r
SO_POSTGRES_PASS="$(< "$SO_POSTGRES_PASS_FILE")" SO_POSTGRES_PASS="$(< "$SO_POSTGRES_PASS_FILE")"
fi fi
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
-- Keep the password out of postgres.log if this DDL errors.
SET log_min_error_statement = panic;
-- Idempotent, race-safe upsert: CREATE, falling back to ALTER if the role
-- already exists or is created concurrently.
DO \$\$ DO \$\$
BEGIN BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${SO_POSTGRES_USER}') THEN BEGIN
EXECUTE format('CREATE ROLE %I WITH LOGIN PASSWORD %L', '${SO_POSTGRES_USER}', '${SO_POSTGRES_PASS}'); EXECUTE format('CREATE ROLE %I WITH LOGIN PASSWORD %L', '${SO_POSTGRES_USER}', '${SO_POSTGRES_PASS}');
ELSE EXCEPTION WHEN duplicate_object OR unique_violation THEN
EXECUTE format('ALTER ROLE %I WITH PASSWORD %L', '${SO_POSTGRES_USER}', '${SO_POSTGRES_PASS}'); EXECUTE format('ALTER ROLE %I WITH LOGIN PASSWORD %L', '${SO_POSTGRES_USER}', '${SO_POSTGRES_PASS}');
END IF; END;
END END
\$\$; \$\$;
GRANT ALL ON SCHEMA public TO "$SO_POSTGRES_USER"; GRANT ALL ON SCHEMA public TO "$SO_POSTGRES_USER";
+2 -11
View File
@@ -8,23 +8,14 @@
{% from 'vars/globals.map.jinja' import GLOBALS %} {% from 'vars/globals.map.jinja' import GLOBALS %}
{% from 'telegraf/map.jinja' import TELEGRAFMERGED %} {% from 'telegraf/map.jinja' import TELEGRAFMERGED %}
{# postgres_wait_ready below requires `docker_container: so-postgres`, which is {# postgres.enabled declares the so-postgres container and postgres_wait_ready
declared in postgres.enabled. Include it here so state.apply postgres.telegraf_users that the requires below reference. Salt de-duplicates the circular include. #}
on its own (e.g. from orch.deploy_newnode) still has that ID in scope. Salt
de-duplicates the circular include. #}
include: include:
- postgres.enabled - postgres.enabled
{% set TG_OUT = TELEGRAFMERGED.output | upper %} {% set TG_OUT = TELEGRAFMERGED.output | upper %}
{% if TG_OUT in ['POSTGRES', 'BOTH'] %} {% if TG_OUT in ['POSTGRES', 'BOTH'] %}
postgres_wait_ready:
cmd.run:
- name: /usr/sbin/so-postgres-wait
- require:
- docker_container: so-postgres
- file: postgres_sbin
# Ensure the shared Telegraf database exists. init-db.sh only runs on a # Ensure the shared Telegraf database exists. init-db.sh only runs on a
# fresh data dir, so hosts upgraded onto an existing /nsm/postgres volume # fresh data dir, so hosts upgraded onto an existing /nsm/postgres volume
# would otherwise never get so_telegraf. # would otherwise never get so_telegraf.
@@ -71,6 +71,8 @@ EOSQL
-v role_user="$ROLE_USER" \ -v role_user="$ROLE_USER" \
-v role_pass="$ROLE_PASS" \ -v role_pass="$ROLE_PASS" \
-U postgres -d so_telegraf <<'EOSQL' -U postgres -d so_telegraf <<'EOSQL'
-- Keep the password out of postgres.log if this DDL errors.
SET log_min_error_statement = panic;
SELECT format( SELECT format(
CASE WHEN EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = :'role_user') CASE WHEN EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = :'role_user')
THEN 'ALTER ROLE %I WITH LOGIN PASSWORD %L' THEN 'ALTER ROLE %I WITH LOGIN PASSWORD %L'
+2 -1
View File
@@ -1531,7 +1531,8 @@ soc:
agentic: false agentic: false
agentMapping: agentMapping:
Orchestrator: sonnet Orchestrator: sonnet
Hunter: sonnet Investigator: sonnet
Detection Engineer: sonnet
onionconfig: onionconfig:
saltstackDir: /opt/so/saltstack saltstackDir: /opt/so/saltstack
bypassEnabled: false bypassEnabled: false
+5 -2
View File
@@ -783,8 +783,11 @@ soc:
Orchestrator: Orchestrator:
description: The initial agent in most agentic conversations. This agent will delegate requests to specialized agents. description: The initial agent in most agentic conversations. This agent will delegate requests to specialized agents.
global: True global: True
Hunter: Investigator:
description: This agent is specialized in querying events. description: This agent investigates alerts, explains events and records, and hunts through event data. It can also acknowledge alerts and escalate to cases.
global: True
Detection Engineer:
description: This agent manages detections and their overrides, including tuning noisy rules and authoring rule content.
global: True global: True
client: client:
assistant: assistant: