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

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

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

Why capabilities are worth using

First, audit what is already privileged

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

Important warning: do not put capabilities on a shared interpreter

Prefer systemd for services you own

File capabilities vs systemd capabilities

Removing or changing a capability

A second example: inspecting ping is not enough anymore

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

A practical audit workflow

When capabilities are the wrong tool

Final thought

References 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. Traditional Unix privilege is blunt: Linux capabilities break that into separate privileges like: 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: On Debian and Ubuntu, the getcap and setcap tools come from libcap2-bin. List files that already carry file capabilities: Typical setuid files are worth reviewing too: That gives you two different privilege surfaces: If you are replacing an old setuid helper, this comparison is the right place to start. A classic example is a service that only needs to listen on port 80 or 443. The relevant capability is: Suppose your service binary lives at /usr/local/bin/myapp. Grant only that capability: Now you can run the service as a non-root user and still bind to port 80. This is a common mistake. Do not do this on a general-purpose interpreter such as: 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. 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. Why this is nicer for long-lived services: Check the resolved unit if you are debugging: Use file capabilities when: Use systemd capability controls when: To remove file capabilities from an executable: If nothing is printed, the file no longer has file capabilities. 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: So if you are auditing a real host, inspect the local system rather than assuming what /usr/bin/ping looks like. That small habit avoids a lot of copy-paste folklore. A few practical rules: This is a bad pattern: This is the kind of pattern you should look for first: When you want to replace broad privilege with something tighter, this sequence works well: Example verification checklist: Capabilities are not a magic replacement for every privileged workflow. They are often the wrong choice when: Least privilege is not just "fewer root shells". It is choosing the least dangerous mechanism that still keeps operations simple. 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. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y libcap2-bin -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y libcap2-bin -weight: 600;">sudo -weight: 500;">apt -weight: 500;">update -weight: 600;">sudo -weight: 500;">apt -weight: 500;">install -y libcap2-bin -weight: 600;">sudo getcap -r / 2>/dev/null -weight: 600;">sudo getcap -r / 2>/dev/null -weight: 600;">sudo getcap -r / 2>/dev/null -weight: 600;">sudo find / -xdev -perm -4000 -type f -printf '%M %u %g %p\n' -weight: 600;">sudo find / -xdev -perm -4000 -type f -printf '%M %u %g %p\n' -weight: 600;">sudo find / -xdev -perm -4000 -type f -printf '%M %u %g %p\n' -weight: 600;">sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp -weight: 600;">sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp -weight: 600;">sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp getcap /usr/local/bin/myapp getcap /usr/local/bin/myapp getcap /usr/local/bin/myapp /usr/local/bin/myapp cap_net_bind_service=ep /usr/local/bin/myapp cap_net_bind_service=ep /usr/local/bin/myapp cap_net_bind_service=ep # /etc/systemd/system/myapp.-weight: 500;">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 # /etc/systemd/system/myapp.-weight: 500;">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 # /etc/systemd/system/myapp.-weight: 500;">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 -weight: 600;">sudo -weight: 500;">systemctl daemon-reload -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable --now myapp.-weight: 500;">service -weight: 600;">sudo -weight: 500;">systemctl daemon-reload -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable --now myapp.-weight: 500;">service -weight: 600;">sudo -weight: 500;">systemctl daemon-reload -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">enable --now myapp.-weight: 500;">service -weight: 500;">systemctl cat myapp.-weight: 500;">service -weight: 500;">systemctl show myapp.-weight: 500;">service -p User -p Group -p AmbientCapabilities -p CapabilityBoundingSet -p NoNewPrivileges -weight: 500;">systemctl cat myapp.-weight: 500;">service -weight: 500;">systemctl show myapp.-weight: 500;">service -p User -p Group -p AmbientCapabilities -p CapabilityBoundingSet -p NoNewPrivileges -weight: 500;">systemctl cat myapp.-weight: 500;">service -weight: 500;">systemctl show myapp.-weight: 500;">service -p User -p Group -p AmbientCapabilities -p CapabilityBoundingSet -p NoNewPrivileges -weight: 600;">sudo setcap -r /usr/local/bin/myapp -weight: 600;">sudo setcap -r /usr/local/bin/myapp -weight: 600;">sudo setcap -r /usr/local/bin/myapp getcap /usr/local/bin/myapp getcap /usr/local/bin/myapp getcap /usr/local/bin/myapp 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 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 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 -weight: 600;">sudo setcap 'cap_sys_admin=+ep' /usr/local/bin/myapp -weight: 600;">sudo setcap 'cap_sys_admin=+ep' /usr/local/bin/myapp -weight: 600;">sudo setcap 'cap_sys_admin=+ep' /usr/local/bin/myapp -weight: 600;">sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp -weight: 600;">sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp -weight: 600;">sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp # file metadata getcap /usr/local/bin/myapp # -weight: 500;">service policy -weight: 500;">systemctl show myapp.-weight: 500;">service -p AmbientCapabilities -p CapabilityBoundingSet -p NoNewPrivileges # listener really came up on a privileged port ss -ltnp '( sport = :80 )' # -weight: 500;">service identity ps -o user,group,comm,args -C myapp # file metadata getcap /usr/local/bin/myapp # -weight: 500;">service policy -weight: 500;">systemctl show myapp.-weight: 500;">service -p AmbientCapabilities -p CapabilityBoundingSet -p NoNewPrivileges # listener really came up on a privileged port ss -ltnp '( sport = :80 )' # -weight: 500;">service identity ps -o user,group,comm,args -C myapp # file metadata getcap /usr/local/bin/myapp # -weight: 500;">service policy -weight: 500;">systemctl show myapp.-weight: 500;">service -p AmbientCapabilities -p CapabilityBoundingSet -p NoNewPrivileges # listener really came up on a privileged port ss -ltnp '( sport = :80 )' # -weight: 500;">service identity ps -o user,group,comm,args -C myapp - root bypasses normal permission checks - non-root users do not - 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 - 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 - executables with file capabilities - executables with the setuid bit - CAP_NET_BIND_SERVICE - /usr/bin/python3 - /usr/bin/node - /usr/bin/bash - put the capability on a dedicated compiled binary - use a -weight: 500;">service manager such as systemd to grant the capability to one -weight: 500;">service - front the app with a reverse proxy that already handles privileged ports - privilege is declared in the unit, not hidden on the file - CapabilityBoundingSet= limits what the -weight: 500;">service can ever retain - AmbientCapabilities= passes the needed capability to a non-root process - NoNewPrivileges=true helps prevent gaining more privilege later - you have one dedicated executable - the privilege should travel with that executable - the program may run outside systemd - the program is a -weight: 500;">service you manage - you want the privilege policy next to the rest of the -weight: 500;">service definition - you want a clean rollback by editing the unit rather than modifying executable metadata - for services, prefer systemd - for one-off dedicated binaries, file capabilities can be fine - for shared interpreters, avoid file capabilities - 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 - 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 -weight: 500;">service user, not only as root - identify what the program actually needs to do - map that to the smallest capability that matches - prefer -weight: 500;">service-level controls if the app is systemd-managed - verify the file or -weight: 500;">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 - 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 - 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= and CapabilityBoundingSet=: 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