The principle of least privilege says every user, process and key should have only the access it needs, and nothing more. When something does get compromised, such as a vulnerable plugin, a leaked API key or a stolen laptop, least privilege limits how far the damage spreads. It’s less a single setting than a habit applied everywhere. Here’s what it looks like on a typical server.

1. Don’t run things as root

  • Log in as a normal user, and use sudo when needed. See disable root SSH login.
  • Run each service as its own unprivileged user:
sudo useradd -r -s /usr/sbin/nologin -d /srv/myapp myapp
# systemd unit
[Service]
User=myapp
Group=myapp

If the app is exploited, the attacker gets myapp’s rights, not root.

  • Bind low ports without root: let Nginx (which drops privileges itself) handle ports 80/443 and proxy to your app on a high port, or grant only that capability: AmbientCapabilities=CAP_NET_BIND_SERVICE. See setuid and capabilities.

2. Scope sudo instead of granting everything

Full sudo (ALL=(ALL) ALL) is root. For users or automation that need one or two actions, allow exactly those:

# /etc/sudoers.d/deploy  (edit with: sudo visudo -f /etc/sudoers.d/deploy)
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp, /usr/bin/systemctl reload nginx

Use full paths, and avoid allowing commands that can spawn shells or edit arbitrary files: editors, less, find -exec, tar with arbitrary args, bash. See sudo and sudoers.

3. Separate sites from each other

On a server hosting several websites:

  • one system user and one PHP-FPM pool per site, with open_basedir. See PHP-FPM tuning;
  • web roots owned by each site’s user, not shared;
  • one database and one database user per site.

A hacked WordPress install then can’t read or modify the others.

4. File permissions: write access only where needed

The web server or PHP should read code, and write only to uploads, caches and sessions:

sudo chown -R deploy:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 750 {} +
sudo find /var/www/example.com -type f -exec chmod 640 {} +
sudo chmod -R g+w /var/www/example.com/public/wp-content/uploads

If PHP can’t write to its own code, many attacks can’t plant persistent backdoors. Config files with secrets: 640, with group access limited to the app. See Linux file permissions and ACLs.

5. Database users with minimal grants

CREATE USER 'shop'@'localhost' IDENTIFIED BY '…';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop'@'localhost';
  • No GRANT ALL ON *.* for applications.
  • Read-only users for reporting and analytics.
  • A separate user with schema rights for migrations, used only during deployments.
  • Bind to localhost or a private IP, never a public one.

See MySQL users and privileges and PostgreSQL users.

6. Sandbox services with systemd

A few lines restrict what a service can touch, even if it’s compromised:

[Service]
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/srv/myapp/data
CapabilityBoundingSet=

See systemd unit files.

7. Containers

  • Run processes as non-root inside the container (USER app in the Dockerfile).
  • Don’t use --privileged, and don’t mount the Docker socket into containers unless absolutely necessary. Access to the Docker socket equals root on the host.
  • Membership of the docker group is root-equivalent. Grant it like sudo. See Docker socket permission and rootless Docker.
  • Read-only root filesystem (read_only: true) where possible.

8. Network: expose only what’s needed

  • A firewall allowing only the public ports. Databases, Redis and admin panels on localhost or a private network. See UFW.
  • SSH restricted to known IPs, a VPN or a bastion where possible. See ProxyJump.

9. Keys and tokens

  • SSH: one key per person and device. Deploy keys that are read-only and per repository. Restricted authorized_keys entries for automation (command=, from=). See key-only SSH.
  • API tokens: minimal scopes (read-only where possible), per service, with expiry.
  • CI/CD: separate credentials for deploys, limited to the target environment. See secrets management.

10. Review regularly

Privileges creep. Old users, temporary sudo rules and forgotten keys pile up. Every few months:

getent group sudo wheel docker
sudo ls /etc/sudoers.d/
for h in /root /home/*; do echo "== $h"; cat "$h/.ssh/authorized_keys" 2>/dev/null; done
mysql -e "SELECT user, host FROM mysql.user;"

Remove what’s no longer needed.

Related: Linux server hardening checklist and auditd.

  • Security
  • Linux
  • Best Practices