mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2026-06-12 13:19:22 +02:00
Merge pull request #15793 from Security-Onion-Solutions/feature/postgres
Harden postgres secrets, TLS enforcement, and admin tooling
This commit is contained in:
@@ -7,6 +7,9 @@
|
||||
|
||||
. /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"
|
||||
|
||||
@@ -15,6 +15,14 @@ postgresconfdir:
|
||||
- group: 939
|
||||
- makedirs: True
|
||||
|
||||
postgressecretsdir:
|
||||
file.directory:
|
||||
- name: /opt/so/conf/postgres/secrets
|
||||
- user: 939
|
||||
- group: 939
|
||||
- mode: 700
|
||||
- makedirs: True
|
||||
|
||||
postgresdatadir:
|
||||
file.directory:
|
||||
- name: /nsm/postgres
|
||||
@@ -54,12 +62,43 @@ postgresconf:
|
||||
- defaults:
|
||||
PGMERGED: {{ PGMERGED }}
|
||||
|
||||
postgreshba:
|
||||
file.managed:
|
||||
- name: /opt/so/conf/postgres/pg_hba.conf
|
||||
- source: salt://postgres/files/pg_hba.conf.jinja
|
||||
- user: 939
|
||||
- group: 939
|
||||
- mode: 640
|
||||
- template: jinja
|
||||
|
||||
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: 939
|
||||
- group: 939
|
||||
- user: root
|
||||
- group: root
|
||||
- file_mode: 755
|
||||
|
||||
{% else %}
|
||||
|
||||
@@ -11,6 +11,7 @@ postgres:
|
||||
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'
|
||||
|
||||
+13
-13
@@ -7,9 +7,7 @@
|
||||
{% if sls.split('.')[0] in allowed_states %}
|
||||
{% from 'vars/globals.map.jinja' import GLOBALS %}
|
||||
{% from 'docker/docker.map.jinja' import DOCKERMERGED %}
|
||||
{% set PASSWORD = salt['pillar.get']('secrets:postgres_pass') %}
|
||||
{% set SO_POSTGRES_USER = salt['pillar.get']('postgres:auth:users:so_postgres_user:user', 'so_postgres') %}
|
||||
{% set SO_POSTGRES_PASS = salt['pillar.get']('postgres:auth:users:so_postgres_user:pass', '') %}
|
||||
|
||||
include:
|
||||
- postgres.auth
|
||||
@@ -31,9 +29,12 @@ so-postgres:
|
||||
{% endfor %}
|
||||
- environment:
|
||||
- POSTGRES_DB=securityonion
|
||||
- POSTGRES_PASSWORD={{ PASSWORD }}
|
||||
# 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={{ SO_POSTGRES_PASS }}
|
||||
- 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 }}
|
||||
@@ -43,6 +44,8 @@ so-postgres:
|
||||
- /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
|
||||
@@ -66,12 +69,18 @@ so-postgres:
|
||||
{% 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
|
||||
|
||||
@@ -80,15 +89,6 @@ delete_so-postgres_so-status.disabled:
|
||||
- name: /opt/so/conf/so-status/so-status.conf
|
||||
- regex: ^so-postgres$
|
||||
|
||||
# Retention is now handled by pg_partman (hourly maintenance via pg_cron
|
||||
# scheduled from postgres/telegraf_users.sls). The so-telegraf-trim script
|
||||
# stays on disk for manual/emergency use but is no longer scheduled.
|
||||
so_telegraf_trim:
|
||||
cron.absent:
|
||||
- name: /usr/sbin/so-telegraf-trim >> /opt/so/log/postgres/telegraf-trim.log 2>&1
|
||||
- identifier: so_telegraf_trim
|
||||
- user: root
|
||||
|
||||
{% else %}
|
||||
|
||||
{{sls}}_state_not_allowed:
|
||||
|
||||
@@ -4,6 +4,9 @@ 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
|
||||
@@ -15,6 +18,12 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
|
||||
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
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{# 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
|
||||
@@ -4,5 +4,5 @@
|
||||
Elastic License 2.0. #}
|
||||
|
||||
{% for key, value in PGMERGED.config.items() %}
|
||||
{{ key }} = '{{ value }}'
|
||||
{{ key }} = '{{ value | string | replace("'", "''") }}'
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
postgres:
|
||||
telegraf:
|
||||
retention_days:
|
||||
description: Number of days of Telegraf metrics to keep in the so_telegraf database. Older rows are deleted nightly by so-telegraf-trim.
|
||||
description: Number of days of Telegraf metrics to keep in the so_telegraf database. Older partitions are dropped hourly by pg_partman.
|
||||
forcedType: int
|
||||
advanced: True
|
||||
helpLink: influxdb
|
||||
|
||||
@@ -42,7 +42,8 @@ postgresKeyperms:
|
||||
file.managed:
|
||||
- replace: False
|
||||
- name: /etc/pki/postgres.key
|
||||
- mode: 640
|
||||
- mode: 400
|
||||
- user: 939
|
||||
- group: 939
|
||||
|
||||
{% else %}
|
||||
|
||||
@@ -119,7 +119,7 @@ postgres_telegraf_role_{{ u }}:
|
||||
# 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) %}
|
||||
{% set retention = salt['pillar.get']('postgres:telegraf:retention_days', 14) | int %}
|
||||
postgres_telegraf_retention_reconcile:
|
||||
cmd.run:
|
||||
- name: |
|
||||
|
||||
@@ -42,6 +42,15 @@ esac
|
||||
FILTER_HOST="${1:-}"
|
||||
SCHEMA="telegraf"
|
||||
|
||||
# Host values are interpolated into SQL below. Hostnames are [A-Za-z0-9._-];
|
||||
# any other character in a tag value or CLI arg is rejected to prevent a
|
||||
# stored-tag (or CLI) → SQL injection via a compromised Telegraf writer.
|
||||
HOST_RE='^[A-Za-z0-9._-]+$'
|
||||
if [ -n "$FILTER_HOST" ] && ! [[ "$FILTER_HOST" =~ $HOST_RE ]]; then
|
||||
echo "Invalid host filter: $FILTER_HOST" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
so_psql() {
|
||||
docker exec so-postgres psql -U postgres -d so_telegraf -At -F $'\t' "$@"
|
||||
}
|
||||
@@ -78,6 +87,10 @@ print_metric() {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/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.
|
||||
|
||||
# Deletes Telegraf metric rows older than the configured retention window from
|
||||
# every minion schema in the so_telegraf database. Intended to run daily from
|
||||
# cron. Retention comes from pillar (postgres.telegraf.retention_days),
|
||||
# defaulting to 14 days. An explicit --days argument overrides the pillar.
|
||||
|
||||
. /usr/sbin/so-common
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [--days N] [--dry-run]
|
||||
|
||||
--days N Override retention in days (default: pillar
|
||||
postgres.telegraf.retention_days, fallback 14)
|
||||
--dry-run Report what would be deleted without modifying anything
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "This script must be run using sudo!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DAYS=""
|
||||
DRY_RUN=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--days) DAYS="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
-h|--help) usage ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$DAYS" ]; then
|
||||
DAYS=$(salt-call --local --out=newline_values_only pillar.get postgres:telegraf:retention_days 2>/dev/null)
|
||||
fi
|
||||
if ! [[ "$DAYS" =~ ^[0-9]+$ ]] || [ "$DAYS" -lt 1 ]; then
|
||||
DAYS=14
|
||||
fi
|
||||
|
||||
log() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') so-telegraf-trim: $*"
|
||||
}
|
||||
|
||||
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
|
||||
log "Database so_telegraf not present; nothing to trim."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Trimming rows older than ${DAYS} days (dry_run=${DRY_RUN})."
|
||||
|
||||
TOTAL_DELETED=0
|
||||
|
||||
# Every metric table in the shared telegraf schema has a 'time' column.
|
||||
# Tag tables (<metric>_tag) don't, so filtering on the column presence is
|
||||
# enough to scope the trim to metric tables only.
|
||||
ROWS=$(so_psql -c "
|
||||
SELECT table_schema || '.' || table_name
|
||||
FROM information_schema.columns
|
||||
WHERE column_name = 'time'
|
||||
AND data_type IN ('timestamp with time zone', 'timestamp without time zone')
|
||||
AND table_schema = 'telegraf'
|
||||
ORDER BY 1;")
|
||||
|
||||
if [ -z "$ROWS" ]; then
|
||||
log "No telegraf metric tables found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for qualified in $ROWS; do
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
count=$(so_psql -c "SELECT count(*) FROM \"${qualified%.*}\".\"${qualified#*.}\" WHERE time < now() - interval '${DAYS} days';")
|
||||
log "would delete ${count:-0} rows from ${qualified}"
|
||||
else
|
||||
# RETURNING count via a CTE so we can log how much was trimmed per table
|
||||
deleted=$(so_psql -c "
|
||||
WITH d AS (
|
||||
DELETE FROM \"${qualified%.*}\".\"${qualified#*.}\"
|
||||
WHERE time < now() - interval '${DAYS} days'
|
||||
RETURNING 1
|
||||
)
|
||||
SELECT count(*) FROM d;")
|
||||
deleted=${deleted:-0}
|
||||
TOTAL_DELETED=$((TOTAL_DELETED + deleted))
|
||||
[ "$deleted" -gt 0 ] && log "deleted ${deleted} rows from ${qualified}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
log "Trim complete. Total rows deleted: ${TOTAL_DELETED}."
|
||||
fi
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
{% 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}}) %}
|
||||
{% 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'}) %}
|
||||
|
||||
Reference in New Issue
Block a user