Add so-postgres Salt states and integration wiring

Phase 1 of the PostgreSQL central data platform:
- Salt states: init, enabled, disabled, config, ssl, auth, sostatus
- TLS via SO CA-signed certs with postgresql.conf template
- Two-tier auth: postgres superuser + so_postgres application user
- Firewall restricts port 5432 to manager-only (HA-ready)
- Wired into top.sls, pillar/top.sls, allowed_states, firewall
  containers map, docker defaults, CA signing policies, and setup
  scripts for all manager-type roles
This commit is contained in:
Mike Reeves
2026-04-08 10:58:52 -04:00
parent 88de246ce3
commit 868cd11874
20 changed files with 422 additions and 2 deletions

View File

@@ -29,6 +29,7 @@
'manager',
'nginx',
'influxdb',
'postgres',
'soc',
'kratos',
'hydra',

View File

@@ -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

View File

@@ -237,3 +237,11 @@ docker:
extra_hosts: []
extra_env: []
ulimits: []
'so-postgres':
final_octet: 89
port_bindings:
- 0.0.0.0:5432:5432
custom_bind_mounts: []
extra_hosts: []
extra_env: []
ulimits: []

View File

@@ -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'
] %}

View File

@@ -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

35
salt/postgres/auth.sls Normal file
View File

@@ -0,0 +1,35 @@
# 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)) %}
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 %}

63
salt/postgres/config.sls Normal file
View File

@@ -0,0 +1,63 @@
# 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
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
- makedirs: True
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 }}
{% else %}
{{sls}}_state_not_allowed:
test.fail_without_changes:
- name: {{sls}}_state_not_allowed
{% endif %}

View File

@@ -0,0 +1,14 @@
postgres:
enabled: False
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'
log_destination: 'stderr'
logging_collector: 'off'
log_min_messages: 'warning'

View File

@@ -0,0 +1,27 @@
# 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$
{% else %}
{{sls}}_state_not_allowed:
test.fail_without_changes:
- name: {{sls}}_state_not_allowed
{% endif %}

88
salt/postgres/enabled.sls Normal file
View File

@@ -0,0 +1,88 @@
# 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 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
- postgres.ssl
- postgres.config
- postgres.sostatus
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
- POSTGRES_PASSWORD={{ PASSWORD }}
- SO_POSTGRES_USER={{ SO_POSTGRES_USER }}
- SO_POSTGRES_PASS={{ 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/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: postgresinitusers
- x509: postgres_crt
- x509: postgres_key
- require:
- file: postgresconf
- file: postgresinitusers
- 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$
{% else %}
{{sls}}_state_not_allowed:
test.fail_without_changes:
- name: {{sls}}_state_not_allowed
{% endif %}

View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -e
# Create application user for SOC platform access
# This script runs on first database initialization only
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
CREATE ROLE "$SO_POSTGRES_USER" WITH LOGIN PASSWORD '$SO_POSTGRES_PASS';
END IF;
END
\$\$;
GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO "$SO_POSTGRES_USER";
EOSQL

View File

@@ -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 }}'
{% endfor %}

13
salt/postgres/init.sls Normal file
View File

@@ -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 %}

7
salt/postgres/map.jinja Normal file
View File

@@ -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) %}

View File

@@ -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 %}

54
salt/postgres/ssl.sls Normal file
View File

@@ -0,0 +1,54 @@
# 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: 640
- group: 939
{% else %}
{{sls}}_state_not_allowed:
test.fail_without_changes:
- name: {{sls}}_state_not_allowed
{% endif %}

View File

@@ -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