Setting up the Alpine Wall Firewall

This guide is no longer maintained

I have switched to nftables instead of awall, it eliminates a layer of abstraction and allows for a much more comfortable configuration syntax. To get you started with nftables, I have written a guide on nftables basics. I will keep this guide online for historical reference only.

Introduction

Since our server’s incoming and outgoing traffic is currently completely unrestricted, let’s take some measures to tighten up our system by setting up our firewall. This guide will cover a very basic setup while describing the basics of how to adapt it for your own purposes, depending on what services you intend to run.

Introduction to the Alpine Wall

The firewall tool that was developed for Alpine Linux is called “Alpine Wall”, or awall for short. It is basically just a frontend for iptables that makes it easier to manage. For further information beyond what is covered in this guide, please refer to the documentation of awall. Here are the pages that were most helpful to me in understanding and configuring it:

Also note that the awall command does not have a manpage. Instead, run awall help to show all options of the awall command and a description of each option.

Installing iptables

By default, Alpine Linux comes without any kind of firewall, so our first step is to install and enable iptables, since awall is only a management utility for iptables. We install the required packages with these commands:

$ doas apk add iptables ip6tables iptables-doc

This installs iptables for IPv4 and IPv6, as well as its manpages. Now we need to start it:

$ doas modprobe ip_tables          # for IPv4
$ doas modprobe ip6_tables         # for IPv6
$ doas rc-service iptables save
$ doas rc-service ip6tables save
$ doas rc-service iptables start
$ doas rc-service ip6tables start

Then we add iptables to OpenRC’s default runlevel to start it at boot:

$ doas rc-update add iptables
$ doas rc-update add ip6tables

Installing Alpine Wall

Iptables is running, but right now it is not doing anything, just allowing all traffic through without any restrictions. We will manage our firewall policies with awall, so let us install it:

$ doas apk add awall

And that’s all it takes, we can now move on to writing our policies!

Writing awall policies

Awall’s default configuration files are located in “/usr/share/awall”, but this is not where we write our policies. All user policies should be added in the “optional” and “private” directories in “/etc/awall”. The “optional” directory will hold all of the actual policies, while the “private” directory can hold any custom services or variables that we want to define.

Writing our main policy

The best way to set up a firewall is to start by blocking everything. Then we can add policies to allow the specific services that we are actually using, to keep our system as airtight as possible. Accordingly, the first policy we write will configure the firewall to drop all packets that are coming in from outside and rejecting all others.

You can imagine dropping a package like just putting it in the trash. When you reject a package however, you don’t just put it in the trash, you tell the sender “I reject this”, which takes a bit more effort (processing) on your part. For this reason we drop everything coming in from the outside by default, because there is going to be a lot coming in from the outside. When we are sending something ourselves though, we might want to know why it doesn’t arrive, so we reject it instead of just having it silently dropped by our firewall.

Our main policy will also define the zones of our network. If your server has several network interfaces (eg. eth0, eth1 and wlan0), you might be using them for different purposes. For example, if only “eth0” and “wlan0” are connected to the internet, we can put them together in a zone called “WAN”. If “eth1” is only connected to other local machines, you could put it in a zone called “LAN”. That way we can differentiate with names that are more descriptive in their purpose than “eth1” or “wlan0”.

Here’s our most basic main configuration file, which I will put in “/etc/awall/optional/main.json”. For this file, I assume that you have just one interface called “eth0” that is connected to the internet:

{
	"description": "Drop all incoming traffic from WAN, reject all other",
	"zone": {
		"WAN": { "iface": "eth0" }
	},
	"policy": [
		{ "in": "WAN", "action": "drop" },
		{ "action": "reject" }
	]
}

This file uses the JSON format, which is not super human friendly, but I was able to pick it up and get used to it pretty quickly from the many examples that I found in other guides, even having never written any JSON before. I have read that support for the YAML format was recently added too, but since I could not find any examples, I stuck to JSON for now.

