Skip to content

Overview

nftwall is a highly flexible firewall solution based on nftables. Configuration is managed through Ansible, which brings several advantages:

  • The firewall system holds no data that needs to be backed up explicitly, since everything is driven by Ansible.
    Of course, the Ansible configuration itself should be backed up in an appropriate place.
  • Extremely fast and flexible provisioning, even for large fleets of firewall systems
  • In high availability (HA) environments, the configuration does not need to be synchronized between systems, because it is distributed centrally and uniformly by Ansible.
    State synchronization is realized via conntrackd
  • Always tuned to the latest Alpine Linux release, i.e. a current kernel
    Support for dpkg- and rpm-based systems (RedHat / Debian / Ubuntu etc.) is also planned

nftwall supports high availability setups. An active–passive setup is possible with two firewalls, which provide greater operational reliability than a single firewall. Firewall settings are synchronized via Ansible and are therefore consistent. Firewall states are kept uniform via conntrackd.
At the IP level, VRRP is used to obtain an additional virtual IP in each subnet, which is shifted back and forth between the firewall nodes, so that even upon failure of the primary node the secondary can take over instantly.

Currently, the nftwall solution is still under development and should NOT be deployed in production environments.

Ansible

Because nftwall is built on Ansible, you should be familiar with Ansible. This documentation does not aim to replace the Ansible documentation.

Terminology

In an nftwall environment, the actual firewall systems running nftwall — where the firewall carries out its duty — are referred to as:

  • Firewall
  • Target system

Additionally, there must be a system that manages the Ansible configuration along with the roles and playbooks. Such systems are called:

  • (Ansible) Controller
  • Ansible Host

Installation

Running an nftwall firewall requires an Alpine Linux system. Alpine Linux is a very slim Linux that runs on a variety of platforms, either bare metal or virtualized. Aside from the firewall system itself, an Ansible host is needed. Ideally an Ansible controller already exists; otherwise such a system would have to be set up first. Please refer to the existing Ansible documentation for that. Running the Ansible controller on the firewall itself is quite possible, but is not best practice.

Preparing the Ansible Host

  1. Install Ansible — the operating system does not matter (BSD, Linux, macOS, …)
    apk install ansible
  2. Inside the Ansible working directory, install the collection either into the collections subfolder:
    cd AnsibleWorkingDirectory
    ansible-galaxy collection installhttps://github.com/imp1sh/ansible_nftwallcollection.git-p ./collections
    or into the standard .ansible folder:
    ansible-galaxy collection installhttps://github.com/imp1sh/ansible_nftwallcollection.git
  3. Besides the nftwall collection there is another collection called managemynetwork, which must also be installed
    cd AnsibleWorkingDirectory
    ansible-galaxy collection install https://github.com/imp1sh/ansible_managemynetwork.git -p ./collections
    or into the standard .ansible folder:
    ansible-galaxy collection install https://github.com/imp1sh/ansible_managemynetwork.git

Best practice is to install the collections into the collections subfolder. The roles are also published on Ansible Galaxy, however using the repos from GitHub is strongly recommended, as they are always more current there.

Ansible Collection / Roles

The Ansible collection ansible_nftwallcollection bundles several roles that are necessary to operate nftwall.

ansible_conntrack

This role is only used in HA (high availability) environments. It serves to synchronize connection states between the cluster members. Should the primary/secondary status switch, the sessions are available there as well, which means already-opened connections keep working. Otherwise, in such a case an open SSH connection would simply drop and run into a timeout.

ansible_dnsmasq

dnsmasq is very versatile software that, besides DHCP, also masters Router Advertisement and can additionally act as a resolving or authoritative name server. In HA environments, however, dnsmasq cannot be used, among other reasons because the RemoveRoute function known from radvd is not available.

This role is no longer being developed further for the purposes of nftwall, because dnsmasq does not satisfy the demands of an HA environment.

ansible_kea

Instead of ansible_dnsmasq, ansible_kea and ansible_radvd come into play. ansible_kea is still under development.

ansible_keepalived

