Database security checklist

8 Database Authentication Checks Your Server Needs (2026)

Database authentication is the control that decides whether reaching your database port is the same thing as owning your data. This page documents all 8 checks in the CtrlOps Database Authentication audit across MySQL, PostgreSQL, MongoDB and Redis: what each one reads, the exact value that passes, the values that warn or fail, and the command to fix it.

Hiren KalariyaLast reviewed: Aug 22, 202614 min read
8
checks in this audit
7
rated high severity
14s
automated run time
8
need root or sudo

Every pass, warning and failure threshold on this page is transcribed from the database-security-authentication audit script that CtrlOps runs, and a build check fails if the two ever disagree. See how the audit runs.

Database authentication is the set of controls that verify who connects to your database and how they prove it: password enforcement, authentication methods, host restrictions and authorization modes, across every engine on the server. This page documents the 8 checks in the CtrlOps Database Authentication audit, in the order the audit runs them.

A weak authentication setup means anyone who reaches your database port, through a misconfigured firewall, an exposed admin tool or a compromised application on the same host, can connect with no credentials at all. Every threshold below is transcribed from the audit script itself, not from general advice. Where the script passes a value, this page says it passes; where the script warns rather than fails, this page says so and explains why the distinction matters.

Key takeaways

Eight checks across four engines, covering the gap between "the database has a password" and "the database asks for one".

  • Seven of the eight are rated HIGH. Only the PostgreSQL hashing algorithm is MEDIUM, because it governs passwords set from now on rather than access available right now.
  • All eight need root. Every one of them either connects through the MySQL or PostgreSQL socket as an administrator, or reads a config file under /etc that is not world-readable. Without sudo the audit reports SKIP rather than guessing.
  • The whole audit takes about 14 seconds when CtrlOps runs it over your existing SSH connection, against roughly 15 to 25 minutes by hand across four engines.
  • Every check skips cleanly on engines you do not run. A host with only PostgreSQL reports four SKIPs and four real results, so the audit is worth running whatever your stack is.
#CheckEngineSeverityWhat it catches
1Root passwordMySQL / MariaDBHIGHBlank root password, full admin for any local account
2Anonymous usersMySQL / MariaDBHIGHAccounts with a blank username
3Passwordless accountsMySQL / MariaDBHIGHNamed accounts with an empty password
4Remote rootMySQL / MariaDBHIGHroot reachable from a non-localhost host
5Auth methodsPostgreSQLHIGHtrust, cleartext and md5 entries
6Password hashingPostgreSQLMEDIUMPasswords stored with a weak algorithm
7AuthorizationMongoDBHIGHAuthorization disabled, every client an admin
8AuthenticationRedisHIGHNo requirepass and no ACL
How the engines are detected. Before any check runs, the audit looks for each engine several ways: a client or server binary on PATH, a running process, and for PostgreSQL, MongoDB and Redis a known config directory or file. An engine that is not detected produces a SKIP naming the engine, never a pass. It then opens one privileged connection per engine and reuses it, which is why all eight checks share the same root requirement and why the whole audit costs 14 seconds rather than eight separate logins.

Check 1: Does the MySQL root account have a password?

High severityNeeds sudo

The MySQL root account is the highest-privilege user the database has. If it has no password, every local account on the server is a database administrator, and if bind-address is also wrong, so is everyone who can reach port 3306.

The audit tests this the way an attacker would rather than by reading a table: from an unprivileged shell it attempts mysql --no-defaults -u root with an empty password. That is a stronger test than inspecting authentication_string, because it accounts for socket authentication and for credentials sitting in a .my.cnf that would otherwise make a passworded account look passwordless.

How to check manually

# As a NON-root user. Success here is the failure condition.
mysql --no-defaults -u root -e 'SELECT 1'

# For context, what the account actually uses
sudo mysql -N -e "SELECT user, host, plugin FROM mysql.user WHERE user='root'"
Result thresholds this check applies
ResultWhenWhat it means
PASSRoot access requires either root shell privileges through socket auth, or a passwordAn unprivileged shell cannot become a database administrator.
FAILMySQL root logs in with no password from a normal shellAny local account owns the database. Set a password, or switch root to socket authentication.
SKIPMySQL and MariaDB are not installed, or the server is down and no privileged connection could be openedThe two reasons are reported separately. A server that is down is not a passing server.

