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.ymlconfigure:subnet 172.16.99.0/24
routers [172.16.99.1]
range 100-200
In the playbook
pb-pf.ymlconfigure: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
Role vbotka.freebsd.dhcp
Role vbotka.freebsd.pf
Root privileges on the managed nodes.
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
See also
FreeBSD Handbook Installing and Configuring a DHCP Server
FreeBSD Handbook Firewalls
FreeBSD Forum pf and bridge
Dell XPS 13 9365 15.0-RELEASE dmesg used as
iocage_05
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
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.ymlbelow for how to create the file.
host_vars
---
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
---
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