440 Configure DHCP and pf

Use case

Use the role vbotka.freebsd.dhcp to configure DHCP. Use the role vbotka.freebsd.pf to configure pf. Redirect ports from the local network to SSH services in the jails.

Tree

shell> tree .
.
├── ansible.cfg
├── files
│   └── pf-rdr-ssh.conf
├── host_vars
│   └── iocage_06
│       ├── dhcp.yml
│       └── pf.yml
├── iocage.ini
├── pb-dhcp.yml
├── pb-pf-setup.yml
└── pb-pf.yml

Synopsis

  • The Ansible controller connects to the managed node on the WLAN interface configured in /etc/rc.conf:

    gateway_enable="YES"
    defaultrouter="10.30.10.1"
    cloned_interfaces="bridge0"
    ifconfig_bridge0="inet 172.16.99.1/24"
    wlans_iwm0="wlan0"
    create_args_wlan0="country US"
    ifconfig_wlan0="WPA SYNCDHCP"
    dhcpd_ifaces="bridge0"
    
  • In the playbook pb-dhcp.yml configure:

    • subnet 172.16.99.0/24

    • routers [172.16.99.1]

    • range 100-200

  • In the playbook pb-pf.yml configure:

    • blacklistd, fail2ban, and sshguard

    • NAT

    • log all blocked

    • allow SSH, HTTP, and HTTPS from the external network

    • pass from localnet and jails’ vnet to any

    • redirect SSH ports to jails

Requirements

Notes

  • In this example, the controller uses Wi-Fi to connect to the managed node.

  • Adjust the following settings to match your network environment:

    • /etc/rc.conf:

      • interface

      • bridge

      • defaultrouter

    • dhcp.yml:

      • bsd_dhcpd_subnet

      • bsd_dhcpd_subnet_from

      • bsd_dhcpd_subnet_to

      • bsd_dhcpd_routers

    • pf.yml:

      • pf_local_net

      • pf_jail_net

      • pf_ssh_redirected_ports

Note

vbotka.freebsd_dhcp is the role freebsd_dhcp in the namespace vbotka.
vbotka.freebsd_pf is the role freebsd_pf in the namespace vbotka.

See also

ansible.cfg

[defaults]
gathering = explicit
callback_result_format = yaml
display_skipped_hosts = False
interpreter_python = auto_silent
log_path = /var/log/ansible.log
deprecation_warnings = False

[connection]
pipelining = true

Inventory iocage.ini

iocage_06

[iocage]
iocage_06

[iocage:vars]
ansible_user=admin
ansible_become=true
ansible_python_interpreter=auto_silent

files

files/pf-rdr-ssh.conf
rdr on $ext_if proto tcp from $localnet to ($ext_if) port 2200 -> 172.16.99.100 port 22
rdr on $ext_if proto tcp from $localnet to ($ext_if) port 2201 -> 172.16.99.101 port 22
rdr on $ext_if proto tcp from $localnet to ($ext_if) port 2202 -> 172.16.99.102 port 22
rdr on $ext_if proto tcp from $localnet to ($ext_if) port 2203 -> 172.16.99.103 port 22
rdr on $ext_if proto tcp from $localnet to ($ext_if) port 2204 -> 172.16.99.104 port 22
rdr on $ext_if proto tcp from $localnet to ($ext_if) port 2205 -> 172.16.99.105 port 22
rdr on $ext_if proto tcp from $localnet to ($ext_if) port 2206 -> 172.16.99.106 port 22
rdr on $ext_if proto tcp from $localnet to ($ext_if) port 2207 -> 172.16.99.107 port 22
rdr on $ext_if proto tcp from $localnet to ($ext_if) port 2208 -> 172.16.99.108 port 22
rdr on $ext_if proto tcp from $localnet to ($ext_if) port 2209 -> 172.16.99.109 port 22

Note

  • The above listing is limited to the first 10 lines.

  • See the playbook pb-pf-setup.yml below for how to create the file.

host_vars

