Database audit fixes

Least-Privilege Permissions: fixing all 6 checks

The least-privilege audit queries mysql.user, pg_roles and pg_hba.conf for the grants that turn one SQL injection into server-wide access: the % wildcard host, FILE, SUPER and superuser.

All six checks read the engine's own catalogue, so each needs a working root login to it (mysql -N -e "SELECT 1" as root, or psql as the postgres user), and a database that is up but unreachable to the audit account reports six skips rather than six passes. A host carrying only the mysql or psql client still counts as having the engine, so it skips the same way even though there is no server on it.

6
checks in this audit
0
rated high severity
6
need root or sudo
~12s
automated run time

Transcribed from the database-security-least-privilege audit script. For what the audit covers rather than how to fix it, see the Least-Privilege Permissions checklist.

Every check

Find your finding

Listed in the order the audit runs them. Take the check name from the third field of the result line and open its page.

  • MySQL Wildcard Hosts

    Soon

    Checks for MySQL accounts using the % wildcard host, which accept connections from any address

    MediumNeeds root3 results
  • MySQL FILE Privilege

    Soon

    Checks whether any non-system MySQL account holds the FILE privilege to read and write server files

    MediumNeeds root3 results
  • MySQL File Import/Export

    Soon

    Checks whether MySQL secure_file_priv confines file import/export and whether local_infile is enabled

    MediumNeeds root3 results
  • MySQL Admin Accounts

    Soon

    Checks for MySQL accounts holding SUPER beyond the expected system accounts

    MediumNeeds root3 results
  • PostgreSQL Superusers

    Soon

    Checks for PostgreSQL superuser roles beyond the built-in postgres role

    MediumNeeds root3 results
  • PostgreSQL Allowed Hosts

    Soon

    Checks pg_hba.conf for host entries that accept connections from any address

    MediumNeeds root3 results
Shared script behaviour

How the Least-Privilege Permissions audit reads your server

Every check in this audit runs after the same preamble, and several of its results only make sense once you know what that preamble could and could not see.

All need root socket access (MYOK / PGOK).

Engine detection: HAS_MY, HAS_PG, HAS_MG, HAS_RD are set from client binaries, server processes or config directories. MYOK=1 only if mysql -N -e 'SELECT 1' works as root over the socket; PGOK=1 only if su postgres -c 'psql -X -tAc "SELECT 1"' works. RDCONF is /etc/redis/redis.conf or /etc/redis.conf; MGCONF is /etc/mongod.conf. Containerised databases are not detected by these probes; audit them with the Docker checks or exec into the container.

Why checks skip

A SKIP is never a pass

A skipped check verified nothing at all. The most common cause is privilege: 6 of the 6 checks in this audit need root for a complete result. Before every run, each script works out which of four privilege modes it is in.

The four privilege modes an audit script can run in, and what each one means for checks that need root
ModeHow it is reachedWhat it means for your results
rootThe audit runs as UID 0.Every privileged branch runs directly. No check skips for lack of permission.
nopasssudo -n true succeeds, so the account has passwordless sudo.Privileged commands run through sudo -n.
passA sudo password was supplied in the CtrlOps audit settings and accepted.Privileged commands run through sudo -S.
noneNone of the above worked.CAN_ROOT=0. Every privileged branch returns SKIP with a "Need root" message rather than guessing.

If many checks skip with a “need root” message, run the audit as root, grant the audit account sudo (with or without NOPASSWD), or supply the sudo password in the CtrlOps audit settings.

FAQ

Least-Privilege Permissions questions

The check audits the grant, not the listener. An account defined as app@% is reachable from anywhere the server accepts TCP, and today that may be nowhere because bind-address is 127.0.0.1 or skip_networking is on, but the account survives the next binding change and the next restore onto a different host. Only the bare % is reported: partial wildcards such as 10.0.% or %.internal are not. Pin each account to localhost or to the app server address and the finding clears.
Less than the name suggests. The MySQL check reads the Super_priv column only, so an account holding the MySQL 8 dynamic privileges SYSTEM_VARIABLES_ADMIN, CONNECTION_ADMIN or SET_USER_ID without SUPER passes. The PostgreSQL check reads rolsuper only, so a role with CREATEROLE, BYPASSRLS or REPLICATION, or membership in pg_read_server_files or pg_execute_server_program, passes while being nearly as powerful. Review SHOW GRANTS and \du output by hand once the flagged accounts are dealt with.
Partly. REVOKE FILE ON *.* takes effect for each account on its next connection and needs no restart, and local_infile can be turned off with SET GLOBAL local_infile=0 and then persisted in the config file. secure_file_priv is read-only at runtime, so narrowing it to a directory requires a restart and belongs in a maintenance window. Before revoking, check what the account really uses: mysqldump --tab and LOAD DATA pipelines legitimately need FILE, and those belong on a dedicated account the application never connects with.
Audit your fleet

Run all 6 Least-Privilege Permissions checks, 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