Production Incident Review

Incident Review: PostgreSQL Locked Out of Its Own Config File

A PostgreSQL instance stopped at start-up with "could not load pg_hba.conf - Permission denied". The cause was one flag in a start-up script, cp -p, faithfully copying the wrong owner from a template on every start. Why fixing only the live file would have brought the outage back.

Module: 23 of 25 Level: Production Incident Review Time: 8 min read Published:
All modules in this track (25)

About this review. This is a real incident from a production platform running on RHEL 8 servers in AWS, written up from the engineering team's notes. Every server name, path, account, and product name is replaced with a generic one; the log lines, commands, and fix are otherwise as recorded.

EnvironmentPre-production, RHEL 8, PostgreSQL 17
When2025
Detected byThe database service failing to start
ImpactOne PostgreSQL instance could not start
Root causepg_hba.conf was owned by the application account instead of postgres, because a start-up script copied it from a template with cp -p
FixCorrected ownership and permissions on both the template and the live file, then restarted
Context

A config file copied at every start

The server ran a dedicated PostgreSQL instance (pgsql-app.service) on port 5434. Before PostgreSQL started, a start-up script refreshed its client authentication file, pg_hba.conf, by copying a template into the data directory with cp -p.

pg_hba.conf decides who may connect to the database, and how. PostgreSQL runs as the postgres user and must be able to read it; PostgreSQL also expects its data directory and files to be owned by postgres.

Symptom

The database starts, then shuts down

The service failed. Its log showed PostgreSQL getting as far as opening its listening sockets, then stopping:

LOG:  listening on IPv4 address "0.0.0.0", port 5434
LOG:  listening on IPv6 address "::", port 5434
LOG:  could not open file "/var/lib/pgsql/17/app/pg_hba.conf": Permission denied
FATAL:  could not load /var/lib/pgsql/17/app/pg_hba.conf
LOG:  database system is shut down
Scroll sideways to see the full diagram →
Investigation

Check the owner, not just the permissions

ls -ltr /opt/app/log/pg_log/                     # find the latest PostgreSQL log
tail -n 80 /opt/app/log/pg_log/postgresql-Fri.log
ls -ld /var/lib/pgsql/17/app                     # the data directory
ls -l  /var/lib/pgsql/17/app/pg_hba.conf         # the file itself
Scroll sideways to see the full diagram →

The file was owned by the application's account and group (appsvc:appgroup), not by postgres:postgres. PostgreSQL, running as postgres, couldn't read it.

Root cause

cp -p copied the wrong owner, every time

cp -p means "preserve": the copy keeps the source file's owner, group, mode, and timestamps, when run as root. That's useful for backups, and wrong here. The template the script copied from was itself owned by the application account, so every start-up wrote a pg_hba.conf with the wrong owner into the data directory.

This is why the fix had to cover two files. Correcting only the live pg_hba.conf would have worked until the next restart, when the start-up script would copy the template's wrong owner back in.

Fix

Correct the source, then the copy

# 1. Fix the template at the source
ls -l /opt/app/templates/pg_hba.conf
chown postgres:postgres /opt/app/templates/pg_hba.conf
chmod 600 /opt/app/templates/pg_hba.conf

# 2. Fix the live file
chown postgres:postgres /var/lib/pgsql/17/app/pg_hba.conf
chmod 600 /var/lib/pgsql/17/app/pg_hba.conf

# 3. Restart and verify
systemctl restart pgsql-app
systemctl status pgsql-app
ss -ltnp | grep -E '5432|5434'
psql -h 127.0.0.1 -p 5434 -U postgres -d appdb -c "select now();"
Scroll sideways to see the full diagram →

The database came up and was reachable again on port 5434 through the connection pooler. The notes record one leftover: the instance's post-start checks still logged "trust" authentication failed for their own internal psql calls. That is a separate authentication issue, of the kind Incident Review 25 explains, and it didn't stop the database from serving.

Prevention

Lessons

  • Copy configuration with explicit ownership. install -o postgres -g postgres -m 600 template.conf /path/pg_hba.conf copies and sets the owner and mode in one step, whatever the template's own ownership is.
  • Fix the source of a generated file, not just the output. If a file is rewritten at every start, the fix belongs in the template or the script that writes it.
  • Use cp -p deliberately. It's right for backups, where you want an exact copy, and wrong when the destination needs its own owner.
  • Read the first error, not the last. The final line said only that the database shut down; the line above it named the file and the reason.
Practise it

Where this track teaches the skills

Check yourself

Pop quiz

Five questions: three on the ideas in this review, and two scenarios where you apply them. The order changes every time you take it, and 4 out of 5 passes.

5 questions. Decide on your answer to each, then check it.

  1. Concept

    What does cp -p preserve when run as root?

    1. Only the file's contents
    2. The source file's owner, group, mode, and timestamps
    3. Only the timestamps
    4. Nothing; -p means "prompt"
    Show answer

    The source file's owner, group, mode, and timestamps

    -p keeps the original's metadata. Copying a template owned by the wrong account therefore produces a copy owned by the wrong account.

  2. Concept

    Why did the fix also change the template's owner, not just the live pg_hba.conf?

    1. The template was corrupted
    2. The start-up script copies the template at every start, so a wrong template would bring the fault back on the next restart
    3. PostgreSQL reads the template directly
    4. It's required by RHEL 8
    Show answer

    The start-up script copies the template at every start, so a wrong template would bring the fault back on the next restart

    When a file is regenerated, the source decides the result. Fixing only the output lasts until the next regeneration.

  3. Concept

    Which command copies a file and sets its owner, group, and mode in one step?

    1. cp -p
    2. mv
    3. install -o postgres -g postgres -m 600 source dest
    4. ln -s
    Show answer

    install -o postgres -g postgres -m 600 source dest

    install sets ownership and mode explicitly on the destination, regardless of the source file's own metadata.

  4. Scenario

    PostgreSQL logs "could not open file pg_hba.conf: Permission denied", and the file's mode is 600. What do you check next?

    1. The disk space
    2. The file's owner: with mode 600, only the owner can read it, and that must be postgres
    3. The listening port
    4. The firewall
    Show answer

    The file's owner: with mode 600, only the owner can read it, and that must be postgres

    600 gives read and write to the owner only. If the owner is any account other than postgres, PostgreSQL can't read the file.

  5. Scenario

    A service's log ends with "database system is shut down". Where is the real cause most likely to be?

    1. In the very last line
    2. A few lines above, in the first FATAL or ERROR message
    3. In the kernel log only
    4. In the client's error message
    Show answer

    A few lines above, in the first FATAL or ERROR message

    The last line usually reports the consequence. The first error before it, here the pg_hba.conf permission failure, names the cause.

Questions about this learning path?

I'm happy to go into more detail on any of this: how to practise troubleshooting safely, which fundamentals matter most early on, or how a break-fix lab is put together.

anupojuprudhvi@gmail.com