Tuesday, June 9, 2026

Introducing netcap --advanced

netcap has traditionally answered a narrow but useful question: which network-facing processes are running with capabilities? That is still a good starting point. Network daemons are usually the first place to look when reviewing local privilege because they are reachable by something outside the process itself.

The problem is that a useful security posture review needs a little more context. It is not enough to know that a process has a socket and some capabilities. An administrator also needs to know where the socket is bound, which interface makes it reachable, which systemd unit owns it, whether the process is still root, whether the bounding set was trimmed, whether ambient
capabilities are present, and whether basic exploit-resistance settings are enabled.

That is what netcap --advanced is for.

It is not a scanner in the network sense. It does not send packets. It reads the local system's own view of sockets and processes and turns that into an exposure and privilege report for the current network namespace.

netcap advanced joins local socket reachability with process privilege and hardening state.

What advanced mode changes

The historical mode is filtered. It looks for applications that use tcp, udp, raw, or packet sockets and also have capabilities. That is useful when the main question is "what network-facing process has privilege?"

Advanced mode changes the question. It inventories reachable binds and listeners regardless of whether the owning process currently has capabilities. Then it adds posture information for the owning process.

That distinction matters. A daemon with caps: (none) can still be important if it listens on every interface, runs as uid 0, has no seccomp filter, and is managed by a service unit that could be hardened. Conversely, a daemon with a small capability set may be acceptable if it binds only to loopback, runs as a dedicated user, has NoNewPrivileges=yes, and has a trimmed bounding set.

Advanced mode also covers protocol families that are easy to miss in ordinary checks. TCP listeners are included. UDP and UDPLITE bound sockets are included. RAW and PACKET sockets are included because they imply packet craft or packet capture behavior. SCTP and DCCP listeners are collected through NETLINK_SOCK_DIAG. VSOCK listeners are collected when the build and kernel support it. Bluetooth RFCOMM and HCI sockets are reported when the Bluetooth headers and procfs data are available.

This is why the output is organized as exposure planes:

  • INET (external)
  • INET (loopback)
  • LINK-LAYER
  • BLUETOOTH
  • VSOCK

The report is local to the current network namespace. If you run it in a container namespace, you get that namespace's view. If you run it on the host, you get the host namespace's view.

For full results, run it as root. The code needs to read other processes' /proc/<pid>/fd entries so it can map socket inodes back to process owners. It also needs enough network privilege to query sock_diag for some protocols. Without those permissions, the output is best effort and can be incomplete.

How the report is built

The design is a "join" across local kernel interfaces. First, netcap --advanced snapshots interface addresses with getifaddrs(). This gives it the names and addresses that will be used later when it decides whether a bind belongs under INET (external) or INET (loopback).

Second, it walks /proc. For each process, it reads enough metadata to build the process node: command name, executable path, real uid, cgroup-derived systemd unit name, capability sets through libcap-ng, ambient capability state, bounding set state, NoNewPrivs, seccomp mode, and the current LSM label when it can be read.

While it is walking /proc/<pid>/fd, it also builds a socket inode ownership map. Procfs exposes socket file descriptors as symlinks such as socket:[123456]. The listener tables later report socket inodes. The inode map is what lets netcap say "this listening SCTP socket belongs to this
process and this unit."

Third, it reads protocol-specific socket sources. The ordinary internet socket tables come from /proc/net/tcp/proc/net/tcp6/proc/net/udp/proc/net/udp6, and the related raw and udplite tables. Packet sockets come from /proc/net/packet. SCTP and DCCP listener data comes from NETLINK_SOCK_DIAG. VSOCK data comes from sock_diag when possible and falls back to /proc/net/vsock when it must. Bluetooth RFCOMM and HCI data comes from /proc/net/rfcomm/proc/net/hci, and adapter information under /sys/class/bluetooth.

Fourth, it projects endpoints into the tree. A listener bound to 127.0.0.1 goes under loopback. A listener bound to a specific external address goes under the interface that owns that address. A wildcard bind, such as 0.0.0.0 or ::, is expanded onto the non-loopback interfaces so the tree
shows where that wildcard is reachable.

That last step is one of the most useful parts of the design. A process did not literally call bind() once per interface. But from an administrator's point of view, a wildcard listener is reachable through every non-loopback address in the namespace. The output chooses the administrative view over the raw syscall view.

Reading the tree

This is the basic hierarchy:

plane -> interface -> protocol -> bind -> port -> process -> caps/defenses/flags
For VSOCK, there is no network interface, so the VSOCK endpoints are rendered directly under the VSOCK plane.

The process line gives identity:
python3 (pid=1234 uid=0 exe=/usr/bin/python3.14 unit=netcap-demo.service)
The comm value is the kernel process name and can be truncated. The exe field is the full executable path when procfs allows it to be read. The unit field is extracted from the cgroup hierarchy and is limited to service or scope names that are useful for remediation.

The caps line shows the permitted capabilities as libcap-ng sees them. The best value is:
caps: (none)
caps: (full) means the process has full root-like capability privilege. Individual names such as net_adminnet_rawsys_adminsetuid, or sys_ptrace need review in a network-facing process. The Linux capabilities(7) manual is the reference for what each capability allows.

Two annotations deserve special attention:

  • [ambient-present]
  • [open-ended-bounding]

Ambient capabilities are inherited across execve() and can surprise people because child programs keep the privilege unless the service takes care to clear it. An open-ended bounding set is not active privilege by itself, but it means the ceiling was not trimmed. A child process may still have a path to regain capabilities through file capabilities or other transitions.

The defenses section summarizes process hardening:

defenses
  runs_as_nonroot: no
  no_new_privs: no
  seccomp: disabled
  lsm: system_u:system_r:unconfined_service_t:s0
runs_as_nonroot is derived from the real uid. no_new_privs and seccomp
come from /proc/<pid>/status, which is documented in proc_pid_status(5). The kernel's
no_new_privs documentation explains why it is useful: it prevents a task from gaining new privilege through later exec transitions.

The flags section is where the report adds interpretation:

  • wildcard-bind
  • privileged-caps
  • reuseport
  • hypervisor-plane
  • ssh-on-vsock-port-22
  • proximity-plane

wildcard-bind means the daemon is bound to all addresses for that address family. privileged-caps means the process has one of the capability classes that is especially interesting for attack-surface review. reuseport means SO_REUSEPORT was detected on the socket. hypervisor-plane means VSOCK. ssh-on-vsock-port-22 means a VSOCK listener is using port 22. That does not prove it is SSH, but it is worth investigating because port 22 has a very specific operational meaning. proximity-plane means Bluetooth reachability.

First commands

Start by finding the interface names in the namespace you are reviewing.

$ netcap --advanced --list-interfaces
enp5s0
lo

Then collect the full tree without color so the output can be pasted into a ticket or report.

$ netcap --advanced --no-color

If one interface is the one that matters, filter the report.

netcap --advanced --interface enp5s0 --no-color
├─ INET (external)
│  └─ enp5s0
│     ├─ raw6
│     │  └─ *
│     │     └─ 58
│     │        └─ NetworkManager (pid=1999 uid=0 
│     │           exe=/usr/bin/NetworkManager unit=NetworkManager.service)
│     │           ├─ caps: dac_override, kill, setgid, setuid, 
│     │           │  net_bind_service, net_admin, net_raw, sys_module, 
│     │           │  sys_chroot, audit_write [open-ended-bounding]
│     │           ├─ defenses
│     │           │  ├─ runs_as_nonroot: no
│     │           │  ├─ no_new_privs: no
│     │           │  ├─ seccomp: disabled
│     │           │  └─ lsm: system_u:system_r:NetworkManager_t:s0
│     │           └─ flags
│     │              ├─ wildcard-bind
│     │              └─ privileged-caps
│     ├─ udp
│     │  └─ *
│     │     ├─ 5353
│     │     │  └─ avahi-daemon (pid=1735 uid=70 
│     │     │     exe=/usr/bin/avahi-daemon unit=avahi-daemon.service)
│     │     │     ├─ caps: (none)
│     │     │     ├─ defenses
│     │     │     │  ├─ runs_as_nonroot: yes
│     │     │     │  ├─ no_new_privs: no
│     │     │     │  ├─ seccomp: disabled
│     │     │     │  └─ lsm: system_u:system_r:avahi_t:s0
│     │     │     └─ flags
│     │     │        ├─ wildcard-bind
│     │     │        └─ reuseport
│     │     ├─ 50795
│     │     │  └─ firefox (pid=6947 uid=4325 
│     │     │     exe=/usr/lib64/firefox/firefox)
│     │     │     ├─ caps: (none)
│     │     │     ├─ defenses
│     │     │     │  ├─ runs_as_nonroot: yes
│     │     │     │  ├─ no_new_privs: no
│     │     │     │  ├─ seccomp: disabled
│     │     │     │  └─ lsm: 
│     │     │     │     unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.
│     │     │     │     c1023
│     │     │     └─ flags
│     │     │        └─ wildcard-bind
│     │     └─ 53013
│     │        └─ firefox (pid=6947 uid=4325 
│     │           exe=/usr/lib64/firefox/firefox)
│     │           ├─ caps: (none)
│     │           ├─ defenses
│     │           │  ├─ runs_as_nonroot: yes
│     │           │  ├─ no_new_privs: no
│     │           │  ├─ seccomp: disabled
│     │           │  └─ lsm: 
│     │           │     unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1
│     │           │     023
│     │           └─ flags
│     │              └─ wildcard-bind
│     └─ udp6
│        └─ *
│           └─ 5353
│              └─ avahi-daemon (pid=1735 uid=70 exe=/usr/bin/avahi-daemon 
│                 unit=avahi-daemon.service)
│                 ├─ caps: (none)
│                 ├─ defenses
│                 │  ├─ runs_as_nonroot: yes
│                 │  ├─ no_new_privs: no
│                 │  ├─ seccomp: disabled
│                 │  └─ lsm: system_u:system_r:avahi_t:s0
│                 └─ flags
│                    ├─ wildcard-bind
│                    └─ reuseport
└─ LINK-LAYER
   └─ enp5s0
      └─ packet
         └─ *
            └─ 2054
               └─ NetworkManager (pid=1999 uid=0 
                  exe=/usr/bin/NetworkManager unit=NetworkManager.service)
                  ├─ caps: dac_override, kill, setgid, setuid, 
                  │  net_bind_service, net_admin, net_raw, sys_module, 
                  │  sys_chroot, audit_write [open-ended-bounding]
                  ├─ defenses
                  │  ├─ runs_as_nonroot: no
                  │  ├─ no_new_privs: no
                  │  ├─ seccomp: disabled
                  │  └─ lsm: system_u:system_r:NetworkManager_t:s0
                  └─ flags
                     └─ privileged-caps
For automation, use JSON.
netcap --advanced --json
The tree is better for human triage. JSON is better for a nightly report, configuration drift check, or a small script that alerts on flags such as wildcard-bind, privileged-capshypervisor-plane, or proximity-plane.

Limits

netcap --advanced is a local snapshot. A process can start or exit while the report is being built. Some procfs entries can be hidden by permissions or mount options. SCTP, DCCP, and VSOCK discovery depends on kernel support and, for the sock_diag path, sufficient privilege. Bluetooth adapter mapping can be heuristic when the kernel does not expose enough adapter information for a socket.

The report also does not tell you if a service is supposed to be there. It tells you what is there, where it is reachable, who it runs as, what capabilities it has, and what hardening is visible. The administrator still has to decide whether that service belongs on the machine.

The upstream project is here: https://github.com/stevegrubb/libcap-ng.
For background on Linux capabilities, start with capabilities(7).
For VSOCK-specific background, see vsock(7).