Keepalived is a wrapper around Linux Virtual Server (LVS). It is a layer 4 load balancer that also offers management of VRRP addresses. In this context it is currently only planned to use the VRRP functionality of this solution. Later on one could also consider using the load-balancing function of this software, should there be plans to offer highly available services behind the firewall.

Again, my nftwall docs do not replace the keepalived documentation / LVS documentation.
Here is an example configuration for VRRP addresses.

kad_vrrpinstances:
- name: "WAN"
master: "alpine1.demo.junicast.de"
slave: "alpine2.demo.junicast.de"
masterif: "eth0"
slaveif: "eth0"
routerid: 51
priorityanchor: 100
advertint: 1
authpass: "sdu39sj3"
vips:
- 192.168.200.4

- name: "WAN6"
master: "alpine1.demo.junicast.de"
slave: "alpine2.demo.junicast.de"
masterif: "eth0"
slaveif: "eth0"
routerid: 52
priorityanchor: 100
advertint: 1
authpass: "Hsdf§$l3"
vips:
- 2001:470:7e68::4

A virtual IP is shared between two systems whose interfaces (here WAN) are located in the same broadcast domain. In addition to the IP addresses that the two hosts themselves carry, there is a virtual IP address that is active on only one of the two systems at any given moment. This IP address is always reachable, even if one of the two systems should fail. This IP address can thus be used as the default gateway or for other purposes.

If needed, multiple virtual IPs can also be configured by adding further entries under vips.

ansible_netconfig

The netconfig role takes care of properly setting up the network interfaces. You can choose between different modes; currently only a few modes are supported for Alpine Linux.

  • method4: [static|manual|dhcp]
  • method6: [static|slaac]

Example of a host_vars definition:

netinterfaces:
vmbr0:
comment: "Main Interface auf mynet"
type: "ethernet"
method4: "static"
method6: "static"
ip4: "10.10.129.4/26"
ip6: "2001:affe:dead:7300:21b:21ff:fec1:a8c0/64"
gw4: "{{ nftvars['nets']['mynet']['gw4'] }}"
gw6: "{{ nftvars['nets']['mynet']['gw6'] }}"

For gw4 and gw6 you can also enter the IP directly; so that I only have to define the default gateway once per network, I put it in group_vars/allhosts.yml, like so:

nftvars:
nets:
mynet:
comment: "Das ist mein Netz"
ip4: "10.123.125.0/24"
ip6: "21a0:1234:1312:1::/64
gw4: "10.123.125.1"
gw6: "21a0:1234:1312:1::1/64"

ansible_nftables

The nftables role represents the heart of nftwall. Since iptables has reached end-of-life on Linux, its successor nftables recommends itself for firewall rules. Modern Linux systems already use this technique, albeit mostly through wrappers such as firewalld. These programs are meant to make life easier, yet they require specialist knowledge and are often cumbersome to handle.
With nftables you work with concepts familiar from iptables. To gain insight into nftables I recommend the netfilter homepage.

The nftables role abstracts the maintenance of firewall rules in a simple way. Incoming connections from the WAN interface are rejected by default. Under the default settings only these connections are permitted:

  • ICMP6
    echo-request, destination-unreachable, packet-too-big, time-exceeded, parameter-problem, mld-listener-query, mld-listener-report, mld-listener-reduction, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, ind-neighbor-solicit, ind-neighbor-advert, mld2-listener-report
  • ICMP
    echo-request, destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem
  • IGMP

Should you have multiple local interfaces, traffic between them is blocked under the default settings as well. Access to the internet for local participants is granted using this Ansible list variable:

nftables_masks:
- upif: "eth0"
downif: "eth1"

This Ansible variable definition causes packets coming from eth1 to be masqueraded with the IP address of eth0. eth0 here is the upstream interface toward the internet.

Incoming Packets / Input

As long as no destination and no source address is specified, you can define rules of family 46 that apply to IPv4 AND IPv6 alike. As soon as you incorporate IP addresses, you have to split into separate rules for IPv4 and IPv6 respectively.

Every now and then you also want to permit incoming packets on a firewall that are not meant for forwarding but for the firewall itself. In Server or Desktop mode this option matters far more often than on a firewall itself, which usually does not expect incoming packets. In Ansible there is both the host and the group level at which you can enable incoming connections. If an opening should only be performed for a specific host, you put it under host_vars in Ansible:

