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
/etcthat 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.
| # | Check | Engine | Severity | What it catches |
|---|---|---|---|---|
| 1 | Root password | MySQL / MariaDB | HIGH | Blank root password, full admin for any local account |
| 2 | Anonymous users | MySQL / MariaDB | HIGH | Accounts with a blank username |
| 3 | Passwordless accounts | MySQL / MariaDB | HIGH | Named accounts with an empty password |
| 4 | Remote root | MySQL / MariaDB | HIGH | root reachable from a non-localhost host |
| 5 | Auth methods | PostgreSQL | HIGH | trust, cleartext and md5 entries |
| 6 | Password hashing | PostgreSQL | MEDIUM | Passwords stored with a weak algorithm |
| 7 | Authorization | MongoDB | HIGH | Authorization disabled, every client an admin |
| 8 | Authentication | Redis | HIGH | No requirepass and no ACL |
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?
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 | When | What it means |
|---|---|---|
| PASS | Root access requires either root shell privileges through socket auth, or a password | An unprivileged shell cannot become a database administrator. |
| FAIL | MySQL root logs in with no password from a normal shell | Any local account owns the database. Set a password, or switch root to socket authentication. |
| SKIP | MySQL and MariaDB are not installed, or the server is down and no privileged connection could be opened | The 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;"Check 2: Are there anonymous MySQL users?
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 | When | What it means |
|---|---|---|
| PASS | No account has a blank username | Every connection must name an account. |
| FAIL | One or more anonymous accounts exist | The count is reported. Anyone can connect without credentials. Run mysql_secure_installation. |
| SKIP | MySQL is not accessible: not installed, server down, or no privileged connection | Nothing 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;"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?
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 | When | What it means |
|---|---|---|
| PASS | Every account authenticates with a password or with socket auth | No named account can be used without a credential. |
| FAIL | One or more named accounts have an empty password | The account names are listed as user@host. Set passwords or drop them. |
| SKIP | MySQL is not accessible | Not 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;"Check 4: Can root connect to MySQL remotely?
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 | When | What it means |
|---|---|---|
| PASS | root exists only on localhost, 127.0.0.1 and ::1 | Remote root login is not possible regardless of the password. |
| FAIL | A root entry exists for any other host | The host values are listed. Remote root is the first thing brute-forcers try. |
| SKIP | MySQL is not accessible | Not 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?
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 | When | What it means |
|---|---|---|
| PASS | No trust, cleartext password or md5 entries remain | Only peer and scram methods are in use. |
| FAIL | One or more trust entries, or one or more cleartext password entries | The count is reported. trust needs no password at all; password sends it in the clear. Switch both to scram-sha-256. |
| WARN | No trust or cleartext entries, but one or more md5 entries | The count is reported. md5 is crackable offline. Migrate to scram-sha-256. |
| SKIP | PostgreSQL is not installed, or the server is down and no privileged connection could be opened | The 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 postgresqlscram-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?
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 | When | What it means |
|---|---|---|
| PASS | The value is exactly scram-sha-256 | New and changed passwords are stored with a modern hash. |
| WARN | The value is anything else | The actual value is reported. Set scram-sha-256 and re-hash existing passwords with ALTER ROLE. |
| SKIP | PostgreSQL is not accessible | Not 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';"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?
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 | When | What it means |
|---|---|---|
| PASS | security.authorization is set to enabled | Connections require valid credentials. |
| FAIL | The setting is missing or set to anything else | Every reachable client has full admin access to every database. |
| WARN | MongoDB is installed but mongod.conf could not be found | The audit cannot confirm either way. Verify the setting in whichever config file your instance actually loads. |
| SKIP | MongoDB is not installed, or root was unavailable to read the config | The 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 mongodbindIp 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?
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 | When | What it means |
|---|---|---|
| PASS | A requirepass value or an ACL (user or aclfile) is configured | Redis asks for a credential. Where it is bound is the network isolation audit, not this one. |
| FAIL | Neither is configured, and Redis is bound beyond loopback | Anyone who can reach the port has full access, including the ability to write files through CONFIG SET. |
| WARN | Neither is configured, but Redis is bound to loopback only | No external attacker can reach it directly, but any local process or an SSRF bug in your application can. Set requirepass anyway. |
| SKIP | Redis is not installed, or root was unavailable to read redis.conf | The 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 redisFor 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 +@writeAUTH 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.0is 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
.envis 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
rootpasses 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.
| Task | By hand, 8 servers | CtrlOps, 8 servers |
|---|---|---|
| Run all 8 checks across 4 engines | About 2.5 hours | About 2 minutes |
| Get a scored report | Not available | Automatic |
| Export a PDF for a client | Not available | One click |
| Fix what failed | Copy-paste from your notes | AI-generated commands behind an approval gate |
| Re-audit after fixing | Another 2.5 hours | Another 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.