Firewall

ROLE common, firewall.yml

We implement firewall rules directly using iptables, with iptables-persistent to reload the configuration on boot. The IPv4 and IPv6 rules are in /etc/iptables/rules.v4 and rules.v6. These files are well commented and explain the purpose of each rule.

The IPv4 and IPv6 rules protect normal applications and those in Docker containers. We only allow incoming connections to ports that were explicitly opened. We always open the ssh port. We also allow pings, but rate limit them, and drop unusually-formed packets.

We run fail2ban to scan the logs for repeated intrusion attempts and temporarily ban IPs. We use a fairly aggressive setting which may trip when trying to configure mail clients. You may want to use the fail2ban-client tool to check the jails if you are having trouble connecting.

Variables

Open ports for new services by adding the symbolic name to the firewall_services list. It recongizes 'ssh', 'mail', 'sieve', 'web', 'dns', and 'bacula'. You define your own names for services by defining variables firewall_opentcp_<name> and/or firewall_openudp_<name> as a list of port numbers. It doesn't matter if port numbers are repeated across services.

firewall_services: [ 'ssh' ]

# Define 'foobar' name for firewall services by defining:
#firewall_opentcp_foobar: [ 12345, 23456 ]
#firewall_openudp_foobar: [ 34567 ]

The firewall_opentcp and firewall_openudp variables are just lists of extra ports that will be opened.

# will append ports from the mnemonic list of services but can be
# initialized with extra ports.
firewall_opentcp: []
firewall_openudp: []

If you want to drop all connections from certain IPs, or allow all connections from others, add them to the following lists.

# Friendly networks that should be ignored by fail2ban, and allowed
# through the firewall
firewall_allow_ips: []
firewall_allow_ips_v6: []
firewall_block_ips: []
firewall_block_ips_v6: []

The fail2ban jails are usually controlled by the firewall services names. Names like "ssh", "mail", "sieve", or "web" are expanded to a list of hashes describing the jails for that service. Each jail hash has a "name" that should be enabled, plus any other key/value pairs that should appear in the jail config as "key = value". The list of jails for a named service is given by a fail2ban_jail_<svc> variable.

fail2ban_jails: '{{ firewall_services }}'

# ssh jails always added
fail2ban_jail_ssh:
  - name: sshd
    mode: aggressive
  - name: pam-generic
    banaction: iptables-allports

fail2ban_jail_mail:
  - name: dovecot
  - name: postfix
  - name: postfix-sasl

fail2ban_jail_sieve:
  - name: sieve

fail2ban_jail_web:
  - name: apache-auth
  - name: apache-overflows

The firewall services list is used by default, but you can set fail2ban_jails to a different list of strings and hashes. Add custom jails or override a built-in definition with a hash with the same name towards the end of the list.

The example below has the built-in ssh jails (always present), the mail jails, an apache jail that operates on the DOCKER-USER chain, and overides the built-in sshd jail definition with a very long ban.

fail2ban_jails:
  - mail
  - name: apache-auth
    chain: DOCKER-USER
  - name: sshd
    mode: aggressive
    bantime: 1000years

You can also customize the default find and ban times with the fail2ban_findtime and fail2ban_bantime variables.

# These are the defaults values
# fail2ban_findtime: 2h
# fail2ban_bantime: 24h

Docker

Network traffic to software in Docker containers is handled differently than traffic to software running natively on the machine. Docker creates small virtual LANs to connect containers within the machine. Traffic going to a Docker container is then routed to this virtual LAN through the iptables FORWARD chain, while traffic going to native software is handled by the INPUT chain.

These chains are in the default "filter" table, in the center of this sprawling diagram of the netfilter packet flow. Unfortunately, most people add their firewall rules to the INPUT chain, which has no effect on FORWARD traffic to Docker containers. There are reams of bad and misleading advice on the net about combining firewalls and Docker, but the solution is simple, do not use the "filter" table.

We do most blocking on the PREROUTING chain of the "mangle" table. Packets go through this chain early in the processing, which has two big benefits. First, it covers both INPUT to native apps and FORWARD to Docker containers. Second, the earlier you can drop a bad packet, the more bad packets you can handle.

We do need some accommodations for Docker. In particular, when using fail2ban for a container, we must make sure that it can find the logs for the container and put its rules on the DOCKER-USER chain. You can find an example of this in group_vars/webmail.yml.

Why These Packages?

The ufw (universal firewall) package is a popular choice for blocking everything except for a few ports, and it has a convenient Ansible module. It is nice for manually opening and closing ports, but it is just a wrapper around iptables and iptables-save files.

I generated iptables-save files directly because it gives a much clearer picture of the complete state of the rules, while UFW mostly just gets in the way if you want to use ipsets or work with the sophisticated rules set up by Docker. The iptables syntax is easy enough to understand once you've seen a few rules.

A second reason was idempotency. UFW keeps its own iptables-save files, so a port stays open until you explicitly tell UFW to close it. With Ansible, we list open ports, so when you remove a port from the list, you may intend it to be closed, but it remains open on the machine until you tell UFW to close it. By generating the rules.v4 and rules.v6 ourselves, we guarantee a known state.

It seems like some people are moving to nftables (nft) instead of iptables. Something to keep an eye on for the future.

The fail2ban package does not stop attacks, merely reduces the rate at which they can occur. This helps with brute force attacks on a password-protected service like IMAP, and provides some noise control for the logs. The version in Debian 10 is fairly recent so we use it stock. The Debian 9 version was old, so we previously added some custom patterns for ssh.

10BASE-2 FTW
One alternative to a firewall is to make your network so unpleasant that attackers move on. Mix in some thicknet or modems with accoustic couplers if you can find them.