Skip to content

ppradela/postgres16-ol8-stig

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PostgreSQL 16 — DISA STIG Hardened Deployment

A complete, production-tested DISA STIG hardening guide for PostgreSQL 16 on Oracle Linux 8. Covers installation, SSL/TLS, pgaudit, systemd hardening, and SIEM forwarding — with every OL8-specific pitfall documented.

PostgreSQL OL8 STIG STIG License

Air-gapped / disconnected network? See README-airgapped.md.


Features

  • DISA STIG baseline — PostgreSQL 9.x STIG V2R4 controls applied to PostgreSQL 16; OS STIG assumed at install time
  • SSL/TLS enforced end-to-end — TLS 1.2 minimum, FIPS 140-2 approved cipher suites, client certificate CA configured
  • pgaudit structured auditing — write, DDL, role, and function events logged with relation names and bound parameters
  • Dual log destination — syslog → SIEM via TLS rsyslog, plus local CSV for independent audit trail
  • pg_hba.conf zero-trusthostssl + SCRAM-SHA-256 for all remote connections; plain TCP rejected; local socket peer-only
  • initdb data checksums — block-level corruption detection enabled at database initialisation
  • systemd hardening drop-in — capabilities cleared, dangerous syscall groups blocked, core dumps disabled; OL8/systemd-239 compatible
  • Post-install SQL hardening — public schema CREATE revoked, template database access restricted, default privileges locked down
  • Documented pitfalls — every non-obvious OL8/PG16 failure mode explained and solved

Architecture

Application Server(s)
        │  TCP 5432 (TLS 1.2+, SCRAM-SHA-256)
        ▼
PostgreSQL 16
  ├── /var/lib/pgsql/data/         — PGDATA            [lv_pgdata on vg_pgdata]
  │   ├── postgresql.conf          — main config (connections, SSL, pgaudit, logging)
  │   └── pg_wal → /pg_wal_volume/ — WAL symlink       [lv_pgwal  on vg_pgdata]
  ├── /etc/postgresql/
  │   ├── pg_hba.conf              — authentication rules (hostssl + scram-sha-256)
  │   └── pg_ident.conf            — OS-to-DB user identity map
  └── /etc/systemd/system/
      └── postgresql.service.d/
          └── hardening.conf       — systemd drop-in (capabilities, syscall filter)
        │
        ├── /pg_log_volume/pg_log/ — local CSV audit trail  [lv_pglogs on vg_pglogs]
        │
        └── syslog (facility LOCAL0)
                │  TLS 6514 (production/CUI) / UDP 514 (lab only)
                ▼
             SIEM

File Structure

repository/
├── config/
│   ├── postgresql.conf            # Main daemon configuration
│   ├── pg_hba.conf                # Host-based authentication rules
│   ├── postgresql-hardening.conf  # systemd hardening drop-in
│   ├── setup.sql                  # Post-install SQL hardening script
│   └── rsyslog-postgresql.conf    # rsyslog SIEM forwarding rule
├── scripts/
│   ├── New-SecurePassword.ps1     # Secure password generator (PowerShell)
│   ├── new-secure-password.sh     # Secure password generator (Bash)
│   └── new-secure-password.py     # Secure password generator (Python 3.6+)
├── README.md                      # This guide (internet-connected deployment)
└── README-airgapped.md            # Air-gapped deployment variant
File Install path
config/postgresql.conf /var/lib/pgsql/data/postgresql.conf
config/pg_hba.conf /etc/postgresql/pg_hba.conf
config/postgresql-hardening.conf /etc/systemd/system/postgresql.service.d/hardening.conf
config/setup.sql run from any writable directory, not installed
config/rsyslog-postgresql.conf /etc/rsyslog.d/postgresql.conf
scripts/New-SecurePassword.ps1 helper script — not installed on server
scripts/new-secure-password.sh helper script — not installed on server
scripts/new-secure-password.py helper script — not installed on server

Prerequisites

Oracle Linux 8 baseline

