Skip to content
CloudOps
Newsletter
All guides
AI for DevOps Security & Hardening By James Joyner IV · · 11 min read

Hardening Redis and Postgres Against the Internet (and Your Own Network)

Lock down Redis and PostgreSQL: binding, requirepass, ACLs, TLS, pg_hba least privilege, scram-sha-256, and finding exposed instances before attackers do.

  • #security
  • #hardening
  • #databases
  • #redis
  • #postgres

The first time I watched a Redis instance get wiped in real time, it wasn’t a sophisticated attack. There was no zero-day, no clever pivot, no insider. Someone had spun up a cache on a cloud VM, bound it to 0.0.0.0, left protected-mode off, and forgotten it had a public IP. Within hours an automated scanner found the open port, ran FLUSHALL, and dropped a ransom key in its place. The total skill required to pull this off was approximately zero.

Data stores are the soft underbelly of most infrastructure. We pour effort into hardening the app tier, then hang a stateful service off the side of the network with default config and assume the firewall has our back. It usually doesn’t. Below is the checklist I actually run on Redis and Postgres, with the real config and commands, plus where an AI assistant genuinely helps and where it absolutely must not be trusted.

Never bind to 0.0.0.0 (the cheapest win you’ll ever get)

Binding determines which network interfaces a service will accept connections on. 0.0.0.0 means “every interface, including the public one.” For a database, that’s almost never what you want.

For Redis, edit redis.conf:

# Bind only to loopback and the private VPC interface.
# Multiple addresses are space-separated, NOT comma-separated.
bind 127.0.0.1 10.0.12.4
protected-mode yes
port 6379

protected-mode yes is Redis’s seatbelt: if no bind is set and no password is configured, Redis refuses connections from anything but loopback. It exists precisely because of the disaster I opened with. Never turn it off to “make things work” — if you’re tempted to, you have a binding or auth problem to fix instead.

For Postgres, the equivalent lives in postgresql.conf:

listen_addresses = 'localhost,10.0.12.4'
port = 5432

The default listen_addresses = 'localhost' is safe. The dangerous move is someone changing it to '*' to unblock a remote client, then never reverting it. If you need remote access, name the private interface explicitly. There’s almost no legitimate reason for a database to listen on a public IP.

Lock the front door: requirepass, ACLs, and scram-sha-256

Binding limits where connections come from. Authentication limits who gets in once they reach the port. You want both.

Modern Redis should never run on the deprecated single-password requirepass alone — use the ACL system. Define users with explicit command and key permissions in redis.conf or via ACL SETUSER:

# Disable the default all-powerful user entirely.
user default off

# App user: read/write a key namespace, no admin commands.
user appservice on >REPLACE_WITH_LONG_RANDOM_SECRET ~app:* +@read +@write -@dangerous

# Read-only metrics user.
user metrics on >REPLACE_WITH_ANOTHER_SECRET ~* +@read +ping -@dangerous

The -@dangerous category strips out the commands that ruin your day. Persist ACLs to a file so they survive restarts:

aclfile /etc/redis/users.acl

On the Postgres side, the equivalent shift is moving every connection to scram-sha-256. In postgresql.conf:

password_encryption = scram-sha-256

Then re-set passwords so they’re stored with the new hash (old md5 hashes don’t auto-upgrade), and enforce the method in pg_hba.conf (more on that file below). This is one of those changes worth a second pair of eyes — a great use of a tool like Claude or Cursor to diff your proposed pg_hba.conf against intent, before you reload.

Rename or disable the commands that wreck you

Even behind auth, defense in depth says: take the loaded gun off the table. Redis lets you rename or disable commands so a compromised app credential can’t issue them:

# Disable outright by renaming to an empty string.
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""

# Or rename to an unguessable string if ops still needs it.
rename-command DEBUG "DEBUG_8f3a9c21b7"

CONFIG is the sneaky one: with it, an attacker can rewrite dir and dbfilename to drop a malicious file anywhere the Redis process can write — a classic path to remote code execution. If your app doesn’t legitimately call CONFIG at runtime (most don’t), kill it.

Pro Tip: Renaming CONFIG breaks tools that introspect Redis at runtime, including some monitoring agents and Sentinel. Inventory what touches your instance first, give those tools a dedicated ACL user, and rename rather than fully disable so you keep a privileged escape hatch.

pg_hba.conf: least privilege at the connection layer

pg_hba.conf is the most powerful and most misread file in a Postgres deployment. It’s evaluated top to bottom, first match wins, so order matters enormously. A permissive line near the top silently defeats stricter lines below it.

A tight ruleset looks like this:

# TYPE  DATABASE      USER         ADDRESS         METHOD
local   all           postgres                     peer
hostssl appdb         appservice   10.0.12.0/24    scram-sha-256
hostssl analytics     readonly     10.0.20.5/32    scram-sha-256
# Explicitly reject everything else, loudly.
host    all           all          0.0.0.0/0       reject

