Setting up an IPSec tunnel using Libreswan
I was interested on how to configure IPSec connections between linux machines, mostly for having another tool in my belt, because sometimes we deploy virtual machines on customer’s infrastructure and not all services are TLS enabled, and IPSec is a way of keeping prying eyes away.
Configuring an IPSec connection using libreswan is well documented on Red Hat’s Securing Networks guide, so I wanted to raise the bar with two extra objectives: use x509 certificates and doing almost all the process with ansible.
This time I wanted to practice with ansible roles, and I created one from scratch, it ended with the following structure:
|-- roles
| `-- setup_ipsec
| |-- default
| | `-- main.yml
| |-- handlers
| | `-- main.yml
| |-- tasks
| | `-- main.yml
| `-- templates
| `-- site2site-x509.conf.j2
Installing packages
The IPSec setup required the installation of the libreswan package, but, as I was using the CentOS 8 vagrant box as base, I also made sure all packages were updates.
- name: update all packages
package:
name: "*"
state: latest
- name: install libreswan
package:
name: libreswan
state: installed
As the previous week I deployed nginx with SSL certificates generated using ansible I wanted to use that knowledge again, so I installed the cryptology related packages on the target machine.
- name: install cryptography module
package:
name: python3-cryptography
state: installed
- name: install python-openssl module
package:
name: python3-pyOpenSSL
state: installed
pyOpenSSL is required to handle PKCS12 Files, the other community.crypto modules could use it, but as it was going to be removed as backend in the 2.0 release of community.crypto, I preferred to use the cryptography mode when possible.
Generating the certificates
This part was borrowed from my previous post, but I made a big change, this time I used the ownca provider instead of generating a self signed certificate. So the first tasks was to generate a Certificate Authority on the control node, I found a concise guide to do this in Lorenzo Fontana’s gist
openssl genrsa -des3 -out rootCA.key 4096
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt
Once I had it I put the private key, the certificate and the password on a vault file to keep them secure.
The corresponding tasks were:
- name: generate a private key
community.crypto.openssl_privatekey:
path: "{{ privatekey_path }}"
backup: yes
- name: generate a CSR
community.crypto.openssl_csr:
common_name: "{{ position }}"
country_name: "{{ cert_country_name }}"
locality_name: "{{ cert_locality_name }}"
state_or_province_name: "{{ cert_state_name }}"
organization_name: "{{ cert_organization_name }}"
organizational_unit_name: "{{ cert_ou_name }}"
privatekey_path: "{{ privatekey_path }}"
subject_alt_name: "{{ ['DNS:' + ansible_facts['fqdn'], 'DNS:' + position ] + ansible_facts['all_ipv4_addresses'] | map('regex_replace', '^', 'IP:') | list }}"
subject:
CN: "{{ position }}"
path: "{{ csr_path }}"
backup: yes
- name: generate a certificate
community.crypto.x509_certificate:
path: "{{ certificate_path }}"
privatekey_path: "{{ privatekey_path }}"
csr_path: "{{ csr_path }}"
provider: ownca
ownca_content: "{{ ownca_content }}"
ownca_privatekey_content: "{{ ownca_privatekey_content }}"
ownca_privatekey_passphrase: "{{ ownca_privatekey_passphrase }}"
Configuring IPSec service
Once I had the certificates in place, it was time to configure the IPSec service, the first step was dealing with the certificates. For managing certificates with certool and pk12tool I needed the root ca certificate and the host certificate in pkcs12 format as files in the target machines.
- name: copy root CA
copy:
content: "{{ ownca_content }}"
dest: "{{ ca_path }}"
notify: import rootca
- name: generate a pkcs12 file
community.crypto.openssl_pkcs12:
certificate_path: "{{ certificate_path }}"
privatekey_path: "{{ privatekey_path }}"
other_certificates: "{{ ca_path }}"
passphrase: "{{ privatekey_passphrase }}"
friendly_name: "{{ position }}"
path: "{{ pkcs12_path }}"
backup: yes
notify: import pkcs12
- name: flush handlers
meta: flush_handlers
I imported the certificates in the NSS library as handlers in order to only do the import process when the certificates changed, because I used the command module and it would be executed every time the playbook is run.
- name: import rootca
command:
cmd: "certutil -d sql:/etc/ipsec.d -A -n rootCA -t C,C,p -a -i {{ ca_path }}"
- name: import pkcs12
command:
cmd: "pk12util -d sql:/etc/ipsec.d -i {{ pkcs12_path }} -W {{ privatekey_passphrase }}"
Then I used the template module to create a configuration file:
- name: copy configuration file
template:
src: ./templates/site2site-x509.conf.j2
dest: /etc/ipsec.d/site2site-x509.conf
notify: restart ipsec
The jinja2 template content was:
conn {{ subnetname }}
also={{ tunnelname }}
leftsubnet={{ leftsubnet }}
rightsubnet={{ rightsubnet }}
conn {{ tunnelname }}
{% if position == 'left' -%}
leftcert={{ leftcert }}
{%- else -%}
rightcert={{ rightcert }}
{%- endif %}
leftid={{ leftid }}
left={{ leftip }}
rightid={{ rightid }}
right={{ rightip }}
auto={{ autostart }}
For enabling the IPSec service on I used the service_facts module to know if firewalld was running, because using immediate when the service is stopped, would cause a failure.
- name: gather service facts
service_facts:
- name: enable firewalld services
firewalld:
service: ipsec
state: enabled
immediate: "{{ ('firewalld.services' in ansible_facts['services'] and ansible_facts['services']['firewalld.service']['state']=='running')|bool }}"
permanent: yes
Time to go
Once I had all the tasks in the role, it was time to run it, I used it on a playbook with a some extra tasks to have a way of testing it.
---
- name: Configure site-to-site vpn
hosts: ipsec
vars:
leftdev: eth2
leftsrc: 192.168.131.2
righttgt: 192.168.132.2
roles:
- setup_ipsec
tasks:
- name: restart ipsec
service:
name: ipsec
state: restarted
enabled: yes
- name: install httpd for testing
package:
name: httpd
state: present
when: position == 'right'
- name: start and enable httpd
service:
name: httpd
state: started
enabled: yes
when: position == 'right'
- name: create an hello world message
copy:
content: "Hello world!"
dest: /var/www/html/hello.txt
owner: apache
group: apache
mode: 0644
when: position == 'right'
tags: test
- name: create a route to the right
command:
cmd: "ip route add {{ rightsubnet }} dev {{ leftdev }} src {{ leftsrc }}"
when: position == 'left'
ignore_errors: yes
- name: get from the http server on the right side
get_url:
url: "http://{{ righttgt }}/hello.txt"
dest: /tmp/hello.txt
use_proxy: no
force: yes
register: httpresult
when: position == 'left'
tags: test
- name: show http result
debug:
var: httpresult
when: httpresult is defined
tags: test
- name: cleanup
file:
path: /tmp/hello.txt
state: absent
tags: test
A footnote about the variables
As I was creating a role, I made extensive use of variables, like in any other role, they can be found on the defaults/main.yml file.
Some of them were related to the ipsec configuration, I used the left/right values suggested on Red Hat’s documentation.
subnetname: mysubnet
tunnelname: mytunnel
leftsubnet: 192.168.131.0/24
rightsubnet: 192.168.132.0/24
autostart: start
leftcert: left
leftid: left
leftip: 10.200.200.101
rightcert: right
rightid: right
rightip: 10.200.200.102
There were a bunch of them for the certificate generation, they were the same of my previous post.
csr_base_path: /etc/pki/tls/csr
privatekey_path: /etc/pki/tls/private/{{ position }}.key
privatekey_passphrase: overrideme
csr_path: "{{ csr_base_path }}/{{ position }}.csr"
certificate_path: /etc/pki/tls/certs/{{ position }}.crt
ca_path: "/etc/pki/tls/certs/rootca.pem"
pkcs12_path: /etc/pki/tls/certs/{{ position }}.p12
cert_country_name: "ES"
cert_locality_name: "Albal"
cert_state_name: "Valencia"
cert_organization_name: "Garmo Labs Inc"
cert_ou_name: "Automation"
Ansible documentation recommends against using variables from vault files directly for readability reasons, and also, nobody distributes vault files with their roles, so the best practice is copying them.
# Get sensitive variables from vault file
ownca_content: "{{ cacertificate }}"
ownca_privatekey_content: "{{ caprivkey }}"
ownca_privatekey_passphrase: "{{ capass }}"
The position variable was defined as a host variable on my inventory file, so I had one host on the left and one on the right.
The complete lab
I made all the files available at my home lab git repo