Socket authentication passes on purpose. On MariaDB and on Debian MySQL packages, root is often configured with the unix_socket plugin, which ties database root to OS root and needs no shared secret at all. That is a stronger configuration than a password, not a weaker one, and the probe above correctly refuses to log in under it.

How to fix it

# Set a real password
sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'a-long-random-password'; FLUSH PRIVILEGES;"

# Or tie root to the OS root account instead (MariaDB, Debian MySQL)
sudo mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket;"
This is the most probed database misconfiguration there is. Automated scanners try passwordless root on port 3306 within minutes of a host appearing on the internet, and they try it again every time the address is rescanned. There is no window in which a blank root password is acceptable, including "just while I finish setting it up".

Check 2: Are there anonymous MySQL users?

High severityNeeds sudo

An anonymous user is an account with a blank username in mysql.user. Some installation paths create them by default, and they let anyone connect without supplying a username at all. They are the reason mysql_secure_installation exists.

The audit counts rows in mysql.user where the user column is empty.

How to check manually

sudo mysql -N -e "SELECT COUNT(*) FROM mysql.user WHERE user=''"
sudo mysql -N -e "SELECT user, host FROM mysql.user WHERE user=''"
Result thresholds this check applies
ResultWhenWhat it means
PASSNo account has a blank usernameEvery connection must name an account.
FAILOne or more anonymous accounts existThe count is reported. Anyone can connect without credentials. Run mysql_secure_installation.
SKIPMySQL is not accessible: not installed, server down, or no privileged connectionNothing was measured, so nothing is claimed.

How to fix it

# The whole-job fix
sudo mysql_secure_installation

# Or by hand
sudo mysql -e "DELETE FROM mysql.user WHERE user=''; FLUSH PRIVILEGES;"
Run the script even if this check passes. mysql_secure_installation also drops the test database and disables remote root, which are check 4 here and a finding in the database hardening audit. It takes about thirty seconds and there is no downside on a server that is already clean.

Check 3: Do any MySQL accounts have no password?

High severityNeeds sudo

Beyond anonymous accounts, ordinary named accounts can also carry an empty password. This usually happens when an application is wired up with a blank password during setup and nobody comes back to it.

The audit lists accounts whose authentication string is empty, excluding two categories that are not findings: accounts using the auth_socket or unix_socket plugins, which authenticate against the OS instead, and the packaged system accounts mysql.sys, mysql.session, mysql.infoschema, mariadb.sys and debian-sys-maint, which are locked by other means. It runs a second, narrower query as a fallback so the check still works on MySQL 8, where the old password column no longer exists.

How to check manually

sudo mysql -N -e "
SELECT CONCAT(user,'@',host) FROM mysql.user
WHERE authentication_string=''
  AND plugin NOT IN ('auth_socket','unix_socket')
  AND user NOT IN ('','mysql.sys','mysql.session','mysql.infoschema','mariadb.sys','debian-sys-maint')"
Result thresholds this check applies
ResultWhenWhat it means
PASSEvery account authenticates with a password or with socket authNo named account can be used without a credential.
FAILOne or more named accounts have an empty passwordThe account names are listed as user@host. Set passwords or drop them.
SKIPMySQL is not accessibleNot installed, server down, or root was unavailable for socket auth.

How to fix it

# Give it a password
sudo mysql -e "ALTER USER 'appuser'@'localhost' IDENTIFIED BY 'a-long-random-password';"

# Or remove it if nothing uses it
sudo mysql -e "DROP USER 'appuser'@'localhost'; FLUSH PRIVILEGES;"
Fix the application config in the same change. A passwordless account usually exists because something is connecting to it with an empty password string. Setting a password without updating the client turns a security finding into an outage. Change the config first, then the account, then restart the application.

Check 4: Can root connect to MySQL remotely?

High severityNeeds sudo

