SERVER SECURITY GUIDE

Linux Users, sudo and Least Privilege on a VPS: A Practical Audit

Most server hardening guides cover the way in: the firewall, SSH, patching. This one covers what happens after someone is inside, or after a bug in one service gives an attacker its permissions. The answer depends on who and what holds root. The principle of least privilege is simple to state and routinely ignored on small servers, where one admin account has unrestricted sudo, deploy scripts run as root and services share a single user. OWASP's authorization cheat sheet defines it as "assigning users only the minimum privileges necessary to complete their job." This guide turns that into an audit you can run in ten minutes and a set of changes that limit the blast radius when something goes wrong. It pairs with the SSH hardening guide and the VPS security checklist.

Quick win (10 minutes)

  • Run awk -F: '($3 == 0) {print $1}' /etc/passwd. Only root should appear.
  • Run getent group sudo wheel docker and check every member is a person who needs it.
  • Run sudo grep -rn NOPASSWD /etc/sudoers /etc/sudoers.d/ and note each result.
  • Run sudo --version | head -1 and compare it with the patched release in the CVE section below.
  • List accounts with a real login shell: grep -vE "nologin|false" /etc/passwd.

Why privilege matters after the break-in

Defences fail in layers. A vulnerable web app, a stolen password or a leaked key gets an attacker in as some user. What they can do next is decided by that user's privileges. If the account is the web server's own low-privilege user, the damage is limited to what that user can read and write. If it can run sudo without limits, or the service runs as root, the attacker owns the machine within seconds and can disable logging, read every secret and delete the backups, as described in the ransomware protection guide.

The same logic applies to special groups. Membership of the docker group is root-equivalent, as explained in the Docker host security guide. Least privilege means each person and each service holds only what its job needs, and nothing more, so a mistake or compromise stays small.

Audit who has root today

Start by finding every route to root. Run these as an administrator:

# accounts with UID 0 (should be only root)
awk -F: '($3 == 0) {print $1}' /etc/passwd

# who can use sudo, directly or via group
getent group sudo wheel
sudo grep -rnE '^[^#]' /etc/sudoers /etc/sudoers.d/

# NOPASSWD rules, which skip authentication
sudo grep -rn NOPASSWD /etc/sudoers /etc/sudoers.d/

# accounts with an empty password
sudo awk -F: '($2 == "") {print $1}' /etc/shadow

# accounts that can log in interactively
grep -vE 'nologin|false' /etc/passwd

Any second account with UID 0 is a root account in disguise and a classic persistence trick; read the UID 0 entry and, if you find one you did not create, go to the breach response guide. For each human account, ask whether they need to administer the server at all. Remove people who left, remove unused accounts with sudo userdel -r name after backing up what you need, and lock accounts you may want later with sudo usermod -L name combined with a nologin shell. Also confirm direct root login over SSH is off; the root login entry explains why.

Give services their own unprivileged accounts

Every service should run as its own user, not as root and not as your admin account. For accounts that should never log in, set the nologin shell, which, according to its manual page, "displays a message that an account is not available and exits non-zero" and is "intended as a replacement shell field to deny login access to an account":

sudo useradd --system --home-dir /nonexistent --shell /usr/sbin/nologin myapp

On systemd systems, set the user in the unit file and add the sandboxing options the systemd documentation provides. User=myapp runs the process unprivileged. NoNewPrivileges=yes ensures "the service process and all its children can never gain new privileges." ProtectSystem=strict mounts "the entire file system hierarchy" read-only except for the API file systems, and you then grant write access to specific paths with ReadWritePaths=. DynamicUser=yes allocates a temporary user for simple services. Bind to ports above 1024 behind a reverse proxy, instead of running anything as root to reach port 80 or 443. For containers, the same ideas apply and are covered in the Docker guide linked above.

Write sudo rules that are narrow and safe

Never edit /etc/sudoers directly with a plain editor. Use visudo, which validates syntax before saving so a typo does not lock out administrators. Put your own rules in separate files under /etc/sudoers.d/ using sudo visudo -f /etc/sudoers.d/deploy. The sudoers manual notes that this directory skips file names "that end in ~ or contain a . character," so name files without dots, or the rule will silently not apply.

Then keep each rule as narrow as the job allows:

  • Prefer named commands over ALL. A deploy user who only needs to restart one service should get exactly that: deploy ALL=(root) /usr/bin/systemctl restart myapp.service.
  • Be careful with NOPASSWD. By default sudo requires authentication, and the NOPASSWD tag changes that. It is reasonable for a narrowly scoped automation rule, and a poor idea for ALL on a human account.
  • Avoid wildcards in arguments. The manual warns that "wildcards can match any character, including white space" and that they "should be used with care."
  • Do not allow editors, pagers or shells. The manual states that "once sudo executes a program, that program is free to do whatever it pleases, including run other programs," which bypasses sudo's control and logging. For file editing, it says "a safer approach is to give the user permission to run sudoedit."
  • Use sudoedit for config changes. sudoedit /etc/nginx/nginx.conf edits a copy as your own user, and writes it back as root.

Check what a user can actually do with sudo -l -U username, and run a quick sudo visudo -c after changes.

Want a second pair of eyes on your accounts?

A free, read-only server check reviews account and privilege configuration along with exposed services, without changing anything.

Get your free server check Talk to a security engineer

Keep sudo itself patched

Privilege tools are themselves targets. In 2025 the sudo project disclosed CVE-2025-32463, a local privilege escalation involving the -R (--chroot) option. Per the project's advisory, "sudo versions 1.9.14 to 1.9.17 inclusive are affected," it is "fixed in sudo 1.9.17p1," and on systems that support /etc/nsswitch.conf "a user may be able to run arbitrary commands as root." The advisory describes the chroot feature as deprecated, with upgrading to 1.9.17p1 or later as the fix it provides.

Check yours with sudo --version. Distributions often backport fixes without changing the upstream version number, so also compare against your vendor's security tracker or run your package manager's update. This is exactly why a routine matters; see the patch management guide. It also shows why local privilege escalation bugs matter on a small server: any low-privilege foothold, such as a compromised web app user, becomes full control when sudo is vulnerable.

Daily habits that keep it tight

Least privilege decays unless you maintain it, so schedule a review at least every quarter. Re-run the audit above, remove access for people who changed roles, and confirm that new services have their own accounts. Keep an eye on the logs: sudo records each command in the authentication log or journal, and the log analysis guide shows how to alert on unexpected use. For commands that are especially sensitive, the sudoers manual also describes log_output for recording terminal output.

Combine this with a second factor on administrator logins, covered in SSH two-factor authentication, so a stolen key alone does not hand over a sudo-capable account. Finally, keep secrets scoped too: a database user restricted to one schema, or an API token limited to one action, limits what an exposed .env file can do. Document who holds what, so nothing depends on memory.

For the file formats behind this audit, see the references for sudoers and /etc/passwd.

Reference: sudo explains the command itself, and sudoers covers the policy file.

Frequently asked questions

What is the principle of least privilege on a server?

Each user and each service gets only the minimum permissions its job needs. OWASP defines it as assigning users only the minimum privileges necessary to complete their job. It limits the damage if an account or service is compromised.

How do I check which users have root access on Linux?

List accounts with UID 0 using awk on /etc/passwd, check members of the sudo or wheel group with getent, and read /etc/sudoers and /etc/sudoers.d for rules. Only root should have UID 0, and every sudo-capable user should need it.

Is NOPASSWD in sudoers safe?

Only for narrow, scoped rules, such as one automation account allowed to run one specific command. By default sudo requires authentication, and NOPASSWD removes it, so avoid it for ALL commands or for human accounts.

Why should I use visudo instead of editing sudoers directly?

visudo checks syntax before saving. A syntax error in a sudoers file can break sudo for everyone, which can lock you out of administration on a server where root login is disabled.

Which sudo versions are affected by CVE-2025-32463?

Sudo versions 1.9.14 to 1.9.17 inclusive are affected, according to the sudo project advisory, and the bug is fixed in 1.9.17p1. Check your distribution's security tracker as well, since vendors often backport fixes.

What does the nologin shell do?

It is a replacement shell field that denies interactive login to an account. It displays a message that the account is not available and exits non-zero. Use it for service accounts that should only run daemons.

Not sure where your server stands?

Get a free, read-only security check, or talk to a security engineer about managed protection.

Get your free server check Talk to a security engineer