host_vars/iocage_06/dhcp.yml
---
bsd_dhcpd_install: false
bsd_dhcpd_enable: true
bsd_dhcpd_debug: false
bsd_dhcpd_sanity: true
bsd_dhcpd_sanity_fatal: true
bsd_dhcpd_backup_conf: true

# rc.conf
bsd_dhcpd_rcconf:
  - name: dhcpd_ifaces
    value: bridge0

# Template
bsd_dhcpd_template: dhcpd.conf-minimal.j2

# Gobal
bsd_dhcpd_domain_name: g3.example.com
bsd_dhcpd_domain_name_servers: [8.8.8.8, 1.1.1.1]
bsd_dhcpd_default_lease_time: 600
bsd_dhcpd_max_lease_time: 7200

# Primary server
bsd_dhcpd_authoritative: true

# Subnet
bsd_dhcpd_subnet: 172.16.99
bsd_dhcpd_netmask: 255.255.255.0
bsd_dhcpd_subnet_from: 100
bsd_dhcpd_subnet_to: 199
bsd_dhcpd_routers: [172.16.99.1]
bsd_dhcpd_subnet_broadcast: 255

# EOF
host_vars/iocage_06/pf.yml
---
pf_install: false

pf_enable: true
pf_blacklistd: true
pf_fail2ban: true
pf_relayd: false
pf_sshguard: true

pf_blacklistd_enable: true
pf_fail2ban_enable: true
pf_sshguard_enable: true

pf_debug: false
pf_backup_conf: true

pfconf_only: false
pfconf_validate: true

# /etc/pf.conf
pf_type: default3
pf_log_enable: true
pf_ext_if: wlan0
pf_int_if: bridge0
pf_local_net: 10.30.10.0/24
pf_jail_net: 172.16.99.0/24
pf_log_all_blocked: log
pf_pass_icmp_types: [echoreq, unreach]
pf_pass_icmp6_types: [echoreq, unreach]
pf_tcp_services: [http, https, ssh]
pf_ssh_redirected_ports: '2200:2299'

# macros
pf_macros:
  ext_if: "{{ pf_ext_if }}"
  int_if: "{{ pf_int_if }}"
  localnet: "{{ pf_local_net }}"
  jailnet: "{{ pf_jail_net }}"
  logall: "{{ pf_log_all_blocked }}"
  icmp_types: "{{ pf_pass_icmp_types }}"
  icmp6_types: "{{ pf_pass_icmp6_types }}"
  tcp_services: "{{ pf_tcp_services }}"
  ssh_redirected_ports: "{{ pf_ssh_redirected_ports }}"

# includes
pf_includes:
  pf_translation: pf-rdr-ssh.conf

# pf blocks
pf_tables:
  - table <sshabuse> persist

pf_options:
  - set skip on lo0
  - set block-policy return
  - set loginterface $ext_if
  - set limit frags 10000

pf_normalization:
  - scrub in on $ext_if all fragment reassemble no-df
  - scrub out on $ext_if all max-mss 1440

pf_translation:
  - "{{ pf_rules_rdr }}"
  - nat on $ext_if from { $localnet, $jailnet } to any -> ($ext_if)
  - nat on $int_if from $jailnet to any -> ($int_if)

pf_filtering:
  - antispoof for $ext_if
  - "{{ pf_anchors }}"
  - block $logall all
  - pass inet proto icmp icmp-type $icmp_types
  - pass inet6 proto icmp6 icmp6-type $icmp6_types
  - pass in on $ext_if proto tcp from $localnet to ($ext_if) port $tcp_services flags S/SA keep state
  - pass in on $ext_if proto tcp from $localnet to { ($ext_if), $jailnet } port { $ssh_redirected_ports, ssh } flags S/SA keep state
  - pass in on $int_if from $jailnet to any keep state
  - pass out on $int_if from any to $jailnet keep state
  - pass out on $ext_if from { self, ($ext_if), $jailnet } to any keep state

