The Principle of Least Privilege
The Principle of Least Privilege (often shortened to PoLP) is a simple idea with huge consequences: every user, program, and key on your system should have only the access it actually needs to do its job, and nothing more. If a thing only needs to read one file, it should not be able to delete the whole disk. This matters because attackers rarely break in through the front door. They sneak in through one small, over-permissioned account, then “move sideways” to take over everything. Least privilege shrinks the damage any single compromise can cause.
Why over-permissioning is so dangerous
When you give an account more power than it needs, you are creating what security people call a large “blast radius” — the area of damage if that account is ever stolen or misused. A web app that runs as root (the all-powerful admin user on Linux) can, if hacked, wipe your server. The same app running as a limited user can, at worst, mess up its own files.
The fix is to think in terms of deny by default, allow on purpose. Start with zero access and add only the specific permissions a task requires.
Least privilege is not a one-time setup. Permissions tend to “creep” upward over time as people grant access to fix something quickly and forget to remove it. Review access regularly.
Linux users and file permissions
On Ubuntu, every process runs as some user. The golden rule is: never run an application as root if it does not need root.
Create a dedicated, locked-down user for each service instead of reusing your login account.
# Create a system user that cannot log in and has no home shell
sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuser
Output:
(no output on success)
The --shell /usr/sbin/nologin part means nobody can open an interactive session as appuser — it exists only to run the app.
Next, give that user ownership of just its own files, not the whole system.
sudo chown -R appuser:appuser /opt/myapp
sudo chmod -R 750 /opt/myapp
The number 750 is a permission code: the owner can read/write/execute, the group can read/execute, and everyone else gets nothing.
| Setup | What it means | Verdict |
|---|---|---|
App runs as root | Full control of the machine | Avoid — huge blast radius |
App runs as dedicated appuser | Only its own files | Recommended |
Files set to chmod 777 | Anyone can read/write/run | Never — open to all local users |
Files set to chmod 640 / 750 | Tight owner-and-group access | Good default |
sudo: grant specific commands, not full power
sudo lets a normal user run commands as another user (usually root). The lazy way is to add someone to the sudo group, which gives them everything. The least-privilege way is to allow only the exact commands they need.
Edit the sudo rules safely with visudo, which checks your syntax before saving:
sudo visudo -f /etc/sudoers.d/deploy
Add a line that lets the deploy user restart only one service — nothing else:
# deploy can ONLY restart the myapp service, no password needed
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp
Now deploy can run sudo systemctl restart myapp but cannot run sudo rm, sudo bash, or anything else as root.
Avoid
NOPASSWD: ALL. That is the same as handing over the root password. Scope every sudo rule to specific command paths.
Database grants
Databases have their own permission system, and it is a classic place where over-permissioning hides. Your application should connect with an account that can touch only its own database, not the entire server.
Here is the wrong way — giving the app account total control in PostgreSQL:
-- DON'T do this: superuser can drop any database, read every table
ALTER USER myapp WITH SUPERUSER;
Here is the least-privilege way. Create a user that can only read and write rows in its own database’s tables:
sudo -u postgres psql
CREATE DATABASE shopdb;
CREATE USER myapp WITH PASSWORD 'a-long-random-password';
-- Let the app connect to just this one database
GRANT CONNECT ON DATABASE shopdb TO myapp;
-- Inside shopdb, allow normal data operations but NOT schema changes
\c shopdb
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO myapp;
The app can now run its queries but cannot drop tables, create new databases, or read other tenants’ data.
Service accounts and systemd hardening
When you run a service under systemd (Ubuntu’s service manager), you can lock it down further so that even if the app is hacked, it can barely reach the rest of the system. Edit the unit file at /etc/systemd/system/myapp.service:
[Service]
User=appuser
Group=appuser
# Block access to the home directories of real users
ProtectHome=true
# Give the service a private, read-only view of /usr, /boot, etc.
ProtectSystem=strict
# Allow writing only to this one directory
ReadWritePaths=/opt/myapp/data
# Stop the process from gaining new privileges (e.g. via setuid binaries)
NoNewPrivileges=true
Apply the changes:
sudo systemctl daemon-reload
sudo systemctl restart myapp
Output:
(no output on success — check status next)
sudo systemctl status myapp
Output:
● myapp.service - My Application
Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
Active: active (running) since Mon 2026-06-15 10:12:03 UTC; 3s ago
Main PID: 4821 (myapp)
Cloud IAM: the same idea, bigger scale
In the cloud, IAM (Identity and Access Management) is the system that decides who can do what. The temptation is to attach a wildcard policy like “allow all actions on all resources” — for example Action: "*" and Resource: "*" in AWS. This is the cloud equivalent of running everything as root.
Instead, write a policy that names the exact actions and the exact resources:
# AWS IAM policy: read objects from ONE bucket, nothing else
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "s3:GetObject"
Resource: "arn:aws:s3:::my-app-uploads/*"
Use short-lived roles (temporary identities) rather than long-lived access keys wherever you can. A leaked key that expires in an hour is far less useful to an attacker than one that lasts forever.
| Approach | Risk if leaked | When to use |
|---|---|---|
Wildcard policy "*" | Catastrophic — full account takeover | Never in production |
| Scoped policy (named actions + ARNs) | Limited to those resources | Default for every service |
| Long-lived access keys | Useful to attacker indefinitely | Only when roles truly aren’t possible |
| Short-lived role credentials | Expire quickly | Preferred for apps and CI/CD |
Best Practices
- Run every service as its own dedicated, non-login user — never as
root. - Scope
sudorules to specific command paths; avoidNOPASSWD: ALL. - Give database accounts rights to only their own database and only the operations they perform.
- Harden
systemdunits withProtectSystem,NoNewPrivileges, and tightReadWritePaths. - In the cloud, name exact IAM actions and resources — never use
"*"wildcards in production. - Prefer short-lived roles and rotated credentials over permanent keys.
- Audit permissions on a schedule and remove access nobody uses anymore.