Ansible.s2sVPN.Fortinet

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

About the author

Mihai is a Senior Network Engineer with more than 15 years of experience