Shami's Blog

DevOps because uptime is not optional

Using Dynamic DNS with pf, iptables, and gomplate

It is always better to tighten the firewall configuration on your servers. Limiting SSH connections from a defined list of IP addresses greatly reduces the attack surface as well as load; the firewall is much more efficient at blocking connections than sshd. The best approach is to have a static IP or connect through a bastion host which is protected via a VPN. But sometimes that is not feasible.

What I am describing here is not scalable, but it is what I started to do at the beginning and worked well enough for me, and most importantly, it’s totally free.

The idea came to me while setting up my last instance of pfSense . pfSense allows you to create firewall rules using DNS names, which in turn can be dynamic.

First you need a dynamic DNS record set up. ClouDNS has a free service. I haven’t tried it but I have used ClouDNS’s services and have no reservation recommending them. They have been stellar for the couple of years I have used them. You can also use another service if you prefer. I will not discuss how to do it here but I might write about how I do it in a future post.

Assuming the following hostnames:

  • user1.example.com
  • user2.example.com
  • user3.example.com

pf

pf has the ability to store IP lists in separate files using tables. A sample would be

 1scrub in all
 2
 3ext_if="igb0"
 4
 5# IP1: HQ
 6# IP2: Branch office 1
 7# IP3: Trusted static IP
 8SafeHosts="{IP1, IP2, IP3}"
 9table <user1> persist file "/etc/pf.user1.example.com"
10table <user2> persist file "/etc/pf.user2.example.com"
11table <user3> persist file "/etc/pf.user3.example.com"
12
13# Do not intercept local traffic
14set skip on { lo0 }
15
16# Allow ping
17pass in quick inet proto icmp all
18
19# Pass from safe hosts (Anti-lockout rule)
20pass in quick proto tcp from $SafeHosts to port ssh flags any
21pass in quick from $SafeHosts
22pass in quick proto tcp from <user1> to port ssh flags any
23pass in quick from <user1>
24pass in quick proto tcp from <user2> to port ssh flags any
25pass in quick from <user2>
26pass in quick proto tcp from <user3> to port ssh flags any
27pass in quick from <user3>
28
29# Pass outbound traffic
30pass out quick all
31
32# Block everything by default
33block in all
34
35# Allow traffic
36pass in proto tcp to $ext_if port http
37pass in proto tcp to $ext_if port https

And now for the script

 1#!/bin/sh
 2
 3# Tolerate no errors
 4set -e
 5
 6RELOAD=0
 7
 8for host in user1.example.com user2.example.com user3.example.com
 9do
10    # Resolve the hostname and compare it to the one we currently have configured
11    curIP=`getent hosts $host| cut -d' ' -f1 | sort`
12    configuredIP=`cat /etc/pf.$host`
13
14    # If the user's IP changed, save the IP and set pf to be reloaded
15    if [ "$curIP" != "$configuredIP" ]; then
16        echo "$curIP" > /etc/pf.$host
17        RELOAD=1
18    fi
19done
20
21if [ $RELOAD -eq 1 ]; then
22    service pf reload
23fi

Simply add this script to CRON and let it run every 5 minutes or at whatever duration you are comfortable with. The script will only reload the firewall if the IPs change so no load on the firewall. Also note that you can save all IPs in a single table but having a table for each user allows finer grain controls.

iptables

iptables has IP sets which should handle this, but I honestly don’t use iptables as often so didn’t have the chance to look into it. For those who understand IP sets, they can modify the script above to modify the required IP set instead of the table files.

For those who don’t, like myself. There is gomplate

Form the gomplate’s documentation

gomplate is a template renderer which supports a growing list of datasources, such as: JSON (including EJSON - encrypted JSON), YAML, AWS EC2 metadata, BoltDB, Hashicorp Consul and Hashicorp Vault secrets.

A sample iptables template would be

 1# Generated by iptables-save, then edited manually
 2*mangle
 3:PREROUTING ACCEPT [3809:279796]
 4:INPUT ACCEPT [3809:279796]
 5:FORWARD ACCEPT [0:0]
 6:OUTPUT ACCEPT [3366:4523482]
 7:POSTROUTING ACCEPT [3366:4523482]
 8COMMIT
 9# Completed on Thu Feb 11 05:39:13 2020
10# Generated by iptables-save v1.8.4 on Thu Feb 11 05:39:13 2020
11*nat
12:PREROUTING ACCEPT [418:25288]
13:INPUT ACCEPT [220:13024]
14:OUTPUT ACCEPT [20:1482]
15:POSTROUTING ACCEPT [20:1482]
16# Completed on Thu Feb 11 05:39:13 2020
17# Generated by iptables-save v1.8.4 on Thu Feb 11 05:39:13 2020
18*filter
19:INPUT DROP [0:0]
20:FORWARD ACCEPT [0:0]
21:OUTPUT ACCEPT [74:8418]
22# Allow all communication on localhost
23-A INPUT -i lo -j ACCEPT
24
25# ICMP
26-A INPUT -p icmp -j ACCEPT
27
28# Allow incoming packets related to established connections
29-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
30
31# SSH
32-A INPUT -s {{ (ds "ips").user1_example_com }}/32 -p tcp -m tcp --dport 22 -j ACCEPT
33-A INPUT -s {{ (ds "ips").user2_example_com }}/32 -p tcp -m tcp --dport 22 -j ACCEPT
34-A INPUT -s {{ (ds "ips").user3_example_com }}/32 -p tcp -m tcp --dport 22 -j ACCEPT
35
36# Web
37-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
38-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
39
40COMMIT
41# Completed on Thu Feb 11 05:39:13 2020

And the script would be

 1#!/bin/sh
 2
 3# Tolerate no errors
 4set -e
 5
 6# Clear the temp file
 7cat /dev/null > /tmp/ips
 8
 9for host in user1.example.com user2.example.com user3.example.com
10do
11    # Resolve the hostname and compare it to the one we currently have configured
12    # Only fetch one record
13    curIP=`getent hosts $host| cut -d' ' -f1 | sort | head -1`
14
15    # Store IPs in a YAML file
16    eval "echo `echo $host | tr '.' '_'`: ${curIP}" >> /tmp/ips
17done
18
19# Generate the rules file
20gomplate_linux-amd64 -d ips=/tmp/ips -f iptables.tpl > /tmp/rules.v4
21
22# If the files are different, that means at least one IP address changed
23diff /etc/iptables/rules.v4 /tmp/rules.v4 || cat /tmp/rules.v4 > /etc/iptables/rules.v4 && /usr/bin/systemctl restart netfilter-persistent.service

Then add this script to CRON and you should be set.

Yes this is a bit tedious and I have already switched to using another solution which I will share in a future post. But I thought this would be useful to someone so I thought I would share.

About Me

Dev gone Ops gone DevOps. Any views expressed on this blog are mine alone and do not necessarily reflect the views of my employer.

Categories