Skip to content
DevOps devops security 6 min read

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.

SetupWhat it meansVerdict
App runs as rootFull control of the machineAvoid — huge blast radius
App runs as dedicated appuserOnly its own filesRecommended
Files set to chmod 777Anyone can read/write/runNever — open to all local users
Files set to chmod 640 / 750Tight owner-and-group accessGood 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.

ApproachRisk if leakedWhen to use
Wildcard policy "*"Catastrophic — full account takeoverNever in production
Scoped policy (named actions + ARNs)Limited to those resourcesDefault for every service
Long-lived access keysUseful to attacker indefinitelyOnly when roles truly aren’t possible
Short-lived role credentialsExpire quicklyPreferred for apps and CI/CD

Best Practices

  • Run every service as its own dedicated, non-login user — never as root.
  • Scope sudo rules to specific command paths; avoid NOPASSWD: ALL.
  • Give database accounts rights to only their own database and only the operations they perform.
  • Harden systemd units with ProtectSystem, NoNewPrivileges, and tight ReadWritePaths.
  • 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.
Last updated June 15, 2026
Was this helpful?