1. Overview of PostgreSQL Security for Healthcare

PostgreSQL is a popular choice for healthcare systems thanks to its flexibility, open-source, and strong set of security features. However, PostgreSQL's default configuration is not secure enough for PHI (Protected Health Information) data. This lesson will guide you on hardening PostgreSQL according to the CIS Benchmark and best practices for healthcare.
1.1. PostgreSQL security layers

| Layers | Name | Ingredients |
|---|---|---|
| 7 | Application | Quarkus / Spring Boot + Connection Pool |
| 6 | Network | Firewall Rules + VPN/Private Network |
| 5 | Authentication | pg_hba.conf + SSL/TLS + Client Certificates |
| 4 | Authorization | Roles + Privileges + Row-Level Security |
| 3 | Encryption | TDE + Column Encryption + Backup Encryption |
| 2 | Audit | pgAudit + Custom Triggers + Log Shipping |
| 1 | Operating System | File Permissions + SELinux/AppArmor |
1.2. Security checklist needs to be completed
| Category | Description | Priorities |
|---|---|---|
| Authentication | scram-sha-256, client cert | Critical |
| SSL/TLS | Encrypted connections | Critical |
| Role Management | Least privilege roles | Critical |
| Network | Listen address, firewall | Critical |
| postgresql.conf | Security parameters | High |
| Connection Limits | Per-role limits | High |
| Schema Isolation | Separate schemas | Medium |
| Password Policy | Rotation, complexity | Medium |
| Logging | Security-related events | High |
2. pg_hba.conf - Authentication Configuration
File pg_hba.conf (Host-Based Authentication) controls who is connected and by what method. This is the first line of defense.
2.1. pg_hba.conf structure
# TYPE DATABASE USER ADDRESS METHOD
# Giải thích:
# TYPE = local (Unix socket), host (TCP/IP), hostssl (TCP/IP + SSL)
# DATABASE = tên database hoặc "all"
# USER = tên role hoặc "all"
# ADDRESS = CIDR range (cho host/hostssl)
# METHOD = authentication method
2.2. Authentication Methods - Comparison
| Method | Security Level | Description | Used for |
|---|---|---|---|
trust | NOT SAFE | No password needed | Only local dev |
password | Low | Password send plaintext | DO NOT use |
md5 | Average | MD5 hash (easy to crack) | Legacy systems |
scram-sha-256 | High | SCRAM authentication | Production |
cert | Very high | Client certificate | Healthcare |
gss | Cao | Kerberos/GSSAPI | Enterprise AD |
2.3. pg_hba.conf for Healthcare Production
# /etc/postgresql/16/main/pg_hba.conf
# =====================================================
# HEALTHCARE POSTGRESQL - PRODUCTION CONFIGURATION
# CIS Benchmark Compliant
# =====================================================
# === Rule 1: Deny ALL by default ===
# pg_hba.conf xử lý từ trên xuống, match đầu tiên thắng
# === Rule 2: Local socket - chỉ cho admin ===
# Chỉ postgres superuser được dùng Unix socket
local all postgres peer
# === Rule 3: Local socket - deny tất cả user khác ===
local all all reject
# === Rule 4: SSL required cho application connections ===
# Application service account (Quarkus microservices)
hostssl healthcare_db app_patient_svc 10.0.1.0/24 scram-sha-256
hostssl healthcare_db app_lab_svc 10.0.1.0/24 scram-sha-256
hostssl healthcare_db app_pharmacy_svc 10.0.1.0/24 scram-sha-256
hostssl healthcare_db app_gateway_svc 10.0.1.0/24 scram-sha-256
# === Rule 5: SSL + Client Certificate cho admin access ===
hostssl all dba_admin 10.0.100.0/24 cert
# === Rule 6: Read-only replica connections ===
hostssl healthcare_db readonly_user 10.0.2.0/24 scram-sha-256
# === Rule 7: Monitoring ===
hostssl postgres monitoring_user 10.0.3.0/24 scram-sha-256
# === Rule 8: Replication ===
hostssl replication repl_user 10.0.10.0/24 cert
# === Rule 9: Deny everything else ===
host all all 0.0.0.0/0 reject
hostssl all all 0.0.0.0/0 reject
2.4. Explain important rules
Rule 2 - peer authentication: Uses OS username mapping. When logging in using Unix socket with user postgres On OS, PostgreSQL automatically authenticates.
# Chỉ hoạt động khi login với OS user postgres
sudo -u postgres psql
Rule 4 - hostssl + scram-sha-256:
hostssl= SSL/TLS requiredscram-sha-256= the strongest password hashing PostgreSQL supports
Rule 5 - cert authentication: Client must present a TLS certificate signed by a trusted CA. The most powerful method for admin.
# Client cert connection
psql "host=db.hospital.local \
port=5432 \
dbname=healthcare_db \
user=dba_admin \
sslmode=verify-full \
sslcert=/path/to/client.crt \
sslkey=/path/to/client.key \
sslrootcert=/path/to/ca.crt"
3. SSL/TLS Configuration
3.1. Create SSL Certificates
#!/bin/bash
# generate-pg-certs.sh
# Tạo CA và certificates cho PostgreSQL
CERT_DIR="/etc/postgresql/ssl"
mkdir -p ${CERT_DIR}
cd ${CERT_DIR}
# === Step 1: Tạo Certificate Authority (CA) ===
openssl genrsa -aes256 -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key \
-out ca.crt \
-subj "/C=VN/ST=HCMC/O=Hospital Network/CN=PostgreSQL CA"
# === Step 2: Tạo Server Certificate ===
openssl genrsa -out server.key 2048
chmod 600 server.key
chown postgres:postgres server.key
openssl req -new -key server.key \
-out server.csr \
-subj "/C=VN/ST=HCMC/O=Hospital Network/CN=db.hospital.local"
# SAN (Subject Alternative Names) cho multiple hostnames
cat > server-ext.cnf << EOF
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = db.hospital.local
DNS.2 = db-primary.hospital.local
DNS.3 = db-replica.hospital.local
IP.1 = 10.0.1.100
EOF
openssl x509 -req -in server.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 365 \
-extfile server-ext.cnf -extensions v3_req
# === Step 3: Tạo Client Certificate (cho DBA admin) ===
openssl genrsa -out client-dba.key 2048
openssl req -new -key client-dba.key \
-out client-dba.csr \
-subj "/C=VN/ST=HCMC/O=Hospital Network/CN=dba_admin"
openssl x509 -req -in client-dba.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out client-dba.crt -days 90
echo "Certificates generated successfully!"
3.2. postgresql.conf - SSL Settings
# /etc/postgresql/16/main/postgresql.conf
# ======================================
# SSL/TLS Configuration
# ======================================
ssl = on
ssl_cert_file = '/etc/postgresql/ssl/server.crt'
ssl_key_file = '/etc/postgresql/ssl/server.key'
ssl_ca_file = '/etc/postgresql/ssl/ca.crt'
# TLS version - chỉ cho phép TLS 1.2 và 1.3
ssl_min_protocol_version = 'TLSv1.2'
# Cipher suites - chỉ strong ciphers
ssl_ciphers = 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'
# Prefer server cipher order
ssl_prefer_server_ciphers = on
# CRL (Certificate Revocation List) - kiểm tra cert bị revoke
ssl_crl_file = '/etc/postgresql/ssl/ca.crl'
3.3. Verify SSL Connection
-- Kiểm tra connection đang dùng SSL
SELECT
pid,
usename,
client_addr,
ssl,
ssl_version,
ssl_cipher
FROM pg_stat_ssl
JOIN pg_stat_activity USING (pid)
WHERE usename IS NOT NULL;
-- Kết quả mong muốn:
-- pid | usename | client_addr | ssl | ssl_version | ssl_cipher
-- ------+---------------+-------------+------+-------------+---------------------------
-- 1234 | app_patient | 10.0.1.10 | true | TLSv1.3 | TLS_AES_256_GCM_SHA384
-- 1235 | dba_admin | 10.0.100.5 | true | TLSv1.3 | TLS_AES_256_GCM_SHA384
4. Role Management - Least Privilege
4.1. Least Privilege Principle for Healthcare

- postgres (superuser) ← ONLY used for maintenance
- dba_admin (CREATEDB, CREATEROLE) — Schema management, backup, monitoring
- app_roles (LOGIN, limited privileges) — app_patient_svc, app_lab_svc, app_pharmacy_svc, app_gateway_svc
- readonly_role (SELECT only) — readonly_analytics, readonly_reporting
- monitoring_user (pg_monitor)
4.2. Create Role Hierarchy
-- =====================================================
-- HEALTHCARE DATABASE ROLE SETUP
-- Run as: postgres superuser
-- =====================================================
-- === Step 1: Tạo Group Roles (không LOGIN) ===
-- Group role cho application services
CREATE ROLE healthcare_app NOLOGIN;
COMMENT ON ROLE healthcare_app IS 'Group role for all application services';
-- Group role cho read-only access
CREATE ROLE healthcare_readonly NOLOGIN;
COMMENT ON ROLE healthcare_readonly IS 'Group role for read-only access (analytics/reporting)';
-- Group role cho PHI access (extra audit)
CREATE ROLE phi_access NOLOGIN;
COMMENT ON ROLE phi_access IS 'Group role for PHI data access - extra audit logging';
-- === Step 2: Tạo Application Service Accounts ===
-- Patient Service
CREATE ROLE app_patient_svc LOGIN
PASSWORD 'CHANGE_ME_USE_VAULT' -- Sẽ rotate qua Vault
CONNECTION LIMIT 20
IN ROLE healthcare_app, phi_access
VALID UNTIL '2025-12-31';
-- Lab Service
CREATE ROLE app_lab_svc LOGIN
PASSWORD 'CHANGE_ME_USE_VAULT'
CONNECTION LIMIT 15
IN ROLE healthcare_app, phi_access
VALID UNTIL '2025-12-31';
-- Pharmacy Service
CREATE ROLE app_pharmacy_svc LOGIN
PASSWORD 'CHANGE_ME_USE_VAULT'
CONNECTION LIMIT 15
IN ROLE healthcare_app
VALID UNTIL '2025-12-31';
-- API Gateway (chỉ cần authenticate, không truy cập data trực tiếp)
CREATE ROLE app_gateway_svc LOGIN
PASSWORD 'CHANGE_ME_USE_VAULT'
CONNECTION LIMIT 50
VALID UNTIL '2025-12-31';
-- === Step 3: DBA Admin ===
CREATE ROLE dba_admin LOGIN
CREATEDB CREATEROLE
CONNECTION LIMIT 3
IN ROLE pg_monitor;
-- === Step 4: Read-only accounts ===
CREATE ROLE readonly_analytics LOGIN
PASSWORD 'CHANGE_ME'
CONNECTION LIMIT 5
IN ROLE healthcare_readonly
VALID UNTIL '2025-06-30';
CREATE ROLE readonly_reporting LOGIN
PASSWORD 'CHANGE_ME'
CONNECTION LIMIT 5
IN ROLE healthcare_readonly
VALID UNTIL '2025-06-30';
-- === Step 5: Monitoring ===
CREATE ROLE monitoring_user LOGIN
PASSWORD 'CHANGE_ME'
CONNECTION LIMIT 3
IN ROLE pg_monitor;
4.3. Schema Isolation
-- =====================================================
-- SCHEMA ISOLATION BY SERVICE
-- =====================================================
-- Tạo database
CREATE DATABASE healthcare_db
OWNER dba_admin
ENCODING 'UTF8'
LC_COLLATE 'en_US.UTF-8';
\c healthcare_db
-- Revoke default public access
REVOKE ALL ON DATABASE healthcare_db FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM PUBLIC;
-- === Tạo schemas cho từng service ===
CREATE SCHEMA patient_schema AUTHORIZATION dba_admin;
CREATE SCHEMA lab_schema AUTHORIZATION dba_admin;
CREATE SCHEMA pharmacy_schema AUTHORIZATION dba_admin;
CREATE SCHEMA audit_schema AUTHORIZATION dba_admin;
CREATE SCHEMA shared_schema AUTHORIZATION dba_admin;
-- === GRANT cho Patient Service ===
GRANT USAGE ON SCHEMA patient_schema TO app_patient_svc;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA patient_schema TO app_patient_svc;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA patient_schema TO app_patient_svc;
-- Shared schema (lookup tables)
GRANT USAGE ON SCHEMA shared_schema TO app_patient_svc;
GRANT SELECT ON ALL TABLES IN SCHEMA shared_schema TO app_patient_svc;
-- Default privileges cho tables tạo trong tương lai
ALTER DEFAULT PRIVILEGES IN SCHEMA patient_schema
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_patient_svc;
ALTER DEFAULT PRIVILEGES IN SCHEMA patient_schema
GRANT USAGE, SELECT ON SEQUENCES TO app_patient_svc;
-- === GRANT cho Lab Service ===
GRANT USAGE ON SCHEMA lab_schema TO app_lab_svc;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA lab_schema TO app_lab_svc;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA lab_schema TO app_lab_svc;
-- Lab service cần đọc patient data
GRANT USAGE ON SCHEMA patient_schema TO app_lab_svc;
GRANT SELECT ON patient_schema.patients TO app_lab_svc;
-- === GRANT cho Pharmacy Service ===
GRANT USAGE ON SCHEMA pharmacy_schema TO app_pharmacy_svc;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA pharmacy_schema TO app_pharmacy_svc;
-- === GRANT cho Read-only ===
GRANT USAGE ON SCHEMA patient_schema, lab_schema, pharmacy_schema TO healthcare_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA patient_schema TO healthcare_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA lab_schema TO healthcare_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA pharmacy_schema TO healthcare_readonly;
-- Default privileges cho readonly
ALTER DEFAULT PRIVILEGES IN SCHEMA patient_schema
GRANT SELECT ON TABLES TO healthcare_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA lab_schema
GRANT SELECT ON TABLES TO healthcare_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA pharmacy_schema
GRANT SELECT ON TABLES TO healthcare_readonly;
-- === Audit schema - append only ===
GRANT USAGE ON SCHEMA audit_schema TO healthcare_app;
GRANT INSERT ON ALL TABLES IN SCHEMA audit_schema TO healthcare_app;
-- KHÔNG grant UPDATE, DELETE cho audit tables
4.4. Password Policy and Rotation
-- Kiểm tra password encryption method
SHOW password_encryption;
-- Kết quả: scram-sha-256
-- Đặt valid_until cho password rotation
ALTER ROLE app_patient_svc VALID UNTIL '2025-06-30';
-- Script kiểm tra roles sắp hết hạn
SELECT
rolname,
rolvaliduntil,
CASE
WHEN rolvaliduntil IS NULL THEN 'No expiry set'
WHEN rolvaliduntil < CURRENT_TIMESTAMP THEN 'EXPIRED!'
WHEN rolvaliduntil < CURRENT_TIMESTAMP + INTERVAL '30 days' THEN 'Expiring soon'
ELSE 'OK'
END AS status
FROM pg_roles
WHERE rolcanlogin = true
ORDER BY rolvaliduntil NULLS LAST;
5. postgresql.conf - Security Parameters
5.1. Core Security Settings
# /etc/postgresql/16/main/postgresql.conf
# =====================================================
# SECURITY CONFIGURATION - Healthcare Production
# =====================================================
# --- Authentication ---
password_encryption = 'scram-sha-256' # CIS: Ensure scram-sha-256
authentication_timeout = '30s' # Timeout cho authentication
# --- Connection Settings ---
listen_addresses = '10.0.1.100' # CHỈ listen trên internal IP
# KHÔNG dùng '*' trong production
port = 5432
max_connections = 200 # Giới hạn tổng connections
superuser_reserved_connections = 3 # Reserve cho emergency admin
# --- Logging (Security-relevant) ---
log_destination = 'stderr'
logging_collector = on
log_directory = '/var/log/postgresql'
log_filename = 'postgresql-%Y-%m-%d.log'
log_file_mode = 0600 # Chỉ postgres user đọc được
# Log connections và disconnections
log_connections = on # CIS: Required
log_disconnections = on # CIS: Required
# Log tất cả DDL statements
log_statement = 'ddl' # Options: none, ddl, mod, all
# 'mod' hoặc 'all' cho audit cao hơn
# Log slow queries (performance + security)
log_min_duration_statement = 1000 # Log queries > 1 second
# Log line prefix - bao gồm đầy đủ context
log_line_prefix = '%m [%p] %q%u@%d from %h '
# %m = timestamp with milliseconds
# %p = process ID
# %u = user name
# %d = database name
# %h = remote host
# Log failed authentication
log_min_messages = 'warning'
# --- Row Security ---
row_security = on # Enable Row-Level Security
# --- Statement Timeout ---
statement_timeout = '60s' # Kill queries chạy quá lâu
idle_in_transaction_session_timeout = '300s' # Kill idle transactions
# --- Shared Preload Libraries ---
shared_preload_libraries = 'pgaudit,pg_stat_statements'
5.2. Check current configuration
-- Script kiểm tra security settings
SELECT name, setting, unit, context
FROM pg_settings
WHERE name IN (
'password_encryption',
'ssl',
'ssl_min_protocol_version',
'log_connections',
'log_disconnections',
'log_statement',
'listen_addresses',
'row_security',
'authentication_timeout',
'statement_timeout'
)
ORDER BY name;
-- Kiểm tra roles có SUPERUSER (nên tối thiểu)
SELECT rolname, rolsuper, rolcreatedb, rolcreaterole, rolreplication
FROM pg_roles
WHERE rolsuper = true;
-- Kiểm tra roles không có password expiry
SELECT rolname, rolvaliduntil
FROM pg_roles
WHERE rolcanlogin = true
AND (rolvaliduntil IS NULL OR rolvaliduntil = 'infinity');
6. Connection Pooling Security - PgBouncer
6.1. Why need PgBouncer
In a microservices system, each service instance creates its own connection pool. PgBouncer helps:
- Reduce the total number of connections to PostgreSQL
- Add an authentication layer
- Rate limiting
- Connection routing