# blacklistd
pf_blacklistd_conf_remote: []
pf_blacklistd_conf_local:
  - {adr: ssh, type: stream, proto: '*', owner: '*', name: '*', nfail: '3', disable: 24h}
  - {adr: ftp, type: stream, proto: '*', owner: '*', name: '*', nfail: '3', disable: 24h}
  - {adr: smtp, type: stream, proto: '*', owner: '*', name: '*', nfail: '3', disable: 24h}
  - {adr: smtps, type: stream, proto: '*', owner: '*', name: '*', nfail: '3', disable: 24h}
  - {adr: submission, type: stream, proto: '*', owner: '*', name: '*', nfail: '3', disable: 24h}
  - {adr: '*', type: '*', proto: '*', owner: '*', name: '*', nfail: '3', disable: '60'}
pf_blacklistd_flags: '-r'
pf_blacklistd_rcconf:
  - {name: blacklistd_flags, value: "{{ pf_blacklistd_flags }}"}

# EOF

Playbook pb-dhcp.yml

- name: Test role vbotka.freebsd.dhcp
  hosts: iocage
  gather_facts: true

  roles:
    - vbotka.freebsd.dhcp

Playbook output - Install packages

(env) > ansible-playbook -i iocage.ini \
                         -t bsd_dhcpd_packages -e bsd_dhcpd_install=true \
                         pb-dhcp.yml
PLAY [Test role vbotka.freebsd.dhcp] *******************************************

TASK [Gathering Facts] *********************************************************
ok: [iocage_06]

TASK [vbotka.freebsd.dhcp : Sanity: Collect installed packages.] ***************
ok: [iocage_06]

TASK [vbotka.freebsd.dhcp : Sanity: Set my_missing_packages] *******************
ok: [iocage_06]

PLAY RECAP *********************************************************************
iocage_06                  : ok=3    changed=0    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0   

Playbook output - Configure DHCP

(env) > ansible-playbook -i iocage.ini pb-dhcp.yml
PLAY [Test role vbotka.freebsd.dhcp] *******************************************

TASK [Gathering Facts] *********************************************************
ok: [iocage_06]

TASK [vbotka.freebsd.dhcp : Sanity: Collect installed packages.] ***************
ok: [iocage_06]

TASK [vbotka.freebsd.dhcp : Sanity: Set my_missing_packages] *******************
ok: [iocage_06]

TASK [vbotka.freebsd.dhcp : Rcconf: Enable and start dhcpd] ********************
ok: [iocage_06]

TASK [vbotka.freebsd.dhcp : Rcconf: Configure dhcpd.] **************************
ok: [iocage_06] => (item={'name': 'dhcpd_ifaces', 'value': 'bridge0'})

TASK [vbotka.freebsd.dhcp : Conf: Configure /usr/local/etc/dhcpd.conf from dhcpd.conf-minimal.j2] ***
changed: [iocage_06]

RUNNING HANDLER [vbotka.freebsd.dhcp : Restart dhcpd] **************************
changed: [iocage_06]

PLAY RECAP *********************************************************************
iocage_06                  : ok=7    changed=2    unreachable=0    failed=0    skipped=6    rescued=0    ignored=0   

Playbook pb-pf-setup.yml

- name: FreeBSD pf role setup.
  hosts: localhost

  vars:

    ssh_fwd_start: 2200
    dhcp_subnet: 172.16.99
    dhcp_ip_start: 100
    stop: 100

  tasks:

    - name: Create pf-rdr-ssh.conf
      tags: pf-rdr-ssh
      ansible.builtin.copy:
        dest: "{{ playbook_dir }}/files/pf-rdr-ssh.conf"
        content: |
          {% for i in range(0, stop) %}
          rdr on $ext_if proto tcp from $localnet to ($ext_if) port {{ ssh_fwd_start + i }} -> {{ dhcp_subnet }}.{{ dhcp_ip_start + i }} port 22
          {% endfor %}

Playbook output - Create files/pf-rdr-ssh.conf

(env) > ansible-playbook -i iocage.ini pb-pf-setup.yml
PLAY [FreeBSD pf role setup.] **************************************************

