I recently had a lot of difficultly working out how to get ufw (the uncomplicated firewall) to work with LXD Linux containers.
Because of this and me not having a lot of time to dig deep into the tool, I decided to revisit some of my old iptables configs and amend them to work with LXD. In the process of doing this, I found that Debian Buster actually defaults to using the new nftables filtering mechanism built on top of netfilter. In this guide, we’ll configure a basic firewall with nftables and move over the legacy iptables rules automatically created by LXD.
Background
Some disclaimers
I do want to note that these rules work for me but are not really any guarantee against being attacked, hacked, monitored, MITMed, or |insert-bad-thing-here|. For me they act as reasonable defense against basic attacks. For a really important web facing box I’d recommend placing a CDN in front of it and routing traffic through the CDN or alternatively using something like Argo Tunnel and only allowing connections through Argo.
A bit of reading
Here are the notes I reviewed before authoring this post. I think they serve as good background for iptables and can be skipped at your own peril :). Even though we’re using nftables, understanding the legacy framework will be needed to understand your existing rules.
Looking at the manpage for iptables we have five built-in tables and each of these contain some chains. These notes are pulled directly from man iptables
but I felt serve as a good overview of some of these tables.
- filter: the default table
- input: for packets destined to local sockets
- forward: for packets being routed through the box
- output: for locally-generated packets
- nat: consulted when a new packet is encounted
- prerouting: for altering packets as soon as they arrive
- input: for altering packets destined for local sockets
- output: for altering locally-generated packets before routing
- postrouting: for altering packets as they are about to go out
- mangle: specialised packet alteration
- prerouting: for altering incoming packets before routing
- postrouting: for altering packets being routed through the box
- raw: configuring exemption from connection tracking
- prerouting: for packets arriving via any network interface)
- postrouting: for packets generated by local processes
- security: used for mandatory access control rules
- input: for packets coming into the box itself
- output: for altering locally-generated packets before routing
- forward: for altering packets being routed through the box
For command line invocation, we’ll be using the following. The long name is appended to the command line with double dash (–) and the single letter is the abbreviated call with a single dash. <> are required parameters [] are optional.
- append(A) add one or more rules to the end of the selected chain
- check(C) check whether a rule matching the specification does exist in the selected chain
- insert(I) [rulenum] insert one or more rules in the selected chain as the given rule number. If no rulenum specified, default to rulenum as 1 (inserted at the head/top/front of the chain).
- list(L) [chain] lists the rules
- new-chain(N) create a new chain
Stateful firewall
Goals
- Protection against port scanning with recent module. /proc/net/xt_recent
- Automatic dropping of repeated attacks from a single IP to a monitored port.
- Work as a NAT gateway for linux container clients.
More reading: https://wiki.archlinux.org/index.php/Simple_stateful_firewall#Setting_up_a_NAT_gateway
Saving an undo state
We’ll begin by exporting the current state of the firewall and reimport it should we break things. For most people, there won’t be any firewall rules to export. In that case, we’ll simply drop the existing rules as we create them if there are errors.
If you’re logged into a remote box, I recommend you copy the reset rules into a script and schedule it to run in 10 minutes so you don’t get logged out :).
Inspect existing rules
Note you’ll need to also inspect the other tables listed above by appending -t <table name>
here. Options are nat, mangle, security, and raw. You’ll likely only have legacy rules.
iptables-legacy -L
If any rules appear from the above commands, then it’s best to export them to a file using the instructions below.
Drop existing rules
I wouldn’t recommend doing this but you can also start from a clean slate. Don’t forget to do this for every table.
iptables -F
iptables-legacy -F
Scheduling the import
To schedule the import you may either use crontab or create a systemd timer. I’d recommend an even simpler method: launch a separate tmux or screen instance, execute the below, and detach from the instance. You may need to create multiple windows for each invocation. It’s a hacky but workable solution.
screen
[create a new window]
# sleep 600 && iptables-legacy-restore /root/iptables-legacy.rules
[create a new window]
# sleep 700 && iptables-nft-restore /root/iptables-nft.rules
Make the rules persistent
We’ll install the iptables persistent package so these are applied during each reboot.
# apt install iptables-persistent
Creating the firewall rules
We’ll perform keep the changes pretty simple.
- We’ll drop invalid packets.
- We’ll blocks packets that originate from private subnets.
- NOTE: Don’t do this if you’re doing this with your home server that’s behind a router. You’ll lock yourself out! Instead, place these rules on the routers internal interface.
- We’ll drop new TCP connection that don’t have the SYN flag set.
- We’ll log rules that are denied.
- We’ll accept continuation of existing or related connections.
- We’ll create NAT rules for the lxdbr0 interface.
iptables --table mangle --append PREROUTING --match conntrack --ctstate INVALID --jump DROP
iptables --table mangle --append PREROUTING --match conntrack --ctstate ESTABLISHED,RELATED --jump ACCEPT
iptables --table mangle --append PREROUTING --protocol TCP ! --syn --match conntrack --ctstate NEW --jump DROP
iptables --table mangle --append PREROUTING --source 0.0.0.0/8 --jump DROP
iptables --table mangle --append PREROUTING --source 10.0.0.0/8 --jump DROP
iptables --table mangle --append PREROUTING --source 127.0.0.0/8 ! --in-interface lo --jump DROP
iptables --table mangle --append PREROUTING --source 169.254.0.0/16 --jump DROP
iptables --table mangle --append PREROUTING --source 172.16.0.0/12 --jump DROP
iptables --table mangle --append PREROUTING --source 192.0.2.0/24 --jump DROP
iptables --table mangle --append PREROUTING --source 192.168.0.0/16 --jump DROP
iptables --table mangle --append PREROUTING --source 240.0.0.0/5 --jump DROP
iptables --table mangle --append PREROUTING --source 224.0.0.0/3 -j DROP
iptables --table mangle --append INPUT --match limit --limit 5/min --jump LOG --log-prefix "iptables denied: " --log-level 7
You can see that equivalent nft rules are created with nft list ruleset
. TBD is to move that NAT rules created by lxdbr0 over. Perhaps just creating a normal bridge and referencing it in the LXD configuration is enough.