Vultr Firewalling with Ansible

Posted on 2018-03-19

  Sysadmin   Ansible   Vultr

This is the second part of a post series What’s new in Ansible 2.5. In the previous post we learned the basic steps to deploy a server on Vultr.

In this follow up post, you will see how we can secure our server.

Vultr Firewall Rules

Firewall Groups

Vultr has the concept of “Firewall Groups” also known as “Security Groups”. One group can have one or more Firewall rules and one server can be assigned to exactly one group.

That said, I think we need to create a firewall group. Let’s create one.

But first we look back what we got in our existing playbook:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
---
- hosts: cloud
  gather_facts: no
  tasks:
  - name: Ensure a cloud server exists
    local_action:
      module: vr_server
      name: "{{ inventory_hostname_short }}"
      os: "CentOS 7 x64"
      plan: "2048 MB RAM,40 GB SSD,2.00 TB BW"
      region: New Jersey

The module to create a firewall group is called vr_firewall_group, unsurprisingly.

We try to add the task to the existing play:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
---
- hosts: cloud
  gather_facts: no
  tasks:
  - name: Create a firewall group
    local_action:
      module: vr_firewall_group
      name: web

  - name: Ensure a cloud server exists
    local_action:
      module: vr_server
      name: "{{ inventory_hostname_short }}"
      os: "CentOS 7 x64"
      plan: "2048 MB RAM,40 GB SSD,2.00 TB BW"
      region: New Jersey

That doesn’t look to wrong, but the challenge here is, we don’t want to create a group for each cloud hosts, just one group in total.

Yes, you are right! This could be achieved by adding a run_once: yes to this task.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
---
- hosts: cloud
  gather_facts: no
  tasks:
  - name: Create a firewall group
    local_action:
      module: vr_firewall_group
      name: web
    run_once: yes

  - name: Ensure a cloud server exists
    local_action:
      module: vr_server
      name: "{{ inventory_hostname_short }}"
      os: "CentOS 7 x64"
      plan: "2048 MB RAM,40 GB SSD,2.00 TB BW"
      region: New Jersey

Yes, that would work. However, there is a drawback when a play uses serial. The run_once: yes would be executed for every batch of host.

That is why I prefer to create a separate play with target localhost for the things that only needs to be applied once in total. And it makes things much more clear, let’s change that.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
---
- hosts: localhost
  gather_facts: no
  tasks:
  - name: Create a firewall group
    vr_firewall_group:
      name: web

- hosts: cloud
  gather_facts: no
  tasks:
  - name: Ensure a cloud server exists
    local_action:
      module: vr_server
      name: "{{ inventory_hostname_short }}"
      os: "CentOS 7 x64"
      plan: "2048 MB RAM,40 GB SSD,2.00 TB BW"
      region: New Jersey

For the new play with target localhost, we skip local_action because the scope is already local. Great, let’s add some HTTP rules.

Firewall Rules

For adding firewall rules, we use the vr_firewall_rule module. It only creates one rule at a time, but with existing Ansible functionality, we are able to apply a list of rules.

We place the task right to the first play right after the firewall group task, because it also only needs to be run once.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
---
- hosts: localhost
  gather_facts: no
  tasks:
  - name: Create a firewall group
    vr_firewall_group:
      name: web

  - name: Create firewall rules
    vr_firewall_rule:
      group: web
      protocol: tcp
      port: "{{ item.port }}"
      cidr: "{{ item.cidr }}"
    with_items:
    - { port: 80, cidr: "0.0.0.0/0" }
    - { port: 443, cidr: "0.0.0.0/0" }

- hosts: cloud
  gather_facts: no
  tasks:
  - name: Ensure a cloud server exists
    local_action:
      module: vr_server
      name: "{{ inventory_hostname_short }}"
      os: "CentOS 7 x64"
      plan: "2048 MB RAM,40 GB SSD,2.00 TB BW"
      region: New Jersey

We added 2 rules with a list of dicts and let ansible loop over it to create each rule.

Once this has been set up, our cloud servers only needs to have the firewall group web assigned.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
---
- hosts: localhost
  gather_facts: no
  tasks:
  - name: Create a firewall group
    vr_firewall_group:
      name: web

  - name: Create firewall rules
    vr_firewall_rule:
      group: web
      protocol: tcp
      port: "{{ item.port }}"
      cidr: "{{ item.cidr }}"
    with_items:
    - { port: 80, cidr: "0.0.0.0/0" }
    - { port: 443, cidr: "0.0.0.0/0" }

- hosts: cloud
  gather_facts: no
  tasks:
  - name: Ensure a cloud server exists
    local_action:
      module: vr_server
      name: "{{ inventory_hostname_short }}"
      os: "CentOS 7 x64"
      plan: "2048 MB RAM,40 GB SSD,2.00 TB BW"
      firewall_group: web
      region: New Jersey

Run the Playbook

I think we are ready to run our evolved playbook! I take the chance to show you another super useful feature of the vultr ansible modules, the --diff support!

We see exactly what changed:

ansible-playbook playbooks/cloud.yml -i hosts/production --diff

PLAY [localhost] ***************************************************************

TASK [Create a firewall group] *************************************************
--- before
+++ after
@@ -1 +1,3 @@
-{}
+{
+    "description": "web"
+}

changed: [localhost]

TASK [Create firewall rules] ***************************************************
--- before
+++ after
@@ -1 +1,9 @@
-{}
+{
+    "FIREWALLGROUPID": "fda48556",
+    "direction": "in",
+    "ip_type": "v4",
+    "port": 80,
+    "protocol": "tcp",
+    "subnet": "0.0.0.0",
+    "subnet_size": "0"
+}

changed: [localhost] => (item={u'cidr': u'0.0.0.0/0', u'start_port': 80})
--- before
+++ after
@@ -1 +1,9 @@
-{}
+{
+    "FIREWALLGROUPID": "fda48556",
+    "direction": "in",
+    "ip_type": "v4",
+    "port": 443,
+    "protocol": "tcp",
+    "subnet": "0.0.0.0",
+    "subnet_size": "0"
+}

changed: [localhost] => (item={u'cidr': u'0.0.0.0/0', u'start_port': 443})

PLAY [cloud] *******************************************************************

TASK [Ensure a cloud server exists] ********************************************
--- before
+++ after
@@ -1,3 +1,3 @@
{
-    "firewall_group": null
+    "firewall_group": "web"
}

changed: [web-01 -> localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=2    unreachable=0    failed=0
web-01                     : ok=1    changed=1    unreachable=0    failed=0

That’s it for today. Hope you enjoyed the reading and looking forward to the follow up posts!