Working with Fortinet firewalls is something new to me. So I’ve started with a virtual Fortinet in GNS3. After doing some policies(ACLs) and checking a few things, I wanted to do a VPN between 2 firewalls. Doing it using CLI and GUI was pretty easy. But nowadays I want to do anything with Ansible and Python, so the next natural step for me was doing it with Ansible.
Using the above topology in GNS3 I’ve started my project. First, it was a little bit difficult to build my var files. That’s because on Fortinet’s you need to specify and configure a phase2 security association for each pair of local/remote subnets. So I’ve used a list of dictionaries to do this, called sas (from security associations). And this list is part of a bigger dictionary called ‘vpn’.
In this dictionary I’ve put all the basic requirements for a site to site vpn: pre-shared-key, DH group, peer IP, the phase1 and phase2 proposal (i’ve used the same one for both), local/remote site name, local/remote subnets.
It would be a good idea to keep the password for the user and the pre-shared key in an ansible vault.(I’ve skipped this step this time.)
Here is one of my var files (varsA):
vpn:
site:
host: 10.0.0.21
username: admin
password: admin
vdom: root
peerIP: 192.168.2.1
pskey: Ohmah4Ookee0
dh: 5
proposal: des-sha512
phase2keylifetime: 3600
subnets:
site_local: Dublin
site_remote: Tokyo
sas:
- name: sa1
local: 172.16.1.0 255.255.255.0
remote: 172.16.2.0 255.255.255.0
- name: sa2
local: 172.16.11.0 255.255.255.0
remote: 172.16.22.0 255.255.255.0
- name: sa3
local: 172.16.1.0 255.255.255.0
remote: 172.16.22.0 255.255.255.0
In many cases, you only have to configure one side of the tunnel. That’s why I’ve used 2 var files: varsA for siteA and varsB for siteB. This way if you need to configure only one side you can use varsA file, and if you need to configure the other end too, you will need to run again the playbook but this time with varsB file.
So in my case, where I’ve configured both sides, it would look like this:
ansible-playbook vpn-setup.yml -e "vars=varsA"
ansible-playbook vpn-setup.yml -e "vars=varsB"
This works because I’ve defined the vars variable in the main YAML file:
---
- hosts: localhost
gather_facts: no
vars_files:
- "{{vars}}"
I’ve defined some variables to make my life easier:
- set_fact: hostname="{{vpn.site.host}}"
user="{{vpn.site.username}}"
password="{{vpn.site.password}}"
vdom="{{vpn.site.vdom}}"
proposal="{{vpn.site.proposal}}"
phase2keylifetime="{{vpn.site.phase2keylifetime}}"
site_local_name="{{vpn.site.subnets.site_local}}"
site_remote_name="{{vpn.site.subnets.site_remote}}"
sas="{{vpn.site.subnets.sas}}"
tags: route,vpn_status
Next, I used the phase1-interface module and phase2-interface module.
In the phase2-interface module, I’ve used the ‘sas’ list of dictionaries to create all phase2 security associations ( or phase2 interfaces).
- name: VPN PHASE2
tags: vpn1
fortios_vpn_ipsec_phase2_interface:
host: "{{ hostname }}"
username: "{{ user }}"
password: "{{ password }}"
vdom: "{{ vdom }}"
https: "False"
state: "present"
vpn_ipsec_phase2_interface:
name: "P2vpn-to-{{site_remote_name}}-{{item.name}}"
phase1name: "P1vpn-to-{{site_remote_name}}"
proposal: "{{ proposal }}"
replay: "enable"
pfs: "enable"
auto_negotiate: "enable"
keylifeseconds: "{{phase2keylifetime }}"
src_subnet: "{{item.local}}"
dst_subnet: "{{item.remote}}"
with_items: "{{sas}}"
This phase2 config looks like this on the firewall after I’ve pushed it:
edit "P2vpn-to-Tokyo-sa1"
set phase1name "P1vpn-to-Tokyo"
set proposal des-sha512
set auto-negotiate enable
set keylifeseconds 3600
set src-subnet 172.16.1.0 255.255.255.0
set dst-subnet 172.16.2.0 255.255.255.0
next
edit "P2vpn-to-Tokyo-sa2"
set phase1name "P1vpn-to-Tokyo"
set proposal des-sha512
set auto-negotiate enable
set keylifeseconds 3600
set src-subnet 172.16.11.0 255.255.255.0
set dst-subnet 172.16.22.0 255.255.255.0
next
edit "P2vpn-to-Tokyo-sa3"
set phase1name "P1vpn-to-Tokyo"
set proposal des-sha512
set auto-negotiate enable
set keylifeseconds 3600
set src-subnet 172.16.1.0 255.255.255.0
set dst-subnet 172.16.22.0 255.255.255.0
next
Next, I have tasks for local and remote addresses and policies to allow traffic inbound and outbound for each security association.
Here you have the tasks that create the inbound policies for each security association:
- name: firewall policy inbound
tags: firewall_in
fortios_firewall_policy:
host: "{{ hostname }}"
username: "{{ user }}"
password: "{{ password }}"
vdom: "{{ vdom }}"
https: "False"
state: "present"
firewall_policy:
policyid: "0"
srcintf:
- name: "P1vpn-to-{{site_remote_name}}"
dstintf:
- name: "port3"
dstaddr:
- name: "{{site_local_name}}-{{item.name}}"
srcaddr:
- name: "{{site_remote_name}}-{{item.name}}"
schedule: "always"
action: "accept"
service:
- name: "PING"
- name: "SSH"
with_items: "{{sas}}"
Again I am using that ‘sas’ list of dictionaries to iterate.
And then the last step is to route traffic through the VPN:
- name: route through vpn tunnel
tags: route
ignore_errors: yes
fortios_router_static:
host: "{{ hostname }}"
username: "{{ user }}"
password: "{{ password }}"
vdom: "{{ vdom }}"
https: "False"
state: "present"
router_static:
dst: "{{item.remote}}"
device: "P1vpn-to-{{site_remote_name}}"
seq_num: "0"
with_items: "{{sas}}"
And that’s it, one side of the VPN is set up. If you need to set up the other end you run the playbook again with varsB file.
After creating the VPN I wanted to add also a way to see the status of the VPN. So using the ‘URI’ ansible module I’ve created some tasks that use the Fortinet Rest API and get some stats for each phase2 security association.
To do this I have a task to log in:
- name: get auth cookie
tags: vpn_status
uri:
url: "http://{{hostname}}/logincheck"
method: POST
validate_certs: no
return_content: yes
status_code: 200
body:
"username={{user}}&secretkey={{password}}"
register: output
- set_fact: cookie="{{output.cookies_string}}"
tags: vpn_status
This task takes the authentication cookie and uses it (in the HTTP header) on the next task to get the phase2 statistics :
- name: get vpn status
tags: vpn_status
uri:
url: "http://{{hostname}}/api/v2/monitor/vpn/ipsec/select/"
method: GET
validate_certs: no
return_content: yes
status_code: 200
headers:
Cookie: "{{cookie}}"
register: output
Then using the output and json_query I’ve extracted the name, status, incoming bytes, and outgoing bytes:
- set_fact:
name: " {{ result | json_query(query1) }}"
status: "{{ result | json_query(query2) }}"
in_bytes: "{{ result | json_query(query3) }}"
out_bytes: "{{ result | json_query(query4) }}"
vars:
query1: "results[?name=='P1vpn-to-{{site_remote_name}}'].proxyid[*].p2name"
query2: "results[?name=='P1vpn-to-{{site_remote_name}}'].proxyid[*].status "
query3: "results[?name=='P1vpn-to-{{site_remote_name}}'].proxyid[*].incoming_bytes "
query4: "results[?name=='P1vpn-to-{{site_remote_name}}'].proxyid[*].outgoing_bytes "
tags: vpn_status
And last, print it on the screen using debug :
- name: print status
vars:
msg: |
VPN status:
Name: {{name}}
Status: {{status}}
inBytes: {{in_bytes}}
outBytes: {{out_bytes}}
debug:
msg: "{{ msg.split('\n')}}"
tags: vpn_status
The output of the VPN status looks like this:
TASK [print status] ****************************************************************************
ok: [localhost] => {
"msg": [
"VPN status:",
"Name: [[u'P2vpn-to-Tokyo-sa1', u'P2vpn-to-Tokyo-sa2', u'P2vpn-to-Tokyo-sa3']]",
"Status: [[u'up', u'up', u'up']]",
"inBytes: [[420, 420, 0]]",
"outBytes: [[780, 780, 0]]",
""
]
}
If you don’t like displaying this on the screen you could save it to a file.
After building the VPN’s, every time you want only to check the status, you can use tags and run only the status tasks, like this:
ansible-playbook vpn-setup.yml -e "vars=varsA" --tags "vpn_status"
You can find all the files here:
https://github.com/czirakim/Ansible.s2sVPN.Fortinet