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_SERVICEto bind to ports below 1024 -
CAP_NET_RAWto use raw and packet sockets -
CAP_SYS_ADMINfor 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 rootwhen 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
List files that already carry file capabilities:
sudo getcap -r / 2>/dev/null
Typical setuid files are worth reviewing too:
sudo find / -xdev -perm -4000 -type f -printf '%M %u %g %p\n'
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
Verify it:
getcap /usr/local/bin/myapp
Expected output:
/usr/local/bin/myapp cap_net_bind_service=ep
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
Apply it:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
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=truehelps 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
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
Verify removal:
getcap /usr/local/bin/myapp
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
pingwith 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
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
This is the kind of pattern you should look for first:
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp
A practical audit workflow
When you want to replace broad privilege with something tighter, this sequence works well:
- identify what the program actually needs to do
- map that to the smallest capability that matches
- prefer service-level controls if the app is systemd-managed
- verify the file or service configuration after the change
- run a real functional test as the target non-root user
- 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
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
- Linux capabilities overview: https://man7.org/linux/man-pages/man7/capabilities.7.html
-
setcap(8)manual: https://man7.org/linux/man-pages/man8/setcap.8.html -
getcap(8)manual: https://man7.org/linux/man-pages/man8/getcap.8.html - systemd execution environment, including
AmbientCapabilities=andCapabilityBoundingSet=: https://www.freedesktop.org/software/systemd/man/systemd.exec.html - Linux ICMP and
ping_group_range: https://man7.org/linux/man-pages/man7/icmp.7.html
Top comments (0)