What this check reads
The check walks a fixed list of five process names - mysqld, mariadbd, postgres, mongod and redis-server - and runs pgrep -x for each one. Where a name matches, it takes the first PID only and reads that process's owner with ps -o user= -p PID, building a list of name=user pairs. Any engine whose owner is the literal string root is collected separately, and one entry on that list is enough to fail the check. If no name matched at all there is no owner to compare and the check skips.
When it applies
Runs on any host and needs no root - pgrep/ps are enough. It looks at running processes only, and only at five exact process names (mysqld, mariadbd, postgres, mongod, redis-server); a stopped engine, an engine whose binary has another name (postmaster on older RHEL), or one running inside a container yields SKIP or is simply absent from the report. Only the first PID of each name is sampled, so a second instance running as a different user is not seen.
What each result means
| Result | When | What it means |
|---|---|---|
| SKIP | No database process running | Nothing was verified: none of the five process names was running, so no process owner was ever read. |
| FAIL | One or more engines run as root | At least one running engine is owned by root, so a file-write primitive inside the database writes anywhere on the host. The message names each engine. |
| PASS | All run as dedicated users (`mysql`, `postgres`, `mongodb`, `redis`) | Every engine process the check sampled is owned by a non-root account. Only the first PID per name was sampled, so a second instance running as root would not appear here. |
Why it matters
All four vendors require or strongly recommend a dedicated unprivileged account. PostgreSQL refuses to start as root. MySQL runs as root only with --user=root which the manual warns against. A SQL-level file-write primitive (SELECT ... INTO OUTFILE, COPY TO PROGRAM, Redis CONFIG SET dir) from a root-owned engine writes anywhere on the host. NIST SP 800-123 (least privilege for server processes); OWASP Database Security Cheat Sheet "run the database as a low-privileged user".
Why it fails, and when it is wrong
pgrep -x postgresmatches the postmaster and its children; on some builds the binary ispostmaster(older RHEL) and nothing matches, giving SKIP.ps -o user=truncates names longer than 8 characters to a UID number on someprocpsbuilds; the comparison torootstill works.- MySQL started manually by an admin (
mysqld &as root) rather than via systemd is the common cause. Docker containers for these engines are not seen. - MongoDB started via a custom systemd unit without
User=mongodb.
How to fix it
Read the process owner
Run ps -eo user,comm and look at the user column for mysqld, mariadbd, postgres, mongod and redis-server. Anything showing root is the finding, and the engine name tells you which packaged account it should be running under instead.
Set the service account
Name the packaged account in a systemd override for the unit, with User= and Group=:
- MySQL/MariaDB: ensure
[mysqld] user=mysqlinmy.cnfandUser=mysqlin the unit. - MongoDB:
User=mongodb/Group=mongodbinmongod.service. - Redis:
User=redisin the unit (the distro packages do this).
Fix the data directory ownership
The new account has to own the data it reads and writes, otherwise the service will not start once you switch it:
sudo chown -R mysql:mysql /var/lib/mysql
sudo chown -R mongodb:mongodb /var/lib/mongodb /var/log/mongodbRestart and confirm
Reload the unit files and restart the service, then re-run the ps command from the first step and confirm the owner changed:
sudo systemctl daemon-reload
sudo systemctl restart mysqlVerify the fix
# Reproduce exactly what the check reads:
for p in mysqld mariadbd postgres mongod redis-server; do
pid=$(pgrep -x "$p" | head -1) && printf '%s=%s\n' "$p" "$(ps -o user= -p "$pid" | tr -d ' ')"
done
# expected: mysql / postgres / mongodb / redis - never root
# Confirm the unit enforces it, so it survives a restart:
systemctl show mysql -p User -p GroupDebugging
Either the engine is genuinely stopped (systemctl status mysql postgresql mongod redis-server) or its process name is not one of the five. Check with ps -eo comm,user | grep -Ei 'sql|mongo|redis'. On older RHEL the PostgreSQL parent is postmaster, which this check does not match.
The running process was started some other way (a manual mysqld &, an old process from before the unit was fixed). Confirm with ps -o pid,user,args -p $(pgrep -x mysqld | head -1), then restart the service properly.
Only the first PID per name is sampled. List them all: pgrep -ax mysqld, ps -eo user,args -C mysqld.
A database in a container is invisible here even though the container process may run as root on the host. Use the Docker Container User check instead.
Changing User= without chown -R mysql:mysql /var/lib/mysql leaves the service unable to start. Change ownership first, then the unit.
Sources
- MySQL 8.4: How to Run MySQL as a Normal User
- MySQL 8.4: Making MySQL Secure Against Attackers
- PostgreSQL: The PostgreSQL User Account
- MongoDB: Production Notes (run as dedicated user)
- Redis: Security (running as unprivileged user)
- NIST SP 800-123 Guide to General Server Security
- systemd.exec: User=/Group=
How the script reads this
Next
Re-run the Configuration & Hardening audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 7 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.
All 7 Configuration & Hardening fixes