diff --git a/salt/postgres/config.sls b/salt/postgres/config.sls index 11ca52649..e458e8455 100644 --- a/salt/postgres/config.sls +++ b/salt/postgres/config.sls @@ -46,10 +46,10 @@ postgresinitdir: - require: - file: postgresconfdir -postgresinitusers: +postgresinitdb: file.managed: - - name: /opt/so/conf/postgres/init/init-users.sh - - source: salt://postgres/files/init-users.sh + - name: /opt/so/conf/postgres/init/init-db.sh + - source: salt://postgres/files/init-db.sh - user: 939 - group: 939 - mode: 755 diff --git a/salt/postgres/enabled.sls b/salt/postgres/enabled.sls index b3abb621e..20d256ae8 100644 --- a/salt/postgres/enabled.sls +++ b/salt/postgres/enabled.sls @@ -31,7 +31,7 @@ so-postgres: - 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. + # init-db.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 @@ -46,7 +46,7 @@ so-postgres: - /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 + - /opt/so/conf/postgres/init/init-db.sh:/docker-entrypoint-initdb.d/init-db.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 @@ -70,7 +70,7 @@ so-postgres: - watch: - file: postgresconf - file: postgreshba - - file: postgresinitusers + - file: postgresinitdb - file: postgres_super_secret - file: postgres_app_secret - x509: postgres_crt @@ -78,7 +78,7 @@ so-postgres: - require: - file: postgresconf - file: postgreshba - - file: postgresinitusers + - file: postgresinitdb - file: postgres_super_secret - file: postgres_app_secret - x509: postgres_crt diff --git a/salt/postgres/files/init-users.sh b/salt/postgres/files/init-db.sh similarity index 100% rename from salt/postgres/files/init-users.sh rename to salt/postgres/files/init-db.sh diff --git a/salt/postgres/telegraf_users.sls b/salt/postgres/telegraf_users.sls index 62490ea52..28d9d6247 100644 --- a/salt/postgres/telegraf_users.sls +++ b/salt/postgres/telegraf_users.sls @@ -39,17 +39,15 @@ postgres_wait_ready: - require: - docker_container: so-postgres -# Ensure the shared Telegraf database exists. init-users.sh only runs on a +# Ensure the shared Telegraf database exists. init-db.sh only runs on a # fresh data dir, so hosts upgraded onto an existing /nsm/postgres volume # 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 + - name: /usr/sbin/so-telegraf-postgres create_db - require: - cmd: postgres_wait_ready + - file: postgres_sbin # 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 @@ -57,68 +55,26 @@ postgres_create_telegraf_db: # 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 + - name: /usr/sbin/so-telegraf-postgres group_role - require: - cmd: postgres_create_telegraf_db + - file: postgres_sbin {% 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("'", "''") %} +{% set p = entry.pass %} 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 + - name: /usr/sbin/so-telegraf-postgres user + - env: + - ROLE_USER: {{ u | tojson }} + - ROLE_PASS: {{ p | tojson }} + - hide_output: True - require: + - file: postgres_sbin - cmd: postgres_telegraf_group_role {% endif %} @@ -130,21 +86,12 @@ postgres_telegraf_role_{{ u }}: {% 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 + - name: /usr/sbin/so-telegraf-postgres retention + - env: + - RETENTION_DAYS: {{ retention }} - require: - cmd: postgres_telegraf_group_role + - file: postgres_sbin {% endif %} diff --git a/salt/postgres/tools/sbin/so-telegraf-postgres b/salt/postgres/tools/sbin/so-telegraf-postgres new file mode 100644 index 000000000..ef7c3f9e6 --- /dev/null +++ b/salt/postgres/tools/sbin/so-telegraf-postgres @@ -0,0 +1,110 @@ +#!/bin/bash +set -e + +# Provision Telegraf state inside the so-postgres container. +# Usage: so-telegraf-postgres +# create_db Ensure the so_telegraf database exists. +# group_role Provision the so_telegraf group role, telegraf/partman schemas, +# pg_partman, pg_cron, and the hourly partman maintenance job. +# user Create or update a per-minion login role granted to so_telegraf. +# Env: ROLE_USER, ROLE_PASS. +# retention Reconcile partman retention on telegraf parents. +# Env: RETENTION_DAYS. + +cmd="${1:?subcommand required}" + +case "$cmd" in + create_db) + 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 + ;; + + group_role) + 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 + ;; + + user) + : "${ROLE_USER:?ROLE_USER is required}" + : "${ROLE_PASS:?ROLE_PASS is required}" + # psql does not substitute :vars inside dollar-quoted strings, so the + # conditional CREATE/ALTER is built outside any DO block and dispatched + # with \gexec. format() handles identifier/literal quoting. + docker exec -i so-postgres psql \ + -v ON_ERROR_STOP=1 \ + -v role_user="$ROLE_USER" \ + -v role_pass="$ROLE_PASS" \ + -U postgres -d so_telegraf <<'EOSQL' +SELECT format( + CASE WHEN EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = :'role_user') + THEN 'ALTER ROLE %I WITH LOGIN PASSWORD %L' + ELSE 'CREATE ROLE %I WITH LOGIN PASSWORD %L' + END, + :'role_user', + :'role_pass' +) \gexec +GRANT CONNECT ON DATABASE so_telegraf TO :"role_user"; +GRANT so_telegraf TO :"role_user"; +EOSQL + ;; + + retention) + : "${RETENTION_DAYS:?RETENTION_DAYS is required}" + # \gset + \if guards against a missing pg_partman without using a DO + # block (psql :var substitution doesn't reach into dollar-quoted code). + docker exec -i so-postgres psql \ + -v ON_ERROR_STOP=1 \ + -v retention_days="$RETENTION_DAYS" \ + -U postgres -d so_telegraf <<'EOSQL' +SELECT CASE WHEN EXISTS (SELECT 1 FROM pg_catalog.pg_extension WHERE extname = 'pg_partman') + THEN 'true' ELSE 'false' END AS has_partman \gset +\if :has_partman +UPDATE partman.part_config +SET retention = :'retention_days' || ' days', + retention_keep_table = false +WHERE parent_table LIKE 'telegraf.%'; +\endif +EOSQL + ;; + + *) + echo "Unknown subcommand: $cmd" >&2 + exit 1 + ;; +esac