MySQL accounts are identified by a user and host pair. A root entry with a host of %, or any specific remote address, means the highest-privilege account is reachable across the network, and every credential-stuffing script on the internet starts with exactly that account name.

The audit lists every root entry whose host is not localhost, 127.0.0.1 or ::1.

How to check manually

sudo mysql -N -e "
SELECT host FROM mysql.user
WHERE user='root' AND host NOT IN ('localhost','127.0.0.1','::1')"
Result thresholds this check applies
ResultWhenWhat it means
PASSroot exists only on localhost, 127.0.0.1 and ::1Remote root login is not possible regardless of the password.
FAILA root entry exists for any other hostThe host values are listed. Remote root is the first thing brute-forcers try.
SKIPMySQL is not accessibleNot installed, server down, or root was unavailable.

How to fix it

sudo mysql -e "
DROP USER IF EXISTS 'root'@'%';
DELETE FROM mysql.user WHERE user='root' AND host NOT IN ('localhost','127.0.0.1','::1');
FLUSH PRIVILEGES;"

If you genuinely need remote administration, create a separate named account with only the privileges it needs and reach it through an SSH tunnel rather than exposing root. The least-privilege audit covers what that account should and should not hold.


Check 5: Does PostgreSQL still allow trust or md5 authentication?

High severityNeeds sudo

PostgreSQL decides how each connection authenticates using rules in its host-based authentication file. Three method values are problems, in descending order of severity: trust accepts the connection with no password at all, password sends the password in cleartext, and md5 uses a hash that can be cracked offline and replayed.

Two details about how the audit reads this matter. It asks the running server for the file path with SHOW hba_file rather than assuming the packaged location, because a cluster can be pointed elsewhere. And it examines the method on every non-comment line, including local lines, not just network ones. A local all all trust line means every account on the host is every database user.

How to check manually

# Ask the server which file it is actually using
HBA=$(sudo -u postgres psql -X -tAc 'SHOW hba_file')

# Read the live rules. The method is the last column.
sudo grep -vE '^[[:space:]]*(#|$)' "$HBA"
Result thresholds this check applies
ResultWhenWhat it means
PASSNo trust, cleartext password or md5 entries remainOnly peer and scram methods are in use.
FAILOne or more trust entries, or one or more cleartext password entriesThe count is reported. trust needs no password at all; password sends it in the clear. Switch both to scram-sha-256.
WARNNo trust or cleartext entries, but one or more md5 entriesThe count is reported. md5 is crackable offline. Migrate to scram-sha-256.
SKIPPostgreSQL is not installed, or the server is down and no privileged connection could be openedThe two reasons are reported separately.

The result reports the worst method present, not all three. A file with both trust and md5 entries reports the trust count and says nothing about md5, so re-run after fixing rather than assuming one pass clears everything.

How to fix it

# In the file that SHOW hba_file named:
# Before
host  all  all  0.0.0.0/0  md5
local all  all             trust

# After
host  all  all  0.0.0.0/0  scram-sha-256
local all  all             peer

sudo systemctl reload postgresql
Changing the method does not re-hash existing passwords. A user whose password was stored under md5 still has an md5 hash after you switch the file to scram-sha-256, and their login will fail. Set password_encryption first (check 6), reload, then run ALTER ROLE username WITH PASSWORD for every account so the stored hash is rewritten. Do that before you change the authentication file, not after.

Check 6: How does PostgreSQL hash new passwords?

Medium severityNeeds sudo

The password_encryption setting decides how PostgreSQL stores a password at the moment it is set. If it is md5, every password created or changed from now on gets a weak hash regardless of what the authentication rules say.

This is the only MEDIUM check on the page, and the reason is timing. Checks 1 through 5 and 7 and 8 describe access that exists right now. This one describes the quality of credentials going forward.

How to check manually

sudo -u postgres psql -X -tAc 'SHOW password_encryption'
Result thresholds this check applies
ResultWhenWhat it means
PASSThe value is exactly scram-sha-256New and changed passwords are stored with a modern hash.
WARNThe value is anything elseThe actual value is reported. Set scram-sha-256 and re-hash existing passwords with ALTER ROLE.
SKIPPostgreSQL is not accessibleNot installed, server down, or root was unavailable.