Oracle Linux 8 is assumed to be DISA STIG hardened at install time using the STIG profile available in the OL8 installer. No additional OS hardening steps are required here.

  • rsyslog installed and running
  • Outbound access to yum.oracle.com during installation
  • A TLS certificate and private key for the PostgreSQL server (RSA 2048+ or ECDSA P-256+; MD5 certificates will fail under OL8 FIPS mode)
  • A log volume mounted at /pg_log_volume with adequate capacity for audit logs

Recommended storage layout

Separate physical disks and LVM logical volumes prevent one workload from starving another. A WAL storm cannot fill PGDATA; a pgaudit log flood cannot affect database writes.

Physical Disks
├── /dev/sda  ← OS disk (existing)
├── /dev/sdb  ← PostgreSQL data + WAL  (fast NVMe/SSD, e.g. 500 GB)
└── /dev/sdc  ← Audit log disk         (standard SSD, e.g. 200 GB)

Volume Groups / Logical Volumes
├── vg_pgdata  on /dev/sdb
│   ├── lv_pgdata   ≥ 2× expected DB size     → /var/lib/pgsql
│   └── lv_pgwal    ≥ 4× max_wal_size (32 GB) → /pg_wal_volume
└── vg_pglogs  on /dev/sdc
    └── lv_pglogs   100 GB to start            → /pg_log_volume

Provision and mount before deployment:

# Data + WAL disk
pvcreate /dev/sdb
vgcreate vg_pgdata /dev/sdb
lvcreate -L 100G -n lv_pgdata vg_pgdata
lvcreate -L 32G  -n lv_pgwal  vg_pgdata
mkfs.xfs -f -L pgdata /dev/vg_pgdata/lv_pgdata
mkfs.xfs -f -L pgwal  /dev/vg_pgdata/lv_pgwal

# Log disk
pvcreate /dev/sdc
vgcreate vg_pglogs /dev/sdc
lvcreate -L 100G -n lv_pglogs vg_pglogs
mkfs.xfs -f -L pglogs /dev/vg_pglogs/lv_pglogs

# Mount points
mkdir -p /var/lib/pgsql /pg_wal_volume /pg_log_volume

Add to /etc/fstab:

/dev/vg_pgdata/lv_pgdata  /var/lib/pgsql       xfs  defaults,noatime,nosuid,nodev          0 2
/dev/vg_pgdata/lv_pgwal   /pg_wal_volume        xfs  defaults,noatime,nosuid,nodev          0 2
/dev/vg_pglogs/lv_pglogs  /pg_log_volume        xfs  defaults,noatime,nosuid,nodev,noexec   0 2
mount -a    # verify all three mounts before continuing

Do not set noexec on /var/lib/pgsql/data or /pg_wal_volume — PostgreSQL executes binaries from those paths. noexec on /pg_log_volume is safe; only CSV/text files are written there.

SELinux file contexts: new mount points will not carry the correct SELinux labels by default. PostgreSQL running as postgresql_t will be denied access to files labelled with the generic default_t. Apply the correct contexts before starting the service:

semanage fcontext -a -t postgresql_db_t  "/var/lib/pgsql(/.*)?"
semanage fcontext -a -t postgresql_db_t  "/pg_wal_volume(/.*)?"
semanage fcontext -a -t postgresql_log_t "/pg_log_volume(/.*)?"
restorecon -Rv /var/lib/pgsql /pg_wal_volume /pg_log_volume

Install PostgreSQL 16 from Oracle Linux 8 EPEL

# Enable the Oracle Linux 8 EPEL repository (if not already enabled)
dnf install oracle-epel-release-el8

# Enable the postgresql:16 module stream
dnf module enable postgresql:16

# Install PostgreSQL 16 server, contrib modules, and pgaudit
dnf install postgresql-server postgresql-contrib
dnf install pgaudit

Verify the service account

The package creates the postgres OS account. Confirm it is locked:

id postgres
passwd -l postgres
passwd -S postgres    # must show: postgres L ...

Deployment

Step 1 — Create the log volume directory

Create the dedicated log directory (or ensure the mount is in place):

install -d -o postgres -g postgres -m 0700 /pg_log_volume
install -d -o postgres -g postgres -m 0700 /pg_log_volume/pg_log

If /pg_log_volume is a separate mount, ensure it is mounted before the PostgreSQL service starts. Add it to /etc/fstab and verify with mount -a.

Step 2 — Create the config directory and install pg_hba.conf

PostgreSQL's hba_file is overridden in postgresql.conf to /etc/postgresql/ to separate authentication policy from the data directory.

install -d -o root -g postgres -m 0750 /etc/postgresql

install -o root -g postgres -m 0640 \
  config/pg_hba.conf /etc/postgresql/pg_hba.conf

# Create a minimal pg_ident.conf (required by the config reference)
install -o root -g postgres -m 0640 \
  /dev/null /etc/postgresql/pg_ident.conf

Edit pg_hba.conf and replace <<APP_SUBNET>> with your actual application server CIDR(s).

Step 3 — Initialise the database cluster

# Enable data checksums and force UTF-8 encoding at initdb time
PGSETUP_INITDB_OPTIONS="--data-checksums --encoding=UTF8 --locale=en_US.UTF-8" \
  postgresql-setup --initdb

initdb may warn that /var/lib/pgsql is a direct mountpoint. This is safe to ignore — PGDATA (/var/lib/pgsql/data) is a regular subdirectory inside the mount, not the mountpoint itself. Silencing the warning would require an extra intermediate directory and deviating from the OL8 package default PGDATA path, which is not worth the trade-off.

Verify checksums were enabled:

sudo -u postgres pg_controldata /var/lib/pgsql/data \
  | grep 'Data page checksum'
# Expected: Data page checksum version:          1

Step 4 — Create the WAL symlink

Move the pg_wal directory created by initdb to the dedicated WAL volume and replace it with a symlink. This must be done before the service is started for the first time.

# Move pg_wal to the dedicated volume
mv /var/lib/pgsql/data/pg_wal /pg_wal_volume/pg_wal

# Symlink back to the expected path
ln -s /pg_wal_volume/pg_wal /var/lib/pgsql/data/pg_wal

# Confirm ownership (initdb sets postgres:postgres; mv preserves it)
chown -R postgres:postgres /pg_wal_volume/pg_wal
chmod 0700 /pg_wal_volume/pg_wal

# Verify
ls -la /var/lib/pgsql/data/pg_wal
# Expected: lrwxrwxrwx ... /var/lib/pgsql/data/pg_wal -> /pg_wal_volume/pg_wal

Step 5 — Install postgresql.conf

install -o postgres -g postgres -m 0600 \
  config/postgresql.conf /var/lib/pgsql/data/postgresql.conf

Edit the site-specific values before starting the service:

Setting What to set
listen_addresses Server IP(s) — never '*' in production
ssl_cert_file Path to the server TLS certificate
ssl_key_file Path to the server TLS private key
ssl_ca_file Path to the CA used to verify client certificates
syslog_facility Must match rsyslog-postgresql.conf
shared_buffers 25 % of installed RAM
effective_cache_size Approximate total RAM available for caching

Step 6 — Install TLS certificates

# Server private key — must match ssl_key_file in postgresql.conf
install -o root -g postgres -m 0640 \
  server.key /etc/pki/tls/private/postgresql.key

# Server certificate — must match ssl_cert_file in postgresql.conf
install -o root -g postgres -m 0644 \
  server.crt /etc/pki/tls/certs/postgresql.crt

# CA certificate for client verification — must match ssl_ca_file in postgresql.conf
install -o root -g postgres -m 0644 \
  ca.crt /etc/pki/tls/certs/postgresql-ca.crt

# CRL — initial file from your CA (see auto-refresh setup below)
install -o root -g root -m 0644 \
  postgresql-crl.pem /etc/pki/tls/certs/postgresql-crl.pem