6.2. PgBouncer Configuration
; /etc/pgbouncer/pgbouncer.ini
[databases]
; database = host=addr port=port dbname=name
healthcare_db = host=10.0.1.100 port=5432 dbname=healthcare_db
[pgbouncer]
; === Connection Settings ===
listen_addr = 10.0.1.50
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
; === TLS Settings ===
; Client-side TLS (app → PgBouncer)
client_tls_sslmode = require
client_tls_key_file = /etc/pgbouncer/ssl/pgbouncer.key
client_tls_cert_file = /etc/pgbouncer/ssl/pgbouncer.crt
client_tls_ca_file = /etc/pgbouncer/ssl/ca.crt
client_tls_protocols = tlsv1.2,tlsv1.3
; Server-side TLS (PgBouncer → PostgreSQL)
server_tls_sslmode = verify-full
server_tls_key_file = /etc/pgbouncer/ssl/client.key
server_tls_cert_file = /etc/pgbouncer/ssl/client.crt
server_tls_ca_file = /etc/pgbouncer/ssl/ca.crt
; === Pool Settings ===
pool_mode = transaction ; transaction pooling cho microservices
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
max_client_conn = 200
max_db_connections = 50
; === Security Limits ===
max_user_connections = 30 ; Per-user connection limit
query_timeout = 60 ; Kill queries > 60s
client_idle_timeout = 300 ; Disconnect idle clients
server_idle_timeout = 600
; === Logging ===
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
stats_period = 60
; === Admin ===
admin_users = pgbouncer_admin
stats_users = pgbouncer_monitor
6.3. PgBouncer Auth File
# /etc/pgbouncer/userlist.txt
# Format: "username" "password_hash"
# Generate hash: psql -c "SELECT concat('\"', usename, '\" \"', passwd, '\"') FROM pg_shadow"
"app_patient_svc" "SCRAM-SHA-256$4096:salt$StoredKey:ServerKey"
"app_lab_svc" "SCRAM-SHA-256$4096:salt$StoredKey:ServerKey"
"app_pharmacy_svc" "SCRAM-SHA-256$4096:salt$StoredKey:ServerKey"
"readonly_analytics" "SCRAM-SHA-256$4096:salt$StoredKey:ServerKey"
7. Network Security
7.1. Firewall Rules (iptables)
#!/bin/bash
# postgresql-firewall.sh
# Firewall rules cho PostgreSQL server
# Flush existing rules
iptables -F INPUT
# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
# Allow SSH from admin network
iptables -A INPUT -p tcp --dport 22 -s 10.0.100.0/24 -j ACCEPT
# Allow PostgreSQL from application subnet
iptables -A INPUT -p tcp --dport 5432 -s 10.0.1.0/24 -j ACCEPT
# Allow PostgreSQL from PgBouncer
iptables -A INPUT -p tcp --dport 5432 -s 10.0.1.50/32 -j ACCEPT
# Allow replication from replica servers
iptables -A INPUT -p tcp --dport 5432 -s 10.0.10.0/24 -j ACCEPT
# Allow monitoring
iptables -A INPUT -p tcp --dport 5432 -s 10.0.3.0/24 -j ACCEPT
# Drop everything else to PostgreSQL port
iptables -A INPUT -p tcp --dport 5432 -j DROP
# Default drop
iptables -P INPUT DROP
# Save rules
iptables-save > /etc/iptables/rules.v4
7.2. Kubernetes Network Policy
# network-policy-postgresql.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgresql-network-policy
namespace: healthcare-db
spec:
podSelector:
matchLabels:
app: postgresql
policyTypes:
- Ingress
- Egress
ingress:
# Cho phép từ application namespace
- from:
- namespaceSelector:
matchLabels:
name: healthcare-app
podSelector:
matchLabels:
role: microservice
ports:
- port: 5432
protocol: TCP
# Cho phép từ PgBouncer
- from:
- podSelector:
matchLabels:
app: pgbouncer
ports:
- port: 5432
protocol: TCP
# Cho phép từ monitoring
- from:
- namespaceSelector:
matchLabels:
name: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- port: 9187 # postgres_exporter
protocol: TCP
egress:
# DNS
- to: []
ports:
- port: 53
protocol: UDP
# Replication
- to:
- podSelector:
matchLabels:
app: postgresql
ports:
- port: 5432
protocol: TCP
8. CIS Benchmark Checklist for PostgreSQL
8.1. Automated Compliance Check Script
-- =====================================================
-- CIS BENCHMARK COMPLIANCE CHECK
-- PostgreSQL 16 for Healthcare
-- =====================================================
-- 1. Ensure login via local UNIX Domain Socket is restricted
SELECT
'CIS 1.1 - Unix Domain Socket' AS check_item,
CASE
WHEN count(*) = 0 THEN 'PASS'
ELSE 'FAIL - Found trust/password auth on local'
END AS result
FROM pg_hba_file_rules
WHERE type = 'local'
AND auth_method IN ('trust', 'password');
-- 2. Ensure password_encryption is scram-sha-256
SELECT
'CIS 2.1 - Password Encryption' AS check_item,
CASE
WHEN setting = 'scram-sha-256' THEN 'PASS'
ELSE 'FAIL - Current: ' || setting
END AS result
FROM pg_settings
WHERE name = 'password_encryption';
-- 3. Ensure SSL is enabled
SELECT
'CIS 3.1 - SSL Enabled' AS check_item,
CASE
WHEN setting = 'on' THEN 'PASS'
ELSE 'FAIL'
END AS result
FROM pg_settings
WHERE name = 'ssl';
-- 4. Ensure TLS version is >= 1.2
SELECT
'CIS 3.2 - TLS Min Version' AS check_item,
CASE
WHEN setting IN ('TLSv1.2', 'TLSv1.3') THEN 'PASS'
ELSE 'FAIL - Current: ' || setting
END AS result
FROM pg_settings
WHERE name = 'ssl_min_protocol_version';
-- 5. Ensure log_connections is enabled
SELECT
'CIS 4.1 - Log Connections' AS check_item,
CASE
WHEN setting = 'on' THEN 'PASS'
ELSE 'FAIL'
END AS result
FROM pg_settings
WHERE name = 'log_connections';
-- 6. Ensure log_disconnections is enabled
SELECT
'CIS 4.2 - Log Disconnections' AS check_item,
CASE
WHEN setting = 'on' THEN 'PASS'
ELSE 'FAIL'
END AS result
FROM pg_settings
WHERE name = 'log_disconnections';
-- 7. Ensure log_statement is set
SELECT
'CIS 4.3 - Log Statement' AS check_item,
CASE
WHEN setting IN ('ddl', 'mod', 'all') THEN 'PASS - ' || setting
ELSE 'FAIL - Not logging statements'
END AS result
FROM pg_settings
WHERE name = 'log_statement';
-- 8. Ensure no roles use md5 password
SELECT
'CIS 5.1 - No MD5 Passwords' AS check_item,
CASE
WHEN count(*) = 0 THEN 'PASS'
ELSE 'FAIL - ' || count(*) || ' roles with md5'
END AS result
FROM pg_shadow
WHERE passwd LIKE 'md5%';
-- 9. Ensure superuser count is minimal
SELECT
'CIS 6.1 - Superuser Count' AS check_item,
CASE
WHEN count(*) <= 1 THEN 'PASS - ' || count(*) || ' superuser(s)'
ELSE 'WARNING - ' || count(*) || ' superusers'
END AS result
FROM pg_roles
WHERE rolsuper = true;
-- 10. Ensure row_security is enabled
SELECT
'CIS 7.1 - Row Security' AS check_item,
CASE
WHEN setting = 'on' THEN 'PASS'
ELSE 'FAIL'
END AS result
FROM pg_settings
WHERE name = 'row_security';
8.2. CIS Controls summary table
| # | Control | Setting | Request value |
|---|---|---|---|
| 1 | Password Encryption | password_encryption | scram-sha-256 |
| 2 | SSL Enabled | ssl | on |
| 3 | Min TLS Version | ssl_min_protocol_version | TLSv1.2 |
| 4 | Log Connections | log_connections | on |
| 5 | Log Disconnections | log_disconnections | on |
| 6 | Log Statements | log_statement | ddl or mod |
| 7 | Listen Address | listen_addresses | Specific IP (no *) |
| 8 | Auth Timeout | authentication_timeout | ≤ 60s |
| 9 | Statement Timeout | statement_timeout | > 0 |
| 10 | Row Security | row_security | on |
9. OS-Level Security
9.1. File Permissions
#!/bin/bash
# check-pg-permissions.sh
# Kiểm tra file permissions cho PostgreSQL
PGDATA="/var/lib/postgresql/16/main"
PGCONF="/etc/postgresql/16/main"
echo "=== Checking PGDATA permissions ==="
# PGDATA directory phải có mode 700
stat -c '%a %U:%G %n' $PGDATA
# Expected: 700 postgres:postgres
echo "=== Checking config file permissions ==="
stat -c '%a %U:%G %n' $PGCONF/postgresql.conf
# Expected: 600 postgres:postgres
stat -c '%a %U:%G %n' $PGCONF/pg_hba.conf
# Expected: 600 postgres:postgres
echo "=== Checking SSL key permissions ==="
stat -c '%a %U:%G %n' /etc/postgresql/ssl/server.key
# Expected: 600 postgres:postgres
echo "=== Checking log directory permissions ==="
stat -c '%a %U:%G %n' /var/log/postgresql
# Expected: 700 postgres:postgres
echo "=== Checking for world-readable data files ==="
find $PGDATA -perm /o+r -type f | head -20
# Expected: No output (no world-readable files)
9.2. systemd Hardening
# /etc/systemd/system/postgresql.service.d/hardening.conf
[Service]
# Restrict filesystem access
ProtectHome = true
ProtectSystem = strict
ReadWritePaths = /var/lib/postgresql /var/log/postgresql /run/postgresql
# Restrict capabilities
CapabilityBoundingSet = CAP_NET_BIND_SERVICE
NoNewPrivileges = true
# Restrict network
RestrictAddressFamilies = AF_INET AF_INET6 AF_UNIX
# Memory protection
MemoryDenyWriteExecute = true
# Restrict system calls
SystemCallFilter = @system-service
SystemCallArchitectures = native
10. Quarkus Application Configuration
10.1. application.properties - Secure Database Connection
# src/main/resources/application.properties
# =====================================================
# PostgreSQL Security Configuration for Quarkus
# =====================================================
# --- Connection ---
quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.url=jdbc:postgresql://pgbouncer.hospital.local:6432/healthcare_db?currentSchema=patient_schema
# --- Credentials (from Vault or Environment) ---
quarkus.datasource.username=${DB_USERNAME}
quarkus.datasource.password=${DB_PASSWORD}
# --- SSL/TLS ---
quarkus.datasource.jdbc.additional-jdbc-properties.ssl=true
quarkus.datasource.jdbc.additional-jdbc-properties.sslmode=verify-full
quarkus.datasource.jdbc.additional-jdbc-properties.sslrootcert=/etc/app/certs/ca.crt
quarkus.datasource.jdbc.additional-jdbc-properties.sslcert=/etc/app/certs/client.crt
quarkus.datasource.jdbc.additional-jdbc-properties.sslkey=/etc/app/certs/client.key
# --- Connection Pool ---
quarkus.datasource.jdbc.min-size=5
quarkus.datasource.jdbc.max-size=20
quarkus.datasource.jdbc.idle-removal-interval=PT5M
quarkus.datasource.jdbc.max-lifetime=PT30M
# --- Validation ---
quarkus.datasource.jdbc.validation-query-sql=SELECT 1
quarkus.datasource.jdbc.background-validation-interval=PT2M
Summary
In this lesson, we implemented a comprehensive PostgreSQL Security Hardening for a healthcare system:
- pg_hba.conf - Configure authentication with
scram-sha-256andcert, deny all connections by default - SSL/TLS - Create CA, server cert, client cert with TLS 1.2+ and strong cipher suites
- Role Management - Design role hierarchy according to least privilege with schema isolation
- postgresql.conf - Security parameters including logging, timeouts, and encryption settings
- PgBouncer - Connection pooling with SSL both ways (client → PgBouncer → PostgreSQL)
- Network Security - Firewall rules and Kubernetes Network Policies
- CIS Benchmark - Automated compliance checking script
- OS-Level - File permissions and systemd hardening
Core principles:
- Defense in Depth: Many overlapping layers of security
- Least Privilege: Each role only has the minimum necessary rights
- Encrypt Everything: SSL/TLS for all connections
- Log Everything: Logs all security-relevant activities
- Validate Always: CIS Benchmark checks periodically
Exercises
-
Set up pg_hba.conf: Create pg_hba.conf file for development environment with Docker Compose, make sure to use
scram-sha-256andhostsslfor all application connections. Test by trying to connect without SSL. -
Create Role Hierarchy: Write SQL script to create a full role hierarchy for the HIS system with 5 microservices. Verify by trying cross-schema access (must be denied).
-
SSL Certificate Chain: Create CA → server cert → client cert chain. Configure PostgreSQL to use mutual TLS. Verify equal
pg_stat_ssl. -
CIS Benchmark Audit: Run CIS compliance check script on current PostgreSQL installation. Fix all FAIL items and run again until all PASS.
| ◀ Previous article | Next article ▶ |
|---|---|
| Lesson 8: MFA, Passkeys & Emergency Access for Medical Staff | Lesson 10: Encrypting At-Rest & In-Transit Data with PostgreSQL |