skip to content

You run setcap cap_net_bind_service=+ep on /usr/local/bin/api and it binds port 80 correctly. After the release pipeline copies that same binary to another host with tar, the service fails with a permission error on bind. Where did the capability go?

level: middleimportance: should knowfreq 45%

answer

  1. not stored in the mode bits
  2. metadata that copies quietly leave behind
  3. an extended attribute, security namespace
  4. tar and cp need an explicit flag
  5. nosuid mounts ignore it too

basics

~20 s

File capabilities live in the security.capability extended attribute on the inode, not in the permission bits. Copying, archiving or rebuilding a binary drops that attribute unless extended attributes are explicitly preserved, so setcap has to be re-run on the target host.

solid answer

~50 s

`setcap` does not change the file's mode; it writes an extended attribute named `security.capability` on the inode. That attribute is a separate piece of metadata, and most everyday tools ignore it: plain `cp`, `tar` without `--xattrs`, `install`, a package build, a `git checkout`, and any editor or linker that writes a fresh file all produce an inode with no capability at all. Writing the attribute also requires privilege — `CAP_SETFCAP` — so even an xattr-aware copy performed as an unprivileged user will silently lose it. Two more things erase it in practice: a destination filesystem that does not store security-namespace attributes, and a mount carrying the `nosuid` option, which makes the kernel ignore file capabilities entirely. The robust fix is to treat the capability as part of deployment, not part of the artifact: run `setcap` on the target host after installation, from the package's post-install step or the configuration-management run.

code

bash · 6 lines
bash
sudo setcap cap_net_bind_service=+ep ./api
getfattr -n security.capability ./api
cp ./api /tmp/api-copy
getcap /tmp/api-copy
sudo cp --preserve=xattr ./api /tmp/api-kept
getcap /tmp/api-kept

go deeper

for a junior

Remember that setcap stores its grant as extended-attribute metadata on the file, not in the mode bits, so a copied or rebuilt binary usually arrives without it and getcap prints nothing.

for a middle

Explain which operations drop the attribute and why — plain cp, tar without --xattrs, recompiling, and unprivileged copies that lack CAP_SETFCAP — and name the extended attribute involved.

for a senior

Diagnose it on a live host in the right order: compare getcap on both machines, check the mount for nosuid, confirm the filesystem stores security-namespace attributes, then read the process's capability masks.

for a principal

Decide where privilege is allowed to live. Treat a file capability as deployment state applied by packaging or configuration management, or keep the artifact ordinary and grant capabilities at the service definition so every review sees them in one place.

## What setcap actually writes A file capability is not a permission bit. `setcap` serialises a small structure — a permitted set, an inheritable set, and an effective flag — and stores it in the extended attribute `security.capability` on the file's inode. `getcap` is just a pretty-printer for that attribute; `getfattr -n security.capability /usr/local/bin/api` shows the raw bytes. Because it is an extended attribute rather than part of the mode, `ls -l` shows nothing unusual. A binary with `cap_net_bind_service=ep` looks exactly like any other `0755` file, which is the first reason this trips people up. ## Why the attribute disappears Extended attributes in the `security` namespace are not carried along by default. Every one of these produces a file without the capability: - `cp src dst` — copies data and, with `-p`, mode, ownership and timestamps, but not extended attributes. You need `--preserve=xattr` (or `-a`, which implies it). - `tar` without `--xattrs` on both create and extract. Most CI pipelines tar an artifact directory with default flags. - `install`, `rsync` without `-X`, `scp`, a container image build step, `git checkout`, or a package manager unpacking a payload that never recorded the attribute. - Anything that replaces the file rather than modifying it: recompiling, re-linking, or an editor that writes a temp file and renames it over the original. The capability was on the old inode, which is now unlinked. On top of that, setting the attribute is itself privileged: it requires `CAP_SETFCAP`, normally meaning root. So an xattr-preserving copy run as an unprivileged user still cannot recreate it — the copy either fails to set it or silently ends up without it. ``` sudo setcap cap_net_bind_service=+ep ./api cp ./api /tmp/api-copy getcap /tmp/api-copy # prints nothing sudo cp --preserve=xattr ./api /tmp/api-kept getcap /tmp/api-kept # /tmp/api-kept cap_net_bind_service=ep ``` ## The two environment traps Even a correctly copied file can be inert. First, the destination filesystem must be able to store security-namespace attributes. ext4, XFS and btrfs do. A typical NFS mount, a `vfat` USB stick, and some network or overlay setups do not, and the `setcap` call fails outright or the attribute never survives. Second, the mount options matter. `nosuid` tells the kernel not to honour set-user-ID and set-group-ID bits *or file capabilities* for programs executed from that filesystem. If `/usr/local` or a data volume is mounted `nosuid` — common for volumes holding uploaded or user-writable content — the binary executes with no capability and no error message at exec time; you only see the failure when the privileged operation is attempted. `mount | grep nosuid` is the check. ## Reading the failure correctly The symptom in the question is the classic one: it works where you set it, fails where you deployed it, and the binaries are byte-for-byte identical. That is the signature of metadata loss rather than a code or configuration difference. The diagnostic sequence is short — compare `getcap` on both hosts, then `mount` for `nosuid`, then check the filesystem type. It is also worth knowing that file capabilities are keyed to the file itself, so a package upgrade that replaces the binary silently reverts it. If the capability was applied by hand once, the next update breaks the service and nobody remembers why. ## Where the grant belongs The durable answer is that a file capability is deployment state, not build state. Apply it where the file lands: - from the package's post-install script, so upgrades reapply it; - from a configuration-management resource that asserts the capability on the path; - or avoid the file capability entirely and have the service manager grant the capability to the process at start-up, so the binary on disk stays ordinary and nothing about copying it matters. The last option is usually preferable at fleet scale: it keeps privilege out of the artifact, survives rebuilds and reinstalls automatically, and leaves one place — the service definition — where a reviewer can see what the process is allowed to do.

  • Why does a plain user copy lose the capability even with cp --preserve=xattr?
    Writing `security.capability` is a privileged operation gated by `CAP_SETFCAP`. An unprivileged process may read the attribute but not create it on the destination inode, so the copy completes with the data intact and the capability missing. Only a privileged copy — or a later `setcap` run as root — can restore it.
  • A package upgrade replaced the binary and the service broke again. How do you make the grant survive?
    Stop applying it by hand. Put the `setcap` call in the package's post-install step so every upgrade reapplies it, assert it from configuration management, or move the grant off the file entirely and have the service manager give the process the capability at start-up. Manual `setcap` on a package-managed path is always one update away from being lost.
  • The file shows the right capability under getcap but the process still cannot bind port 80. What else would you check?
    Check the mount. `nosuid` makes the kernel ignore file capabilities as well as set-user-ID bits for anything executed from that filesystem, with no error at exec time. Also confirm the capability actually reached the process by reading CapPrm and CapEff in `/proc/<pid>/status`, and check whether something in the start-up chain restricted the bounding set.

saying these in an interview costs you the question

  • setcap changes the file's permission bits
  • Any cp or tar carries file capabilities along
  • Capabilities work on any filesystem, including NFS
  • A rebuild of the binary keeps its capability
  • nosuid only affects setuid binaries, not capabilities

context