TASK [Create pf-rdr-ssh.conf] **************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Playbook pb-pf.yml

- name: Test role vbotka.freebsd.pf
  hosts: iocage
  gather_facts: true

  roles:
    - vbotka.freebsd.pf

Playbook output - Install packages

(env) > ansible-playbook -i iocage.ini \
                         -t pf_packages -e pf_install=true \
                         pb-pf.yml
PLAY [Test role vbotka.freebsd.pf] *********************************************

TASK [Gathering Facts] *********************************************************
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Packages: Install packages.] *************************
ok: [iocage_06]

PLAY RECAP *********************************************************************
iocage_06                  : ok=2    changed=0    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0   

Playbook output - Configure pf

Starting and restarting the firewall breaks active SSH connections (see the handlers for details). Consequently, both the start and reload handlers can fail to complete cleanly, causing the SSH session to go stale. Therefore, configure the rules first before enabling the service:

(env) > ansible-playbook -i iocage.ini -e pf_enable=false pb-pf.yml
PLAY [Test role vbotka.freebsd.pf] *********************************************

TASK [Gathering Facts] *********************************************************
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Rcconf-blacklistd: Stat /etc/rc.d/blacklistd] ********
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Blacklistd: Backup /etc/blacklistd.conf.orig] ********
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Blacklistd: Configure /etc/blacklistd.conf] **********
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Rcconf-fail2ban: Stat /usr/local/etc/rc.d/fail2ban] ***
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Fail2ban: Configure /usr/local/etc/fail2ban/fail2ban.local] ***
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Fail2ban: Configure /usr/local/etc/fail2ban/jail.local] ***
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Rcconf-sshguard: Stat /usr/local/etc/rc.d/sshguard] ***
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Configure sshguard.conf] *****************************
ok: [iocage_06] => (item={'key': 'BACKEND', 'value': '/usr/local/libexec/sshg-fw-pf'})
ok: [iocage_06] => (item={'key': 'WHITELIST_FILE', 'value': '/usr/local/etc/sshguard.whitelist'})

TASK [vbotka.freebsd.pf : Create directory /etc/pf] ****************************
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Copy includes files to /etc/pf] **********************
ok: [iocage_06] => (item={'key': 'pf_translation', 'value': 'pf-rdr-ssh.conf'})

TASK [vbotka.freebsd.pf : Pfconf: Configure and validate rules using template default3-pf.conf.j2] ***
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Rcconf-blacklistd: Enable and start blacklistd.] *****
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Rcconf-blacklistd: Configure blacklistd.] ************
ok: [iocage_06] => (item={'name': 'blacklistd_flags', 'value': '-r'})

TASK [vbotka.freebsd.pf : Rcconf-fail2ban: Enable and start fail2ban.] *********
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Rcconf-fail2ban: Configure fail2ban.] ****************
ok: [iocage_06] => (item={'name': 'fail2ban_flags', 'value': ''})

TASK [vbotka.freebsd.pf : Rcconf-sshguard: Enable and start sshguard.] *********
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Rcconf-pf: Disable and stop pf.] *********************
changed: [iocage_06]

TASK [vbotka.freebsd.pf : Rcconf-pflog: Enable and start pflog.] ***************
ok: [iocage_06]

RUNNING HANDLER [vbotka.freebsd.pf : Disable and stop pf] **********************
changed: [iocage_06]

PLAY RECAP *********************************************************************
iocage_06                  : ok=20   changed=2    unreachable=0    failed=0    skipped=40   rescued=0    ignored=0   

Playbook output - Enable pf

(env) > ansible-playbook -i iocage.ini -t pf_rcconf_pf pb-pf.yml
PLAY [Test role vbotka.freebsd.pf] *********************************************

TASK [Gathering Facts] *********************************************************
ok: [iocage_06]

TASK [vbotka.freebsd.pf : Rcconf-pf: Enable and start pf.] *********************
changed: [iocage_06]

RUNNING HANDLER [vbotka.freebsd.pf : Start pf] *********************************
changed: [iocage_06]

