Firewalls under Linux

A firewall is software that closes ports to prevent either hacking (from the outside) or telemetry (from the inside). Servers traditionally receive the brunt of outside attempts, while clients (phones, notebooks, work stations) may have untrusted apps installed. We look at Linux servers and clients, and briefly at Windows clients.

Port Basics

Two network protocols are typically used today (on the transport layer):

The majority of connections are tcp because they are more stable, delivering information if something goes wrong; however all this comes with a performance cost. Gaming and other quickness-oriented applications use udp and do not care if one packet or the other gets lost, as long as everything is fast. Some services may use both, typically udp for quickness with a tcp fall-back.

Every connection has a source (client) and destination (server). When a client initiates a connection to a server protocol:ip:port, it also has to tell the server where to send the reply, as in client protocol:ip:port. Server ports are typically in the range 0..1024 (well-known ports) while client ports are typically in the range 49152..65535 (ephemeral ports). Servers may, after an initial tcp connection, switch to an ephemeral port for the remainder of the communication. From my personal experience, any tcp port above 10000 and any udp port above 1024 seems to be treated as ephemeral.

Linux: Server

On a Linux server, typical incoming ports are 22 (ssh, tcp+udp), 443 (https, tcp), and sometimes 67 (dhcp server, udp), 993 (imaps, tcp) or 995 (pop3s, tcp), and 989-990 (ftps, tcp).

Typical outgoing ports include all of the above and additionally 20-21 (ftp, tcp), 53 (dns, udp+tcp), 68 (dhcp client, udp), 80 (http, tcp), 123 (ntp, udp), 465 (smtps, tcp) and 8245 (dyndns, tcp).

We first look at the console to see which services are transmitting data:

netstat -a --tcp --udp
iptraf-ng

... and then go the old-fashioned iptables way:

# this host (input):
iptables -A INPUT -p icmp -j ACCEPT
iptables -A INPUT -p tcp --dport ssh -j ACCEPT
iptables -A INPUT -p tcp --dport https -j ACCEPT
iptables -A INPUT -p tcp --dport imaps -j ACCEPT
iptables -A INPUT -p udp --dport 1024:65535 -j ACCEPT
iptables -A INPUT -p tcp --dport 10000:65535 -j ACCEPT
iptables -A INPUT -p tcp ! --syn -j ACCEPT

# this host (output):
iptables -A OUTPUT -p icmp -j ACCEPT
iptables -A OUTPUT -p tcp --dport ssh -j ACCEPT
iptables -A OUTPUT -p tcp --dport https -j ACCEPT
iptables -A OUTPUT -p tcp --dport http -j ACCEPT
iptables -A OUTPUT -p tcp --dport ftp -j ACCEPT
iptables -A OUTPUT -p tcp --dport ftp-data -j ACCEPT
iptables -A OUTPUT -p udp --dport ntp -j ACCEPT
iptables -A OUTPUT -p tcp --dport domain -j ACCEPT
iptables -A OUTPUT -p udp --dport domain -j ACCEPT
iptables -A OUTPUT -p tcp --dport pop3 -j ACCEPT
iptables -A OUTPUT -p tcp --dport smtps -j ACCEPT
iptables -A OUTPUT -p udp --dport 1024:65535 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 10000:65535 -j ACCEPT
iptables -A OUTPUT -p tcp ! --syn -j ACCEPT

# restrict access conservatively:
iptables -P FORWARD DROP
iptables -P INPUT DROP
iptables -P OUTPUT DROP

One optional thing is to allow any follow-up tcp packets, i.e. everything after the initial SYN (last line per section); this makes the tcp 10000:65535 (one line above) superfluous. And ping uses a non-tcp/udp protocol called icmp, which we also allow.

Linux: Client

On a client, typical outgoing ports are 20-21 (ftp, tcp), 22 (ssh, udp+tcp), 53 (dns, udp+tcp), 68 (dhcp client, udp), 80 (http, tcp), 123 (ntp, udp), 443 (https, tcp), 993 (imaps, tcp) or 995 (pops, tcp), 989-990 (ftps, tcp).

Typical incoming ports are 22 (ssh, udp+tcp).

Traditionally, Linux clients have not used firewalls because when all apps are open source, any unwanted behaviour would most probably be found by someone on the Internet and would then become widely known. However with binary-only apps (e.g. gog galaxy or steam) becoming more popular, we must assume malicious behaviour too.

We use the more comfortable ufw [docs] [blog1] which has a GUI called gufw [docs]; it uses iptables [chains] under the hood. Rules are host-wide and saved in /etc/ufw/, with /etc/default/ufw also pertinent.

