Ansible playbooks
Following on from last weeks post I've had a go at writing Ansible playbooks. This post is going to go over writing a playbook to configure knockd. The configuration is going to mirror the setup from my previous post on knockd.
Playbook anatomy
In essence playbooks are YAML files which contain a list of Ansible tasks to run on one or more hosts. A very simple example might look something like the following:
---
- hosts: pi
tasks:
- name: Run uptime
command: /usr/bin/uptime
The hosts
key tells Ansible which group from the inventory it should connect
to and the tasks
key contains one or more tasks to run. At a basic level, a
task is just a call to an Ansible module.
Playbooks are run with the ansible-playbook
command. Running the playbook
above produces output similar to the following:
$ ansible-playbook --verbose example.yaml
Using /etc/ansible/ansible.cfg as config file
PLAY [pi] **********************************************************************
TASK [setup] *******************************************************************
ok: [192.168.1.129]
TASK [Run uptime] **************************************************************
changed: [192.168.1.129] => {"changed": true, "cmd": ["/usr/bin/uptime"], "delta": "0:00:00.067352", "end": "2016-08-14 13:28:49.088053", "rc": 0, "start": "2016-08-14 13:28:49.020701", "stderr": "", "stdout": " 13:28:49 up 1:41, 2 users, load average: 0.33, 0.23, 0.15", "stdout_lines": [" 13:28:49 up 1:41, 2 users, load average: 0.33, 0.23, 0.15"], "warnings": []}
PLAY RECAP *********************************************************************
192.168.1.129 : ok=2 changed=1 unreachable=0 failed=0
If your interested the Ansible playbook documentation more detail.
Installing knockd
The first task was to get knockd
installed. This was just a case of calling
the package module:
tasks:
- name: ensure knockd package is installed
package: name=knockd state=present
It's worth pointing out the package module actually calls the apt module to get knockd installed. This reminded me slightly of Resource Providers in Puppet.
Setting up iptables
The obvious place to start with iptables is the iptables module. My first attempt at using the module looked something like this:
tasks:
- name: allow local network traffic
iptables: chain=INPUT in_interface=lo jump=ACCEPT
- name: reject local network traffic on the wrong interface
iptables: chain=INPUT in_interface=!lo destination=127.0.0.0/8 jump=REJECT
- name: allow network traffic from existing connections
iptables: chain=INPUT ctstate=RELATED,ESTABLISHED jump=ACCEPT
- name: allow icmp ping from the local network
iptables: chain=INPUT protocol=icmp source=192.168.0.0/16 jump=ACCEPT
- name: allow ssh from the local network
iptables: chain=INPUT protocol=tcp destination_port=22 source=192.168.0.0/16 jump=ACCEPT
- name: create the KNOCKD chain with iptables
command: iptables -N KNOCKD
ignore_errors: true
- name: add the kNOCKD chain to the INPUT chain
iptables: chain=INPUT jump=KNOCKD comment='Check rules added by knockd'
- name: reject any unmatched network inbound traffic
iptables: chain=INPUT jump=REJECT
- name: reject all network traffic in the FORWARD chain
iptables: chain=FORWARD jump=REJECT
Unfortunately there are a few issues with this configuration:
- Configuration made with the iptables module is not persistent.
- The iptables module cannot create custom chains. To get around this the command module is used, although this command will fail on subsiquent runs.
- There are quite a few tasks...
None of these problems are deal breakers, however I ended up just copying the iptables configuration across using the copy module instead:
tasks:
- name: install iptables configuration
copy: owner=root group=root mode=0644 src=iptables.rules dest=/etc/network/iptables
To make the configuration persistent I added two additional tasks to install
the if-pre-up.d
script and ran it to load the iptables rules using
iptables-restore
.
tasks:
- name: install iptables configuration
copy: owner=root group=root mode=0644 src=iptables.rules dest=/etc/network/iptables
- name: install script to load iptables configuration
copy: owner=root group=root mode=0755 src=load_iptables.sh dest=/etc/network/if-pre-up.d/iptables
- name: load iptables rules
command: /etc/network/if-pre-up.d/iptables
Using handlers
The tasks above work fine, however the load iptables rules
task will be
called every time Ansible runs. Really it should only be called if the iptables
configuration is updated. This can be achieved by changing the
load iptables rules
task into a handler task:
tasks:
- name: install script to load iptables configuration
copy: owner=root group=root mode=0755 src=load_iptables.sh dest=/etc/network/if-pre-up.d/iptables
- name: install iptables configuration
copy: owner=root group=root mode=0644 src=iptables.rules dest=/etc/network/iptables
notify:
- reload iptables rules
handlers:
- name: reload iptables rules
command: /etc/network/if-pre-up.d/iptables
Configuring knockd
The last thing to do was get the knockd configuration in place and start the
service. The tasks below use the copy, replace and service modules to populate
/etc/knockd.conf
, update /etc/default/knockd
and enable the service:
tasks:
- name: install knockd configuration
copy: owner=root group=root mode=0640 src=knockd.conf dest=/etc/knockd.conf
notify:
- restart knockd
- name: enable knockd
replace: dest=/etc/default/knockd regexp='^(START_KNOCKD)=0$' replace='\1=1'
notify:
- restart knockd
- name: enable knockd service
service: name=knockd enabled=yes state=started
handlers:
- name: restart knockd
service: name=knockd state=restarted
Using templates
Instead of leaving the knockd ports hard-coded in knockd.conf
I decided to use the
template module and define the ports inside the
playbook:
vars:
knockd_ports:
- 4438
- 1813
- 8235
tasks:
- name: install knockd configuration
template: owner=root group=root mode=0640 src=knockd.conf.j2 dest=/etc/knockd.conf
notify:
- restart knockd
The options passed to the module are identical with the exception of the
src
option. Instead of pointing directly to the file, a jinja2
template is used. The content of the template looks something like
this:
[options]
UseSyslog
[openSSH]
sequence = {{ knockd_ports | join(',') }}
seq_timeout = 5
command = /sbin/iptables -A KNOCKD -s %IP% -p tcp --dport 22 -j ACCEPT
tcpflags = syn
All together
Below is the complete playbook:
---
- hosts: pi
vars:
knockd_ports:
- 4438
- 1813
- 8235
tasks:
- name: ensure knockd package is installed
package: name=knockd state=present
- name: install script to load iptables configuration
copy: owner=root group=root mode=0755 src=load_iptables.sh dest=/etc/network/if-pre-up.d/iptables
- name: install iptables configuration
copy: owner=root group=root mode=0644 src=iptables.rules dest=/etc/network/iptables
notify:
- reload iptables rules
- name: install knockd configuration
template: owner=root group=root mode=0640 src=knockd.conf.j2 dest=/etc/knockd.conf
notify:
- restart knockd
- name: enable knockd
replace: dest=/etc/default/knockd regexp='^(START_KNOCKD)=0$' replace='\1=1'
notify:
- restart knockd
- name: enable knockd service
service: name=knockd enabled=yes state=started
handlers:
- name: reload iptables rules
command: /etc/network/if-pre-up.d/iptables
- name: restart knockd
service: name=knockd state=restarted
Running from beginning to end takes just over two minutes on a completely new Raspbian image:
[ansible@control ~]$ time ansible-playbook knockd.yaml
PLAY [pi] **********************************************************************
TASK [setup] *******************************************************************
ok: [192.168.1.129]
TASK [ensure knockd package is installed] **************************************
changed: [192.168.1.129]
TASK [install script to load iptables configuration] ***************************
changed: [192.168.1.129]
TASK [install iptables configuration] ******************************************
changed: [192.168.1.129]
TASK [install knockd configuration] ********************************************
changed: [192.168.1.129]
TASK [enable knockd] ***********************************************************
changed: [192.168.1.129]
TASK [enable knockd service] ***************************************************
ok: [192.168.1.129]
RUNNING HANDLER [reload iptables rules] ****************************************
changed: [192.168.1.129]
RUNNING HANDLER [restart knockd] ***********************************************
changed: [192.168.1.129]
PLAY RECAP *********************************************************************
192.168.1.129 : ok=9 changed=7 unreachable=0 failed=0
real 2m13.090s
user 0m18.306s
sys 0m6.078s
Closing thoughts
Again I've been fairly impressed with Ansible. Writing a playbook was pretty straightforward and the module documentation makes it easy to quickly create tasks.
It's worth pointing out that playbook above is fairly monolithic, and certainly doesn't follow all Ansible best practices. In the future I would like to look at using features like Ansible roles to improve this.