If you want to specify multiple interfaces in a zone, or multiple zones, you could do it like this:

	"zone": {
		"WAN": { "iface": [ "eth0", "wlan0"] },
		"LAN": { "iface": "eth1" },
		"VPN": { "iface": "wg0" }
	},

Finally, the “policy” section defines two policies, the first of which drops (“action”: “drop”) all incoming traffic on WAN (“in”: “WAN”). The second one simply rejects (“action”: “reject”) everything that doesn’t match the previous policy.

Now before we enable this, let us first add some exceptions to these rules. Otherwise we will not be able to connect with SSH anymore!

Incoming SSH connections

Our goal for this policy is allowing SSH connections that are coming in over the internet. There is no need to allow outgoing SSH connections, so we will be as strict as possibly by only allowing incoming ones. Additionally, we will limit the number of requests that can be made to 3 in 30 seconds to drop some connection attempts by bots.

First I will show you the policy file, then I will explain it step by step. This is the file, which I will place at “/etc/awall/optional/incoming-ssh.json”:

{
	"description": "Allow incoming SSH on WAN (TCP/22)",
	"filter": [
		{
			"in": "WAN",
			"out": "_fw",
			"service": "ssh",
			"action": "accept",
			"conn-limit": { "count": 3, "interval": 30 }
		}
	]
}

First, we define which packets this policy is supposed to apply to. The packets have to be coming from the outside (“in”: “WAN”) and be addressed to a local service on the server itself (“out”: “_fw”). The “_fw” zone basically just means “services on this computer”. Then we specify which protocol and port to use (“service”: “ssh”). Now you might be wondering: How does this state the protocol and service? Doesn’t it just say “ssh”? Well, to make our life easier, there is a default list of common services, which is located at “/usr/share/awall/mandatory/services.json”. You can see there that writing “ssh” is just a shortcut for this:

{ "proto": "tcp", "port": 22 }

So if you had your SSH daemon listening on port 1234 instead of the default port 22, you could write the following in the SSH policy:

"service": { "proto": "tcp", "port": 1234 },

Alternatively, if you anticipate having to use this in several policies, you should define your own custom service. You should not edit the default file, instead you can make a list of custom services (or even several lists) in the directory “/etc/awall/private/” and then import them with your main policy file. You can find the instructions for this a bit lower in this guide under the header “Defining custom services”.

Getting back to the configuration, all packages matching the above description are accepted (“action”: “accept”). Note the last line though! “conn-limit” defines a limit to the number of connections that can be made in a specific time frame. In this case, we are limiting it to 3 connections in 30 seconds. So I can start a connection 3 times in those 30 seconds, but any additional ones will be dropped until the 30 seconds are over. So if someone was spamming my server with ssh connection attempts, only three would actually go through, the rest would be dropped, saving some resources.

Now we have a policy for ensuring SSH access, but there are a few more basic things we might need!

Basic outgoing services (DNS, HTTP, HTTPS, ping)

In many cases, your server will need to access the internet, for example to get updates or to download files from other servers. For this purpose we make a basic policy for outgoing services that includes allowing outgoing DNS, HTTP, HTTPS and ICMP echo requests (used for the command “ping”). As these are very common, they are also part of the default services and we can just use the keywords. I will create this policy at “/etc/awall/optional/outgoing.json”:

{
	"description": "Allow basic outgoing traffic from local to WAN",

	"filter": [
		{
			"in": "_fw",
			"out": "WAN",
			"service": [ "dns", "http", "https", "ping" ],
			"action": "accept"
		}
	]
}

Here you can see that we are applying the rule to packets originating locally (“in: “_fw”), going out to the world (“out”: “WAN”) and using the ports and protocols of the services we selected. All of these packets are accepted and can pass the firewall.

Allowing incoming ping (ICMP echo)