nftablesopenhost:
- dport: "http"
family: 6
comment: "Allow HTTP from specific IPv6 address only"
proto: "tcp"
saddr: " 2a00:f30:1234::1/64 "

If, on the other hand, you want to roll out a rule across multiple hosts, you can regulate that via group membership in Ansible. Define it in group_vars/all.yml:

nftablesopengroup:
gruppenname:
- dport: 53
inif: "eth1"
family: 46
proto: "tcpudp"
comment: "DNS Allow on eth0"

DNAT

Destination NAT is generally needed when you want to admit incoming requests from the internet to local systems. Because often only the firewall itself has a public IPv4 address, you have to resort to NAT. Say you want to forward requests on port 801 to a local IP; you use the Ansible variable:

nftables_dnat:
- dport: 801
ip: "172.16.88.16"
proto: "tcpudp"
inif: "eth0"
outif: "eth1"
comment: "Forward Port 801 to host 17"

Forwarding

When you have the luxury of handing public addresses to your participants, you usually want to forgo NAT and instead lay down firewall rules that permit specific traffic. For this you use the nftables_forward variable in Ansible:

nftables_forward:
- inif: "eth0"
daddr: "2001:aaaa:123:1000::/64"
saddr: "2001:faa:b33:1::/64"
dport: "22"
proto: "tcpudp"
family: 6
outif: "eth1"
comment: "Allow SSH to client3 from net1"

Most parameters of the variable are optional, so you can, for example, allow any traffic between eth1 and eth2:

- inif: "eth1"
outif: "eth2"
family: 46
comment: "Allow all eth1 to eth2"
- inif: "eth2"
outif: "eth1"
family: 46
comment: "Allow all eth2 to eth1"

For family you can choose between 4, 6, or 46. For the proto parameter you can choose between tcp, udp, and tcpudp.

Variables

Ansible offers the built-in perk of being able to use variables. It is strongly advisable to leverage this for managing firewall rules as well. For example, you can define the IP address of a web server as a variable and reuse it in rules. Should the web server's IP ever change, you won't have to correct that address in umpteen spots throughout Ansible, but only in one place. With variables it is essential to use consistent naming, so you don't constantly have to look up what a variable was called again.
This is an example of how you can harness variables. (group_vars/all.yml)

nftvars:
hosts:
jabber server:
ip6: "2a00:fe1:fff:6::5/64"
ip4: "10.11.12.13/24"
comment: "chat.mydomain.de jabber server"
nets:
home:
ip4: "10.10.128.0/20"
ip6: "2a10:f30:3f::/48"
comment: "Home network"
office:
ip4: "192.168.12.0/24"
ip6: "21a0:af12::/32"
comment: "Mein besonders grosses Bueronetz"
ports:
ssh:
value: "22"
proto: "tcp"
comment: "SSH service default port"

The variables are applied, for example, like this:

nftablesopenhost:
- dport: "80"
family: 6
comment: "Allow HTTP from Office prefix"
proto: "tcp"
saddr: "{{ nftvars['nets']['office']['ip6'] }}"

Ports from /etc/services

nftables natively supports supplying the designator from /etc/services instead of a port number. Hence it is perfectly legitimate in nftwall to use a name from that file instead of a numeric port:

nftablesopenhost:
- dport: "http"
family: 6
comment: "Allow HTTP from Office prefix"
proto: "tcp"
saddr: "{{ nftvars['nets']['N6_office']['ip6'] }}"

ansible_sysctl

The ansible_sysctl role ensures that the respective kernel parameters enable forwarding of IPv4 and IPv6 packets. Without these changes, operating a firewall would not be possible. This role is fairly inflexible and does not set sysctl parameters to taste, but rather predefined as we deem necessary for running a firewall.

Preparing the Firewall

  • Install Alpine Linux on the host designated for firewall duty. Two interfaces are enough for a normal firewall; if needed, more interfaces are of course possible, at least for downstream participants. Multi-homing is currently not supported.
  • tbc.

Videos

Links