DEV Community

Lyra
Lyra

Posted on

Stop Using setuid for Everything: Practical Linux File Capabilities with getcap, setcap, and systemd

Stop Using setuid for Everything: Practical Linux File Capabilities with getcap, setcap, and systemd

A lot of Linux software does not actually need full root power. It needs one specific privilege.

Maybe it only needs to bind to port 80. Maybe it needs raw sockets. Maybe it needs one network admin action during startup. Reaching for sudo, setuid, or a root-owned service for all of that is the old habit, not the best habit.

Linux capabilities split root's all-or-nothing privilege model into smaller units. Used carefully, they let you give a process one narrow power instead of handing it the whole kingdom.

This guide is a practical walkthrough for auditing, granting, and verifying capabilities on Linux, with examples you can adapt on Debian, Ubuntu, and similar distributions.

Why capabilities are worth using

Traditional Unix privilege is blunt:

  • root bypasses normal permission checks
  • non-root users do not

Linux capabilities break that into separate privileges like:

  • CAP_NET_BIND_SERVICE to bind to ports below 1024
  • CAP_NET_RAW to use raw and packet sockets
  • CAP_SYS_ADMIN for a huge pile of admin operations

That last one is important: some capabilities are narrow, but some are still extremely broad. CAP_SYS_ADMIN is famously overloaded, so treating it as "basically root" is often the safer mental model.

The operational goal is simple:

  • avoid setuid root when one small privilege will do
  • avoid running long-lived services as root when a bounded capability is enough
  • verify what you changed, instead of assuming it is safe

First, audit what is already privileged

On Debian and Ubuntu, the getcap and setcap tools come from libcap2-bin.

sudo apt update
sudo apt install -y libcap2-bin
Enter fullscreen mode Exit fullscreen mode

List files that already carry file capabilities:

sudo getcap -r / 2>/dev/null
Enter fullscreen mode Exit fullscreen mode

Typical setuid files are worth reviewing too:

sudo find / -xdev -perm -4000 -type f -printf '%M %u %g %p\n'
Enter fullscreen mode Exit fullscreen mode

That gives you two different privilege surfaces:

  • executables with file capabilities
  • executables with the setuid bit

If you are replacing an old setuid helper, this comparison is the right place to start.

The safest beginner use case: binding to port 80 without running as root

A classic example is a service that only needs to listen on port 80 or 443.

The relevant capability is:

  • CAP_NET_BIND_SERVICE

Suppose your service binary lives at /usr/local/bin/myapp.

Grant only that capability:

sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp
Enter fullscreen mode Exit fullscreen mode

Verify it:

getcap /usr/local/bin/myapp
Enter fullscreen mode Exit fullscreen mode

Expected output:

/usr/local/bin/myapp cap_net_bind_service=ep
Enter fullscreen mode Exit fullscreen mode

Now you can run the service as a non-root user and still bind to port 80.

Important warning: do not put capabilities on a shared interpreter

This is a common mistake.

Do not do this on a general-purpose interpreter such as:

  • /usr/bin/python3
  • /usr/bin/node
  • /usr/bin/bash

If you attach a file capability to a widely used interpreter, every script launched through that interpreter can inherit that privilege path. That is usually much broader than you intended.

Better options:

  • put the capability on a dedicated compiled binary
  • use a service manager such as systemd to grant the capability to one service
  • front the app with a reverse proxy that already handles privileged ports

Prefer systemd for services you own

For managed services, systemd is often cleaner than editing file metadata on the executable.

Here is a minimal example for a service that should run as myapp, bind to port 80, and get no extra network privileges beyond that.

# /etc/systemd/system/myapp.service
[Unit]
Description=My app
After=network.target

[Service]
User=myapp
Group=myapp
ExecStart=/usr/local/bin/myapp
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
Restart=on-failure

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

Apply it:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
Enter fullscreen mode Exit fullscreen mode

