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:
1 | 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