LibreSwan IPSEC Tunnel with Ansible

On the previous post I wrote about deploying a AWS Network Stack with Terraform and how to use Terraform to deploy a Linux Instance with LibreSwan installed.

I’ve been wanting to learn Ansible for a while now, so on this post we are going to use it to make it easy to deploy VPN Configuration into new VPN Instances. I won’t get into details of how Ansible works because they have a great Documentation, its easy to understand the basics and start playing with it!

On this setup we will be using the two VPCs in eu-central-1 and eu-west-1 that we built on the previous posts, with the difference that a second vpn-instance was added per vpc, so we can have 2 vpc-instance for redundancy, and also the Security Groups were changed because I notice I had left it allowing everything (Oops!).

The full thing can be found here on my Git Repo.

Playbook Tree

This is the File structure used by our vpn-instances Playbook:

└── vpn-instances
    ├── hosts
    ├── libreswan.j2
    ├── libreswan_secrets.j2
    ├── vpn-playbook.yml
    └── vpn_vars
        ├── vault.yml
        ├── vpn01-euc1-euw1.yml
        └── vpn02-euc1-euw1.yml

Hosts is our Ansible Inventory File, it should list all of our VPN Instances:

[euc1]
vpn01.euc1.netoops.net ansible_host=18.196.67.47
vpn02.euc1.netoops.net ansible_host=52.58.236.243

[euw1]
vpn01.euw1.netoops.net ansible_host=34.243.28.111
vpn02.euw1.netoops.net ansible_host=52.211.207.206

Libreswan.j2 and libreswan_secrets.j2 are our Jinja2 Template Files, these holds the LibreSwan Template, and the Playbook will pass our defined variables to it and deploy it to our vpn instances.

{% if ( left_side in inventory_hostname) %}
conn {{ conn_name }}
  left=%defaultroute
  leftid={{ left_id }}
  right={{ right_peer }}
  authby=secret
  leftsubnet=0.0.0.0/0
  rightsubnet=0.0.0.0/0
  auto=start
  # route-based VPN requires marking and an interface
  mark=5/0xffffffff
  vti-interface={{ vti_left }}
  # do not setup routing because we don't want to send 0.0.0.0/0 over the tunnel
  vti-routing=no

{% endif %}

{% if ( right_side in inventory_hostname) %}
conn {{ conn_name }}
  right=%defaultroute
  rightid={{ right_id }}
  left={{left_peer}}
  authby=secret
  leftsubnet=0.0.0.0/0
  rightsubnet=0.0.0.0/0
  auto=start
  # route-based VPN requires marking and an interface
  mark=5/0xffffffff
  vti-interface={{ vti_right }}
  # do not setup routing because we don't want to send 0.0.0.0/0 over the tunnel
  vti-routing=no
{% endif %}%
{{ left_peer }} {{right_peer}} : PSK "{{ vpn_psk }}"%

vpn-playbook.yml Is the Ansible Playbook. Here’s where we tell Ansible what to do and how to do it. The Playbook has a YAML format, where you define Tasks and defines hosts where the Tasks needs to be applied.

---
- hosts: vpn0x.xxx1.*,vpn0y.other-yyy1.*
  gather_facts: no
  vars_files:
    - ./vpn_vars/vault.yml
    - ./vpn_vars/vpn0x-xxx1-yyy1.yml

  tasks:
  - name: write the vpn config file
    template: src=libreswan.j2 dest=/etc/ipsec.d/{{ conn_name }}.conf
    become: true
    register: tunnel

  - name: write the vpn secrets file
    template: src=libreswan_secrets.j2 dest=/etc/ipsec.d/{{ conn_name }}.secrets
    become: true


  - name: Activate the tunnel
    shell: "{{ item }}"
    with_items:
      - ipsec auto --rereadsecrets
      - ipsec auto --add {{ conn_name }}
      - ipsec auto --up {{ conn_name }}
    become: true
    when: tunnel.changed


  - name: Install Routes left
    shell: ip route add {{ right_subnet }} dev {{ vti_left }}
    when: (left_side in inventory_hostname) and tunnel.changed
    become: true

  - name: Install Routes Right
    shell: ip route add {{ left_subnet }} dev {{ vti_right }}
    when: (right_side in inventory_hostname) and tunnel.changed
    become: true

  - name: ensure ipsec is running
    service: name=ipsec state=started
    become: true

The vault.yml is an Ansible Vault, here is where we are going to Store our PSKs as we don’t want them to be available in Clear-Text in our configuration Files, and specially not in clear-text on a Git Repo, so do remember to also add the vault.yml to your .gitignore 🙂

Finally we have the vpn01-euc1-euw1.yml and vpn02-euc1-euw1.yml file. These files holds our Variables, with all the Parameters that we need to setup each one of our IPSEC VPN Pairs. Here is the example of one of them:

---
#Name for the VPN Connetion
conn_name: euc1-euw1
#EUW1, Left Side
left_id: 34.243.28.111
left_peer: 34.243.28.111
left_side: vpn01.euw1.netoops.net
vti_left: vti0
left_subnet: 10.250.0.0/24

#EUC1, right Side
right_peer: 18.196.67.47
right_id: 18.196.67.47
right_side: vpn01.euc1.netoops.net
vti_right: vti0
right_subnet: 10.240.0.0/24

#PSK to be used. Note: PSK is stored on vault-psk
vpn_psk: "{{ vault_vpn01_psk }}"%

For each new P2P IPSEC That we want to setup, we will create a new file with a suggestive name, we are going to set the variables with the common VPN params as Local Peer IP and ID, Remote Peer IP and ID, Subnet of Each Side, the VTI Tunnel Number (one VTI per Tunnel), and last but not least, our Pre-Shared Key. If you pay attention on the vpn_psk variable, you will see that it points to another variable with a prefix of vault_, this variable is stored on our Ansible Vault, and for each new Tunnel we should define a new PSK on the Vault. Try not to use the same PSK everywhere!

With your variables done, all we need to do now is go back to our Playbook and change the Variables File, pointing to the var_files we defined above and filter the hosts to apply the configuration to:

- hosts: vpn01.euc1.*,vpn01.euw1.*
  gather_facts: no
  vars_files:
    - ./vpn_vars/vault.yml
    - ./vpn_vars/vpn01-euc1-euw1.yml

That’s it. The playbook will run against the inventory file, will match the host-entries vpn01.euc1* and vpn01.euw1* (wildcards allowed, take a look on Ansible Patterns), we will source our encrypted-variables from vault.yml and our vpn variables from vpn01-euc1-euw1.yml. We only need now to execute the Playbook and the 2 VPN Instances in EUC1 and EUW1 should have their IPSEC Tunnels established and proper routes set to talk to each-other.

AWS Cross-Region Talk in no time 🙂 So far we got the 2 VPN Instances to establish the Tunnel and setup the Static Routes, the next step is to make the AWS Route Tables itself to route traffic from the AWS Subnets into the proper VPN Instances. Lets do that in a different post.

I hope this post helps to see that a Network Engineer doesn’t need to be a Programmer or a Systems Wizard to be able to start writing some Network Automation tools that will help us on our day-by-day tasks, we just need to go a little bit out of our comfort zone and start learning new and interesting skills, its fun, try it!