apt-get install ufw gufw

nano -w /etc/default/ufw
    IPV6=yes

# because we will switch output default to DROP
nano -w /etc/ufw/before.rules
    -A ufw-before-output -p icmp --icmp-type destination-unreachable -j ACCEPT
    -A ufw-before-output -p icmp --icmp-type source-quench -j ACCEPT
    -A ufw-before-output -p icmp --icmp-type time-exceeded -j ACCEPT
    -A ufw-before-output -p icmp --icmp-type parameter-problem -j ACCEPT
    -A ufw-before-output -p icmp --icmp-type echo-request -j ACCEPT
    
    # output equivalents (dhcp, dns) for input defaults
    -A ufw-before-output -p udp --sport 68 --dport 67 -j ACCEPT
    -A ufw-before-output -p udp --dport domain -j ACCEPT
    -A ufw-before-output -p tcp --dport domain -j ACCEPT

ufw reload

clear && tail -f /var/log/ufw.log

With gufw you can start a Windows-like click-fest and save the results in a profile, saved in /etc/gufw/ e.g. as Home.profile, from which you can extract the ufw commands. We do it manually here:

sudo su    # you must be root
ufw enable # back: ufw disable
clear && ufw status verbose

ufw default deny incoming
ufw default deny outgoing
ping heise.de # with the before.rules above, should still work

ufw allow out ssh
ssh other.local

ufw allow out 9001
tor-browser

ufw allow out imaps
ufw allow out smtps
thunderbird

ufw allow out ntp
timedatectl status
systemctl restart systemd-timesyncd
systemctl status systemd-timesyncd.service

ufw allow out 6881:6999/tcp # torrent for new Linux distros
ufw allow out 6881:6999/udp
ufw allow in 6881:6999/tcp
ufw allow in 6881:6999/udp

By default, out dns and https are enabled, as well as in ssh. A level below ufw command configurability, in (but not out) dns and dhcp are visible in iptables -L INPUT; and "all except SYN" is visible in iptables -L ufw-before-input as well as iptables -L ufw-before-output. The rest has been configured above.

Apps may still use http(s) for telemetry, so a per-app application approach is needed; profiles are in /etc/ufw/applications.d/ and saved in Windows ini file format. The command structure is described deep within man ufw.

However we are in for a disappointment [forum1] [forum2]: This is all only for inbound traffic; as far as I could test there is no such thing for outbound. Proof:

ufw app list # which (app) profiles exist

nano -w /etc/ufw/applications.d/firefox
    [Firefox]
    title=Firefox browser
    description=Internet browser with plugins
    ports=80/tcp

ufw app update firefox
ufw app info Firefox
ufw allow from any app Firefox to any

iptables -L ufw-user-output && iptables -L ufw-user-input

... we see that out is unchanged, while in has a useless sapp_Firefox entry.

One thing left for us is using iptables with owner/group setting [blog1]. Here we use it to allow port access that no other application has.

addgroup accesshttp
adduser myuser accesshttp

iptables -I OUTPUT 1 -m owner --gid-owner accesshttp -p tcp --dport 80 -j ACCEPT
sg accesshttp -c 'firefox'
iptables -D OUTPUT 1

# because cannot be maintained by gufw
nano -w /etc/ufw/before.rules
    -A ufw-before-output -m owner --gid-owner accesshttp -p tcp --dport 80 -j ACCEPT

ufw reload
iptables -L ufw-before-output
sg accesshttp -c 'firefox'

... essentially using the group accesshttp as enabling profile. While starting the app is somewhat cumbersome, we can now start the same app with and without port access. So this is our restrictive-by-default method to allow per app.

The opposite approach is to make iptables more permissive and restrict per app. You can either use the same approach as above, or do it like Android with SELinux [blog1] and configure port access per app (the simpler AppArmor is not per-port). Good luck.

Windows: Client

Windows apps are usually binary-only and thus must be considered potentially malicious. In general, Windows clients follow the same pattern as Linux clients, adding the odd 445 (Active Directory, SMB), 593 (Exchange rpc) or 691 (Exchange routing).

One difference is that Windows generally filters per app ("personal firewall"). On Windows 10, you can configure the built-in firewall by the usual click-fest. Once upon a time on Windows XP, I used Tiny Personal Firewall and let it ask me per connection attempt, making the more trustworthy ones permanent. So at least for output per-app, Windows does it better.

Conclusion

I hope you enjoyed our little journey into typical firewall usage. Have a nice day!

EOF (Apr:2021)