diff --git a/pillar/telegraf/creds.sls b/pillar/telegraf/creds.sls new file mode 100644 index 000000000..8521bfbd9 --- /dev/null +++ b/pillar/telegraf/creds.sls @@ -0,0 +1,12 @@ +# 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. + +# Per-minion Telegraf Postgres credentials. so-telegraf-cred on the manager is +# the single writer; it mutates /opt/so/saltstack/local/pillar/telegraf/creds.sls +# under flock. Pillar_roots order (local before default) means the populated +# copy shadows this default on any real grid; this file exists so the pillar +# key is always defined on fresh installs and when no minions have creds yet. +telegraf: + postgres_creds: {} diff --git a/pillar/top.sls b/pillar/top.sls index d3b24677c..712629dbf 100644 --- a/pillar/top.sls +++ b/pillar/top.sls @@ -17,6 +17,7 @@ base: - sensoroni.adv_sensoroni - telegraf.soc_telegraf - telegraf.adv_telegraf + - telegraf.creds - versionlock.soc_versionlock - versionlock.adv_versionlock - soc.license @@ -38,6 +39,9 @@ base: {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %} - elasticsearch.auth {% endif %} + {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/postgres/auth.sls') %} + - postgres.auth + {% endif %} {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %} - kibana.secrets {% endif %} @@ -60,6 +64,8 @@ base: - redis.adv_redis - influxdb.soc_influxdb - influxdb.adv_influxdb + - postgres.soc_postgres + - postgres.adv_postgres - elasticsearch.nodes - elasticsearch.soc_elasticsearch - elasticsearch.adv_elasticsearch @@ -100,6 +106,9 @@ base: {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %} - elasticsearch.auth {% endif %} + {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/postgres/auth.sls') %} + - postgres.auth + {% endif %} {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %} - kibana.secrets {% endif %} @@ -125,6 +134,8 @@ base: - redis.adv_redis - influxdb.soc_influxdb - influxdb.adv_influxdb + - postgres.soc_postgres + - postgres.adv_postgres - backup.soc_backup - backup.adv_backup - zeek.soc_zeek @@ -144,6 +155,9 @@ base: {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %} - elasticsearch.auth {% endif %} + {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/postgres/auth.sls') %} + - postgres.auth + {% endif %} {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %} - kibana.secrets {% endif %} @@ -158,6 +172,8 @@ base: - redis.adv_redis - influxdb.soc_influxdb - influxdb.adv_influxdb + - postgres.soc_postgres + - postgres.adv_postgres - elasticsearch.nodes - elasticsearch.soc_elasticsearch - elasticsearch.adv_elasticsearch @@ -257,6 +273,9 @@ base: {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %} - elasticsearch.auth {% endif %} + {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/postgres/auth.sls') %} + - postgres.auth + {% endif %} {% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %} - kibana.secrets {% endif %} @@ -282,6 +301,8 @@ base: - redis.adv_redis - influxdb.soc_influxdb - influxdb.adv_influxdb + - postgres.soc_postgres + - postgres.adv_postgres - zeek.soc_zeek - zeek.adv_zeek - bpf.soc_bpf diff --git a/salt/allowed_states.map.jinja b/salt/allowed_states.map.jinja index 6d8b0e2a0..c831b45fe 100644 --- a/salt/allowed_states.map.jinja +++ b/salt/allowed_states.map.jinja @@ -29,6 +29,8 @@ 'manager', 'nginx', 'influxdb', + 'postgres', + 'postgres.auth', 'soc', 'kratos', 'hydra', diff --git a/salt/backup/config_backup.sls b/salt/backup/config_backup.sls index a09c67b1b..a4297444b 100644 --- a/salt/backup/config_backup.sls +++ b/salt/backup/config_backup.sls @@ -32,3 +32,4 @@ so_config_backup: - daymonth: '*' - month: '*' - dayweek: '*' + diff --git a/salt/ca/files/signing_policies.conf b/salt/ca/files/signing_policies.conf index 4fc04aacc..5424d7b37 100644 --- a/salt/ca/files/signing_policies.conf +++ b/salt/ca/files/signing_policies.conf @@ -54,6 +54,20 @@ x509_signing_policies: - extendedKeyUsage: serverAuth - days_valid: 820 - copypath: /etc/pki/issued_certs/ + postgres: + - minions: '*' + - signing_private_key: /etc/pki/ca.key + - signing_cert: /etc/pki/ca.crt + - C: US + - ST: Utah + - L: Salt Lake City + - basicConstraints: "critical CA:false" + - keyUsage: "critical keyEncipherment" + - subjectKeyIdentifier: hash + - authorityKeyIdentifier: keyid,issuer:always + - extendedKeyUsage: serverAuth + - days_valid: 820 + - copypath: /etc/pki/issued_certs/ elasticfleet: - minions: '*' - signing_private_key: /etc/pki/ca.key diff --git a/salt/common/tools/sbin/so-image-common b/salt/common/tools/sbin/so-image-common index f15f90e73..e8f604681 100755 --- a/salt/common/tools/sbin/so-image-common +++ b/salt/common/tools/sbin/so-image-common @@ -31,6 +31,7 @@ container_list() { "so-hydra" "so-nginx" "so-pcaptools" + "so-postgres" "so-soc" "so-suricata" "so-telegraf" @@ -55,6 +56,7 @@ container_list() { "so-logstash" "so-nginx" "so-pcaptools" + "so-postgres" "so-redis" "so-soc" "so-strelka-backend" diff --git a/salt/docker/defaults.yaml b/salt/docker/defaults.yaml index 044ec98b0..81ff07190 100644 --- a/salt/docker/defaults.yaml +++ b/salt/docker/defaults.yaml @@ -237,3 +237,11 @@ docker: extra_hosts: [] extra_env: [] ulimits: [] + 'so-postgres': + final_octet: 47 + port_bindings: + - 0.0.0.0:5432:5432 + custom_bind_mounts: [] + extra_hosts: [] + extra_env: [] + ulimits: [] diff --git a/salt/firewall/containers.map.jinja b/salt/firewall/containers.map.jinja index 2d1135e5f..b39ba2b31 100644 --- a/salt/firewall/containers.map.jinja +++ b/salt/firewall/containers.map.jinja @@ -11,6 +11,7 @@ 'so-kratos', 'so-hydra', 'so-nginx', + 'so-postgres', 'so-redis', 'so-soc', 'so-strelka-coordinator', @@ -34,6 +35,7 @@ 'so-hydra', 'so-logstash', 'so-nginx', + 'so-postgres', 'so-redis', 'so-soc', 'so-strelka-coordinator', @@ -77,6 +79,7 @@ 'so-kratos', 'so-hydra', 'so-nginx', + 'so-postgres', 'so-soc' ] %} diff --git a/salt/firewall/defaults.yaml b/salt/firewall/defaults.yaml index a11492e88..e9c82401d 100644 --- a/salt/firewall/defaults.yaml +++ b/salt/firewall/defaults.yaml @@ -98,6 +98,10 @@ firewall: tcp: - 8086 udp: [] + postgres: + tcp: + - 5432 + udp: [] kafka_controller: tcp: - 9093 @@ -193,6 +197,7 @@ firewall: - kibana - redis - influxdb + - postgres - elasticsearch_rest - elasticsearch_node - localrules @@ -379,6 +384,7 @@ firewall: - kibana - redis - influxdb + - postgres - elasticsearch_rest - elasticsearch_node - docker_registry @@ -590,6 +596,7 @@ firewall: - kibana - redis - influxdb + - postgres - elasticsearch_rest - elasticsearch_node - docker_registry @@ -799,6 +806,7 @@ firewall: - kibana - redis - influxdb + - postgres - elasticsearch_rest - elasticsearch_node - docker_registry @@ -1011,6 +1019,7 @@ firewall: - kibana - redis - influxdb + - postgres - elasticsearch_rest - elasticsearch_node - docker_registry diff --git a/salt/firewall/map.jinja b/salt/firewall/map.jinja index 58d8c189d..61f8215b8 100644 --- a/salt/firewall/map.jinja +++ b/salt/firewall/map.jinja @@ -1,5 +1,6 @@ {% from 'vars/globals.map.jinja' import GLOBALS %} {% from 'docker/docker.map.jinja' import DOCKERMERGED %} +{% from 'telegraf/map.jinja' import TELEGRAFMERGED %} {% import_yaml 'firewall/defaults.yaml' as FIREWALL_DEFAULT %} {# add our ip to self #} @@ -55,4 +56,16 @@ {% endif %} +{# Open Postgres (5432) to minion hostgroups when Telegraf is configured to write to Postgres #} +{% set TG_OUT = TELEGRAFMERGED.output | upper %} +{% if TG_OUT in ['POSTGRES', 'BOTH'] %} +{% if role.startswith('manager') or role == 'standalone' or role == 'eval' %} +{% for r in ['sensor', 'searchnode', 'heavynode', 'receiver', 'fleet', 'idh', 'desktop', 'import'] %} +{% if FIREWALL_DEFAULT.firewall.role[role].chain["DOCKER-USER"].hostgroups[r] is defined %} +{% do FIREWALL_DEFAULT.firewall.role[role].chain["DOCKER-USER"].hostgroups[r].portgroups.append('postgres') %} +{% endif %} +{% endfor %} +{% endif %} +{% endif %} + {% set FIREWALL_MERGED = salt['pillar.get']('firewall', FIREWALL_DEFAULT.firewall, merge=True) %} diff --git a/salt/manager/tools/sbin/so-minion b/salt/manager/tools/sbin/so-minion index 76b067817..86bab25e6 100755 --- a/salt/manager/tools/sbin/so-minion +++ b/salt/manager/tools/sbin/so-minion @@ -273,7 +273,7 @@ function deleteMinionFiles () { log "ERROR" "Failed to delete $PILLARFILE" return 1 fi - + rm -f $ADVPILLARFILE if [ $? -ne 0 ]; then log "ERROR" "Failed to delete $ADVPILLARFILE" @@ -281,6 +281,39 @@ function deleteMinionFiles () { fi } +# Remove this minion's postgres Telegraf credential from the shared creds +# pillar and drop the matching role in Postgres. Always returns 0 so a dead +# or unreachable so-postgres doesn't block minion deletion — in that case we +# log a warning and leave the role behind for manual cleanup. +function remove_postgres_telegraf_from_minion() { + local MINION_SAFE + MINION_SAFE=$(echo "$MINION_ID" | tr '.-' '__' | tr '[:upper:]' '[:lower:]') + local PG_USER="so_telegraf_${MINION_SAFE}" + + log "INFO" "Removing postgres telegraf cred for $MINION_ID" + + so-telegraf-cred remove "$MINION_ID" >/dev/null 2>&1 || true + + if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^so-postgres$'; then + if ! docker exec -i so-postgres psql -v ON_ERROR_STOP=1 -U postgres -d so_telegraf >/dev/null 2>&1 < " >&2 + exit 2 +} + +seed_creds_file() { + mkdir -p "$(dirname "$CREDS")" || return 1 + if [[ ! -f "$CREDS" ]]; then + (umask 027 && printf 'telegraf:\n postgres_creds: {}\n' > "$CREDS") || return 1 + chown socore:socore "$CREDS" 2>/dev/null || true + chmod 640 "$CREDS" || return 1 + fi +} + +OP=$1 +MID=$2 +[[ -z "$OP" || -z "$MID" ]] && usage + +case "$OP" in + add) + SAFE=$(echo "$MID" | tr '.-' '__' | tr '[:upper:]' '[:lower:]') + seed_creds_file || exit 1 + if so-yaml.py get -r "$CREDS" "telegraf.postgres_creds.${MID}.user" >/dev/null 2>&1; then + exit 0 + fi + PASS=$(tr -dc 'A-Za-z0-9~!@#^&*()_=+[]|;:,.<>?-' < /dev/urandom | head -c 72) + so-yaml.py replace "$CREDS" "telegraf.postgres_creds.${MID}.user" "so_telegraf_${SAFE}" >/dev/null + so-yaml.py replace "$CREDS" "telegraf.postgres_creds.${MID}.pass" "$PASS" >/dev/null + ;; + remove) + [[ -f "$CREDS" ]] || exit 0 + so-yaml.py remove "$CREDS" "telegraf.postgres_creds.${MID}" >/dev/null 2>&1 || true + ;; + *) + usage + ;; +esac diff --git a/salt/manager/tools/sbin/so-yaml.py b/salt/manager/tools/sbin/so-yaml.py index 79dcfcac0..d0d5209f9 100755 --- a/salt/manager/tools/sbin/so-yaml.py +++ b/salt/manager/tools/sbin/so-yaml.py @@ -39,9 +39,16 @@ def showUsage(args): def loadYaml(filename): - file = open(filename, "r") - content = file.read() - return yaml.safe_load(content) + try: + with open(filename, "r") as file: + content = file.read() + return yaml.safe_load(content) + except FileNotFoundError: + print(f"File not found: {filename}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error reading file {filename}: {e}", file=sys.stderr) + sys.exit(1) def writeYaml(filename, content): @@ -285,7 +292,8 @@ def add(args): def removeKey(content, key): pieces = key.split(".", 1) if len(pieces) > 1: - removeKey(content[pieces[0]], pieces[1]) + if pieces[0] in content: + removeKey(content[pieces[0]], pieces[1]) else: content.pop(key, None) diff --git a/salt/manager/tools/sbin/so-yaml_test.py b/salt/manager/tools/sbin/so-yaml_test.py index 2da8a0be9..56581f7e3 100644 --- a/salt/manager/tools/sbin/so-yaml_test.py +++ b/salt/manager/tools/sbin/so-yaml_test.py @@ -973,3 +973,21 @@ class TestReplaceListObject(unittest.TestCase): expected = "key1:\n- id: '1'\n status: updated\n- id: '2'\n status: inactive\n" self.assertEqual(actual, expected) + + +class TestLoadYaml(unittest.TestCase): + + def test_load_yaml_missing_file(self): + with patch('sys.exit', new=MagicMock()) as sysmock: + with patch('sys.stderr', new=StringIO()) as mock_stderr: + soyaml.loadYaml("/tmp/so-yaml_test-does-not-exist.yaml") + sysmock.assert_called_with(1) + self.assertIn("File not found:", mock_stderr.getvalue()) + + def test_load_yaml_read_error(self): + with patch('sys.exit', new=MagicMock()) as sysmock: + with patch('sys.stderr', new=StringIO()) as mock_stderr: + with patch('builtins.open', side_effect=PermissionError("denied")): + soyaml.loadYaml("/tmp/so-yaml_test-unreadable.yaml") + sysmock.assert_called_with(1) + self.assertIn("Error reading file", mock_stderr.getvalue()) diff --git a/salt/manager/tools/sbin/soup b/salt/manager/tools/sbin/soup index 2d36cf7eb..a838e3275 100755 --- a/salt/manager/tools/sbin/soup +++ b/salt/manager/tools/sbin/soup @@ -485,7 +485,44 @@ elasticsearch_backup_index_templates() { tar -czf /nsm/backup/3.0.0_elasticsearch_index_templates.tar.gz -C /opt/so/conf/elasticsearch/templates/index/ . } +ensure_postgres_local_pillar() { + # Postgres was added as a service after 3.0.0, so the new pillar/top.sls + # references postgres.soc_postgres / postgres.adv_postgres unconditionally. + # Managers upgrading from 3.0.0 have no /opt/so/saltstack/local/pillar/postgres/ + # (make_some_dirs only runs at install time), so the stubs must be created + # here before salt-master restarts against the new top.sls. + echo "Ensuring postgres local pillar stubs exist." + local dir=/opt/so/saltstack/local/pillar/postgres + mkdir -p "$dir" + [[ -f "$dir/soc_postgres.sls" ]] || touch "$dir/soc_postgres.sls" + [[ -f "$dir/adv_postgres.sls" ]] || touch "$dir/adv_postgres.sls" + chown -R socore:socore "$dir" +} + +ensure_postgres_secret() { + # On a fresh install, generate_passwords + secrets_pillar seed + # secrets:postgres_pass in /opt/so/saltstack/local/pillar/secrets.sls. That + # code path is skipped on upgrade (secrets.sls already exists from 3.0.0 + # with import_pass/influx_pass but no postgres_pass), so the postgres + # container's POSTGRES_PASSWORD_FILE and SOC's PG_ADMIN_PASS would be empty + # after highstate. Generate one now if missing. + local secrets_file=/opt/so/saltstack/local/pillar/secrets.sls + if [[ ! -f "$secrets_file" ]]; then + echo "WARNING: $secrets_file missing; skipping postgres_pass backfill." + return 0 + fi + if so-yaml.py get -r "$secrets_file" secrets.postgres_pass >/dev/null 2>&1; then + echo "secrets.postgres_pass already set; leaving as-is." + return 0 + fi + echo "Seeding secrets.postgres_pass in $secrets_file." + so-yaml.py add "$secrets_file" secrets.postgres_pass "$(get_random_value)" + chown socore:socore "$secrets_file" +} + 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. @@ -502,6 +539,20 @@ post_to_3.1.0() { salt-call state.apply salt.cloud.config concurrent=True fi + # Backfill the Telegraf creds pillar for every accepted minion. so-telegraf-cred + # add is idempotent — it no-ops when an entry already exists — so this is safe + # to run on every soup. The subsequent state.apply creates/updates the matching + # Postgres roles from the reconciled pillar. + echo "Reconciling Telegraf Postgres creds for accepted minions." + for mid in $(salt-key --out=json --list=accepted 2>/dev/null | jq -r '.minions[]?' 2>/dev/null); do + [[ -n "$mid" ]] || continue + /usr/sbin/so-telegraf-cred add "$mid" || echo " warning: so-telegraf-cred add $mid failed" >&2 + done + # Run through the master (not --local) so state compilation uses the + # master's configured file_roots; the manager's /etc/salt/minion has no + # file_roots of its own and --local would fail with "No matching sls found". + salt-call state.apply postgres.telegraf_users queue=True || true + POSTVERSION=3.1.0 } diff --git a/salt/orch/deploy_newnode.sls b/salt/orch/deploy_newnode.sls index c05a812a3..ee241ef33 100644 --- a/salt/orch/deploy_newnode.sls +++ b/salt/orch/deploy_newnode.sls @@ -25,8 +25,33 @@ manager_run_es_soc: - salt: {{NEWNODE}}_update_mine {% endif %} +# so-minion has already added the new minion's entry to telegraf/creds.sls +# via so-telegraf-cred before this orch fires. Reconcile the Postgres role +# on the manager so the new minion can authenticate on its first highstate, +# then refresh the minion's pillar so its telegraf.conf renders with the +# freshly-written cred. +manager_create_postgres_telegraf_role: + salt.state: + - tgt: {{ MANAGER }} + - sls: + - postgres.telegraf_users + - queue: True + - require: + - salt: {{NEWNODE}}_update_mine + +{{NEWNODE}}_refresh_pillar: + salt.function: + - name: saltutil.refresh_pillar + - tgt: {{ NEWNODE }} + - kwarg: + wait: True + - require: + - salt: manager_create_postgres_telegraf_role + {{NEWNODE}}_run_highstate: salt.state: - tgt: {{ NEWNODE }} - highstate: True - queue: True + - require: + - salt: {{NEWNODE}}_refresh_pillar diff --git a/salt/postgres/auth.sls b/salt/postgres/auth.sls new file mode 100644 index 000000000..4f486ff02 --- /dev/null +++ b/salt/postgres/auth.sls @@ -0,0 +1,37 @@ +# 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. + +{% from 'allowed_states.map.jinja' import allowed_states %} +{% if sls in allowed_states %} + + {% set DIGITS = "1234567890" %} + {% set LOWERCASE = "qwertyuiopasdfghjklzxcvbnm" %} + {% set UPPERCASE = "QWERTYUIOPASDFGHJKLZXCVBNM" %} + {% set SYMBOLS = "~!@#^&*()-_=+[]|;:,.<>?" %} + {% set CHARS = DIGITS~LOWERCASE~UPPERCASE~SYMBOLS %} + {% set so_postgres_user_pass = salt['pillar.get']('postgres:auth:users:so_postgres_user:pass', salt['random.get_str'](72, chars=CHARS)) %} + +# Admin cred only. Per-minion Telegraf creds live in telegraf/creds.sls, +# managed by /usr/sbin/so-telegraf-cred (called from so-minion). +postgres_auth_pillar: + file.managed: + - name: /opt/so/saltstack/local/pillar/postgres/auth.sls + - mode: 640 + - reload_pillar: True + - contents: | + postgres: + auth: + users: + so_postgres_user: + user: so_postgres + pass: "{{ so_postgres_user_pass }}" + - show_changes: False +{% else %} + +{{sls}}_state_not_allowed: + test.fail_without_changes: + - name: {{sls}}_state_not_allowed + +{% endif %} diff --git a/salt/postgres/config.sls b/salt/postgres/config.sls new file mode 100644 index 000000000..11ca52649 --- /dev/null +++ b/salt/postgres/config.sls @@ -0,0 +1,111 @@ +# 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. + +{% from 'allowed_states.map.jinja' import allowed_states %} +{% if sls.split('.')[0] in allowed_states %} +{% from 'postgres/map.jinja' import PGMERGED %} + +# Postgres Setup +postgresconfdir: + file.directory: + - name: /opt/so/conf/postgres + - user: 939 + - group: 939 + - makedirs: True + +postgressecretsdir: + file.directory: + - name: /opt/so/conf/postgres/secrets + - user: 939 + - group: 939 + - mode: 700 + - require: + - file: postgresconfdir + +postgresdatadir: + file.directory: + - name: /nsm/postgres + - user: 939 + - group: 939 + - makedirs: True + +postgreslogdir: + file.directory: + - name: /opt/so/log/postgres + - user: 939 + - group: 939 + - makedirs: True + +postgresinitdir: + file.directory: + - name: /opt/so/conf/postgres/init + - user: 939 + - group: 939 + - require: + - file: postgresconfdir + +postgresinitusers: + file.managed: + - name: /opt/so/conf/postgres/init/init-users.sh + - source: salt://postgres/files/init-users.sh + - user: 939 + - group: 939 + - mode: 755 + +postgresconf: + file.managed: + - name: /opt/so/conf/postgres/postgresql.conf + - source: salt://postgres/files/postgresql.conf.jinja + - user: 939 + - group: 939 + - template: jinja + - defaults: + PGMERGED: {{ PGMERGED }} + +postgreshba: + file.managed: + - name: /opt/so/conf/postgres/pg_hba.conf + - source: salt://postgres/files/pg_hba.conf + - user: 939 + - group: 939 + - mode: 640 + +postgres_super_secret: + file.managed: + - name: /opt/so/conf/postgres/secrets/postgres_password + - user: 939 + - group: 939 + - mode: 600 + - contents_pillar: 'secrets:postgres_pass' + - show_changes: False + - require: + - file: postgressecretsdir + +postgres_app_secret: + file.managed: + - name: /opt/so/conf/postgres/secrets/so_postgres_pass + - user: 939 + - group: 939 + - mode: 600 + - contents_pillar: 'postgres:auth:users:so_postgres_user:pass' + - show_changes: False + - require: + - file: postgressecretsdir + +postgres_sbin: + file.recurse: + - name: /usr/sbin + - source: salt://postgres/tools/sbin + - user: root + - group: root + - file_mode: 755 + +{% else %} + +{{sls}}_state_not_allowed: + test.fail_without_changes: + - name: {{sls}}_state_not_allowed + +{% endif %} diff --git a/salt/postgres/defaults.yaml b/salt/postgres/defaults.yaml new file mode 100644 index 000000000..7ad82f453 --- /dev/null +++ b/salt/postgres/defaults.yaml @@ -0,0 +1,19 @@ +postgres: + enabled: True + telegraf: + retention_days: 14 + config: + listen_addresses: '*' + port: 5432 + max_connections: 100 + shared_buffers: 256MB + ssl: 'on' + ssl_cert_file: '/conf/postgres.crt' + ssl_key_file: '/conf/postgres.key' + ssl_ca_file: '/conf/ca.crt' + hba_file: '/conf/pg_hba.conf' + log_destination: 'stderr' + logging_collector: 'off' + log_min_messages: 'warning' + shared_preload_libraries: pg_cron + cron.database_name: so_telegraf diff --git a/salt/postgres/disabled.sls b/salt/postgres/disabled.sls new file mode 100644 index 000000000..4b5b62328 --- /dev/null +++ b/salt/postgres/disabled.sls @@ -0,0 +1,33 @@ +# 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. + +{% from 'allowed_states.map.jinja' import allowed_states %} +{% if sls.split('.')[0] in allowed_states %} + +include: + - postgres.sostatus + +so-postgres: + docker_container.absent: + - force: True + +so-postgres_so-status.disabled: + file.comment: + - name: /opt/so/conf/so-status/so-status.conf + - regex: ^so-postgres$ + +so_postgres_backup: + cron.absent: + - name: /usr/sbin/so-postgres-backup > /dev/null 2>&1 + - identifier: so_postgres_backup + - user: root + +{% else %} + +{{sls}}_state_not_allowed: + test.fail_without_changes: + - name: {{sls}}_state_not_allowed + +{% endif %} diff --git a/salt/postgres/enabled.sls b/salt/postgres/enabled.sls new file mode 100644 index 000000000..b3abb621e --- /dev/null +++ b/salt/postgres/enabled.sls @@ -0,0 +1,109 @@ +# 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. + +{% from 'allowed_states.map.jinja' import allowed_states %} +{% if sls.split('.')[0] in allowed_states %} +{% from 'vars/globals.map.jinja' import GLOBALS %} +{% from 'docker/docker.map.jinja' import DOCKERMERGED %} +{% set SO_POSTGRES_USER = salt['pillar.get']('postgres:auth:users:so_postgres_user:user', 'so_postgres') %} + +include: + - postgres.auth + - postgres.ssl + - postgres.config + - postgres.sostatus + - postgres.telegraf_users + +so-postgres: + docker_container.running: + - image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-postgres:{{ GLOBALS.so_version }} + - hostname: so-postgres + - networks: + - sobridge: + - ipv4_address: {{ DOCKERMERGED.containers['so-postgres'].ip }} + - port_bindings: + {% for BINDING in DOCKERMERGED.containers['so-postgres'].port_bindings %} + - {{ BINDING }} + {% endfor %} + - environment: + - POSTGRES_DB=securityonion + # Passwords are delivered via mounted 0600 secret files, not plaintext env vars. + # The upstream postgres image resolves POSTGRES_PASSWORD_FILE; entrypoint.sh and + # init-users.sh resolve SO_POSTGRES_PASS_FILE the same way. + - POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password + - SO_POSTGRES_USER={{ SO_POSTGRES_USER }} + - SO_POSTGRES_PASS_FILE=/run/secrets/so_postgres_pass + {% if DOCKERMERGED.containers['so-postgres'].extra_env %} + {% for XTRAENV in DOCKERMERGED.containers['so-postgres'].extra_env %} + - {{ XTRAENV }} + {% endfor %} + {% endif %} + - binds: + - /opt/so/log/postgres/:/log:rw + - /nsm/postgres:/var/lib/postgresql/data:rw + - /opt/so/conf/postgres/postgresql.conf:/conf/postgresql.conf:ro + - /opt/so/conf/postgres/pg_hba.conf:/conf/pg_hba.conf:ro + - /opt/so/conf/postgres/secrets:/run/secrets:ro + - /opt/so/conf/postgres/init/init-users.sh:/docker-entrypoint-initdb.d/init-users.sh:ro + - /etc/pki/postgres.crt:/conf/postgres.crt:ro + - /etc/pki/postgres.key:/conf/postgres.key:ro + - /etc/pki/tls/certs/intca.crt:/conf/ca.crt:ro + {% if DOCKERMERGED.containers['so-postgres'].custom_bind_mounts %} + {% for BIND in DOCKERMERGED.containers['so-postgres'].custom_bind_mounts %} + - {{ BIND }} + {% endfor %} + {% endif %} + {% if DOCKERMERGED.containers['so-postgres'].extra_hosts %} + - extra_hosts: + {% for XTRAHOST in DOCKERMERGED.containers['so-postgres'].extra_hosts %} + - {{ XTRAHOST }} + {% endfor %} + {% endif %} + {% if DOCKERMERGED.containers['so-postgres'].ulimits %} + - ulimits: + {% for ULIMIT in DOCKERMERGED.containers['so-postgres'].ulimits %} + - {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }} + {% endfor %} + {% endif %} + - watch: + - file: postgresconf + - file: postgreshba + - file: postgresinitusers + - file: postgres_super_secret + - file: postgres_app_secret + - x509: postgres_crt + - x509: postgres_key + - require: + - file: postgresconf + - file: postgreshba + - file: postgresinitusers + - file: postgres_super_secret + - file: postgres_app_secret + - x509: postgres_crt + - x509: postgres_key + +delete_so-postgres_so-status.disabled: + file.uncomment: + - name: /opt/so/conf/so-status/so-status.conf + - regex: ^so-postgres$ + +so_postgres_backup: + cron.present: + - name: /usr/sbin/so-postgres-backup > /dev/null 2>&1 + - identifier: so_postgres_backup + - user: root + - minute: '5' + - hour: '0' + - daymonth: '*' + - month: '*' + - dayweek: '*' + +{% else %} + +{{sls}}_state_not_allowed: + test.fail_without_changes: + - name: {{sls}}_state_not_allowed + +{% endif %} diff --git a/salt/postgres/files/init-users.sh b/salt/postgres/files/init-users.sh new file mode 100644 index 000000000..e28b11f0f --- /dev/null +++ b/salt/postgres/files/init-users.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +# Create or update application user for SOC platform access +# This script runs on first database initialization via docker-entrypoint-initdb.d +# The password is properly escaped to handle special characters +if [ -z "${SO_POSTGRES_PASS:-}" ] && [ -n "${SO_POSTGRES_PASS_FILE:-}" ] && [ -r "$SO_POSTGRES_PASS_FILE" ]; then + SO_POSTGRES_PASS="$(< "$SO_POSTGRES_PASS_FILE")" +fi +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + DO \$\$ + BEGIN + IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${SO_POSTGRES_USER}') THEN + EXECUTE format('CREATE ROLE %I WITH LOGIN PASSWORD %L', '${SO_POSTGRES_USER}', '${SO_POSTGRES_PASS}'); + ELSE + EXECUTE format('ALTER ROLE %I WITH PASSWORD %L', '${SO_POSTGRES_USER}', '${SO_POSTGRES_PASS}'); + END IF; + END + \$\$; + GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO "$SO_POSTGRES_USER"; + -- Lock the SOC database down at the connect layer; PUBLIC gets CONNECT + -- by default, which would let per-minion telegraf roles open sessions + -- here. They have no schema/table grants inside so reads fail, but + -- revoking CONNECT closes the soft edge entirely. + REVOKE CONNECT ON DATABASE "$POSTGRES_DB" FROM PUBLIC; + GRANT CONNECT ON DATABASE "$POSTGRES_DB" TO "$SO_POSTGRES_USER"; +EOSQL + +# Bootstrap the Telegraf metrics database. Per-minion roles + schemas are +# reconciled on every state.apply by postgres/telegraf_users.sls; this block +# only ensures the shared database exists on first initialization. +if ! psql -U "$POSTGRES_USER" -tAc "SELECT 1 FROM pg_database WHERE datname='so_telegraf'" | grep -q 1; then + psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -c "CREATE DATABASE so_telegraf" +fi diff --git a/salt/postgres/files/pg_hba.conf b/salt/postgres/files/pg_hba.conf new file mode 100644 index 000000000..e7d31c05f --- /dev/null +++ b/salt/postgres/files/pg_hba.conf @@ -0,0 +1,16 @@ +# 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. +# +# Managed by Salt — do not edit by hand. +# Client authentication config: only local (Unix socket) connections and TLS-wrapped TCP +# connections are accepted. Plain-text `host ...` lines are intentionally omitted so a +# misconfigured client with sslmode=disable cannot negotiate a cleartext session. + +# Local connections (Unix socket, container-internal) use peer/trust. +local all all trust + +# TCP connections MUST use TLS (hostssl) and authenticate with SCRAM. +hostssl all all 0.0.0.0/0 scram-sha-256 +hostssl all all ::/0 scram-sha-256 diff --git a/salt/postgres/files/postgresql.conf.jinja b/salt/postgres/files/postgresql.conf.jinja new file mode 100644 index 000000000..2ddc52a51 --- /dev/null +++ b/salt/postgres/files/postgresql.conf.jinja @@ -0,0 +1,8 @@ +{# 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. #} + +{% for key, value in PGMERGED.config.items() %} +{{ key }} = '{{ value | string | replace("'", "''") }}' +{% endfor %} diff --git a/salt/postgres/init.sls b/salt/postgres/init.sls new file mode 100644 index 000000000..2e3c9ffb7 --- /dev/null +++ b/salt/postgres/init.sls @@ -0,0 +1,13 @@ +# 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. + +{% from 'postgres/map.jinja' import PGMERGED %} + +include: +{% if PGMERGED.enabled %} + - postgres.enabled +{% else %} + - postgres.disabled +{% endif %} diff --git a/salt/postgres/map.jinja b/salt/postgres/map.jinja new file mode 100644 index 000000000..5250ca8fd --- /dev/null +++ b/salt/postgres/map.jinja @@ -0,0 +1,7 @@ +{# 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. #} + +{% import_yaml 'postgres/defaults.yaml' as PGDEFAULTS %} +{% set PGMERGED = salt['pillar.get']('postgres', PGDEFAULTS.postgres, merge=True) %} diff --git a/salt/postgres/soc_postgres.yaml b/salt/postgres/soc_postgres.yaml new file mode 100644 index 000000000..4b25cd4f5 --- /dev/null +++ b/salt/postgres/soc_postgres.yaml @@ -0,0 +1,89 @@ +postgres: + enabled: + description: Whether the PostgreSQL database container is enabled on this grid. Backs the assistant store and the Telegraf metrics database. + forcedType: bool + readonly: True + helpLink: influxdb + telegraf: + retention_days: + description: Number of days of Telegraf metrics to keep in the so_telegraf database. Older partitions are dropped hourly by pg_partman. + forcedType: int + helpLink: postgres + config: + max_connections: + description: Maximum number of concurrent PostgreSQL connections. + forcedType: int + global: True + helpLink: postgres + shared_buffers: + description: Amount of memory PostgreSQL uses for shared buffers (e.g. 256MB, 1GB). Raising this improves read cache hit rate at the cost of system RAM. + global: True + helpLink: postgres + log_min_messages: + description: Minimum severity of server messages written to the PostgreSQL log. + options: + - debug1 + - info + - notice + - warning + - error + - log + - fatal + global: True + helpLink: postgres + listen_addresses: + description: Interfaces PostgreSQL listens on. Must remain '*' so clients on the docker bridge network can connect. + global: True + advanced: True + helpLink: postgres + port: + description: TCP port PostgreSQL listens on inside the container. Firewall rules and container port mapping assume 5432. + forcedType: int + global: True + advanced: True + helpLink: postgres + ssl: + description: Whether PostgreSQL accepts TLS connections. Must remain 'on' — pg_hba.conf requires hostssl for TCP. + global: True + advanced: True + helpLink: postgres + ssl_cert_file: + description: Path (inside the container) to the TLS server certificate. Salt-managed. + global: True + advanced: True + helpLink: postgres + ssl_key_file: + description: Path (inside the container) to the TLS server private key. Salt-managed. + global: True + advanced: True + helpLink: postgres + ssl_ca_file: + description: Path (inside the container) to the CA bundle PostgreSQL uses to verify client certificates. Salt-managed. + global: True + advanced: True + helpLink: postgres + hba_file: + description: Path (inside the container) to the pg_hba.conf authentication file. Salt-managed — edit salt/postgres/files/pg_hba.conf. + global: True + advanced: True + helpLink: postgres + log_destination: + description: Where PostgreSQL writes its server log. 'stderr' routes to the container log stream. + global: True + advanced: True + helpLink: postgres + logging_collector: + description: Whether to run a separate logging collector process. Disabled because the docker log stream already captures stderr. + global: True + advanced: True + helpLink: postgres + shared_preload_libraries: + description: Comma-separated list of extensions loaded at server start. Required for pg_cron which drives pg_partman maintenance — do not remove. + global: True + advanced: True + helpLink: postgres + cron.database_name: + description: Database pg_cron schedules jobs in. Must be so_telegraf so partman maintenance runs in the right database context. + global: True + advanced: True + helpLink: postgres diff --git a/salt/postgres/sostatus.sls b/salt/postgres/sostatus.sls new file mode 100644 index 000000000..4a61af3d1 --- /dev/null +++ b/salt/postgres/sostatus.sls @@ -0,0 +1,21 @@ +# 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. + +{% from 'allowed_states.map.jinja' import allowed_states %} +{% if sls.split('.')[0] in allowed_states %} + +append_so-postgres_so-status.conf: + file.append: + - name: /opt/so/conf/so-status/so-status.conf + - text: so-postgres + - unless: grep -q so-postgres /opt/so/conf/so-status/so-status.conf + +{% else %} + +{{sls}}_state_not_allowed: + test.fail_without_changes: + - name: {{sls}}_state_not_allowed + +{% endif %} diff --git a/salt/postgres/ssl.sls b/salt/postgres/ssl.sls new file mode 100644 index 000000000..4223ead34 --- /dev/null +++ b/salt/postgres/ssl.sls @@ -0,0 +1,55 @@ +# 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. + +{% from 'allowed_states.map.jinja' import allowed_states %} +{% if sls.split('.')[0] in allowed_states %} +{% from 'vars/globals.map.jinja' import GLOBALS %} +{% from 'ca/map.jinja' import CA %} + +postgres_key: + x509.private_key_managed: + - name: /etc/pki/postgres.key + - keysize: 4096 + - backup: True + - new: True + {% if salt['file.file_exists']('/etc/pki/postgres.key') -%} + - prereq: + - x509: /etc/pki/postgres.crt + {%- endif %} + - retry: + attempts: 5 + interval: 30 + +postgres_crt: + x509.certificate_managed: + - name: /etc/pki/postgres.crt + - ca_server: {{ CA.server }} + - subjectAltName: DNS:{{ GLOBALS.hostname }}, IP:{{ GLOBALS.node_ip }} + - signing_policy: postgres + - private_key: /etc/pki/postgres.key + - CN: {{ GLOBALS.hostname }} + - days_remaining: 7 + - days_valid: 820 + - backup: True + - timeout: 30 + - retry: + attempts: 5 + interval: 30 + +postgresKeyperms: + file.managed: + - replace: False + - name: /etc/pki/postgres.key + - mode: 400 + - user: 939 + - group: 939 + +{% else %} + +{{sls}}_state_not_allowed: + test.fail_without_changes: + - name: {{sls}}_state_not_allowed + +{% endif %} diff --git a/salt/postgres/telegraf_users.sls b/salt/postgres/telegraf_users.sls new file mode 100644 index 000000000..62490ea52 --- /dev/null +++ b/salt/postgres/telegraf_users.sls @@ -0,0 +1,157 @@ +# 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. + +{% from 'allowed_states.map.jinja' import allowed_states %} +{% if sls.split('.')[0] in allowed_states %} +{% from 'vars/globals.map.jinja' import GLOBALS %} +{% from 'telegraf/map.jinja' import TELEGRAFMERGED %} + +{# postgres_wait_ready below requires `docker_container: so-postgres`, which is + declared in postgres.enabled. Include it here so state.apply postgres.telegraf_users + on its own (e.g. from orch.deploy_newnode) still has that ID in scope. Salt + de-duplicates the circular include. #} +include: + - postgres.enabled + +{% set TG_OUT = TELEGRAFMERGED.output | upper %} +{% if TG_OUT in ['POSTGRES', 'BOTH'] %} + +# docker_container.running returns as soon as the container starts, but on +# first-init docker-entrypoint.sh starts a temporary postgres with +# `listen_addresses=''` to run /docker-entrypoint-initdb.d scripts, then +# shuts it down before exec'ing the real CMD. A default pg_isready check +# (Unix socket) passes during that ephemeral phase and races the shutdown +# with "the database system is shutting down". Checking TCP readiness on +# 127.0.0.1 only succeeds after the final postgres binds the port. +postgres_wait_ready: + cmd.run: + - name: | + for i in $(seq 1 60); do + if docker exec so-postgres pg_isready -h 127.0.0.1 -U postgres -q 2>/dev/null; then + exit 0 + fi + sleep 2 + done + echo "so-postgres did not accept TCP connections within 120s" >&2 + exit 1 + - require: + - docker_container: so-postgres + +# Ensure the shared Telegraf database exists. init-users.sh only runs on a +# fresh data dir, so hosts upgraded onto an existing /nsm/postgres volume +# would otherwise never get so_telegraf. +postgres_create_telegraf_db: + cmd.run: + - name: | + if ! docker exec so-postgres psql -U postgres -tAc "SELECT 1 FROM pg_database WHERE datname='so_telegraf'" | grep -q 1; then + docker exec so-postgres psql -v ON_ERROR_STOP=1 -U postgres -c "CREATE DATABASE so_telegraf" + fi + - require: + - cmd: postgres_wait_ready + +# Provision the shared group role and schema once. Every per-minion role is a +# member of so_telegraf, and each Telegraf connection does SET ROLE so_telegraf +# (via options='-c role=so_telegraf' in the connection string) so tables created +# on first write are owned by the group role and every member can INSERT/SELECT. +postgres_telegraf_group_role: + cmd.run: + - name: | + docker exec -i so-postgres psql -v ON_ERROR_STOP=1 -U postgres -d so_telegraf <<'EOSQL' + DO $$ + BEGIN + IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'so_telegraf') THEN + CREATE ROLE so_telegraf NOLOGIN; + END IF; + END + $$; + GRANT CONNECT ON DATABASE so_telegraf TO so_telegraf; + CREATE SCHEMA IF NOT EXISTS telegraf AUTHORIZATION so_telegraf; + GRANT USAGE, CREATE ON SCHEMA telegraf TO so_telegraf; + CREATE SCHEMA IF NOT EXISTS partman; + CREATE EXTENSION IF NOT EXISTS pg_partman SCHEMA partman; + CREATE EXTENSION IF NOT EXISTS pg_cron; + -- Telegraf (running as so_telegraf) calls partman.create_parent() + -- on first write of each metric, which needs USAGE on the partman + -- schema, EXECUTE on its functions/procedures, and write access to + -- partman.part_config so it can register new partitioned parents. + GRANT USAGE, CREATE ON SCHEMA partman TO so_telegraf; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA partman TO so_telegraf; + GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA partman TO so_telegraf; + GRANT EXECUTE ON ALL PROCEDURES IN SCHEMA partman TO so_telegraf; + -- partman creates per-parent template tables (partman.template_*) at + -- runtime; default privileges extend DML/sequence access to them. + ALTER DEFAULT PRIVILEGES IN SCHEMA partman + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO so_telegraf; + ALTER DEFAULT PRIVILEGES IN SCHEMA partman + GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO so_telegraf; + -- Hourly partman maintenance. cron.schedule is idempotent by jobname. + SELECT cron.schedule( + 'telegraf-partman-maintenance', + '17 * * * *', + 'CALL partman.run_maintenance_proc()' + ); + EOSQL + - require: + - cmd: postgres_create_telegraf_db + +{% set creds = salt['pillar.get']('telegraf:postgres_creds', {}) %} +{% for mid, entry in creds.items() %} +{% if entry.get('user') and entry.get('pass') %} +{% set u = entry.user %} +{% set p = entry.pass | replace("'", "''") %} + +postgres_telegraf_role_{{ u }}: + cmd.run: + - name: | + docker exec -i so-postgres psql -v ON_ERROR_STOP=1 -U postgres -d so_telegraf <<'EOSQL' + DO $$ + BEGIN + IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '{{ u }}') THEN + EXECUTE format('CREATE ROLE %I WITH LOGIN PASSWORD %L', '{{ u }}', '{{ p }}'); + ELSE + EXECUTE format('ALTER ROLE %I WITH PASSWORD %L', '{{ u }}', '{{ p }}'); + END IF; + END + $$; + GRANT CONNECT ON DATABASE so_telegraf TO "{{ u }}"; + GRANT so_telegraf TO "{{ u }}"; + EOSQL + - require: + - cmd: postgres_telegraf_group_role + +{% endif %} +{% endfor %} + +# Reconcile partman retention from pillar. Runs after role/schema setup so +# any partitioned parents Telegraf has already created get their retention +# refreshed whenever postgres.telegraf.retention_days changes. +{% set retention = salt['pillar.get']('postgres:telegraf:retention_days', 14) | int %} +postgres_telegraf_retention_reconcile: + cmd.run: + - name: | + docker exec -i so-postgres psql -v ON_ERROR_STOP=1 -U postgres -d so_telegraf <<'EOSQL' + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM pg_catalog.pg_extension WHERE extname = 'pg_partman') THEN + UPDATE partman.part_config + SET retention = '{{ retention }} days', + retention_keep_table = false + WHERE parent_table LIKE 'telegraf.%'; + END IF; + END + $$; + EOSQL + - require: + - cmd: postgres_telegraf_group_role + +{% endif %} + +{% else %} + +{{sls}}_state_not_allowed: + test.fail_without_changes: + - name: {{sls}}_state_not_allowed + +{% endif %} diff --git a/salt/postgres/tools/sbin/so-postgres-backup b/salt/postgres/tools/sbin/so-postgres-backup new file mode 100644 index 000000000..9db522336 --- /dev/null +++ b/salt/postgres/tools/sbin/so-postgres-backup @@ -0,0 +1,39 @@ +#!/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-common + +# Backups contain role password hashes and full chat data; keep them 0600. +umask 0077 + +TODAY=$(date '+%Y_%m_%d') +BACKUPDIR=/nsm/backup +BACKUPFILE="$BACKUPDIR/so-postgres-backup-$TODAY.sql.gz" +MAXBACKUPS=7 + +mkdir -p $BACKUPDIR + +# Skip if already backed up today +if [ -f "$BACKUPFILE" ]; then + exit 0 +fi + +# Skip if container isn't running +if ! docker ps --format '{{.Names}}' | grep -q '^so-postgres$'; then + exit 0 +fi + +# Dump all databases and roles, compress +docker exec so-postgres pg_dumpall -U postgres | gzip > "$BACKUPFILE" + +# Retention cleanup +NUMBACKUPS=$(find $BACKUPDIR -type f -name "so-postgres-backup*" | wc -l) +while [ "$NUMBACKUPS" -gt "$MAXBACKUPS" ]; do + OLDEST=$(find $BACKUPDIR -type f -name "so-postgres-backup*" -printf '%T+ %p\n' | sort | head -n 1 | awk -F" " '{print $2}') + rm -f "$OLDEST" + NUMBACKUPS=$(find $BACKUPDIR -type f -name "so-postgres-backup*" | wc -l) +done diff --git a/salt/postgres/tools/sbin/so-postgres-manage b/salt/postgres/tools/sbin/so-postgres-manage new file mode 100644 index 000000000..3729d5c0d --- /dev/null +++ b/salt/postgres/tools/sbin/so-postgres-manage @@ -0,0 +1,80 @@ +#!/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-common + +usage() { + echo "Usage: $0 [args]" + echo "" + echo "Supported Operations:" + echo " sql Execute a SQL command, requires: " + echo " sqlfile Execute a SQL file, requires: " + echo " shell Open an interactive psql shell" + echo " dblist List databases" + echo " userlist List database roles" + echo "" + exit 1 +} + +if [ $# -lt 1 ]; then + usage +fi + +# Check for prerequisites +if [ "$(id -u)" -ne 0 ]; then + echo "This script must be run using sudo!" + exit 1 +fi + +COMMAND=$(basename $0) +OP=$1 +shift + +set -eo pipefail + +log() { + echo -e "$(date) | $COMMAND | $@" >&2 +} + +so_psql() { + docker exec so-postgres psql -U postgres -d securityonion "$@" +} + +case "$OP" in + + sql) + [ $# -lt 1 ] && usage + so_psql -c "$1" + ;; + + sqlfile) + [ $# -ne 1 ] && usage + if [ ! -f "$1" ]; then + log "File not found: $1" + exit 1 + fi + docker cp "$1" so-postgres:/tmp/sqlfile.sql + docker exec so-postgres psql -U postgres -d securityonion -f /tmp/sqlfile.sql + docker exec so-postgres rm -f /tmp/sqlfile.sql + ;; + + shell) + docker exec -it so-postgres psql -U postgres -d securityonion + ;; + + dblist) + so_psql -c "\l" + ;; + + userlist) + so_psql -c "\du" + ;; + + *) + usage + ;; +esac diff --git a/salt/postgres/tools/sbin/so-postgres-restart b/salt/postgres/tools/sbin/so-postgres-restart new file mode 100644 index 000000000..8e3e516dd --- /dev/null +++ b/salt/postgres/tools/sbin/so-postgres-restart @@ -0,0 +1,10 @@ +#!/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-common + +/usr/sbin/so-restart postgres $1 diff --git a/salt/postgres/tools/sbin/so-postgres-start b/salt/postgres/tools/sbin/so-postgres-start new file mode 100644 index 000000000..0893eaa2d --- /dev/null +++ b/salt/postgres/tools/sbin/so-postgres-start @@ -0,0 +1,10 @@ +#!/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-common + +/usr/sbin/so-start postgres $1 diff --git a/salt/postgres/tools/sbin/so-postgres-stop b/salt/postgres/tools/sbin/so-postgres-stop new file mode 100644 index 000000000..6fd0d9165 --- /dev/null +++ b/salt/postgres/tools/sbin/so-postgres-stop @@ -0,0 +1,10 @@ +#!/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-common + +/usr/sbin/so-stop postgres $1 diff --git a/salt/postgres/tools/sbin/so-stats-show b/salt/postgres/tools/sbin/so-stats-show new file mode 100644 index 000000000..3cf7a05d8 --- /dev/null +++ b/salt/postgres/tools/sbin/so-stats-show @@ -0,0 +1,157 @@ +#!/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. + +# Point-in-time host metrics from the Telegraf Postgres backend. +# Sanity-check tool for verifying metrics are landing before the grid +# dashboards consume them. +# +# Assumes Telegraf's postgresql output is configured with +# tags_as_foreign_keys = true, tags_as_jsonb = true, fields_as_jsonb = true, +# so metric tables are (time, tag_id, fields jsonb) and tag tables are +# (tag_id, tags jsonb). + +. /usr/sbin/so-common + +usage() { + cat <&2 + exit 1 +fi + +so_psql() { + docker exec so-postgres psql -U postgres -d so_telegraf -At -F $'\t' "$@" +} + +if ! docker exec so-postgres psql -U postgres -lqt 2>/dev/null | cut -d\| -f1 | grep -qw so_telegraf; then + echo "Database so_telegraf not found. Is telegraf.output set to POSTGRES or BOTH?" + exit 2 +fi + +table_exists() { + local table="$1" + [ -n "$(so_psql -c "SELECT 1 FROM information_schema.tables WHERE table_schema='${SCHEMA}' AND table_name='${table}' LIMIT 1;")" ] +} + +# Discover hosts from cpu_tag (every minion reports cpu). +if ! table_exists "cpu_tag"; then + echo "${SCHEMA}.cpu_tag not found. Has Telegraf written any rows yet?" + exit 0 +fi + +HOSTS=$(so_psql -c " + SELECT DISTINCT tags->>'host' + FROM \"${SCHEMA}\".cpu_tag + WHERE tags ? 'host' + ORDER BY 1;") + +if [ -z "$HOSTS" ]; then + echo "No hosts found in ${SCHEMA}. Is Telegraf configured to write to Postgres?" + exit 0 +fi + +print_metric() { + so_psql -c "$1" +} + +for host in $HOSTS; do + if ! [[ "$host" =~ $HOST_RE ]]; then + echo "Skipping host with invalid characters in tag value: $host" >&2 + continue + fi + if [ -n "$FILTER_HOST" ] && [ "$host" != "$FILTER_HOST" ]; then + continue + fi + + echo "====================================================================" + echo " Host: $host" + echo "====================================================================" + + if table_exists "cpu"; then + print_metric " + SELECT 'cpu ' AS metric, + to_char(c.time, 'YYYY-MM-DD HH24:MI:SS') AS ts, + round((100 - (c.fields->>'usage_idle')::numeric), 1) || '% used' + FROM \"${SCHEMA}\".cpu c + JOIN \"${SCHEMA}\".cpu_tag t USING (tag_id) + WHERE t.tags->>'host' = '${host}' AND t.tags->>'cpu' = 'cpu-total' + ORDER BY c.time DESC LIMIT 1;" + fi + + if table_exists "mem"; then + print_metric " + SELECT 'memory ' AS metric, + to_char(m.time, 'YYYY-MM-DD HH24:MI:SS') AS ts, + round((m.fields->>'used_percent')::numeric, 1) || '% used (' || + pg_size_pretty((m.fields->>'used')::bigint) || ' of ' || + pg_size_pretty((m.fields->>'total')::bigint) || ')' + FROM \"${SCHEMA}\".mem m + JOIN \"${SCHEMA}\".mem_tag t USING (tag_id) + WHERE t.tags->>'host' = '${host}' + ORDER BY m.time DESC LIMIT 1;" + fi + + if table_exists "disk"; then + print_metric " + SELECT 'disk ' || rpad(t.tags->>'path', 12) AS metric, + to_char(d.time, 'YYYY-MM-DD HH24:MI:SS') AS ts, + round((d.fields->>'used_percent')::numeric, 1) || '% used (' || + pg_size_pretty((d.fields->>'used')::bigint) || ' of ' || + pg_size_pretty((d.fields->>'total')::bigint) || ')' + FROM \"${SCHEMA}\".disk d + JOIN \"${SCHEMA}\".disk_tag t USING (tag_id) + WHERE t.tags->>'host' = '${host}' + AND d.time = (SELECT max(d2.time) + FROM \"${SCHEMA}\".disk d2 + JOIN \"${SCHEMA}\".disk_tag t2 USING (tag_id) + WHERE t2.tags->>'host' = '${host}') + ORDER BY t.tags->>'path';" + fi + + if table_exists "system"; then + print_metric " + SELECT 'load ' AS metric, + to_char(s.time, 'YYYY-MM-DD HH24:MI:SS') AS ts, + (s.fields->>'load1') || ' / ' || + (s.fields->>'load5') || ' / ' || + (s.fields->>'load15') || ' (1/5/15m)' + FROM \"${SCHEMA}\".system s + JOIN \"${SCHEMA}\".system_tag t USING (tag_id) + WHERE t.tags->>'host' = '${host}' + ORDER BY s.time DESC LIMIT 1;" + fi + + echo "" +done diff --git a/salt/soc/defaults.map.jinja b/salt/soc/defaults.map.jinja index 2821bb8e5..00a9604f6 100644 --- a/salt/soc/defaults.map.jinja +++ b/salt/soc/defaults.map.jinja @@ -24,6 +24,11 @@ {% 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}) %} +{% if GLOBALS.postgres is defined and GLOBALS.postgres.auth is defined %} +{% set PG_ADMIN_PASS = salt['pillar.get']('secrets:postgres_pass', '') %} +{% do SOCDEFAULTS.soc.config.server.modules.update({'postgres': {'hostUrl': GLOBALS.manager_ip, 'port': 5432, 'username': GLOBALS.postgres.auth.users.so_postgres_user.user, 'password': GLOBALS.postgres.auth.users.so_postgres_user.pass, 'adminUser': 'postgres', 'adminPassword': PG_ADMIN_PASS, 'dbname': 'securityonion', 'sslMode': 'require', 'assistantEnabled': true, 'esHostUrl': 'https://' ~ GLOBALS.manager_ip ~ ':9200', 'esUsername': GLOBALS.elasticsearch.auth.users.so_elastic_user.user, 'esPassword': GLOBALS.elasticsearch.auth.users.so_elastic_user.pass, 'esVerifyCert': false}}) %} +{% endif %} + {% 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 %} diff --git a/salt/telegraf/defaults.yaml b/salt/telegraf/defaults.yaml index ef6c2bc77..ead122b0a 100644 --- a/salt/telegraf/defaults.yaml +++ b/salt/telegraf/defaults.yaml @@ -1,5 +1,6 @@ telegraf: enabled: False + output: BOTH config: interval: '30s' metric_batch_size: 1000 diff --git a/salt/telegraf/etc/telegraf.conf b/salt/telegraf/etc/telegraf.conf index aafcf6d77..02d969ff3 100644 --- a/salt/telegraf/etc/telegraf.conf +++ b/salt/telegraf/etc/telegraf.conf @@ -8,6 +8,14 @@ {%- set ZEEK_ENABLED = salt['pillar.get']('zeek:enabled', True) %} {%- set MDENGINE = GLOBALS.md_engine %} {%- set LOGSTASH_ENABLED = LOGSTASH_MERGED.enabled %} +{%- set TG_OUT = TELEGRAFMERGED.output | upper %} +{%- set PG_HOST = GLOBALS.manager_ip %} +{#- Per-minion telegraf creds live in the grid-wide telegraf/creds.sls pillar, + written by /usr/sbin/so-telegraf-cred on the manager. Each minion looks up + its own entry by grains.id. #} +{%- 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', '') %} # Global tags can be specified here in key="value" format. [global_tags] role = "{{ GLOBALS.role.split('-') | last }}" @@ -72,6 +80,7 @@ # OUTPUT PLUGINS # ############################################################################### +{%- if TG_OUT in ['INFLUXDB', 'BOTH'] %} # Configuration for sending metrics to InfluxDB [[outputs.influxdb_v2]] urls = ["https://{{ INFLUXDBHOST }}:8086"] @@ -85,6 +94,41 @@ tls_key = "/etc/telegraf/telegraf.key" ## Use TLS but skip chain & host verification # insecure_skip_verify = false +{%- endif %} + +{%- if TG_OUT in ['POSTGRES', 'BOTH'] and PG_USER and PG_PASS %} +# Configuration for sending metrics to PostgreSQL. +# options='-c role=so_telegraf' makes every connection SET ROLE to the shared +# group role so tables created on first write are owned by so_telegraf, and +# all per-minion members can INSERT/SELECT them via role inheritance. +# fields_as_jsonb/tags_as_jsonb keep metric tables at a fixed column count so +# high-cardinality inputs (docker, procstat, kafka) don't blow past the +# Postgres 1600-column-per-table limit. +[[outputs.postgresql]] + connection = "host={{ PG_HOST }} port=5432 user={{ PG_USER }} password={{ PG_PASS }} dbname=so_telegraf sslmode=verify-full sslrootcert=/etc/telegraf/ca.crt options='-c role=so_telegraf'" + schema = "telegraf" + tags_as_foreign_keys = true + tags_as_jsonb = true + fields_as_jsonb = true + # Every metric table is a daily time-range partitioned parent managed by + # pg_partman. Retention drops old partitions instead of row-by-row DELETEs. + {% raw %} + # pg_partman 5.x requires the control column (time) to be NOT NULL, so + # ALTER it before create_parent(). And create_parent() splits + # p_parent_table on '.' to look up raw identifiers, so the literal must + # be 'schema.name' (not '"schema"."name"' as .table|quoteLiteral emits). + # IF NOT EXISTS keeps the three templates idempotent so a Telegraf + # restart after any DB-side surgery re-runs them safely. + create_templates = [ + '''CREATE TABLE IF NOT EXISTS {{ .table }} ({{ .columns }}) PARTITION BY RANGE ("time")''', + '''ALTER TABLE {{ .table }} ALTER COLUMN "time" SET NOT NULL''', + '''SELECT partman.create_parent(p_parent_table := {{ printf "%s.%s" .table.Schema .table.Name | quoteLiteral }}, p_control := 'time', p_type := 'range', p_interval := '1 day', p_premake := 3) WHERE NOT EXISTS (SELECT 1 FROM partman.part_config WHERE parent_table = {{ printf "%s.%s" .table.Schema .table.Name | quoteLiteral }})''' + ] + tag_table_create_templates = [ + '''CREATE TABLE IF NOT EXISTS {{ .table }} ({{ .columns }}, PRIMARY KEY (tag_id))''' + ] + {% endraw %} +{%- endif %} ############################################################################### # PROCESSOR PLUGINS # diff --git a/salt/telegraf/soc_telegraf.yaml b/salt/telegraf/soc_telegraf.yaml index 40ae7fed8..4b9a2e3d1 100644 --- a/salt/telegraf/soc_telegraf.yaml +++ b/salt/telegraf/soc_telegraf.yaml @@ -4,6 +4,15 @@ telegraf: forcedType: bool 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. + options: + - INFLUXDB + - POSTGRES + - BOTH + global: True + advanced: True + helpLink: influxdb config: interval: description: Data collection interval. diff --git a/salt/top.sls b/salt/top.sls index c7c6aa65d..ff789e89d 100644 --- a/salt/top.sls +++ b/salt/top.sls @@ -68,6 +68,7 @@ base: - backup.config_backup - nginx - influxdb + - postgres - soc - kratos - hydra @@ -95,6 +96,7 @@ base: - backup.config_backup - nginx - influxdb + - postgres - soc - kratos - hydra @@ -123,6 +125,7 @@ base: - registry - nginx - influxdb + - postgres - strelka.manager - soc - kratos @@ -153,6 +156,7 @@ base: - registry - nginx - influxdb + - postgres - strelka.manager - soc - kratos @@ -181,6 +185,7 @@ base: - manager - nginx - influxdb + - postgres - strelka.manager - soc - kratos diff --git a/salt/vars/eval.map.jinja b/salt/vars/eval.map.jinja index 3c2e66a97..3cba33797 100644 --- a/salt/vars/eval.map.jinja +++ b/salt/vars/eval.map.jinja @@ -1,4 +1,5 @@ {% from 'vars/elasticsearch.map.jinja' import ELASTICSEARCH_GLOBALS %} +{% from 'vars/postgres.map.jinja' import POSTGRES_GLOBALS %} {% from 'vars/sensor.map.jinja' import SENSOR_GLOBALS %} {% set ROLE_GLOBALS = {} %} @@ -6,6 +7,7 @@ {% set EVAL_GLOBALS = [ ELASTICSEARCH_GLOBALS, + POSTGRES_GLOBALS, SENSOR_GLOBALS ] %} diff --git a/salt/vars/import.map.jinja b/salt/vars/import.map.jinja index f9dfa0c25..8dea3ad7d 100644 --- a/salt/vars/import.map.jinja +++ b/salt/vars/import.map.jinja @@ -1,4 +1,5 @@ {% from 'vars/elasticsearch.map.jinja' import ELASTICSEARCH_GLOBALS %} +{% from 'vars/postgres.map.jinja' import POSTGRES_GLOBALS %} {% from 'vars/sensor.map.jinja' import SENSOR_GLOBALS %} {% set ROLE_GLOBALS = {} %} @@ -6,6 +7,7 @@ {% set IMPORT_GLOBALS = [ ELASTICSEARCH_GLOBALS, + POSTGRES_GLOBALS, SENSOR_GLOBALS ] %} diff --git a/salt/vars/manager.map.jinja b/salt/vars/manager.map.jinja index c6b348341..009dd5607 100644 --- a/salt/vars/manager.map.jinja +++ b/salt/vars/manager.map.jinja @@ -1,12 +1,14 @@ {% from 'vars/elasticsearch.map.jinja' import ELASTICSEARCH_GLOBALS %} {% from 'vars/logstash.map.jinja' import LOGSTASH_GLOBALS %} +{% from 'vars/postgres.map.jinja' import POSTGRES_GLOBALS %} {% set ROLE_GLOBALS = {} %} {% set MANAGER_GLOBALS = [ ELASTICSEARCH_GLOBALS, - LOGSTASH_GLOBALS + LOGSTASH_GLOBALS, + POSTGRES_GLOBALS ] %} diff --git a/salt/vars/managersearch.map.jinja b/salt/vars/managersearch.map.jinja index c2a3d9628..369efe5a4 100644 --- a/salt/vars/managersearch.map.jinja +++ b/salt/vars/managersearch.map.jinja @@ -1,12 +1,14 @@ {% from 'vars/elasticsearch.map.jinja' import ELASTICSEARCH_GLOBALS %} {% from 'vars/logstash.map.jinja' import LOGSTASH_GLOBALS %} +{% from 'vars/postgres.map.jinja' import POSTGRES_GLOBALS %} {% set ROLE_GLOBALS = {} %} {% set MANAGERSEARCH_GLOBALS = [ ELASTICSEARCH_GLOBALS, - LOGSTASH_GLOBALS + LOGSTASH_GLOBALS, + POSTGRES_GLOBALS ] %} diff --git a/salt/vars/postgres.map.jinja b/salt/vars/postgres.map.jinja new file mode 100644 index 000000000..ce65d2d1f --- /dev/null +++ b/salt/vars/postgres.map.jinja @@ -0,0 +1,16 @@ +{# 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. #} + +{% import 'vars/init.map.jinja' as INIT %} + +{% + set POSTGRES_GLOBALS = { + 'postgres': {} + } +%} + +{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/postgres/auth.sls') %} +{% do POSTGRES_GLOBALS.postgres.update({'auth': INIT.PILLAR.postgres.auth}) %} +{% endif %} diff --git a/salt/vars/standalone.map.jinja b/salt/vars/standalone.map.jinja index 0e49a327d..6488eb998 100644 --- a/salt/vars/standalone.map.jinja +++ b/salt/vars/standalone.map.jinja @@ -1,5 +1,6 @@ {% from 'vars/elasticsearch.map.jinja' import ELASTICSEARCH_GLOBALS %} {% from 'vars/logstash.map.jinja' import LOGSTASH_GLOBALS %} +{% from 'vars/postgres.map.jinja' import POSTGRES_GLOBALS %} {% from 'vars/sensor.map.jinja' import SENSOR_GLOBALS %} {% set ROLE_GLOBALS = {} %} @@ -8,6 +9,7 @@ [ ELASTICSEARCH_GLOBALS, LOGSTASH_GLOBALS, + POSTGRES_GLOBALS, SENSOR_GLOBALS ] %} diff --git a/setup/so-functions b/setup/so-functions index 23098cac8..3cd665076 100755 --- a/setup/so-functions +++ b/setup/so-functions @@ -821,6 +821,7 @@ create_manager_pillars() { soc_pillar idh_pillar influxdb_pillar + postgres_pillar logrotate_pillar patch_pillar nginx_pillar @@ -1053,6 +1054,7 @@ generate_passwords(){ HYDRAKEY=$(get_random_value) HYDRASALT=$(get_random_value) REDISPASS=$(get_random_value) + POSTGRESPASS=$(get_random_value) SOCSRVKEY=$(get_random_value 64) IMPORTPASS=$(get_random_value) } @@ -1355,6 +1357,12 @@ influxdb_pillar() { " token: $INFLUXTOKEN" > $local_salt_dir/pillar/influxdb/token.sls } +postgres_pillar() { + title "Create the postgres pillar file" + touch $adv_postgres_pillar_file + touch $postgres_pillar_file +} + make_some_dirs() { mkdir -p /nsm mkdir -p "$default_salt_dir" @@ -1364,7 +1372,7 @@ make_some_dirs() { mkdir -p $local_salt_dir/salt/firewall/portgroups mkdir -p $local_salt_dir/salt/firewall/ports - for THEDIR in bpf elasticsearch ntp firewall redis backup influxdb strelka sensoroni soc docker zeek suricata nginx telegraf logstash soc manager kratos hydra idh elastalert stig global kafka versionlock hypervisor vm; do + for THEDIR in bpf elasticsearch ntp firewall redis backup influxdb postgres strelka sensoroni soc docker zeek suricata nginx telegraf logstash soc manager kratos hydra idh elastalert stig global kafka versionlock hypervisor vm; do mkdir -p $local_salt_dir/pillar/$THEDIR touch $local_salt_dir/pillar/$THEDIR/adv_$THEDIR.sls touch $local_salt_dir/pillar/$THEDIR/soc_$THEDIR.sls @@ -1844,7 +1852,8 @@ secrets_pillar(){ printf '%s\n'\ "secrets:"\ " import_pass: $IMPORTPASS"\ - " influx_pass: $INFLUXPASS" > $local_salt_dir/pillar/secrets.sls + " influx_pass: $INFLUXPASS"\ + " postgres_pass: $POSTGRESPASS" > $local_salt_dir/pillar/secrets.sls fi } diff --git a/setup/so-variables b/setup/so-variables index a0d7aadc1..975debf20 100644 --- a/setup/so-variables +++ b/setup/so-variables @@ -202,6 +202,12 @@ export influxdb_pillar_file adv_influxdb_pillar_file="$local_salt_dir/pillar/influxdb/adv_influxdb.sls" export adv_influxdb_pillar_file +postgres_pillar_file="$local_salt_dir/pillar/postgres/soc_postgres.sls" +export postgres_pillar_file + +adv_postgres_pillar_file="$local_salt_dir/pillar/postgres/adv_postgres.sls" +export adv_postgres_pillar_file + logrotate_pillar_file="$local_salt_dir/pillar/logrotate/soc_logrotate.sls" export logrotate_pillar_file diff --git a/setup/so-verify b/setup/so-verify index 8d23275ea..672ed70cc 100755 --- a/setup/so-verify +++ b/setup/so-verify @@ -71,7 +71,8 @@ log_has_errors() { grep -vE "remove_failed_vm.sls" | \ grep -vE "failed to copy: httpReadSeeker" | \ grep -vE "Error response from daemon: failed to resolve reference" | \ - grep -vE "log-.*-pipeline_failed_attempts" &> "$error_log" + grep -vE "log-.*-pipeline_failed_attempts" | \ + grep -vE " -v ON_ERROR_STOP=1" &> "$error_log" if [[ $? -eq 0 ]]; then # This function succeeds (returns 0) if errors are detected