Server
Database security: from the port to field encryption
Keep the database off the internet; least-privilege users, TLS, backups, encryption of sensitive fields and auditing. A practical guide for PostgreSQL and MySQL.
· 11 min read

The database is the most valuable part of a system. Users, payments, documents, personal data — it is all here. Even so, databases are often set up on the principle of "the app works, so everything is fine": default settings, a single root or postgres user, listening on every network interface.
In this article we look at the main layers of database protection: network, users and privileges, encrypted connections, encryption of sensitive fields, backups and auditing. Examples are for PostgreSQL and MySQL/MariaDB, with the application side shown in Laravel and Node.
What the problem is
The most common database mistakes:
- The port is open to the internet. MySQL (3306), PostgreSQL (5432), Redis (6379) or MongoDB (27017) are visible from outside. This is a direct path for password guessing and for vulnerabilities in the database software itself.
- The application connects as a superuser. If the app uses
rootorpostgres, a single SQL injection may give control over the whole server: other databases, file reads, command execution. - Weak or reused passwords. The same password across several environments, a
.envfile left in git history. - Unencrypted connections. If the application and the database run on different servers, queries and results travel across the network in plain text.
- Sensitive fields stored in the clear. Passport data, personal identification numbers, tokens stored as plain text. Any copy of the database is a full leak.
- A database web interface is exposed. Tools like phpMyAdmin or Adminer sit at a public address.
Why it matters for the expertise
During the expertise the server is port-scanned from outside, so an open database port is found immediately. In the expertise of our own product, open ports were also a separate finding (there it was FTP and panel ports), see open ports and server hardening. In addition:
- paths like
/phpmyadmin,/adminer.php,/pmaare checked by common names; - if an SQL injection is found, its impact depends on the database user's privileges — least privilege limits the damage;
- A02 "Cryptographic Failures" in the OWASP Top 10 (2021) covers missing encryption of sensitive data at rest and in transit, and A05 "Security Misconfiguration" covers default settings.
Principle: only the application can see the database, the application can do only what it needs, and sensitive data stays unreadable even if it leaks from the database.
Check it yourself
Where the database listens
On the server:
sudo ss -tlnp | grep -E "3306|5432|6379|27017"
127.0.0.1 or a Unix socket is good. 0.0.0.0 or * means that, without a firewall, the database is reachable from outside.
From outside (from another machine, only against your own server):
nmap -Pn -p 3306,5432,6379,27017 your-server-ip
Users and privileges
PostgreSQL:
SELECT rolname, rolsuper, rolcreaterole, rolcreatedb FROM pg_roles WHERE rolcanlogin;
MySQL:
SELECT user, host FROM mysql.user;
SHOW GRANTS FOR 'app'@'localhost';
The application user must not have SUPER, FILE, GRANT OPTION or ALL PRIVILEGES ON *.*. A % in the host column means the user can connect from any address.
Web interfaces
for p in phpmyadmin pma adminer.php adminer phpMyAdmin; do
printf "%s " "$p"; curl -s -o /dev/null -w "%{http_code}\n" "https://example.uz/$p/"
done
The fix
1. Bind the database to localhost
PostgreSQL postgresql.conf:
listen_addresses = 'localhost'
MySQL/MariaDB mysqld.cnf:
[mysqld]
bind-address = 127.0.0.1
Redis redis.conf: bind 127.0.0.1 ::1 and protected-mode yes, plus a password or ACL.
If the database runs on a separate server, it should listen only on the internal network and the firewall should allow only the application server's IP:
sudo ufw allow from 10.0.0.5 to any port 5432 proto tcp
In PostgreSQL, pg_hba.conf follows the same principle:
# TYPE DATABASE USER ADDRESS METHOD
local appdb app scram-sha-256
hostssl appdb app 10.0.0.5/32 scram-sha-256
Do not use the trust method, and choose scram-sha-256 for password hashing (password_encryption = scram-sha-256).
2. Least-privilege users
Separate at least three roles: migrations (change the schema), the application (works with data only) and backups or reporting (read only).
PostgreSQL:
CREATE ROLE app LOGIN PASSWORD '...';
REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA public TO app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app;
ALTER DEFAULT PRIVILEGES FOR ROLE migrator IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app;
MySQL:
CREATE USER 'app'@'localhost' IDENTIFIED BY '...';
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app'@'localhost';
CREATE USER 'report'@'localhost' IDENTIFIED BY '...';
GRANT SELECT ON appdb.* TO 'report'@'localhost';
Generate passwords with openssl rand -base64 24 and keep them only in .env or a secrets manager. Use a separate password for each environment (test, production). More: managing secrets and keys.
3. Encrypted connections (TLS)
If the database and the application share a server and connect over a Unix socket or 127.0.0.1, TLS is not required. If they are on different servers, it is mandatory.
On the PostgreSQL server, configure ssl = on and a certificate, and use hostssl in pg_hba.conf. Laravel config/database.php:
'pgsql' => [
// ...
'sslmode' => env('DB_SSLMODE', 'verify-full'),
],
In MySQL you can require a user to connect only over TLS:
ALTER USER 'app'@'10.0.0.5' REQUIRE SSL;
A Node.js (pg) example:
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { ca: fs.readFileSync('/etc/ssl/db-ca.pem'), rejectUnauthorized: true },
});
rejectUnauthorized: false disables certificate verification and defeats the purpose of TLS.
4. Encrypting sensitive fields
Disk encryption protects you if the server is stolen, but not against data obtained through a database dump or SQL injection. Add application-level encryption for the most sensitive fields.
In Laravel, use the encrypted cast:
protected function casts(): array
{
return [
'passport_number' => 'encrypted',
'api_secret' => 'encrypted',
];
}
If you need to search by an encrypted field, store a keyed hash (HMAC) in a separate column and search on that. Passwords, on the other hand, are not encrypted but hashed with bcrypt or argon2id.
Important: the encryption key (APP_KEY) is stored separately from the database. If the key and the database leak together, encryption will not help. Plan key rotation in advance as well.
5. Close web interfaces
If you need phpMyAdmin or Adminer, do not leave them at a public address: only via an SSH tunnel or an allowlist. Best of all, do not install them on the production server at all, and connect with a local client over an SSH tunnel when needed:
ssh -L 5432:127.0.0.1:5432 user@your-server
6. Backups and auditing
Database backups must be encrypted and stored elsewhere, and restores tested regularly — see backups and restore for details.
For auditing:
- in PostgreSQL,
log_connections = on,log_disconnections = on, andlog_statement = 'ddl'for schema changes; thepgauditextension for detailed auditing; - in MySQL, logging of failed connections, and an audit plugin in Percona or MariaDB;
- at the application level, who viewed or changed a sensitive record (an audit table).
Logging every statement (log_statement = 'all') takes a lot of space and puts sensitive data into the logs — enable it only for temporary diagnostics.
7. Updates
The database software needs updating too. Versions past end of support no longer receive security fixes. Check the version (SELECT version();) and compare it with the vendor's support schedule.
On the application side
Database settings cannot fully close a bug in the application. The basic rules:
- parameterised queries only, no concatenating values into
whereRawor raw SQL; - an allowlist for
orderByand column names; - no SQL queries or database details in error messages (
APP_DEBUG=false); - never put a connection string into a front-end or mobile app.
Short checklist
- Database ports are closed to the internet; the database listens on
127.0.0.1or the internal network pg_hba.conf/ MySQLhostallow only the necessary addresses, notrust- The application connects as a least-privilege user, not a superuser
- Separate roles for migrations, the application and reporting
- Strong passwords, different for each environment;
.envis not in git - TLS with certificate verification between separate servers
- The most sensitive fields are encrypted at application level, the key is kept separately
- No phpMyAdmin/Adminer at a public address
- Encrypted backups and restore tests
- Connections and schema changes are logged; the database version is supported
Summary
Database protection consists of a few simple layers: hide it from the network, restrict privileges, encrypt connections and sensitive fields, back it up and monitor it. Each layer limits the damage if another one fails. In our process, the Claude AI agent reviews the database configuration, pg_hba.conf, user privileges and the queries in the application code, and the expert applies the changes in a safe order without stopping the running system.
If you want to know whether your database is ready for the expertise, request a security scan. We will help you close the gaps before the official check.