CRL auto-refresh: PostgreSQL has no native OCSP or CDP support — ssl_crl_file holds a static local file. Without automated refresh, revoked certificates stay trusted until the file is manually updated. Automate refresh with a systemd timer.

Create /etc/systemd/system/pg-crl-refresh.service:

[Unit]
Description=Refresh PostgreSQL CRL from CDP
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=root
# Get the CDP URL from your CA certificate:
#   openssl x509 -in /etc/pki/tls/certs/postgresql.crt -noout -text | grep -A4 "CRL Distribution"
# Most enterprise CAs publish CRLs in DER format. Remove -inform DER if your CA publishes PEM.
ExecStart=/bin/bash -c '\
  curl -fsSL --max-time 30 -o /tmp/pg-crl.der "<<CDP_URL>>" && \
  openssl crl -inform DER -outform PEM \
    -in /tmp/pg-crl.der \
    -out /etc/pki/tls/certs/postgresql-crl.pem && \
  rm -f /tmp/pg-crl.der'
ExecStartPost=/bin/systemctl reload postgresql

[Install]
WantedBy=multi-user.target

Create /etc/systemd/system/pg-crl-refresh.timer:

[Unit]
Description=Daily PostgreSQL CRL refresh

[Timer]
OnCalendar=daily
RandomizedDelaySec=3600
Persistent=true

[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now pg-crl-refresh.timer
# Verify the timer is scheduled:
systemctl list-timers pg-crl-refresh.timer

Replace <<CDP_URL>> with the CRL Distribution Point URL from your CA certificate. Refresh frequency should be shorter than the CRL's Next Update field — daily is typical for enterprise PKIs.

FIPS mode note: On OL8 with FIPS enabled, certificates must use RSA 2048+ or ECDSA P-256+. MD5-signed certificates and SHA-1 key identifiers will cause connection failures. Verify with openssl x509 -in server.crt -text -noout | grep -E 'Signature Algorithm|Public-Key'.

Step 7 — Install the systemd hardening drop-in

install -d -o root -g root -m 0755 \
  /etc/systemd/system/postgresql.service.d

install -o root -g root -m 0644 \
  config/postgresql-hardening.conf \
  /etc/systemd/system/postgresql.service.d/hardening.conf

systemctl daemon-reload

Step 8 — Enable and start

systemctl enable --now postgresql
systemctl status postgresql

Step 9 — Run the SQL hardening script

# Copy to a location the postgres user can read (e.g. /tmp)
install -o postgres -g postgres -m 0400 config/setup.sql /tmp/setup.sql
sudo -u postgres psql -f /tmp/setup.sql
rm -f /tmp/setup.sql

Review the verification output at the end of the script and confirm:

  • pgaudit.log includes write,ddl,role,function
  • ssl is on, ssl_min_protocol_version is TLSv1.2
  • password_encryption is scram-sha-256
  • log_connections and log_disconnections are on
  • The public schema ACL does not include =C/postgres (CREATE for PUBLIC)

For each new application database, repeat the extension and privilege steps:

\c myappdb
CREATE EXTENSION IF NOT EXISTS pgaudit;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
ALTER DEFAULT PRIVILEGES REVOKE ALL ON TABLES    FROM PUBLIC;
ALTER DEFAULT PRIVILEGES REVOKE ALL ON SEQUENCES FROM PUBLIC;
ALTER DEFAULT PRIVILEGES REVOKE ALL ON FUNCTIONS FROM PUBLIC;

Creating application users and databases

Generate a strong password, then create the application user and database. Three equivalent password generators are included in this repository — use whichever matches your environment:

# Bash (Linux)
./scripts/new-secure-password.sh -p postgresql

# Python (any platform with Python 3.6+)
python3 scripts/new-secure-password.py -p postgresql

# PowerShell (Windows / cross-platform)
.\scripts\New-SecurePassword.ps1 -Profile PostgreSQL

All three use cryptographic randomness with rejection sampling, ensuring uniform character distribution and at least one character from each required class. Use --help / -h for full options (custom length, bulk generation, exclude ambiguous characters, etc.).

Create the user and database:

# Create a role with an interactive password prompt (paste the generated password)
sudo -u postgres createuser -P myappuser

# Create the database owned by the new role
sudo -u postgres createdb -E UNICODE -O myappuser myappdb

Then apply the per-database hardening from Step 9:

sudo -u postgres psql -d myappdb -c "
  CREATE EXTENSION IF NOT EXISTS pgaudit;
  CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
  REVOKE CREATE ON SCHEMA public FROM PUBLIC;
  ALTER DEFAULT PRIVILEGES REVOKE ALL ON TABLES    FROM PUBLIC;
  ALTER DEFAULT PRIVILEGES REVOKE ALL ON SEQUENCES FROM PUBLIC;
  ALTER DEFAULT PRIVILEGES REVOKE ALL ON FUNCTIONS FROM PUBLIC;
"

Add a corresponding hostssl line in pg_hba.conf for the new database and user, then systemctl reload postgresql.

Step 10 — Configure rsyslog forwarding to SIEM

install -o root -g root -m 0644 \
  config/rsyslog-postgresql.conf /etc/rsyslog.d/postgresql.conf

Edit /etc/rsyslog.d/postgresql.conf and replace siem.example.mil with your SIEM's hostname or IP.

systemctl restart rsyslog
# Verify delivery:
logger -p local0.info "PostgreSQL rsyslog test $(date)"

Step 11 — Configure firewalld

Remove all default services and allow only what this host requires:

# Identify the active zone (commonly 'public' on a freshly installed host)
firewall-cmd --get-active-zones

# Remove every pre-configured service from the zone
# Replace 'public' if your active zone differs
for svc in $(firewall-cmd --zone=public --list-services); do
  firewall-cmd --permanent --zone=public --remove-service="$svc"
done

# Allow SSH (adjust or remove if management is via jump host or out-of-band)
firewall-cmd --permanent --zone=public --add-service=ssh

# Allow PostgreSQL connections from application subnets only
# Replace with your actual subnet
firewall-cmd --permanent --zone=public \
  --add-rich-rule='rule family="ipv4" source address="<<APP_SUBNET>>/24" port port="5432" protocol="tcp" accept'

# Reload and verify
firewall-cmd --reload
firewall-cmd --zone=public --list-all

Smoke Tests

Run these immediately after first start and after every configuration change.

# 1. Confirm service is running and no errors in the journal
systemctl status postgresql
journalctl -u postgresql --no-pager | tail -20

# 2. Confirm data checksums are enabled
sudo -u postgres psql -c "SHOW data_checksums;"
# Expected: on

# 3. Confirm SSL is enabled in configuration
sudo -u postgres psql -c "SHOW ssl;"
# Expected: on
# Note: "SHOW ssl" confirms the server accepts SSL connections.
# Local socket connections (sudo -u postgres psql) will always show ssl=f
# in pg_stat_ssl — this is normal. SSL is verified over TCP in test 8 below.

# 4. Confirm pgaudit is loaded and logging
sudo -u postgres psql -c "SHOW shared_preload_libraries;"
# Expected: pgaudit,pg_stat_statements

sudo -u postgres psql -c "SELECT name, setting FROM pg_settings WHERE name LIKE 'pgaudit%';"

# 5. Confirm SCRAM-SHA-256 is enforced
sudo -u postgres psql -c "SHOW password_encryption;"
# Expected: scram-sha-256

# 6. Confirm idle-in-transaction timeout is active
sudo -u postgres psql -c "SHOW idle_in_transaction_session_timeout;"
# Expected: 5min (or your configured value)

# 7. Attempt a plain TCP connection (must be rejected)
psql -h <<SERVER_IP>> -U postgres postgres
# Expected: connection rejected (hostssl rules require SSL; plain host is rejected)

# 8. Verify SSL connection with client certificate
psql "host=<<SERVER_IP>> dbname=postgres user=<<USER>> \
  sslmode=verify-full sslcert=client.crt sslkey=client.key sslrootcert=root.crt" \
  -c "SELECT current_user, inet_client_addr(), ssl FROM pg_stat_ssl WHERE pid = pg_backend_pid();"

# 9. Confirm audit events appear in syslog
journalctl -u postgresql --no-pager | grep -i pgaudit | tail -10

Maintenance Schedule

Task Frequency
Rotate TLS certificates before expiry Per certificate validity period
Verify pg-crl-refresh.timer is active; confirm CRL Next Update has not expired: openssl crl -in /etc/pki/tls/certs/postgresql-crl.pem -noout -text | grep -E 'Last Update|Next Update' Per CRL validity period
Review pg_hba.conf against current network topology On every network change
Check Oracle EPEL for updated postgresql-server and pgaudit packages Monthly
Review pgaudit logs for anomalous patterns Weekly (or via SIEM alerting)
Verify log rotation and available space on /pg_log_volume and /pg_wal_volume Monthly
Run smoke tests after any configuration change After every change
Review and rotate application database passwords Per site password policy
Verify data checksum status after major upgrades After each upgrade

Key Lessons — What NOT to Do on OL8

These issues are documented here to prevent recurrence during deployment.

initdb — data checksums must be set at creation time

--data-checksums cannot be enabled after initdb. If the cluster is initialised without this flag, it must be rebuilt. Enforce it via PGSETUP_INITDB_OPTIONS as shown in Step 3. Verify immediately after initdb:

sudo -u postgres pg_controldata /var/lib/pgsql/data | grep checksum

hba_file — directory must exist before service start

postgresql.conf overrides hba_file to /etc/postgresql/pg_hba.conf. If that directory does not exist when PostgreSQL starts, it emits a misleading error about an invalid configuration parameter. Create the directory (Step 2) before the first service start.

pg_hba.conf — host vs hostssl

host allows both SSL and non-SSL TCP connections. Only hostssl enforces SSL. A pg_hba.conf that uses host with scram-sha-256 still permits unencrypted connections, violating V-233517. Every remote rule must use hostssl.

ssl_ciphers — TLS 1.3 suites are not controlled by this setting

ssl_ciphers in postgresql.conf controls only TLS 1.2 cipher suites. TLS 1.3 cipher selection is governed entirely by the system OpenSSL FIPS policy (/etc/crypto-policies/). On OL8 with FIPS enabled, OpenSSL automatically restricts to FIPS-approved TLS 1.3 suites regardless of ssl_ciphers.

pgaudit — must be in shared_preload_libraries

pgaudit is not a runtime-loadable extension. Loading it via LOAD 'pgaudit' in a session or in session_preload_libraries provides only session-level audit and does not satisfy STIG V-233535. It must be listed in shared_preload_libraries and the service restarted.

pgaudit — CREATE EXTENSION required per database

shared_preload_libraries loads the pgaudit module globally, but CREATE EXTENSION pgaudit must be run in every database where object-level auditing (pgaudit.log_relation) is needed. Running setup.sql covers the default (postgres) database; repeat in each application database.

csvlog — requires logging_collector

Setting log_destination = 'csvlog' without logging_collector = on silently produces no log files. Both settings are required together. syslog in log_destination works independently of logging_collector.

hardening.conf — systemd drop-in rules for OL8 (systemd 239)

Failed at step NAMESPACE: No such file or directory is thrown when any path in ReadWritePaths= or ReadOnlyPaths= does not exist when the mount namespace is assembled. The error never names the offending path.

If you add a ReadWritePaths= directive to the drop-in, list all three PostgreSQL mount points or the service will refuse to start:

ReadWritePaths=/var/lib/pgsql /pg_wal_volume /pg_log_volume
  1. Never set ProtectSystem=strict on OL8. Under systemd 239 with SELinux enforcing, strict prevents the kernel from executing binaries under /usr/bin/. The stock unit's ProtectSystem=full is the safe maximum.

  2. Never set MemoryDenyWriteExecute=true with JIT enabled. PostgreSQL JIT allocates mmap(PROT_EXEC) regions. If JIT is confirmed disabled (jit=off in postgresql.conf), this can be set to true. Default in the supplied drop-in: false.

  3. List-type keys accumulate across drop-ins. To replace CapabilityBoundingSet= or SystemCallFilter=, issue an empty assignment first to clear the inherited value, then set the desired value. The drop-in follows this pattern.

  4. ProtectHostname=, ProtectClock=, RestrictSUIDSGID=, ProtectKernelLogs= require systemd 240+/245+/253+. Using them on OL8 causes silent parse failures. They are not included in the supplied drop-in.

  5. Many hardening directives implicitly force NoNewPrivileges=true, which breaks SELinux domain transitions on OL8. SystemCallFilter=, RestrictNamespaces=, LockPersonality=, RestrictRealtime=, ProtectKernelTunables=, ProtectKernelModules=, MemoryDenyWriteExecute=, and PrivateDevices= all force NNP at kernel level — setting NoNewPrivileges=false has no effect. On systemd 239, this blocks the init_t → postgresql_t transition (AVC: denied { nnp_transition }), causing postgres to run as init_t and be denied access to all postgresql_db_t files. The supplied drop-in excludes all NNP-triggering directives. SELinux mandatory access control provides equivalent containment for most of what those directives protect against.

SELinux — custom mount points need explicit file contexts

New LVM mount points (/pg_wal_volume, /pg_log_volume, or a non-default PGDATA mount) inherit the default_t SELinux type. PostgreSQL running as postgresql_t is denied access to default_t files. Run semanage fcontext and restorecon as shown in the storage layout section before the first service start. If the service fails with Permission denied on a file that has correct Unix ownership, check ausearch -m AVC -ts recent — the denial will name the source and target SELinux contexts.

FIPS mode — certificate and cipher requirements

On OL8 with FIPS enabled (fips-mode-setup --enable):

  • Server certificates must use RSA 2048+ or ECDSA P-256+. RSA 1024 and MD5-signed certificates are rejected at connection time.
  • SHA-1 is not permitted for certificate signatures.
  • gpg may emit out of core handler ignored in FIPS mode — this is a harmless warning.
  • The scram-sha-256 authentication method is FIPS-compliant. md5 password hashing is not; never use it.

ssl_crl_file — no native OCSP or CDP support

ssl_crl_file is not deprecated — STIG V-233520 requires it. The constraint is that PostgreSQL has no built-in OCSP checking and does not auto-fetch CRLs from the CDP URL embedded in certificates. OpenSSL's server-side TLS verification does not follow CDP URLs either. The file is a static snapshot: a revoked certificate stays trusted until the file is manually replaced.

The pg-crl-refresh timer installed in Step 6 mitigates this. If the timer is disabled or the CDP URL changes, new connections will silently accept revoked certificates. Monitor:

# Timer status
systemctl status pg-crl-refresh.timer

# CRL expiry — Next Update must be in the future
openssl crl -in /etc/pki/tls/certs/postgresql-crl.pem -noout -text \
  | grep -E 'Last Update|Next Update'

License

MIT — free to use, modify, and distribute.


Author

Przemysław Pradela

Built through real production deployment and iterative troubleshooting on Oracle Linux 8.

GitHub LinkedIn Website

Contributions and issue reports welcome.

About

DISA STIG hardened PostgreSQL 16 deployment for Oracle Linux 8. Covers TLS 1.2+, SCRAM-SHA-256, pgaudit, systemd drop-in, LVM storage layout, SELinux, CRL refresh, and rsyslog→SIEM forwarding. Includes air-gapped variant and password generators (Bash/Python/PowerShell).

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors