TL;DR: Serious application-level network filtering on Linux is possible using netfilter NFQUEUE
. However, NFQUEUE
is a low-level facility for developers. Wait until firewall solutions are created that provide abstraction through a user-friendly interface.
Update 14.02.2019: systemd
is using cgroupsv2
in combination with bpf
to enable per systemd-unit firewalling:
Application-level network filtering using iptables
The “owner” module
The owner
module allows to filter outgoing network traffic:
iptables -A OUTPUT -m owner --uid-owner 0 -j LOG
iptables -A OUTPUT -m owner --uid-owner 0 -j DROP
Use cases:
- log all traffic caused by root user
- prohibit or restrict network access of certain applications
More infos:
iptables -m owner --help
man iptables-extensions
⚠️ The owner
module can only filter outgoing traffic originating from a specific uid
or gid
. It cannot filter based on other attributes, such as pid
, path to executable, hash of executable and so on.
Example 1 - PoC
- Download busybox’ ping utility:
curl -O -L https://www.busybox.net/downloads/binaries/1.30.0-i686/busybox_PING
- Make it executable:
chmod +x busybox_PING
- Try to run it as user:
vagrant@lx-box ~ $ ./busybox_PING -w 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
ping: permission denied (are you root?) man capabilities
- The missing kernel capability is
CAP_NET_RAW
. - Add the missing capability to the downloaded executable:
root@lx-box ~ # getcap /home/vagrant/busybox_PING
root@lx-box ~ # setcap cap_net_raw+ep /home/vagrant/busybox_PING
root@lx-box ~ # getcap /home/vagrant/busybox_PING
/home/vagrant/busybox_PING = cap_net_raw+ep
- Try to run ping again:
vagrant@lx-box ~ $ ./busybox_PING -w 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=63 time=5.827 ms
^C
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 5.827/5.827/5.827 ms
vagrant@lx-box ~ $
- Now that everything has been prepared, let’s test the
iptables
owner
module. - Make sure the
iptables
chains are empty and configured withACCEPT
policies:
root@lx-box ~ # iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
- Next, deny outgoing ICMP packets system-wide:
iptables -A OUTPUT -p icmp --icmp-type 8 -m state --state NEW,ESTABLISHED,RELATED -j REJECT
iptables -A INPUT -p icmp --icmp-type 0 -m state --state ESTABLISHED,RELATED -j REJECT
… which results in:
root@lx-box ~ # iptables -L -n
Chain INPUT (policy ACCEPT)
target prot opt source destination
REJECT icmp -- 0.0.0.0/0 0.0.0.0/0 icmptype 0 state RELATED,ESTABLISHED reject-with icmp-port-unreachable
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
REJECT icmp -- 0.0.0.0/0 0.0.0.0/0 icmptype 8 state NEW,RELATED,ESTABLISHED reject-with icmp-port-unreachable
- Ping is now prohibited via
iptables
:
vagrant@lx-box ~ $ ./busybox_PING -w 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
ping: sendto: Operation not permitted
- Now, allow ping for user
vagrant
usingiptables
:
iptables -I OUTPUT -p icmp --icmp-type 8 -m owner --uid-owner vagrant -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -I INPUT -p icmp --icmp-type 0 -m state --state ESTABLISHED,RELATED -j ACCEPT
⚠️ The owner
module, of course, only works for outgoing packets, because iptables
only knows the pid
/gid
of local applications. For incoming packets (e.g. ping answer from a foreign system), ICMP traffic must be allowed system-wide. However, only ESTABLISHED
/RELATED
connections are allowed, no NEW
connections, which reduces exposure.
- Check
iptables
rules and note thatpkts
count for ICMP is 0:
root@lx-box ~ # iptables -L -n -v
Chain INPUT (policy ACCEPT 284 packets, 14172 bytes)
pkts bytes target prot opt in out source destination
0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 0 state RELATED,ESTABLISHED
0 0 REJECT icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 0 state RELATED,ESTABLISHED reject-with icmp-port-unreachable
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 210 packets, 19868 bytes)
pkts bytes target prot opt in out source destination
0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 8 owner UID match 1000 state NEW,RELATED,ESTABLISHED
0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 8 owner UID match 1000 state NEW,RELATED,ESTABLISHED
1 84 REJECT icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 8 state NEW,RELATED,ESTABLISHED reject-with icmp-port-unreachable
- Check if ping works, by sending 2 packets:
vagrant@lx-box ~ $ ./busybox_PING -w 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=63 time=12.635 ms
64 bytes from 8.8.8.8: seq=1 ttl=63 time=5.352 ms
^C
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 5.352/8.993/12.635 ms
vagrant@lx-box ~ $
It works 👍.
- Again, check
iptables
rules and verify if our “-m owner
”- rule was actually used:
root@lx-box ~ # iptables -L -n -v
Chain INPUT (policy ACCEPT 771 packets, 39604 bytes)
pkts bytes target prot opt in out source destination
2 168 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 0 state RELATED,ESTABLISHED
0 0 REJECT icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 0 state RELATED,ESTABLISHED reject-with icmp-port-unreachable
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 543 packets, 50832 bytes)
pkts bytes target prot opt in out source destination
2 168 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 8 owner UID match 1000 state NEW,RELATED,ESTABLISHED
0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 8 owner UID match 1000 state NEW,RELATED,ESTABLISHED
1 84 REJECT icmp -- * * 0.0.0.0/0 0.0.0.0/0 icmptype 8 state NEW,RELATED,ESTABLISHED reject-with icmp-port-unreachable
Example 2 - Application-level network filtering based on group ids (gid)
Create user groups for specific applications and associate iptables
rules with them (still easier than dealing with SELinux… 😏 ):
groupadd noinet
usermod -G noinet YOUR_USER # newgrp noinet
# place rule at top of OUTPUT chain
iptables -I OUTPUT 1 -m owner --gid-owner noinet -j REJECT
sg noinet -c "firefox"
[FF won't be able to connect to inet]
firefox
[FF will be able to connect to inet]
# remove iptables rule
iptables -L --line-numbers
iptables -D OUTPUT 1
Filtering network access per application using SELinux is described here: http://blog.siphos.be/2015/08/filtering-network-access-per-application/. Drawbacks:
iptables
rules & SELinux policies must be created.- This approach requires correct file system labeling (SELinux security attribute) of all applications, which is not the case with most Linux distributions that ship default SELinux policies.
Desktop system firewalls with application-level network filtering features
Two projects under active development, still in early stages:
- Douane: https://gitlab.com/douaneapp/Douane
- OpenSnitch: https://github.com/evilsocket/opensnitch
They are both making use of NFQUEUE
(https://home.regit.org/netfilter-en/using-nfqueue-and-libnetfilter_queue/). NFQUEUE
allows to delegate packet filtering decision making to a user-space software.
Application-level network filtering using other methods
Unshare
Take away net cap from Firefox, rendering it unusable:
root@lx-box ~ # ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=63 time=47.5 ms
^C
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 47.568/47.568/47.568/0.000 ms
root@lx-box ~ # unshare -n ping 8.8.8.8
connect: Network is unreachable