Three things to internalize. First, prefer hostssl over host so unencrypted connections are rejected outright rather than silently allowed. Second, scope ADDRESS to the narrowest CIDR that works — /32 for a single host beats a /24 subnet. Third, the catch-all reject at the bottom turns “forgot a rule” from an accidental allow into a clean denial. The line you must never ship is host all all 0.0.0.0/0 trust — that’s authentication-free access from anywhere.

After editing, reload without a full restart:

sudo -u postgres psql -c "SELECT pg_reload_conf();"

Roles, PUBLIC, and the privileges nobody granted on purpose

Postgres ships with a quietly dangerous default: the built-in PUBLIC pseudo-role automatically gets CONNECT on new databases and CREATE and USAGE on the public schema. That means any role that can authenticate can often create objects and read more than you intended.

Revoke the defaults and grant back deliberately:

-- Stop every role from connecting to this DB by default.
REVOKE CONNECT ON DATABASE appdb FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM PUBLIC;

-- A read-only role, scoped precisely.
CREATE ROLE readonly NOLOGIN;
GRANT CONNECT ON DATABASE analytics TO readonly;
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;

-- Make it apply to future tables too.
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;

-- The app's login role inherits the read-only set plus writes.
CREATE ROLE appservice LOGIN PASSWORD 'set-this-out-of-band';
GRANT readonly TO appservice;
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO appservice;

Notice the login role gets a real password, but I never paste production secrets into a chat window, a commit, or an AI prompt. When I ask an assistant to review role design, I hand it the structure with placeholders, never the live credential. Auditing privilege sprawl is exactly the kind of tedious review where a second reviewer shines — see the security-hardening write-ups and the reusable prompts for framing those reviews.

TLS in transit, connection limits, and audit logging

An attacker on the same network segment can sniff plaintext database traffic trivially. Encrypt it.

Redis (6+) supports native TLS:

tls-port 6379
port 0
tls-cert-file /etc/redis/tls/redis.crt
tls-key-file /etc/redis/tls/redis.key
tls-ca-cert-file /etc/redis/tls/ca.crt
tls-auth-clients yes

Setting port 0 disables the plaintext listener entirely. tls-auth-clients yes requires clients to present their own certificate — mutual TLS, which beats a shared password for service-to-service auth.

Postgres enables TLS in postgresql.conf:

ssl = on
ssl_cert_file = '/etc/postgresql/tls/server.crt'
ssl_key_file = '/etc/postgresql/tls/server.key'
ssl_min_protocol_version = 'TLSv1.2'

Cap concurrent connections so a runaway client or a connection-exhaustion attempt can’t starve the service:

max_connections = 200
superuser_reserved_connections = 3

And turn on audit logging so you can answer “who connected and what did they do” after the fact:

log_connections = on
log_disconnections = on
log_statement = 'ddl'
log_line_prefix = '%m [%p] %u@%d from %h '

Feeding these logs into your alerting pipeline closes the loop — the monitoring alerts dashboard is where I wire mine, so a sudden spike in failed auth from a new host actually pages someone.

Find your own exposed instances before a scanner does

Hardening config is worthless if you don’t verify it from the network’s point of view. Run an internal scan from outside the host — the same thing an attacker’s automated sweep would do, just pointed at your own ranges:

# Are the database ports reachable where they shouldn't be?
nmap -p 6379,6380,5432 -sV --open 10.0.12.0/24

# Confirm Redis refuses unauthenticated commands.
nmap -p 6379 --script redis-info 10.0.12.4

# Confirm Postgres won't talk without TLS/auth.
nmap -p 5432 --script pg-version 10.0.12.4

If redis-info returns server stats without a password, you have an unauthenticated instance — fix it before you do anything else. Also scan from a host outside your VPC against your public egress IPs; the answer there should be a flat “filtered” or “closed” on every database port. The goal is for your own scan to be boring.

Pro Tip: Schedule the nmap sweep as a recurring job and diff results against a known-good baseline. A new open database port appearing on the network is one of the highest-signal, lowest-noise security alerts you can build, and it catches the “someone spun up a quick test instance” mistakes that cause most breaches.

Where AI helps, and where it doesn’t

I lean on AI assistants heavily for this work, but in a specific lane: treat the model as a fast, well-read junior engineer doing review, not a hand on the production lever. It’s excellent at catching an over-broad CIDR in pg_hba.conf, spotting that protected-mode got disabled, or explaining what a rename-command change will break. Pair that with an automated code review pass on your config repo and you get a solid first filter.

The hard rules: a human verifies every generated config before it’s applied, all of this stays strictly defensive, and you never hand the model a real password, certificate key, or connection string — use placeholders and substitute secrets out of band. The model proposes; you, the firewall, and your audit logs dispose.

None of this is exotic. Bind narrowly, authenticate strongly, strip dangerous commands, encrypt in transit, scope privileges, log access, and scan yourself like an attacker would. Do those seven things and the lazy automated sweep that owns most exposed data stores simply slides off. The boring instance is the safe one.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.