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
sudowhen 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
localhostor 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 appin 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
dockergroup 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_keysentries 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.