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:

  1. Configuration made with the iptables module is not persistent.
  2. The iptables module cannot create custom chains. To get around this the command module is used, although this command will fail on subsiquent runs.
  3. 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.