How to fix it

# In postgresql.conf
password_encryption = 'scram-sha-256'

sudo systemctl reload postgresql

# Then rewrite each stored hash
sudo -u postgres psql -c "ALTER ROLE appuser WITH PASSWORD 'the-same-or-a-new-password';"
Setting the same password again is the whole fix. The hash is computed at the moment ALTER ROLE runs, so re-setting a password to its existing value is enough to migrate it, and you can do that without coordinating a credential rotation with every application at once. Rotate afterwards if the old password was ever exposed.

Check 7: Does MongoDB require authentication?

High severityNeeds sudo

MongoDB ships with authorization off. Without security.authorization set to enabled, any client that can open a connection can read, write and drop every database on the instance, with no username and no password.

The audit reads mongod.conf for an authorization: enabled line.

How to check manually

sudo grep -E '^[[:space:]]*authorization' /etc/mongod.conf
Result thresholds this check applies
ResultWhenWhat it means
PASSsecurity.authorization is set to enabledConnections require valid credentials.
FAILThe setting is missing or set to anything elseEvery reachable client has full admin access to every database.
WARNMongoDB is installed but mongod.conf could not be foundThe audit cannot confirm either way. Verify the setting in whichever config file your instance actually loads.
SKIPMongoDB is not installed, or root was unavailable to read the configThe two reasons are reported separately.

The WARN case is worth separating from the FAIL. A missing mongod.conf usually means a container or a custom install reading its config from elsewhere, which is a gap in the audit rather than a gap in your security. It is still a warning, because an unverified MongoDB is exactly the thing that gets ransomed.

How to fix it

# 1. Create the admin user FIRST, while auth is still off
mongosh --eval '
  db.getSiblingDB("admin").createUser({
    user: "adminUser",
    pwd: "a-long-random-password",
    roles: [{ role: "userAdminAnyDatabase", db: "admin" }]
  })'

# 2. Then enable it in /etc/mongod.conf
# security:
#   authorization: enabled

sudo systemctl restart mongod
Create the user before you enable authorization, not after. Once authorization is on and no user exists, MongoDB has a localhost exception that lets you create the first account, but it is narrow and version-dependent, and getting it wrong means restarting the instance with auth disabled to recover. The order above avoids the problem entirely. Pair this with bindIp from the network isolation audit: authentication and binding are separate controls and an exposed MongoDB should have both.

Check 8: Does Redis require a password?

High severityNeeds sudo

Redis has no authentication by default. It also executes commands immediately, which means an unauthenticated instance is not only a data exposure but a code execution path: CONFIG SET can be used to write an SSH key or a crontab entry.

The audit reads redis.conf for a requirepass line with a value, or for ACL configuration in the form of a user or aclfile directive, and separately reads the bind line to decide how bad a missing credential is.

How to check manually

sudo grep -E '^(requirepass|user |aclfile )' /etc/redis/redis.conf
sudo grep -E '^bind[[:space:]]' /etc/redis/redis.conf
Result thresholds this check applies
ResultWhenWhat it means
PASSA requirepass value or an ACL (user or aclfile) is configuredRedis asks for a credential. Where it is bound is the network isolation audit, not this one.
FAILNeither is configured, and Redis is bound beyond loopbackAnyone who can reach the port has full access, including the ability to write files through CONFIG SET.
WARNNeither is configured, but Redis is bound to loopback onlyNo external attacker can reach it directly, but any local process or an SSRF bug in your application can. Set requirepass anyway.
SKIPRedis is not installed, or root was unavailable to read redis.confThe two reasons are reported separately.

Note what PASS does not mean here. A requirepass on a Redis bound to 0.0.0.0 passes this check, because this check is about authentication and the bind address belongs to the network isolation audit. Read the two together: a password on an internet-facing Redis is better than nothing and still not where you want to be.

How to fix it

# In /etc/redis/redis.conf
requirepass a-long-random-string-not-a-word
bind 127.0.0.1 ::1
protected-mode yes

sudo systemctl restart redis

For Redis 6 and later, ACLs give per-user access control instead of one shared secret:

# In /etc/redis/redis.conf
user appuser on >a-long-random-password ~app:* +@read +@write
Pick a long random string, not a passphrase. Redis authenticates with a single AUTH command and no rate limiting, so a short requirepass is brute-forceable at network speed. Thirty-two random characters costs nothing to store in your application config and takes the attack off the table.

What this audit does not cover

Being explicit about scope is part of the point of publishing thresholds. This audit answers one question per engine: does the database ask for a credential, and is that credential meaningful.

  • Whether the port is reachable. Binding, exposed ports and web admin tools are the network isolation audit. A perfectly authenticated database on 0.0.0.0 is still a brute-force target.
  • Where the passwords are stored. Config files in web roots, file permissions and leaked git history are the credential storage audit. A strong password in a world-readable .env is not a strong password.
  • Whether credentials travel encrypted. TLS enforcement and certificate expiry are the transport encryption audit.
  • What each account can do once connected. Wildcard hosts, FILE privilege and superuser sprawl are the least-privilege audit.
  • Password strength. The audit detects the absence of a credential, never the quality of one. A root account with the password root passes check 1.
  • Managed database services. Amazon RDS, Cloud SQL and Azure Database expose none of these files or sockets to a host session. These checks are for databases you install and configure yourself.

The manual audit problem at scale

Running these 8 checks by hand takes 15 to 25 minutes per server, and the reason is not the number of commands. It is that four engines means four different mental models: a privilege table in MySQL, a rules file in PostgreSQL, a YAML key in MongoDB, a flat config directive in Redis. Each has its own idea of what "no authentication" looks like.

That is also why this audit is the one people skip. Checking MySQL is easy and most teams do it. Checking that password_encryption is still scram-sha-256 on the PostgreSQL instance somebody provisioned last quarter is the kind of thing nobody thinks to do until an auditor asks.

And it drifts. A trust line added during a migration is still there in September. An account created with a blank password for a one-off import outlives the import by years. Nothing on the server complains, because from the database's point of view the configuration is valid. That is the same server management drift problem that compounds across every audit category, except here the finding is silent by design.


How CtrlOps runs all 8 checks in one click

Instead of running these commands on every server by hand, CtrlOps runs the whole Database Authentication audit over the SSH connection you already have open. Nothing is installed on the server: no agent, no daemon, no package, no new credentials. The checks are read-only POSIX shell that executes, streams its results back, and leaves nothing behind.

Watch it run

The recording below walks through the SSH & Access audit rather than this one, but the flow is identical for every audit in the catalog: pick it, watch the checks stream in, read the score, send the failures to the AI Terminal. The written steps that follow cover the same ground if you would rather read than watch.

Step 1: Open the Audit tab

Connect to your server in CtrlOps and open the Audit tab in the left sidebar.

Step 2: Select Authentication

Choose the Authentication audit from the Database category. All 8 checks are listed with their descriptions, severity levels and the estimated run time (about 14 seconds in total). Toggle off any check you want to skip.

Step 3: Run it

The audit runs over your existing SSH session. Results stream in as they complete, so you watch each check pass, warn or fail rather than waiting on a final report.

Step 4: Read the report

You get a summary containing:

  • A hardening score out of 100
  • A severity breakdown: how many HIGH, MEDIUM and LOW findings
  • Pass, warning, failed and skipped counts
  • A findings table you can sort and filter

Step 5: Fix what failed

Select the failed findings and send them to the AI Terminal. Each proposed fix command appears with an explanation of what it changes. Nothing runs until you review and approve it, which matters more here than on most audits: several of these fixes drop accounts or restart a database.

Step 6: Re-run and compare

Run the same audit again after applying the fixes and watch the hardening score move. That delta is the thing a spreadsheet never gave you: evidence that the fix landed.

TaskBy hand, 8 serversCtrlOps, 8 servers
Run all 8 checks across 4 enginesAbout 2.5 hoursAbout 2 minutes
Get a scored reportNot availableAutomatic
Export a PDF for a clientNot availableOne click
Fix what failedCopy-paste from your notesAI-generated commands behind an approval gate
Re-audit after fixingAnother 2.5 hoursAnother 2 minutes

