ha/safe
Three-node high-availability and security-hardening configuration example.
ha/safe uses a three-node high-availability topology to demonstrate TLS, client certificates, password checks, backup encryption, the CRIT parameter template, and related security settings. It is a configuration example to customize, not a compliance-certified template.
Overview
- Configuration:
ha/safe - Nodes: 3 INFRA, etcd, and PostgreSQL nodes; optional delayed replica
- Operating systems:
el8,el9,el10,d12,d13,u22,u24,u26 - Architecture:
x86_64; some security extensions do not have ARM64 packages - Related configurations:
ha/trio,ha/full
Generate the configuration:
./configure -c ha/safe -g [-i <primary_ip>]
-g randomizes only credentials recognized by the configuration wizard. You must still replace MinIO users, the pgBackRest cipher_pass, and other template example values.
Hardening Controls
| Setting | Template Behavior | Boundary and Follow-up |
|---|---|---|
| PostgreSQL HBA | Main TCP rules use ssl; public administrator access uses cert | Local ident and selected localhost pwd rules remain |
| PgBouncer | pgbouncer_sslmode: require | Clients must still verify the server certificate where required |
| Patroni | REST API uses HTTPS and a constrained listen address | Basic Auth remains; rotate the password |
| Password check | passwordcheck is preloaded through pg_libs | Affects only newly set or changed passwords |
| Account lifetime | Built-in and example application users set expire_in: 7300 | Twenty years is not a rotation policy; shorten it to organizational requirements |
| Listen addresses | PostgreSQL is limited to ${ip},${vip},${lo} | Firewalls and HBA are still required |
| Backup | Uses MinIO with AES-256-CBC | pgBR.${pg_cluster} is a predictable example and must be replaced |
| PostgreSQL parameters | pg-meta uses crit.yml | Strict synchronous mode can block writes without a synchronous replica |
| Logging | CRIT logs connection and disconnection events | Fine-grained SQL auditing requires explicit pgaudit configuration |
| Security extensions | Installs passwordcheck, credcheck, pgaudit, and related packages | Installation does not preload, create, or configure an extension |
| Delayed replica | Provides a commented one-hour delayed-cluster example | Not created by default; enable it explicitly |
Preflight Checklist
- Replace every public example credential, especially
minio_users,pgbackrest_repo, application users, and API passwords. - Confirm that the three nodes occupy independent failure domains, and update IPs, VIP, and domains for the target network.
- Configure database clients with
sslmode=verify-fulland a trusted CA. - Confirm that the availability impact of strict synchronous mode meets application requirements.
- Preload and configure
pgaudit,credcheck, and other extensions as required. - Check extension package availability on ARM64.
- Test backup recovery, failover, and certificate verification.
See Security Model, Authentication, Encrypted Communication, and Data Security for the underlying mechanisms.
Configuration
Source: pigsty/conf/ha/safe.yml
---
#==============================================================#
# File : safe.yml
# Desc : Pigsty 3-node security enhance template
# Ctime : 2020-05-22
# Mtime : 2025-12-12
# Docs : https://pigsty.io/docs/conf/safe
# License : Apache-2.0 @ https://pigsty.io/docs/about/license/
# Copyright : 2018-2026 Ruohang Feng / Vonng ([email protected])
#==============================================================#
#===== SECURITY ENHANCEMENT CONFIG TEMPLATE WITH 3 NODES ======#
# * 3 infra nodes, 3 etcd nodes, single minio node
# * 3-instance pgsql cluster with an extra delayed instance
# * crit.yml templates, no data loss, checksum enforced
# * enforce ssl on postgres & pgbouncer, use postgres by default
# * enforce an expiration date for all users (20 years by default)
# * enforce strong password policy with passwordcheck extension
# * enforce changing default password for all users
# * log connections and disconnections
# * restrict listen ip address for postgres/patroni/pgbouncer
all:
children:
infra: # infra cluster for proxy, monitor, alert, etc
hosts: # 1 for common usage, 3 nodes for production
10.10.10.10: { infra_seq: 1 } # identity required
10.10.10.11: { infra_seq: 2, repo_enabled: false }
10.10.10.12: { infra_seq: 3, repo_enabled: false }
vars: { patroni_watchdog_mode: 'off' }
minio: # minio cluster, s3 compatible object storage
hosts: { 10.10.10.10: { minio_seq: 1 } }
vars: { minio_cluster: minio }
etcd: # dcs service for postgres/patroni ha consensus
hosts: # 1 node for testing, 3 or 5 for production
10.10.10.10: { etcd_seq: 1 } # etcd_seq required
10.10.10.11: { etcd_seq: 2 } # assign from 1 ~ n
10.10.10.12: { etcd_seq: 3 } # odd number please
vars: # cluster level parameter override roles/etcd
etcd_cluster: etcd # mark etcd cluster name etcd
etcd_safeguard: false # safeguard against purging
pg-meta: # 3 instance postgres cluster `pg-meta`
hosts:
10.10.10.10: { pg_seq: 1, pg_role: primary }
10.10.10.11: { pg_seq: 2, pg_role: replica }
10.10.10.12: { pg_seq: 3, pg_role: replica , pg_offline_query: true }
vars:
pg_cluster: pg-meta
pg_conf: crit.yml
pg_users:
- { name: dbuser_meta , password: Pleas3-ChangeThisPwd ,expire_in: 7300 ,pgbouncer: true ,roles: [ dbrole_admin ] ,comment: pigsty admin user }
- { name: dbuser_view , password: Make.3ure-Compl1ance ,expire_in: 7300 ,pgbouncer: true ,roles: [ dbrole_readonly ] ,comment: read-only viewer for meta database }
pg_databases:
- { name: meta ,baseline: cmdb.sql ,comment: pigsty meta database ,schemas: [ pigsty ] ,extensions: [ { name: vector } ] }
pg_services:
- { name: standby , ip: "*" ,port: 5435 , dest: default ,selector: "[]" , backup: "[? pg_role == `primary`]" }
pg_crontab: # https://pigsty.io/docs/pgsql/admin/crontab
- '00 01 * * * /pg/bin/pg-backup full'
pg_listen: '${ip},${vip},${lo}'
pg_vip_enabled: true
pg_vip_address: 10.10.10.2/24
# OPTIONAL delayed cluster for pg-meta
#pg-meta-delay: # delayed instance for pg-meta (1 hour ago)
# hosts: { 10.10.10.13: { pg_seq: 1, pg_role: primary, pg_upstream: 10.10.10.10, pg_delay: 1h } }
# vars: { pg_cluster: pg-meta-delay }
####################################################################
# Parameters #
####################################################################
vars: # global variables
version: v4.5.0 # pigsty version string
admin_ip: 10.10.10.10 # admin node ip address
region: default # upstream mirror region: default|china|europe
node_tune: oltp # node tuning specs: oltp,olap,tiny,crit
pg_conf: oltp.yml # pgsql tuning specs: {oltp,olap,tiny,crit}.yml
#docker_registry_mirrors: ["https://docker.1panel.live","https://docker.1ms.run","https://docker.xuanyuan.me","https://registry-1.docker.io"]
patroni_ssl_enabled: true # secure patroni RestAPI communications with SSL?
pgbouncer_sslmode: require # pgbouncer client ssl mode: disable|allow|prefer|require|verify-ca|verify-full, disable by default
pg_default_service_dest: postgres # default service destination to postgres instead of pgbouncer
pgbackrest_method: minio # pgbackrest repo method: local,minio,[user-defined...]
#----------------------------------#
# MinIO Related Options
#----------------------------------#
minio_users: # and configure `pgbackrest_repo` & `minio_users` accordingly
- { access_key: dba , secret_key: S3User.DBA.Strong.Password, policy: consoleAdmin }
- { access_key: pgbackrest , secret_key: Min10.bAckup ,policy: readwrite }
pgbackrest_repo: # pgbackrest repo: https://pgbackrest.org/configuration.html#section-repository
local: # default pgbackrest repo with local posix fs
path: /pg/backup # local backup directory, `/pg/backup` by default
retention_full_type: count # retention full backups by count
retention_full: 2 # keep 2, at most 3 full backup when using local fs repo
minio: # optional minio repo for pgbackrest
s3_key: pgbackrest # <-------- CHANGE THIS, SAME AS `minio_users` access_key
s3_key_secret: Min10.bAckup # <-------- CHANGE THIS, SAME AS `minio_users` secret_key
cipher_pass: 'pgBR.${pg_cluster}' # <-------- CHANGE THIS, you can use cluster name as part of password
type: s3 # minio is s3-compatible, so s3 is used
s3_endpoint: sss.pigsty # minio endpoint domain name, `sss.pigsty` by default
s3_region: us-east-1 # minio region, us-east-1 by default, useless for minio
s3_bucket: pgsql # minio bucket name, `pgsql` by default
s3_uri_style: path # use path style uri for minio rather than host style
path: /pgbackrest # minio backup path, default is `/pgbackrest`
storage_port: 9000 # minio port, 9000 by default
storage_ca_file: /etc/pki/ca.crt # minio ca file path, `/etc/pki/ca.crt` by default
block: y # Enable block incremental backup
bundle: y # bundle small files into a single file
bundle_limit: 20MiB # Limit for file bundles, 20MiB for object storage
bundle_size: 128MiB # Target size for file bundles, 128MiB for object storage
cipher_type: aes-256-cbc # enable AES encryption for remote backup repo
retention_full_type: time # retention full backup by time on minio repo
retention_full: 14 # keep full backup for last 14 days
#----------------------------------#
# Access Control
#----------------------------------#
# add passwordcheck extension to enforce strong password policy
pg_libs: '$libdir/passwordcheck, pg_stat_statements, auto_explain'
pg_extensions:
- passwordcheck, supautils, pgsodium, pg_vault, pg_session_jwt, anonymizer, pgsmcrypto, pgauditlogtofile, pgaudit #, pgaudit17, pgaudit16, pgaudit15, pgaudit14
- pg_auth_mon, credcheck, pgcryptokey, pg_jobmon, logerrors, login_hook, set_user, pgextwlist, pg_auditor, sslutils, noset #pg_tde #pg_snakeoil
pg_default_roles: # default roles and users in postgres cluster
- { name: dbrole_readonly ,login: false ,comment: role for global read-only access }
- { name: dbrole_offline ,login: false ,comment: role for restricted read-only access }
- { name: dbrole_readwrite ,login: false ,roles: [ dbrole_readonly ] ,comment: role for global read-write access }
- { name: dbrole_admin ,login: false ,roles: [ pg_monitor, dbrole_readwrite ] ,comment: role for object creation }
- { name: postgres ,superuser: true ,expire_in: 7300 ,comment: system superuser }
- { name: replicator ,replication: true ,expire_in: 7300 ,roles: [ pg_monitor, dbrole_readonly ] ,comment: system replicator }
- { name: dbuser_dba ,superuser: true ,expire_in: 7300 ,roles: [ dbrole_admin ] ,pgbouncer: true ,pool_mode: session, pool_connlimit: 16 , comment: pgsql admin user }
- { name: dbuser_monitor ,roles: [ pg_monitor ] ,expire_in: 7300 ,pgbouncer: true ,parameters: { log_min_duration_statement: 1000 } ,pool_mode: session ,pool_connlimit: 8 ,comment: pgsql monitor user }
pg_default_hba_rules: # postgres host-based auth rules by default, order by `order`
- { user: '${dbsu}' ,db: all ,addr: local ,auth: ident ,title: 'dbsu access via local os user ident' ,order: 100}
- { user: '${dbsu}' ,db: replication ,addr: local ,auth: ident ,title: 'dbsu replication from local os ident' ,order: 150}
- { user: '${repl}' ,db: replication ,addr: localhost ,auth: ssl ,title: 'replicator replication from localhost' ,order: 200}
- { user: '${repl}' ,db: replication ,addr: intra ,auth: ssl ,title: 'replicator replication from intranet' ,order: 250}
- { user: '${repl}' ,db: postgres ,addr: intra ,auth: ssl ,title: 'replicator postgres db from intranet' ,order: 300}
- { user: '${monitor}' ,db: all ,addr: localhost ,auth: pwd ,title: 'monitor from localhost with password' ,order: 350}
- { user: '${monitor}' ,db: all ,addr: infra ,auth: ssl ,title: 'monitor from infra host with password' ,order: 400}
- { user: '${admin}' ,db: all ,addr: infra ,auth: ssl ,title: 'admin @ infra nodes with pwd & ssl' ,order: 450}
- { user: '${admin}' ,db: all ,addr: world ,auth: cert ,title: 'admin @ everywhere with ssl & cert' ,order: 500}
- { user: '+dbrole_readonly',db: all ,addr: localhost ,auth: ssl ,title: 'pgbouncer read/write via local socket' ,order: 550}
- { user: '+dbrole_readonly',db: all ,addr: intra ,auth: ssl ,title: 'read/write biz user via password' ,order: 600}
- { user: '+dbrole_offline' ,db: all ,addr: intra ,auth: ssl ,title: 'allow etl offline tasks from intranet' ,order: 650}
pgb_default_hba_rules: # pgbouncer host-based authentication rules, order by `order`
- { user: '${dbsu}' ,db: pgbouncer ,addr: local ,auth: peer ,title: 'dbsu local admin access with os ident' ,order: 100}
- { user: 'all' ,db: all ,addr: localhost ,auth: pwd ,title: 'allow all user local access with pwd' ,order: 150}
- { user: '${monitor}' ,db: pgbouncer ,addr: intra ,auth: ssl ,title: 'monitor access via intranet with pwd' ,order: 200}
- { user: '${monitor}' ,db: all ,addr: world ,auth: deny ,title: 'reject all other monitor access addr' ,order: 250}
- { user: '${admin}' ,db: all ,addr: intra ,auth: ssl ,title: 'admin access via intranet with pwd' ,order: 300}
- { user: '${admin}' ,db: all ,addr: world ,auth: deny ,title: 'reject all other admin access addr' ,order: 350}
- { user: 'all' ,db: all ,addr: intra ,auth: ssl ,title: 'allow all user intra access with pwd' ,order: 400}
#----------------------------------#
# Repo, Node, Packages
#----------------------------------#
repo_remove: true # remove existing repo on admin node during repo bootstrap
node_repo_remove: true # remove existing node repo for node managed by pigsty
#node_selinux_mode: enforcing # set selinux mode: enforcing,permissive,disabled
node_firewall_mode: zone # firewall mode: zone (default), off (disable), none (skip & self-managed)
repo_extra_packages: [ pg18-main ] #,pg18-core ,pg18-time ,pg18-gis ,pg18-rag ,pg18-fts ,pg18-olap ,pg18-feat ,pg18-lang ,pg18-type ,pg18-util ,pg18-func ,pg18-admin ,pg18-stat ,pg18-sec ,pg18-fdw ,pg18-sim ,pg18-etl]
pg_version: 18 # default postgres version
#pg_extensions: [ pg18-time ,pg18-gis ,pg18-rag ,pg18-fts ,pg18-olap ,pg18-feat ,pg18-lang ,pg18-type ,pg18-util ,pg18-func ,pg18-admin ,pg18-stat ,pg18-sec ,pg18-fdw ,pg18-sim ,pg18-etl]
#----------------------------------------------#
# PASSWORD : https://pigsty.io/docs/setup/security/
#----------------------------------------------#
#grafana_admin_username: admin
grafana_admin_password: You.Have2Use-A_VeryStrongPassword
grafana_view_password: DBUser.Viewer
#pg_admin_username: dbuser_dba
pg_admin_password: PessWorb.Should8eStrong-eNough
#pg_monitor_username: dbuser_monitor
pg_monitor_password: MekeSuerYour.PassWordI5secured
#pg_replication_username: replicator
pg_replication_password: doNotUseThis-PasswordFor.AnythingElse
#patroni_username: postgres
patroni_password: don.t-forget-to-change-thEs3-password
#haproxy_admin_username: admin
haproxy_admin_password: GneratePasswordWith-pwgen-s-16-1
minio_secret_key: S3User.MinIO
etcd_root_password: Etcd.Root
...
Feedback
Was this page helpful?
Thanks for the feedback! Please let us know how we can improve.
Sorry to hear that. Please let us know how we can improve.