PLAY RECAP *********************************************************************
iocage_06                  : ok=3    changed=2    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0   

Results

isc-dhcpd status

shell> ssh admin@iocage_06 sudo service isc-dhcpd status
dhcpd is running as pid 47758.

/usr/local/etc/dhcpd.conf

shell> ssh admin@iocage_06 cat /usr/local/etc/dhcpd.conf
# Ansible managed

# Global options
option domain-name "g3.example.com";
option domain-name-servers 8.8.8.8, 1.1.1.1;
default-lease-time 600;
max-lease-time 7200;

# Denotes this server as the primary DHCP server for the local network
authoritative;

# Internal subnet configuration
subnet 172.16.99.0 netmask 255.255.255.0 {
  range 172.16.99.100 172.16.99.199;
  option routers 172.16.99.1;
  option broadcast-address 172.16.99.255;
}

pf status

shell> ssh admin@iocage_06 sudo service pf status
Status: Disabled for 0 days 00:00:08          Debug: Urgent

Interface Stats for wlan0             IPv4             IPv6
  Bytes In                         8077074                0
  Bytes Out                         927531                0
  Packets In
    Passed                           10128                0
    Blocked                           1433                0
  Packets Out
    Passed                            7831                0
    Blocked                              0                0

State Table                          Total             Rate
  current entries                       25               
  searches                           20539         2567.4/s
  inserts                              552           69.0/s
  removals                             527           65.9/s
Counters
  match                               2045          255.6/s
  bad-offset                             0            0.0/s
  fragment                               0            0.0/s
  short                                  0            0.0/s
  normalize                              0            0.0/s
  memory                                 0            0.0/s
  bad-timestamp                          0            0.0/s
  congestion                             0            0.0/s
  ip-option                              0            0.0/s
  proto-cksum                           15            1.9/s
  state-mismatch                         0            0.0/s
  state-insert                           0            0.0/s
  state-limit                            0            0.0/s
  src-limit                              0            0.0/s
  synproxy                               0            0.0/s
  map-failed                             0            0.0/s
  translate                              0            0.0/s

/etc/pf.conf

shell> ssh admin@iocage_06 cat /etc/pf.conf
# Ansible managed
# template: default3-pf.conf.j2

# MACROS
ext_if = "wlan0"
int_if = "bridge0"
localnet = "10.30.10.0/24"
jailnet = "172.16.99.0/24"
logall = "log"
icmp_types = "{ echoreq, unreach }"
icmp6_types = "{ echoreq, unreach }"
tcp_services = "{ http, https, ssh }"
ssh_redirected_ports = "2200:2299"

# TABLES
table <sshabuse> persist

# OPTIONS
set skip on lo0
set block-policy return
set loginterface $ext_if
set limit frags 10000

# NORMALIZATION
scrub in on $ext_if all fragment reassemble no-df
scrub out on $ext_if all max-mss 1440

# TRANSLATION
nat on $ext_if from { $localnet, $jailnet } to any -> ($ext_if)
nat on $int_if from $jailnet to any -> ($int_if)
include "/etc/pf/pf-rdr-ssh.conf"

# FILTERING
antispoof for $ext_if
anchor "blacklistd/*" in on $ext_if
anchor "f2b/*"
block $logall all
pass inet proto icmp icmp-type $icmp_types
pass inet6 proto icmp6 icmp6-type $icmp6_types
pass in on $ext_if proto tcp from $localnet to ($ext_if) port $tcp_services flags S/SA keep state
pass in on $ext_if proto tcp from $localnet to { ($ext_if), $jailnet } port { $ssh_redirected_ports, ssh } flags S/SA keep state
pass in on $int_if from $jailnet to any keep state
pass out on $int_if from any to $jailnet keep state
pass out on $ext_if from { self, ($ext_if), $jailnet } to any keep state

# EOF

Note

To connect via SSH to a jail (for example, a jail at IP 172.16.99.114), run:

shell> ssh -p 2214 admin@iocage_06