├── inventory ├── roles └── setup │ ├── templates │ ├── hostname.j2 │ ├── dhcpd.flags.j2 │ ├── interfaces.j2 │ ├── dhcpd.conf.j2 │ └── pf.conf.j2 │ └── tasks │ ├── hostname.yml │ ├── interfaces.yml │ ├── netstart.yml │ ├── main.yml │ ├── pf.yml │ ├── dhcpd.yml │ ├── time.yml │ ├── rc.yml │ └── sysctl.yml ├── play_setup.yml ├── host_vars └── obsd-test │ ├── ansible.yml │ ├── general.yml │ ├── bootstrap.yml │ └── interfaces.yml ├── bootstrap.yml ├── LICENSE └── README.md /inventory: -------------------------------------------------------------------------------- 1 | obsd-test 2 | -------------------------------------------------------------------------------- /roles/setup/templates/hostname.j2: -------------------------------------------------------------------------------- 1 | # Generated by ansible 2 | {{ hostname }}.{{ domain }} 3 | -------------------------------------------------------------------------------- /play_setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | gather_facts: true 4 | 5 | roles: 6 | - setup 7 | -------------------------------------------------------------------------------- /roles/setup/tasks/hostname.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: set hostname 3 | template: 4 | src: hostname.j2 5 | dest: /etc/myname 6 | -------------------------------------------------------------------------------- /roles/setup/templates/dhcpd.flags.j2: -------------------------------------------------------------------------------- 1 | dhcpd_flags="{% for int in interfaces%}{% if interfaces.get(int).dhcp_server == true %}{{ int }} {% endif %}{% endfor %}" 2 | -------------------------------------------------------------------------------- /host_vars/obsd-test/ansible.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ansible vars 3 | ansible_host: 10.0.0.100 4 | ansible_connection: paramiko 5 | ansible_user: root 6 | ansible_python_interpreter: /usr/local/bin/python2.7 7 | -------------------------------------------------------------------------------- /host_vars/obsd-test/general.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # general vars 3 | hostname: obsd-test 4 | virtual: yes 5 | tz: /usr/share/zoneinfo/Europe/Amsterdam 6 | domain: lan 7 | dns1: 208.67.222.222 8 | dns2: 208.67.220.220 9 | -------------------------------------------------------------------------------- /roles/setup/tasks/interfaces.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: create interface configuration files 3 | template: 4 | src: interfaces.j2 5 | dest: /etc/hostname.{{ item.key }} 6 | with_dict: "{{ interfaces }}" 7 | register: interfaces_changed 8 | -------------------------------------------------------------------------------- /roles/setup/tasks/netstart.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: set netstart permissions 3 | file: 4 | dest: /etc/netstart 5 | mode: a+x 6 | 7 | - name: execute netstart 8 | command: /etc/netstart 9 | when: interfaces_changed.changed 10 | -------------------------------------------------------------------------------- /roles/setup/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: include tasks 3 | include: "{{ item }}" 4 | with_items: 5 | - hostname.yml 6 | - interfaces.yml 7 | - rc.yml 8 | - netstart.yml 9 | - dhcpd.yml 10 | - time.yml 11 | - sysctl.yml 12 | - pf.yml 13 | -------------------------------------------------------------------------------- /roles/setup/templates/interfaces.j2: -------------------------------------------------------------------------------- 1 | # Generated by ansible 2 | {% if interfaces.get(item.key).dhcp_client == true %} 3 | dhcp 4 | {% endif %} 5 | {% if interfaces.get(item.key).dhcp_client == false %} 6 | inet {{ interfaces.get(item.key).ip }} {{ interfaces.get(item.key).netmask }} 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /roles/setup/tasks/pf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: create pf.conf 3 | template: 4 | src: pf.conf.j2 5 | dest: /etc/pf.conf 6 | validate: pfctl -n -f %s 7 | register: pf 8 | 9 | - name: load config to pf if needed 10 | command: pfctl -f /etc/pf.conf 11 | when: pf.changed 12 | 13 | -------------------------------------------------------------------------------- /bootstrap.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | gather_facts: false 4 | 5 | tasks: 6 | - name: set /etc/installurl 7 | raw: echo "{{ installurl }}" > /etc/installurl 8 | 9 | - name: install python 10 | raw: pkg_add {{ python_package }} 11 | 12 | - name: set python symlinks 13 | raw: "{{ item }}" 14 | with_items: "{{ commands }}" 15 | -------------------------------------------------------------------------------- /roles/setup/tasks/dhcpd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: create dhcpd.conf 3 | template: 4 | src: dhcpd.conf.j2 5 | dest: /etc/dhcpd.conf 6 | register: dhcpd 7 | 8 | - name: restart dhcpd if needed 9 | service: 10 | name: dhcpd 11 | state: restarted 12 | when: dhcpd.changed 13 | 14 | - name: start dhcpd if needed 15 | service: 16 | name: dhcpd 17 | state: started 18 | -------------------------------------------------------------------------------- /host_vars/obsd-test/bootstrap.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # bootstrap vars 3 | python_package: python-2.7.14 4 | installurl: https://mirror.leaseweb.com/pub/OpenBSD 5 | commands: 6 | - ln -sf /usr/local/bin/python2.7 /usr/local/bin/python 7 | - ln -sf /usr/local/bin/python2.7-2to3 /usr/local/bin/2to3 8 | - ln -sf /usr/local/bin/python2.7-config /usr/local/bin/python-config 9 | - ln -sf /usr/local/bin/pydoc2.7 /usr/local/bin/pydoc 10 | -------------------------------------------------------------------------------- /host_vars/obsd-test/interfaces.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # interface vars 3 | interfaces: 4 | vmx0: 5 | name: wan 6 | fw: external 7 | dhcp_client: yes 8 | dhcp_server: no 9 | 10 | vmx1: 11 | name: lan 12 | fw: internal 13 | dhcp_client: no 14 | dhcp_server: yes 15 | ip: 192.168.1.1 16 | netmask: 255.255.255.0 17 | subnet: 192.168.1.0 18 | dhcp_range_begin: 192.168.1.100 19 | dhcp_range_end: 192.168.1.199 20 | -------------------------------------------------------------------------------- /roles/setup/templates/dhcpd.conf.j2: -------------------------------------------------------------------------------- 1 | # Generated by ansible 2 | {% for int in interfaces %} 3 | {% if interfaces.get(int).dhcp_server == true %} 4 | shared-network {{ interfaces.get(int).name }} { 5 | option domain-name {{ domain }}; 6 | option domain-name-servers {{ dns1 }}, {{ dns2 }}; 7 | option routers {{ interfaces.get(int).ip }}; 8 | subnet {{ interfaces.get(int).subnet }} netmask {{ interfaces.get(int).netmask }} { 9 | range {{ interfaces.get(int).dhcp_range_begin }} {{ interfaces.get(int).dhcp_range_end }}; 10 | } 11 | } 12 | 13 | {% endif %} 14 | {% endfor %} 15 | -------------------------------------------------------------------------------- /roles/setup/tasks/time.yml: -------------------------------------------------------------------------------- 1 | - name: configure /etc/localtime 2 | file: 3 | src: "{{ tz }}" 4 | dest: "/etc/localtime" 5 | state: link 6 | force: yes 7 | register: tz_changed 8 | 9 | - name: see if this is first playbook run 10 | stat: 11 | path: /root/ansible_first_run 12 | register: first_run 13 | 14 | - name: register first_run for playbook 15 | file: 16 | dest: /root/ansible_first_run 17 | state: touch 18 | when: first_run.stat.exists == False 19 | 20 | - name: kick ntpd if this is first playbook run 21 | command: ntpd -s 22 | when: first_run.stat.exists == False 23 | 24 | - name: kick ntpd if tz was changed 25 | command: ntpd -s 26 | when: tz_changed.changed 27 | -------------------------------------------------------------------------------- /roles/setup/tasks/rc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: see if rc.conf.local exists 3 | stat: 4 | path: /etc/rc.conf.local 5 | register: rc_conf_local 6 | 7 | - name: create rc.conf.local if necessary 8 | copy: 9 | remote_src: yes 10 | src: /etc/rc.conf 11 | dest: /etc/rc.conf.local 12 | when: rc_conf_local.stat.exists == False 13 | 14 | - name: configure rc.conf.local vmd flag if necessary 15 | lineinfile: 16 | dest: /etc/rc.conf.local 17 | regexp: "^vmd_flags=" 18 | line: "vmd_flags=" 19 | when: virtual == True 20 | 21 | - name: create input for dhcpd flags 22 | set_fact: 23 | dhcpd_flags_input: "{{ lookup('template', 'dhcpd.flags.j2') }}" 24 | 25 | - name: configure rc.conf.local dhcpd flags 26 | lineinfile: 27 | dest: /etc/rc.conf.local 28 | regexp: "^dhcpd_flags=" 29 | line: "{{ dhcpd_flags_input }}" 30 | -------------------------------------------------------------------------------- /roles/setup/tasks/sysctl.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: see if /etc/sysctl.conf exists 3 | stat: 4 | path: /etc/sysctl.conf 5 | register: sysctl_conf 6 | 7 | - name: /etc/sysctl.conf if necessary 8 | file: 9 | dest: /etc/sysctl.conf 10 | state: touch 11 | when: sysctl_conf.stat.exists == False 12 | 13 | - name: enable temporary IPv4 forwarding if necessary 14 | command: sysctl net.inet.ip.forwarding=1 15 | when: sysctl_conf.stat.exists == False 16 | 17 | - name: put IPv4 forwarding enable in sysctl.conf if necessary 18 | lineinfile: 19 | dest: /etc/sysctl.conf 20 | line: "net.inet.ip.forwarding=1" 21 | when: sysctl_conf.stat.exists == False 22 | 23 | - name: make sure IPv4 forwarding is enabled in sysctl.conf 24 | lineinfile: 25 | dest: /etc/sysctl.conf 26 | regexp: "net.inet.ip.forwarding=" 27 | line: "net.inet.ip.forwarding=1" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 jwdevos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /roles/setup/templates/pf.conf.j2: -------------------------------------------------------------------------------- 1 | # Generated by ansible 2 | 3 | # define macros for interfaces and networks 4 | internal_ints = "{ {% for int in interfaces if interfaces.get(int).fw == "internal" %} 5 | {% if not loop.last %}{{ int }}, {% endif %} 6 | {% if loop.last %}{{ int }} {% endif %} 7 | {% endfor %} 8 | }" 9 | internal_nets = "{ {% for int in interfaces if interfaces.get(int).fw == "internal" %} 10 | {% if not loop.last %}{{ int }}:network, {% endif %} 11 | {% if loop.last %}{{ int }}:network {% endif %} 12 | {% endfor %} 13 | }" 14 | external_int = "{% for int in interfaces %} 15 | {% if interfaces.get(int).fw == "external" %}{{ int }}{% endif %} 16 | {% endfor %}" 17 | external_net = "{% for int in interfaces %} 18 | {% if interfaces.get(int).fw == "external" %}{{ int }}:network{% endif %} 19 | {% endfor %}" 20 | 21 | # don't send RST for dropped packets 22 | set block-policy drop 23 | 24 | #omit lo0 interface from being processed in pf 25 | set skip on lo0 26 | 27 | # IPv4 NAT to outside rule 28 | match out on $external_int inet from $internal_nets nat-to ($external_int) 29 | 30 | # default deny rule 31 | block all 32 | 33 | #allow incoming ping on external_int 34 | pass in on $external_int inet proto icmp icmp-type echoreq 35 | 36 | # allow ssh to external_int from any ip 37 | pass in on $external_int inet proto tcp from any to ($external_int) port 22 38 | 39 | # allow outgoing traffic 40 | pass from { self, $internal_nets } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **-- this repo will be archived and moved to https://codeberg.org/jwdevos/openbsd_firewall_ansible --** 2 | 3 | # openbsd_firewall_ansible 4 | This Ansible project is used to deploy and manage an OpenBSD firewall running pf and dhcpd. The playbooks deploy a running firewall taking nothing more than a fresh install of OpenBSD. The playbooks produce a minimum viable product to demonstrate a working firewall. They are intended for demonstration purposes but can be taken freely to suit your own needs. The cool thing is that all of the unique configuration is in the variables under host_vars. That means you can easily make configuration changes to a running box and reload only the services that were affected. With pf, the firewall keeps running if you just reload the configuration. 5 | 6 | **How to use** 7 | Start with a fresh install of OpenBSD reachable via SSH as root on an IPv4 address. You'll need at least two network interfaces. Have this Ansible project ready on another host that can reach the OpenBSD box. 8 | 9 | This project should work with the defaults right away but you should set all your config by editing the host_vars. You can set the IP address of the target host in the /host_vars/obsd-test/ansible.yml file. 10 | 11 | Just run the two following playbooks from the main project directory (./openbsd_firewall_ansible/): 12 | ``` 13 | ansible-playbook bootstrap.yml -i inventory --ask-pass 14 | ansible-playbook play_setup.yml -i inventory --ask-pass 15 | ``` 16 | The play_setup.yml playbook can also be used for making changes after the initial deployment. The intention is to do all of the management of the firewall with the playbooks. 17 | 18 | **What's happening?** 19 | OpenBSD doesn't have Python by default. The bootstrap playbook makes sure that package managent is configured on the host and that Python is available, installing it if necessary. 20 | The setup playbook actually calls an ansible role taking care of lots of things. It takes the content of the host_vars and makes sure following things get done: 21 | 1. Set the system hostname to the correct value if necessary 22 | 2. Make sure interface configuration files are set to the correct state 23 | 3. Configure the rc.conf.local file for dhcpd and virtual tools if necessary 24 | 4. Restart netwoking if relevant configuration was changed 25 | 5. Configure dhcpd and make sure the service is started 26 | 6. Configure time 27 | 7. Configure IP forwarding 28 | 8. Configure the pf firewall, testing the configuration file and reloading the firewall if relevant configuration was changed 29 | 30 | Most of the items in the list above rely on the Jinja2 templates in the templates-directory. These templates make use of the content of the host_vars. 31 | 32 | **Some notes** 33 | * Additional firewall rules have to be added in the template file at this moment. The intention is to look into generating the rules, taking the content from host_vars 34 | * For 9 out of 10 use cases there is only going to be one external interface. This project was coded for that use case only. Adding more than one interface variable set to external might harm the playbook execution or the functionality of the running firewall 35 | pf template breaks with multiple interfaces as external 36 | * The code was tested on OpenBSD 6.2 37 | * There is a rule in pf allowing ssh connections to the external interface. This rule is used for testing and should not be loaded for production systems without further measures 38 | --------------------------------------------------------------------------------