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.
Copyright © 2020-2023 David Loffredo, licensed under CC BY-SA 4.0.