Server security guide · Romania

How do I secure PostgreSQL or MySQL in production?

A database holds customer records, invoices and credentials, so it is where most attacks end up. Most leaks are not clever exploits. They are a port left open to the internet, an application connecting as superuser, and a password nobody has changed in years.

Updated:

Short answer

  1. Make the database listen only on localhost or the private network, never on a public address.
  2. Give the application its own role with rights on its own schema only, and keep the superuser account for local administration.
  3. In PostgreSQL enforce scram-sha-256 in pg_hba.conf and TLS with hostssl; in MySQL set bind-address, remove root@% and run mysql_secure_installation.
  4. Keep passwords in a secrets manager, run a daily logical backup plus point-in-time recovery, and log failed connections.
PostgreSQL authentication
scram-sha-256, never md5 or trust
Listen address
127.0.0.1 or the private-network IP
Application account
own role, no SUPERUSER, no DROP
Backups
daily pg_dump / mysqldump plus PITR from WAL or binlog

Network and authentication: the first two layers

A database server does not need to be reachable from the internet, even with a strong password. Bind it to a private address and do remote administration through an SSH tunnel or a VPN. The second layer is authentication: pg_hba.conf decides who may connect, from where, to which database and by what method. Remove trust and md5 from every network line. password_encryption has defaulted to scram-sha-256 since PostgreSQL 14, and hostssl rejects unencrypted connections. MySQL's equivalents are bind-address, accounts tied to an exact host instead of %, and require_secure_transport = ON.

Least privilege: the application is not the administrator

The most common mistake is an application that connects as postgres or root so that migrations just work. When that application has a SQL injection, the attacker inherits everything the account can do: every database, extensions, reading files from disk. Create one role for the application with SELECT, INSERT, UPDATE and DELETE on its own tables, and a separate role for migrations that only the deployment job uses. Reporting roles get SELECT and nothing else. Each role has its own password, rotated when someone leaves and kept in your secrets manager, not in the code.

  • One role per application and per environment; staging never uses the production password
  • ALTER DEFAULT PRIVILEGES, so new tables inherit the right rights
  • log_connections on, with the logs shipped to a central system

What Romanian law and regulators expect

A database is almost always where personal data lives, so GDPR Article 32 applies directly: encryption, access control and the ability to restore. After a leak, ANSPDCP must be notified within 72 hours, and its first question is who had access and how. For entities under OUG 155/2024, access management and backup are explicit minimum measures. Invoices issued through e-Factura have legal retention periods, which makes a tested database backup an obligation rather than an option.

Equivalent security settings in PostgreSQL and MySQL / MariaDB
ControlPostgreSQLMySQL / MariaDB
Listen addresslisten_addresses in postgresql.confbind-address in my.cnf
Who connects, from wherepg_hba.conf, one line per role and networkuser@host, never %
Authenticationscram-sha-256caching_sha2_password (default since 8.0)
Encryption in transitssl = on, hostssl in pg_hba.confrequire_secure_transport = ON
Logical backup plus point in timepg_dump + WAL archiving (PITR)mysqldump / mariabackup + binlog

Commands and configuration

postgresql.conf and pg_hba.conf: private network only, scram-sha-256 only, TLS only
# postgresql.conf (fragment)
listen_addresses = '10.0.0.10'
password_encryption = scram-sha-256
ssl = on
log_connections = on

# pg_hba.conf
# TYPE     DATABASE  USER      ADDRESS        METHOD
local      all       postgres                 peer
hostssl    app_db    app_user  10.0.0.5/32    scram-sha-256
host       all       all       0.0.0.0/0      reject
PostgreSQL: application role with least privilege
CREATE ROLE app_user LOGIN PASSWORD 'password-from-the-secrets-manager';
GRANT CONNECT ON DATABASE app_db TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
MySQL: bind to the private network, require TLS, no root@%
# /etc/mysql/mysql.conf.d/mysqld.cnf (fragment)
[mysqld]
bind-address = 10.0.0.10
require_secure_transport = ON
local_infile = 0

-- in the mysql client
DROP USER IF EXISTS 'root'@'%';
CREATE USER 'app'@'10.0.0.5' IDENTIFIED BY 'password-from-the-secrets-manager';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app'@'10.0.0.5';
FLUSH PRIVILEGES;

Practical examples

Logistics software provider in Timisoara

The application ran as the postgres superuser to keep migrations simple. A vulnerable search form let an attacker read tables across every customer schema. Afterwards each tenant got its own role, the application lost superuser rights, and pg_hba.conf required scram-sha-256 over hostssl.

E-commerce shop in Cluj-Napoca with MySQL open to a desktop client

A developer had created root@% and opened port 3306 to use a desktop client from home. Automated scanners found it within hours and began guessing passwords. The account was dropped, bind-address moved to the private address, and remote access went through an SSH tunnel.

Common mistakes

  • Leaving listen_addresses = '*' or bind-address = 0.0.0.0 and relying on the password alone.
  • Connecting the application as postgres or root because migrations 'worked that way'.
  • Having a backup that was never restored and no point-in-time recovery for an accidental DELETE.
  • Keeping the database password in the repository, identical on staging and production.

How 4hosting helps

  • PostgreSQL & ClickHouse covers replication, PITR backups and tuning, and MongoDB & Redis includes hardening of those engines.
  • A review of pg_hba.conf and database roles is the kind of task you can book with your included hours: 1–8 h a month on VPS, 5–10 h on dedicated servers (depending on plan), 10 h on GPU servers.
  • Request them from the DevOps Services section of the client panel; unused hours are not carried over, and extra hours are accepted and billed separately.
  • Firewall & Network Isolation keeps the database server on a private network, apart from the application server.

Related questions

  • Do I need TLS between the application and the database on the same server?

    Over a Unix socket or 127.0.0.1 the traffic never leaves the machine, so TLS is optional. Once the database sits on another server, even in the same private network, TLS becomes mandatory: the provider's network is shared, and a single misconfiguration can expose it.

  • What is PITR and why is pg_dump not enough?

    pg_dump gives you the state at the hour it ran; everything after is lost. Point-in-time recovery uses the WAL archive, or the binlog in MySQL, to rebuild the database to the minute before a mistaken DELETE. You use the two together.

  • Should I encrypt the disk or the database?

    Disk encryption protects you from physical theft or a retired drive, not from an attacker connected to the running database. For sensitive columns such as national ID numbers or payment data, application-level encryption with keys kept outside the database is the layer that counts.

Request a security review

A production server starts getting attacked within minutes of receiving a public IP address, however small the project is. This guide ranks the controls by impact, from SSH access to incident response, with the Romanian rules that apply along the way. Every topic has its own page with a short answer, examples and ready-to-use configuration.