Conclusion

Database authentication is the control everything else depends on. Network isolation reduces who can try, credential storage protects the secret, transport encryption hides it in flight, but if the database does not ask for a credential in the first place, none of those matter.

Seven of these eight checks are HIGH severity for that reason, and all eight are the kind of thing that is correct on the day you set it up and quietly wrong two migrations later. The CtrlOps Security Audit runs the set in about 14 seconds per server, agentlessly, across all four engines at once.

The neighbouring checklists cover the other database layers: credential storage, configuration and hardening, least-privilege permissions, network isolation and transport encryption.


Frequently asked questions

Set a real credential on every account, remove anonymous ones, restrict administrative accounts to localhost, and use modern authentication methods: scram-sha-256 for PostgreSQL, caching_sha2_password or socket auth for MySQL. Enable security.authorization for MongoDB and set requirepass or an ACL for Redis. Then re-audit after every configuration change, because this is the layer that drifts silently.

For network connections, caching_sha2_password, the default since MySQL 8.0. For MariaDB, ed25519. The legacy mysql_native_password plugin uses SHA-1 and is deprecated. For a local administrative account, socket authentication (auth_socket or unix_socket) is stronger than any password, because it ties database root to OS root and there is no secret to leak, rotate or brute-force. This audit passes socket auth for exactly that reason.

Because each one either opens an administrative connection through the local socket, or reads a config file that is deliberately not world-readable. Reading mysql.user requires database administrator rights, the PostgreSQL authentication file is owned by the postgres user, and mongod.conf and redis.conf are typically mode 640. Without root the audit reports SKIP and names the reason rather than reporting a pass it did not earn.

No. md5 is vulnerable to a pass-the-hash attack: an attacker who captures the stored hash can authenticate with it without ever knowing the password. The PostgreSQL project has recommended scram-sha-256 since version 10. Note that this audit warns rather than failing on md5, and fails on trust and cleartext password, because those two require no valid credential at all while md5 at least requires one.

Because a local line with trust is not harmless. It means every account on the server is every database user, which turns any local code execution, a compromised web application for instance, into full database access without a credential. The audit reads the method column on every non-comment line in the file the server actually loaded, which it finds with SHOW hba_file rather than assuming a path.

It means Redis asks for a credential. It does not mean Redis is unreachable: a requirepass on an instance bound to 0.0.0.0 passes this check while still being exposed to the internet. Bind address is the network isolation audit. Read the two together, and note that this audit warns even on a loopback-only Redis with no password, because a local process or an SSRF bug reaches loopback without crossing the network.

They are accounts with a blank username in mysql.user, created by some installation paths, that accept connections without a username. Remove them with DELETE FROM mysql.user WHERE user=''; and FLUSH PRIVILEGES;, or run mysql_secure_installation, which also drops the test database and disables remote root in the same pass.

Yes. A root entry with a % host means the best-known account name on the server is reachable from any address, and that is where every credential-stuffing script starts. Restrict root to localhost, 127.0.0.1 and ::1, and reach the database through an SSH tunnel for remote administration. If an application needs remote access, give it its own account with only the privileges it uses.

It detects which engines are present, opens one privileged connection per engine over your existing SSH session, then runs 8 read-only checks: probing MySQL root login from an unprivileged shell, querying mysql.user for anonymous, passwordless and remote-root accounts, reading the live PostgreSQL authentication file and password_encryption, and reading the MongoDB and Redis config files. Nothing is installed and nothing is written. The whole audit takes about 14 seconds.

Each check reports SKIP for engines that are not installed, naming the engine. A host with only PostgreSQL gets four SKIPs and four real results, and the SKIPs do not count against the hardening score as failures. This is why the audit is worth running on any server regardless of stack: it tells you what is there as well as what is wrong with it.

It reads local sockets and config files on databases you administer. It does not apply to managed services such as Amazon RDS, Google Cloud SQL or Azure Database, where none of those files are reachable from a host session and authentication is configured through the provider console. It also does not cover Kubernetes-orchestrated clusters or serverless databases. For those, use the security assessment tooling built into the platform.

Audit your fleet

Run all these checks on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals