// DevOps

A new server isn't a blank slate. It's an unlocked door.

Published on 2026-05-28

A few days ago I investigated a compromise of a production server. Not theoretical — a real one, with a Monero miner, a C2 agent and a backdoor. The attack was fully automated. No one was specifically hunting this server — a bot continuously scans the entire Internet, tries default passwords on open databases and does its job.

From the moment the server appeared on the network to the first attack took less than six hours. To a successful breach — less than a day.

Three conditions that made it possible: PostgreSQL was exposed, the password was postgres, the firewall was not enabled. That’s it. Nothing else was required.


Why this happens even with experienced people

There is a certain psychology with a new server. You just brought it up, everything works, the app responds — and you move on to the next task. Security seems like something you can set up later. Later — when there is time. Later — before production. Later — after release.

Later never comes. Or it comes, but already in the form of top showing /tmp/mysql at 400% CPU.

A new server on the Internet is not a blank slate. It’s an unlocked door in a house located on a busy street. Scanners find it within hours, not days.


What actually happened

The attacker found an open port 5432 on the Internet. They ran a brute-force. The password postgres — this is not even brute-force, it’s the first line in any dictionary. They gained access to PostgreSQL with superuser privileges.

Next comes a legitimate PostgreSQL feature — COPY FROM PROGRAM. It allows a superuser to execute an arbitrary shell command directly from an SQL query. The attacker passed a base64-encoded bash script through it, which downloaded and ran a dropper.

But the cleverest part was the persistence mechanic. Event triggers were added to the database — triggers that fire on every DDL operation (CREATE TABLE, ALTER, DROP…). On each such event they silently recreated a superuser role with a password known to the attacker. So even if an admin noticed and removed the malicious role — the next migration or CREATE INDEX would automatically restore it.

Elegant and truly nasty.


Three rules that stop 99% of such attacks

I’m intentionally not writing “ten rules” or a “complete checklist”. Not because the rest isn’t important — but because these three rules close off the majority of automated attacks that actually occur in the wild.

Databases must not be accessible from the Internet. PostgreSQL, MySQL, Redis, MongoDB — none of them are intended for direct external access. In docker-compose.yml the line "5432:5432" for a database is almost always a mistake. The application will reach the database inside the Docker network by hostname. Externally this port is only useful to attackers.

If you need access for development — bind only to localhost:

ports:
  - "127.0.0.1:5432:5432"

For production — remove ports entirely.

Default passwords are not passwords. postgres, password, root, admin — these are the top entries in any brute-force dictionary. A scanner will try them in seconds, not hours. Generate a password at deploy time:

POSTGRES_PASSWORD=$(openssl rand -base64 32)

Or use secret management: HashiCorp Vault, AWS Secrets Manager, Doppler. The main thing — never commit passwords to the repository and don’t leave default values in .env.

The firewall must be enabled from the first minute. Not before release. Not after setup. From the first minute after creating the server. The right model — deny everything, allow only what’s necessary:

ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw enable

After that open specific ports as needed. Not the other way around.


About monitoring you should add

The three rules above are about prevention. But if you also want to see what’s happening, add minimal logging to PostgreSQL. By default it doesn’t log successful connections or DDL statements. That means you have no visibility into what’s happening with the database.

In postgresql.conf:

log_connections = on
log_disconnections = on
log_statement = 'ddl'

And a simple cron script to check for anomalies:

#!/bin/bash
SUPERUSERS=$(psql -U postgres -t -c "SELECT count(*) FROM pg_user WHERE usesuper = true AND usename != 'postgres';")
if [ "$SUPERUSERS" -gt 0 ]; then
    echo "ALERT: unexpected superusers detected" | mail -s "Security Alert" admin@example.com
fi

This is already enough to notice something suspicious earlier than top will.


Why this matters more than it seems

According to Shodan, over 800,000 PostgreSQL instances are exposed on the Internet. A significant portion use default or weak passwords. No one is hacking them manually — these are fully automated scanners running around the clock. They don’t choose victims by business size or value of data. They just go down the list of open ports.

Your server is no exception. It’s already on that list. The question is only whether there is a lock on the door.

Protecting against this doesn’t require complex technologies, expensive tools, or deep security expertise. It requires three things that can be done in ten minutes on the first login to the server. Just don’t postpone them.

// Reviews

Related reviews

I came with an expensive request to configure a VPS server, but during the consultation Mikhail suggested a much simpler, more affordable solution. In the end I saved time and money. Mikhail — a true expert who works for the client's result, not for the fee. I recommend him!

I came with an expensive request to configure a VPS server, but during the consultation Mikhail suggested a much simpler and more cost-effective solution. In the end I saved budget and time. Mikhail — a true expert who …

kfhzasorin

VPS setup, server setup

2026-05-12 · ★ 5/5

// Contact

Need help?

Get in touch with me and I'll help solve the problem

Send request
Write and get a quick reply