Firewall with ansible

6 minute read

This article is part 7 in a series: Server setup on UpCloud

When you run a server, it’s very important that it is secure (or rather, as secure as possible). An insecure server is a compromised server. That’s why you should always use some type of firewall. When I provision my servers, I always use a small ansible role which sets up the internal firewall and - depending on the server usage - allows and disallows a set of rules. I always make it block everything on ingoing as a default, and open up the ports that I actually want to be able to use.

This part of the series will go through the firewall, and as in the earlier versions ubuntu is the OS that I use. I will focus on the UFW firewall, which is kind of a wrapper around IPTables. You might wonder why I don’t go straight to IPTables, which is basically around for every type of distro… well, honestly? I’m not really that comfortable with it, so I always use UFW as it is so easy…

UFW - Uncomplicated FireWall

The UFW package is basically a wrapper around IPTables, IPTables is not really that complex, but UFW makes it even easier and easier is nice, as long as we don’t loose any functionality! UFW have been around for a while, and it have been a default installed package in Ubuntu since the 8.x version of the distro. It’s written in python and is released under the GNU general public license. So no installation is really needed, but just for the sake of it and to make sure it’s up-to-date, we will actually add the installation process to our ansible role too. We will want one of the latest versions (of course), mainly because we will want the forwarding capabilities, which makes it possible to forward messages to the cluster network.

Installation and default rules

Setting up the initial parts of ufw is quite easy. The user who does it requires root access or sudo command capabilities, which our ansible user have. The first thing to think about is to make sure that we do not enable a fully closed firewall without first opening the SSH port (22) for ourselves to connect to the server! Other than port 22 could be closed as ingoing port as a default, because we never know what services we use, and we add a rule for each port that we want open.

I would personally recommend that we have a certain machine, like the master machine, on which we allow SSH on the public interface, while the other machines only allows for it on the private network interface. That way we are forced to use a proxy to connect, while all the people who do not have access to the proxy can try to connect as much as they want, as the port is closed!

When working with UFW, we will set up rules, each rule is either a default rule for the specific interface (default as in “all ports uses the rule”) or a rule is for a specific port. We will use a default outgoing rule for all machines, and one ingoing, we will also set both ingoing and outgoing as a default allow rule on the private network interface so that all the servers on the private network can connect with whichever service that they wish. I will not go through setting up a proxy for SSH or similar, that is something you may create yourself if you wish to use that approach, in this tutorial, all machines will have their ssh port open on the public network.
UFW is by default installed on the ubuntu distros, but we will add a apt stage to the script to make sure its present and installed.

The rules that we will set up as default rules for all machines are the following:

  • default deny incoming on public
  • default allow outgoing on public
  • default allow incoming on private
  • default allow outgoing on private
  • default allow port 22 on public
  • default allow forward

We will later on (in other parts of the series), append extra rules for specific machines, so the ansible scripts that we write later on will contain functionality to pass extra rules into the role.

Firewall role

When we create the new ansible role for firewall, we will use two files:

roles/firewall/defaults/main.yml and roles/firewall/tasks/main.yml, the defaults main.yml file is pretty much the same as a vars file, it’s a file to define variables in, but in this one, the default values, that are used in case nothing else is defined is set. That is, if we don’t override the value, it will contain the default value.

What we want to specify in this file is not the base rules, they will always be applied in the task file, but rather just a empty list which we call firewall_rules. This list is what we set when we want to apply more rules than the ones that is always applied, and for now, we don’t need to care more about it!

firewall_rules: []

The task itself will be quite a short one too. It contains four stages, Installation, Default rules, Rules which are passed and then the enabling of ufw:

- name: Install UFW.
    name: ufw
    state: present
- name: Setup default firewall rules.
  command: "ufw {{ item }}"
    - default deny incoming on eth0
    - default allow outgoing on eth0
    - default allow incoming on eth1
    - default allow outgoin on eth1
    - default allow FORWARD
    - allow ssh

- name: Set up custom firewall rules.
  command: "ufw {{ item }}"
  with_items: firewall_rules

- name: Start UFW.
  command: ufw --force enable # Force flag used to skip `are you sure` query.

When a playbook uses the firewall role, we can now either just use the default rules (internal machines as etcd cluster and dns server might be a good idea with only default rules for example) or we can pass a list of extra rules to add.

- name: Playbook
  become: true
  hosts: all
  gather_facts: yes
    - { role: common, dns_ip: "{{ lookup('env', 'DNS_IP') }}" }
    - { role: firewall, firewall_rules: ['allow http', 'allow https'] }

And that’s that! With this role, we will have closed the ports on the servers that should have closed ports!