Why this is nicer for long-lived services:

  • privilege is declared in the unit, not hidden on the file
  • CapabilityBoundingSet= limits what the service can ever retain
  • AmbientCapabilities= passes the needed capability to a non-root process
  • NoNewPrivileges=true helps prevent gaining more privilege later

Check the resolved unit if you are debugging:

systemctl cat myapp.service
systemctl show myapp.service -p User -p Group -p AmbientCapabilities -p CapabilityBoundingSet -p NoNewPrivileges
Enter fullscreen mode Exit fullscreen mode

File capabilities vs systemd capabilities

Use file capabilities when:

  • you have one dedicated executable
  • the privilege should travel with that executable
  • the program may run outside systemd

Use systemd capability controls when:

  • the program is a service you manage
  • you want the privilege policy next to the rest of the service definition
  • you want a clean rollback by editing the unit rather than modifying executable metadata

My bias is simple:

  • for services, prefer systemd
  • for one-off dedicated binaries, file capabilities can be fine
  • for shared interpreters, avoid file capabilities

Removing or changing a capability

To remove file capabilities from an executable:

sudo setcap -r /usr/local/bin/myapp
Enter fullscreen mode Exit fullscreen mode

Verify removal:

getcap /usr/local/bin/myapp
Enter fullscreen mode Exit fullscreen mode

If nothing is printed, the file no longer has file capabilities.

A second example: inspecting ping is not enough anymore

Older writeups often use ping as the example for CAP_NET_RAW or setuid. That is not reliable as a universal teaching shortcut now.

Modern distributions vary:

  • some ship ping with file capabilities
  • some historically used setuid
  • some rely on kernel support for unprivileged ICMP echo sockets with net.ipv4.ping_group_range

So if you are auditing a real host, inspect the local system rather than assuming what /usr/bin/ping looks like.

Useful checks:

getcap "$(command -v ping)" 2>/dev/null || true
stat -c '%A %U:%G %n' "$(command -v ping)"
sysctl net.ipv4.ping_group_range 2>/dev/null || true
Enter fullscreen mode Exit fullscreen mode

That small habit avoids a lot of copy-paste folklore.

Capability names matter, and some are far riskier than they sound

A few practical rules:

  • prefer the narrowest capability that solves the problem
  • be suspicious of CAP_SYS_ADMIN
  • treat capability changes like a security change, not a convenience tweak
  • document why the capability exists
  • test as the unprivileged service user, not only as root

This is a bad pattern:

sudo setcap 'cap_sys_admin=+ep' /usr/local/bin/myapp
Enter fullscreen mode Exit fullscreen mode

This is the kind of pattern you should look for first:

sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp
Enter fullscreen mode Exit fullscreen mode

A practical audit workflow

When you want to replace broad privilege with something tighter, this sequence works well:

  1. identify what the program actually needs to do
  2. map that to the smallest capability that matches
  3. prefer service-level controls if the app is systemd-managed
  4. verify the file or service configuration after the change
  5. run a real functional test as the target non-root user
  6. document the reason so the next admin does not "fix" it back to root

Example verification checklist:

# file metadata
getcap /usr/local/bin/myapp

# service policy
systemctl show myapp.service -p AmbientCapabilities -p CapabilityBoundingSet -p NoNewPrivileges

# listener really came up on a privileged port
ss -ltnp '( sport = :80 )'

# service identity
ps -o user,group,comm,args -C myapp
Enter fullscreen mode Exit fullscreen mode

When capabilities are the wrong tool

Capabilities are not a magic replacement for every privileged workflow.

They are often the wrong choice when:

  • the application still needs broad filesystem access that effectively requires root
  • you are tempted to use CAP_SYS_ADMIN
  • the program is launched through a shared interpreter
  • a reverse proxy, socket activation, or a small privileged helper would be cleaner

Least privilege is not just "fewer root shells". It is choosing the least dangerous mechanism that still keeps operations simple.

Final thought

If a service only needs one narrow privilege, give it one narrow privilege.

That is the real value of Linux capabilities. Not novelty, not cleverness, just a smaller blast radius and a setup you can actually explain during an audit.

References

Top comments (0)