Sometimes we might want to test whether our server is online, or whether we have internet connectivity from our computer. A common way to do this is with the “ping” command, which uses ICMP echo requests. With the following config, we will allow this, though with a limit on how many of these packets we allow in a certain timeframe to avoid malicious ping requests taking up server resources. I place the following contents in the file “/etc/awall/optional/ping.json”:

{
	"description": "Allow incoming ping on WAN with flow-limit",

	"filter": [
		{
			"in": "WAN",
			"out": "_fw",
			"service": "ping",
			"action": "accept",
			"flow-limit": { "count": 10, "interval": 6 }
		}
	]
}

Unlike with our incoming SSH policy, we don’t use “conn-limit” to limit the connections. Instead, we use “flow-limit”, which doesn’t just limit the number of new connections, but the number of packets. For SSH we would not want this, because many packets could be exchanged after the initial connection attempt. For ping, it is much simpler since each “ping” is one ICMP echo packet. So we restrict the maximum number of incoming packets to 10 in 6 seconds. Anything more than that will be dropped, since we can assume that any normal host would usually just send one packet per second. We allow a bit more deviation than that because network issues and latency could cause packets to be delayed, meaning that more than 10 packets could arrive in 10 seconds.

Listing, enabling and activating our policies

Awall has a few commands in store for us to manage our policies. The first that you should use is “awall list”. Let’s try it:

$ doas awall list
main		disabled	Drop incoming traffic from WAN, reject all other
outgoing	disabled	Allow outgoing traffic from local to WAN
ping		disabled	Allow incoming ping on WAN with flow-limit
ssh		disabled	Allow incoming SSH on WAN with conn-limit (TCP/22)

Here we can see all the policies we defined, as well as their status and description. To enable and disable our policies, we use “awall enable” and “awall disable”, like so:

$ doas awall enable main outgoing ping incoming-ssh
$ doas awall disable ping
$ doas awall enable ping

Go ahead and use these commands to enable all of the policies we created. Confirm that they are enabled with “awall list”. Then, to activate the current configuration, we use the “awall activate” command:

$ doas awall activate
New firewall configuration activated
Press RETURN to commit changes permanently: 

Then you press the “return” key (also called “enter”) to permanently activate them. If you want to activate them without needing to press a key, you can also use “awall activate -f”.

Then your rules will be applied and enforced for any future connections!

Defining custom services

When we want to use services that are not defined in the default list, we have two choices: Either we write the protocol and port of the service manually in each policy file, or we define a custom service. The advantage of defining custom services in a separate file is that our policies stay clear and descriptive and we can easily reuse the same non-default services in several policy files. And in case we ever need to modify a port for example, we can just edit the custom services file instead of having to edit each policy individually.

As I mentioned at the start of the article, these custom services can be defined in a file in the directory “/etc/awall/private/”. In this case, I will simply name the file “/etc/awall/private/custom-services.json”. Let’s say I have SSH running on port 1234 instead of the default 22. So below, I will define a service called “custom-ssh”, as well as another example service that we will use in a future guide:

{
	"service": {
		"custom-ssh": [
			{ "proto": "tcp", "port": 1234 }
		],
		"wireguard": [
			{ "proto": "udp", "port": 12345 }
		]
	}
}

Then we need to import this “custom-services.json”, which we will do with our main policy file that we created at the very start:

{
	"description": "..."
	"import": [ "custom-services" ],
	"zone": {...},
	"policy": [...]
}

As you see, we have added the “import” line with the name of our custom services file. Now we can use the services that we defined in any of our other policies, as long as the main policy is active. To apply our changes, don’t forget to run “awall activate” again, as I described earlier!

Summary

We now have a basic and minimal firewall set up that we can easily extend by adding additional policies for any other services that we need. To continue, here are some links to our other guides, some of which also build on this one:

All guides in the DIY Server series

If you have any questions or suggestions, don’t hesitate to email me at contact@regrow.earth!