Transit tunneling

2017-11-14

So when we started with the ‘wire up your corporate network’ to aws we were introduced to the transitive vpc.

The automagic aws solution includes CSR’s and lambda goodness, much of which was a little too much to grasp and pay for at the time.

Being a little (too) cost averse and still learning the ropes, we started building a strongswan+quagga solution. I’ll document that one at a later stage.

The one we opted for though, was a Fortigate (we are a Fortigate shop) based solution that ties in with the cloudformation bits we do for the vpcs.

Refering back to the same picture (draw.io use it!) in the multi account post.

We will focus on the cgw, vpc and Fortigate config in the bits below.

Ansible group_vars

So in an attempt to take the complexity out of vpc creations we ended up with something like this

---
acls:
  - { name: Inbound, rule: 100, outgoing: false, cidr: 0.0.0.0/0 }
  - { name: Outbound, rule: 100, outgoing: true, cidr: 0.0.0.0/0 }
pub_sec_group:
  name: PublicSecurityGroup
  incoming:
    - { port: 22, cidr: 1.1.1.1/32 }
    - { port: 80, cidr: 0.0.0.0/0 }
    - { port: 443, cidr: 0.0.0.0/0 }
  outgoing:
    - { port: 80, cidr: 0.0.0.0/0 }
    - { port: 443, cidr: 0.0.0.0/0 }
prv_sec_group:
  name: PrivateSecurityGroup
  incoming:
    - { port: 80, sec_group: PublicSecurityGroup } #Will be prefixed by {{vpc.name}}
    - { port: 443, sec_group: PublicSecurityGroup }
    - { port: -1, cidr: 10.0.0.0/8, protocol: -1 }
  outgoing: []
vpcs:
  aws_foo_sandbox:
    foosnd:
      name: "{{stack_name}}"
      cidr: 10.10.1.0/24
      igw: 1
      subnets:
        - { name: PublicSNa, az: eu-west-1a, cidr: 10.10.1.0/28, route_table: RtPub, natgw: NatGW1 }
        - { name: PublicSNb, az: eu-west-1b, cidr: 10.10.1.16/28, route_table: RtPub }
        - { name: PrivateSNa, az: eu-west-1a, cidr: 10.10.1.32/27, route_table: RtPriv }
        - { name: PrivateSNb, az: eu-west-1b, cidr: 10.10.1.64/27, route_table: RtPriv }
      acls: "{{acls}}"
      pub_sec_group: "{{pub_sec_group}}"
      prv_sec_group: "{{prv_sec_group}}"
      route_tables:
        - name: RtPub
          routes:
            - { name: igw, cidr: 0.0.0.0/0, igw: 1 }
        - name: RtPriv
          routes:
            - { name: NatGW1, cidr: 0.0.0.0/0, natgw: NatGW1 }
          vpnconnection: 1
      vpn:
        name: "{{stack_name}}TransitVPN"
        gateways:
          - name: "FortigateFW1"
          - name: "FortigateFW2"
      instances: []
  aws_foo_nonprod:
    foodev:
      name: "{{stack_name}}"
      cidr: 10.10.2.0/24
      igw: 1
      subnets:
        - { name: PublicSNa, az: eu-west-1a, cidr: 10.10.2.0/28, route_table: RtPub, natgw: NatGW1 }
        - { name: PublicSNb, az: eu-west-1b, cidr: 10.10.2.16/28, route_table: RtPub }
        - { name: PrivateSNa, az: eu-west-1a, cidr: 10.10.2.32/27, route_table: RtPriv }
        - { name: PrivateSNb, az: eu-west-1b, cidr: 10.10.2.64/27, route_table: RtPriv }
      acls: "{{acls}}"
      pub_sec_group: "{{pub_sec_group}}"
      prv_sec_group: "{{prv_sec_group}}"
      route_tables:
        - name: RtPub
          routes:
            - { name: igw, cidr: 0.0.0.0/0, igw: 1 }
        - name: RtPriv
          routes:
            - { name: NatGW1, cidr: 0.0.0.0/0, natgw: NatGW1 }
          vpnconnection: 1
      vpn:
        name: "{{stack_name}}TransitVPN"
        gateways:
          - name: "FortigateFW1"
          - name: "FortigateFW2"
      instances: []
    footst:
      name: "{{stack_name}}"
      cidr: 10.10.3.0/24
      igw: 1
      subnets:
        - { name: PublicSNa, az: eu-west-1a, cidr: 10.10.3.0/28, route_table: RtPub, natgw: NatGW1 }
        - { name: PublicSNb, az: eu-west-1b, cidr: 10.10.3.16/28, route_table: RtPub }
        - { name: PrivateSNa, az: eu-west-1a, cidr: 10.10.3.32/27, route_table: RtPriv }
        - { name: PrivateSNb, az: eu-west-1b, cidr: 10.10.3.64/27, route_table: RtPriv }
      acls: "{{acls}}"
      pub_sec_group: "{{pub_sec_group}}"
      prv_sec_group: "{{prv_sec_group}}"
      route_tables:
        - name: RtPub
          routes:
            - { name: igw, cidr: 0.0.0.0/0, igw: 1 }
        - name: RtPriv
          routes:
            - { name: NatGW1, cidr: 0.0.0.0/0, natgw: NatGW1 }
          vpnconnection: 1
      vpn:
        name: "{{stack_name}}TransitVPN"
        gateways:
          - name: "FortigateFW1"
          - name: "FortigateFW2"
      instances: []

In this case depicting a sandbox and nonprod account, the latter with 2 vpc’s.

The vpcnames are short (6 chars) for a reason - Fortigate has some limitations on its config names, so the shorter the better.

Also since we don’t use vdoms in our Fortigate config, the names must be globally (across all accounts) unique.

CGW setup

Since the cgw is needed only once in every account we created a cgw_cf role for the cloudformation wrapper

---
- hosts: localhost
  connection: local
  gather_facts: False
  vars:
    stack_name: cgw
    gateways:
      - name: FortigateFW1
        ip: "1.1.1.1"
        asn: 64111
      - name: FortigateFW2
        ip: "2.2.2.2"
        asn: 64111
    region: eu-west-1
  roles:
    - { name: sts_assume_role, target_account: "{{aws_accounts[acc]}}" }
    - cgw_cf

The template in the cgw_cf role looks like this

---
  AWSTemplateFormatVersion: "2010-09-09"
  Description: "AWS CGW template {{stack_name}}"
  Resources: 
    {% for gw in gateways %}
    {{gw.name}}CustomerGateway:
      Type: "AWS::EC2::CustomerGateway"
      Properties:
        Type: ipsec.1
        BgpAsn: {{gw.asn}}
        IpAddress: {{gw.ip}}
        Tags:
          - { Key: Name, Value: {{gw.name}}CustomerGateway }
    {% endfor %}
  Outputs:
    StackName:
      Value: !Ref AWS::StackName
    Regionname:
      Value: !Ref AWS::Region
    {% for gw in gateways %}
    {{gw.name}}CustomerGateway:
      Value: !Ref {{gw.name}}CustomerGateway
      Export:
        Name: {{gw.name}}
    {% endfor %}

this creates cloudformation Exports that will be used to import the CGW id’s in the VPN setup.

VPC setup

Passing the stack_name and acc variables into this playbook will create the vpc and configure the tunnels inside the fortigate firewalls.

The stack_name represents the vpc variable in the group_vars.

---
- hosts: localhost
  connection: local
  gather_facts: False
  vars:
    vpc: "{{vpcs[acc][stack_name]}}"
    region: eu-west-1
  roles:
    - name: sts_assume_role
      target_account: "{{aws_accounts[acc]}}"

    - name: vpc_cf

    - name: get_aws_vpnconfig
      vpn1: "{{output.stack_outputs[stack_name+'FortigateFW1VPNConnection']}}"
      vpn2: "{{output.stack_outputs[stack_name+'FortigateFW2VPNConnection']}}"

    - name: fortigate
      fwName: Transit-Hub-FW1
      config: "{{awsvpn1}}"
      ssh_key: "~/pems/Transit.pem"
      ssh_uid: "admin"
      ssh_host: "1.1.1.1"
      localip: 10.10.0.10
      hubName: HUB-to-DC1

    - name: fortigate
      fwName: Transit-Hub-FW2
      config: "{{awsvpn2}}"
      ssh_key: "~/pems/Transit.pem"
      ssh_uid: "admin"
      ssh_host: "2.2.2.2"
      localip: 10.10.0.100
      setmetrix: yes
      hubName: HUB-to-DC2

vpc_cf role

here is the jinja template contents:

---
  AWSTemplateFormatVersion: "2010-09-09"
  Description: "AWS VPC template {{vpc.name}}"
  Resources: 
    {{vpc.name}}VPC:
      Type: AWS::EC2::VPC
      Properties:
        CidrBlock: {{vpc.cidr}}
        EnableDnsSupport: true
        EnableDnsHostnames: true
        Tags:
          - { Key: Application, Value: !Ref "AWS::StackId" }
          - { Key: Name, Value: {{vpc.name}}VPC }

    {% if vpc.igw is defined %}
    {{vpc.name}}InternetGateway: 
      Type: AWS::EC2::InternetGateway
      Properties: 
        Tags: 
          - { Key: Application, Value: !Ref "AWS::StackId" }
          - { Key: Name, Value: {{vpc.name}}InternetGateway }

    {{vpc.name}}AttachGateway: 
      Type: AWS::EC2::VPCGatewayAttachment
      Properties: 
        VpcId: !Ref {{vpc.name}}VPC
        InternetGatewayId: !Ref {{vpc.name}}InternetGateway
    {% endif %}

    {% for rt in vpc.route_tables %}
    {{vpc.name}}{{rt.name}}RouteTable: 
      Type: AWS::EC2::RouteTable
      Properties: 
        VpcId: !Ref {{vpc.name}}VPC
        Tags: 
          - { Key: Application, Value: !Ref "AWS::StackId" }
          - { Key: Name, Value: {{vpc.name}}{{rt.name}}RouteTable }
    {% endfor %}

    {{vpc.name}}NetworkAcl: 
      Type: AWS::EC2::NetworkAcl
      Properties: 
        VpcId: !Ref {{vpc.name}}VPC
        Tags: 
          - { Key: Application, Value: !Ref "AWS::StackId" }
          - { Key: Name, Value: {{vpc.name}}NetworkAcl }

    {% for subnet in vpc.subnets %}
    {{vpc.name}}{{subnet.name}}Subnet: 
      Type: AWS::EC2::Subnet
      Properties: 
        AvailabilityZone: {{subnet.az}}
        VpcId: !Ref {{vpc.name}}VPC
        CidrBlock: {{subnet.cidr}}
        Tags: 
          - { Key: Application, Value: !Ref "AWS::StackId" }
          - { Key: Name, Value: {{vpc.name}}{{subnet.name}} }
    {{vpc.name}}{{subnet.name}}SubnetRouteTableAssociation: 
      Type: AWS::EC2::SubnetRouteTableAssociation
      Properties: 
        SubnetId: !Ref {{vpc.name}}{{subnet.name}}Subnet
        RouteTableId: !Ref {{vpc.name}}{{subnet.route_table}}RouteTable
    {{vpc.name}}{{subnet.name}}SubnetNetworkAclAssociation: 
      Type: AWS::EC2::SubnetNetworkAclAssociation
      Properties: 
        SubnetId: !Ref {{vpc.name}}{{subnet.name}}Subnet
        NetworkAclId: !Ref {{vpc.name}}NetworkAcl
    {% if subnet.natgw is defined %}
    {{vpc.name}}{{subnet.natgw}}IPAddress: 
      Type: AWS::EC2::EIP
      DependsOn: {{vpc.name}}AttachGateway
      Properties: 
        Domain: vpc
    {{vpc.name}}{{subnet.natgw}}NatGateway: 
      Type: AWS::EC2::NatGateway
      Properties: 
        AllocationId: !GetAtt {{vpc.name}}{{subnet.natgw}}IPAddress.AllocationId
        SubnetId: !Ref {{vpc.name}}{{subnet.name}}Subnet
    {% endif %}
    {% endfor %}

    {% for acl in vpc.acls %}
    {{vpc.name}}{{acl.name}}NetworkAclEntry: 
      Type: AWS::EC2::NetworkAclEntry
      Properties: 
        NetworkAclId: !Ref {{vpc.name}}NetworkAcl
        RuleNumber: {{acl.rule}}
        RuleAction: allow
        Egress: {{acl.outgoing}}
        CidrBlock: {{acl.cidr}}
        {% if acl.from is defined %}
        Protocol: {{acl.protocol|default('6')}}
          {% if acl.protocol is defined and acl.protocol == 1 %}
        Icmp:
          Code: -1
          Type: -1
          {% endif %}
        PortRange: 
          From: {{acl.from}}
          To: {{acl.to}}
        {% else %}
        Protocol: -1
        {% endif %}
    {% endfor %}

    {{vpc.name}}PublicSecurityGroup:
      Type: AWS::EC2::SecurityGroup
      Properties: 
        VpcId: !Ref {{vpc.name}}VPC
        GroupDescription: Enable access via ports
        SecurityGroupIngress:
          {% for incoming in vpc.pub_sec_group.incoming %}
          - IpProtocol: {{incoming.protocol|default('tcp')}}
            FromPort: {{incoming.port}}
            ToPort: {{incoming.port}}
            CidrIp: {{incoming.cidr}}
          {% endfor %}
        SecurityGroupEgress:
          {% for outgoing in vpc.pub_sec_group.outgoing %}
          - IpProtocol: {{outgoing.protocol|default('tcp')}}
            FromPort: {{outgoing.port}}
            ToPort: {{outgoing.port}}
            {% if outgoing.sec_group is defined %}
            SourceSecurityGroupId: !Ref {{vpc.name}}{{outgoing.sec_group}}
            {% else %}
            CidrIp: {{outgoing.cidr}}
            {% endif %}
          {% endfor %}
        Tags: 
          - { Key: Application, Value: !Ref "AWS::StackId" }
          - { Key: Name, Value: {{vpc.name}}PublicSecurityGroup }

    {{vpc.name}}PrivateSecurityGroup: 
      Type: AWS::EC2::SecurityGroup
      Properties: 
        VpcId: !Ref {{vpc.name}}VPC
        GroupDescription: Enable access via ports
        SecurityGroupIngress: 
          {% for incoming in vpc.prv_sec_group.incoming %}
          - IpProtocol: {{incoming.protocol|default('tcp')}}
            FromPort: {{incoming.port}}
            ToPort: {{incoming.port}}
            {% if incoming.sec_group is defined %}
            SourceSecurityGroupId: !Ref {{vpc.name}}{{incoming.sec_group}}
            {% else %}
            CidrIp: {{incoming.cidr}}
            {% endif %}
          {% endfor %}
        Tags: 
          - { Key: Application, Value: !Ref "AWS::StackId" }
          - { Key: Name, Value: {{vpc.name}}PrivateSecurityGroup }

    {% for rt in vpc.route_tables %}
    {% for route in rt.routes %}
    {{vpc.name}}{{rt.name}}{{route.name}}Route: 
      Type: AWS::EC2::Route
      {% if route.vpngateway is defined %}
      DependsOn: [{{vpc.vpn.name}}VPNGateway, {{vpc.vpn.name}}VPCGatewayAttachment]
      {% endif %}
      Properties: 
        RouteTableId: !Ref {{vpc.name}}{{rt.name}}RouteTable
        DestinationCidrBlock: {{route.cidr}}
        {% if route.instance is defined %}
        InstanceId: !Ref {{vpc.name}}{{route.instance}}EC2Instance
        {% elif route.natgw is defined %}
        NatGatewayId: !Ref {{vpc.name}}{{route.natgw}}NatGateway 
        {% elif route.igw is defined %}
        GatewayId: !Ref {{vpc.name}}InternetGateway
        {% elif route.vpngateway is defined %}
        GatewayId: !Ref {{vpc.vpn.name}}VPNGateway
        {% endif %}
    {% endfor %}
    {% if rt.vpnconnection is defined %}
    {% for gw in vpc.vpn.gateways %}
    {{vpc.name}}{{gw.name}}VPNGatewayRoutePropagation:
      Type: "AWS::EC2::VPNGatewayRoutePropagation"
      DependsOn: {{vpc.name}}{{gw.name}}VPNConnection
      Properties: 
        RouteTableIds: [!Ref {{vpc.name}}{{rt.name}}RouteTable]
        VpnGatewayId: !Ref {{vpc.vpn.name}}VPNGateway
    {% endfor %}
    {% endif %}
    {% endfor %}

    {% for instance in vpc.instances %}
    {{vpc.name}}{{instance.name}}EC2Instance: 
      Type: AWS::EC2::Instance
      DependsOn: {{vpc.name}}AttachGateway
      Properties: 
        ImageId: {{instance.imageid|default(ec2_imageid)}}
        InstanceType:  {{instance.instancetype|default(ec2_type)}}
        KeyName: {{ec2_keyname}}
        SourceDestCheck: {{instance.sourcedestcheck|default('true')}}
        Tags: 
          - { Key: Application, Value: !Ref "AWS::StackId"  }
          - { Key: Name, Value: {{vpc.name}}{{instance.name}}EC2Instance }
        NetworkInterfaces: 
          - GroupSet: [ !Ref {{vpc.name}}SecurityGroup ]
            DeviceIndex: 0
            DeleteOnTermination: true
            SubnetId: !Ref {{vpc.name}}{{instance.subnet}}Subnet
    {{vpc.name}}{{instance.name}}IPAddress: 
      Type: AWS::EC2::EIP
      DependsOn: {{vpc.name}}AttachGateway
      Properties: 
        Domain: vpc
        InstanceId: !Ref {{vpc.name}}{{instance.name}}EC2Instance
    {% endfor %}

    {% if vpc.vpn is defined %}
    {{vpc.vpn.name}}VPNGateway:
      Type: "AWS::EC2::VPNGateway"
      DependsOn: {{vpc.name}}VPC
      Properties:
        Type: ipsec.1
        Tags:
          - { Key: Name, Value: {{vpc.vpn.name}}VPNGateway }
    {{vpc.vpn.name}}VPCGatewayAttachment:
      Type: "AWS::EC2::VPCGatewayAttachment"
      DependsOn: {{vpc.name}}VPC
      Properties:
        VpcId: !Ref {{vpc.name}}VPC
        VpnGatewayId: !Ref {{vpc.vpn.name}}VPNGateway
    {% for gw in vpc.vpn.gateways %}
    {{vpc.name}}{{gw.name}}VPNConnection:
      Type: "AWS::EC2::VPNConnection"
      Properties:
        Type: ipsec.1
        CustomerGatewayId: !ImportValue {{gw.name}}
        StaticRoutesOnly: false
        VpnGatewayId: !Ref {{vpc.vpn.name}}VPNGateway
        Tags:
          - { Key: Name, Value: {{vpc.name}}{{gw.name}}VPNConnection }

    {% endfor %}
    {% endif %}

    {{vpc.name}}FlowLog:
      Type: "AWS::EC2::FlowLog"
      Properties:
        DeliverLogsPermissionArn : arn:aws:iam::{{aws_accounts[acc]}}:role/PublishFlowLogs
        LogGroupName : vpc-flow-logs
        ResourceId : !Ref {{vpc.name}}VPC
        ResourceType : VPC
        TrafficType : ALL

  Outputs:
    StackName:
      Value: !Ref AWS::StackName
    Regionname:
      Value: !Ref AWS::Region
    {{vpc.name}}VPC:
      Value: !Ref {{vpc.name}}VPC
      Export:
        Name: {{vpc.name}}VPC
    {{vpc.name}}PublicSecurityGroup: 
      Value: !Ref {{vpc.name}}PublicSecurityGroup
      Export:
        Name: {{vpc.name}}PublicSecurityGroup
    {{vpc.name}}PrivateSecurityGroup: 
      Value: !Ref {{vpc.name}}PrivateSecurityGroup
      Export:
        Name: {{vpc.name}}PrivateSecurityGroup
    {% for subnet in vpc.subnets %}
    {{vpc.name}}{{subnet.name}}Subnet: 
      Value: !Ref {{vpc.name}}{{subnet.name}}Subnet
      Export:
        Name: {{vpc.name}}{{subnet.name}}Subnet
    {% endfor %}
    {% for instance in vpc.instances %}
    {{vpc.name}}{{instance.name}}Private:
      Value: !GetAtt {{vpc.name}}{{instance.name}}EC2Instance.PrivateIp
    {{vpc.name}}{{instance.name}}Public:
      Value: !GetAtt {{vpc.name}}{{instance.name}}EC2Instance.PublicIp
    {% endfor %}
    {% if vpc.vpn is defined %}
    {% for gw in vpc.vpn.gateways %}
    {{vpc.name}}{{gw.name}}VPNConnection:
      Value: !Ref {{vpc.name}}{{gw.name}}VPNConnection
    {% endfor %}
    {% endif %}

the vpc.instances list would contain instance info (like when we deployed quagga and strongswan), but isn’t important here.

get_aws_vpn_config role

Whoa, so this is probably where rolling your own module to extract the info would have been wize, but since I am half lazy, half inspired to see how much I can squeeze into 1 command line…

---
- block:
    - name: get the ip password combo for "{{vpn1}}"
      shell: >
        AWS_ACCESS_KEY_ID="{{ assumed_role.sts_creds.access_key | default(omit) }}"
        AWS_SECRET_ACCESS_KEY="{{ assumed_role.sts_creds.secret_key | default(omit) }}"
        AWS_SESSION_TOKEN="{{ assumed_role.sts_creds.session_token | default(omit) }}"
        aws ec2 describe-vpn-connections 
        | jq '.VpnConnections[] | select(.VpnConnectionId=="{{vpn1}}").CustomerGatewayConfiguration' -r 
        | xmlstarlet sel -t -m '/vpn_connection/ipsec_tunnel' 
        -v 'concat("cgw", position(), "_outside: ", ./customer_gateway/tunnel_outside_address/ip_address/text(), "!!!")'
        -v 'concat("cgw", position(), "_inside: ", ./customer_gateway/tunnel_inside_address/ip_address/text(), "!!!")' 
        -v 'concat("cgw", position(), "_asn: ", ./customer_gateway/bgp/asn/text(), "!!!")'
        -v 'concat("vgw", position(), "_outside: ", ./vpn_gateway/tunnel_outside_address/ip_address/text(), "!!!")'
        -v 'concat("vgw", position(), "_inside: ", ./vpn_gateway/tunnel_inside_address/ip_address/text(), "!!!")'
        -v 'concat("vgw", position(), "_asn: ", ./vpn_gateway/bgp/asn/text(), "!!!")'
        -v 'concat("psk", position(), ": ", ./ike/pre_shared_key/text(),"!!!")'
      register: vpnconfig
    - set_fact:
        awsvpn1: "{{('{ '+(vpnconfig.stdout_lines[0].split('!!!')|difference([''])|join(', '))+' }')|from_yaml}}"
    - debug: var=awsvpn1
    - name: get the ip password combo for "{{vpn2}}"
      shell: > 
        AWS_ACCESS_KEY_ID="{{ assumed_role.sts_creds.access_key | default(omit) }}"
        AWS_SECRET_ACCESS_KEY="{{ assumed_role.sts_creds.secret_key | default(omit) }}"
        AWS_SESSION_TOKEN="{{ assumed_role.sts_creds.session_token | default(omit) }}"
        aws ec2 describe-vpn-connections 
        | jq '.VpnConnections[] | select(.VpnConnectionId=="{{vpn2}}").CustomerGatewayConfiguration' -r 
        | xmlstarlet sel -t -m '/vpn_connection/ipsec_tunnel' 
        -v 'concat("cgw", position(), "_outside: ", ./customer_gateway/tunnel_outside_address/ip_address/text(), "!!!")'
        -v 'concat("cgw", position(), "_inside: ", ./customer_gateway/tunnel_inside_address/ip_address/text(), "!!!")' 
        -v 'concat("cgw", position(), "_asn: ", ./customer_gateway/bgp/asn/text(), "!!!")'
        -v 'concat("vgw", position(), "_outside: ", ./vpn_gateway/tunnel_outside_address/ip_address/text(), "!!!")'
        -v 'concat("vgw", position(), "_inside: ", ./vpn_gateway/tunnel_inside_address/ip_address/text(), "!!!")'
        -v 'concat("vgw", position(), "_asn: ", ./vpn_gateway/bgp/asn/text(), "!!!")'
        -v 'concat("psk", position(), ": ", ./ike/pre_shared_key/text(),"!!!")'
      register: vpnconfig
    - set_fact:
        awsvpn2: "{{('{ '+(vpnconfig.stdout_lines[0].split('!!!')|difference([''])|join(', '))+' }')|from_yaml}}"
    - debug: var=awsvpn2
  when: not absent is defined

This uses jq to find the relevant output from aws ec2 describe-vpn-connections and then xmlstarlet to extract the aws inside,outside ips,asn,and psk for both tunnels.

Once that is done… it creates the 2 variables we will use in the fortigate config in the next role

fortigate role

---
- name: remove the fortigate config
  block:
    - name: make the policy delete script
      shell: >
        echo 'config firewall policy' > "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        ssh -i "{{ssh_key}}" "{{ssh_uid}}@{{ssh_host}}" show firewall policy
        | grep "{{stack_name}}" -B 1
        | grep 'edit '
        | sed 's/.*edit /del /g' >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        echo end >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        echo 'config vpn ipsec phase2-interface' >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        ssh -i "{{ssh_key}}" "{{ssh_uid}}@{{ssh_host}}" show vpn ipsec phase2-interface
        | grep "{{stack_name}}" -B 1
        | grep 'edit '
        | sed 's/.*edit /del /g' >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        echo end >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        echo 'config vpn ipsec phase1-interface' >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        ssh -i "{{ssh_key}}" "{{ssh_uid}}@{{ssh_host}}" show vpn ipsec phase1-interface
        | grep "{{stack_name}}" -B 1
        | grep 'edit '
        | sed 's/.*edit /del /g' >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        echo end >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        echo 'config router bgp' >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        echo 'config neighbor ' >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        ssh -i "{{ssh_key}}" "{{ssh_uid}}@{{ssh_host}}" show router bgp
        | grep "{{stack_name}}" -B 2
        | grep 'edit "'
        | sed 's/.*edit /del /g' >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        echo end >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
        echo end >> "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh";
    - name: run the delete script
      shell: cat "./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.del.ssh" | ssh -i "{{ssh_key}}" "{{ssh_uid}}@{{ssh_host}}"
      register: delfortiout
    - debug: var=delfortiout
  when: absent is defined

- name: do fortigate config
  block:
  - name: get the tunnel list to see if it exists
    shell: "ssh -i {{ssh_key}} {{ssh_uid}}@{{ssh_host}} get ipsec tunnel list | grep -c {{stack_name}}T1P1"
    ignore_errors: true
    register: tunlist
  - block:
      - name: stampout "{{fwName}}"
        template: src=fortigate.ssh.j2 dest="./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.ssh"
      - name: run the tunnel script
        shell: "cat ./generated/vpc/{{stack_name}}.fortigate.{{fwName}}.ssh | ssh -i {{ssh_key}} {{ssh_uid}}@{{ssh_host}}"
        register: fortiout
      - debug: var=fortiout
    when: tunlist.stdout_lines[0] == '0'
  when: absent is not defined

The first fugly block is there to generate the delete scripts in case you are removing the vpc’s config from the Fortigate firewalls

The second block is the create, if it doesn’t exist, that makes an .ssh file and then cat it to the Fortigate firewall.

The template itself is basically a bunch of fortigate config statements provided by our resident Fortigate experts.

The Fortigates are pretty cool tools, you can put them in ‘debug’ mode and have them spit out a recording of what you do in its web interface. That can then be put into a jinja2 template and used like we do below.

config vpn ipsec phase1-interface
  edit "{{stack_name}}T1P1"
  set interface "port1"
  set local-gw "{{localip}}"
  set keylife 28800
  set peertype any
  set proposal aes128-sha1
  set dhgrp 2
  set remote-gw "{{config.vgw1_outside}}"
  set psksecret "{{config.psk1}}"
  set dpd-retryinterval 10
end

config vpn ipsec phase2-interface
  edit "{{stack_name}}T1P2"
  set phase1name "{{stack_name}}T1P1"
  set proposal aes128-sha1
  set dhgrp 2
  set keylifeseconds 3600
end

config system interface
  edit "{{stack_name}}T1P1"
  set vdom "root"
  set ip {{config.cgw1_inside}} 255.255.255.255
  set allowaccess ping
  set type tunnel
  set tcp-mss 1379
  set remote-ip {{config.vgw1_inside}}
  set interface "port1"
end

config vpn ipsec phase1-interface
  edit "{{stack_name}}T2P1"
  set interface "port1"
  set local-gw "{{localip}}"
  set keylife 28800
  set peertype any
  set proposal aes128-sha1
  set dhgrp 2
  set remote-gw "{{config.vgw2_outside}}"
  set psksecret "{{config.psk2}}"
  set dpd-retryinterval 10
end

config vpn ipsec phase2-interface
  edit "{{stack_name}}T2P2"
  set phase1name "{{stack_name}}T2P1"
  set proposal aes128-sha1
  set dhgrp 2
  set keylifeseconds 3600
end

config system interface
  edit "{{stack_name}}T2P1"
  set vdom "root"
  set ip {{config.cgw2_inside}} 255.255.255.255
  set allowaccess ping
  set type tunnel
  set tcp-mss 1379
  set remote-ip {{config.vgw2_inside}}
  set interface "port1"
end

config router bgp
  config neighbor
  edit "{{config.vgw1_inside}}"
  set soft-reconfiguration enable
  set description "{{stack_name}}T1"
  set remote-as {{config.vgw1_asn}}
  {% if setmetrix is defined %}
  set route-map-out "Set-Metrix"
  {% endif %}
  next
  edit "{{config.vgw2_inside}}"
  set soft-reconfiguration enable
  set description "{{stack_name}}T2"
  set remote-as {{config.vgw2_asn}}
  {% if setmetrix is defined %}
  set route-map-out "Set-Metrix"
  {% endif %}
  end
end

config firewall policy
  edit 0
  set name "{{stack_name}}T1P1-f"
  set srcintf "{{stack_name}}T1P1"
  set dstintf "port1"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
end

config firewall policy
  edit 0
  set name "f-{{stack_name}}T1P1"
  set srcintf "port1"
  set dstintf "{{stack_name}}T1P1"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
  set comments "Reverse of {{stack_name}}T1P1-f"
end

config firewall policy
  edit 0
  set name "{{stack_name}}T2P1-f"
  set srcintf "{{stack_name}}T2P1"
  set dstintf "port1"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
end

config firewall policy
  edit 0
  set name "f-{{stack_name}}T2P1"
  set srcintf "port1"
  set dstintf "{{stack_name}}T2P1"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
  set comments "Reverse of {{stack_name}}T2P1-f"
end

config firewall policy
  edit 0
  set name "{{stack_name}}T1P1-s"
  set srcintf "{{stack_name}}T1P1"
  set dstintf "ssl.root"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
end

config firewall policy
  edit 0
  set name "s-{{stack_name}}T1P1"
  set srcintf "ssl.root"
  set dstintf "{{stack_name}}T1P1"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
  set comments "Reverse of {{stack_name}}T1P1-s"
end

config firewall policy
  edit 0
  set name "{{stack_name}}T2P1-s"
  set srcintf "{{stack_name}}T2P1"
  set dstintf "ssl.root"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
end

config firewall policy
  edit 0
  set name "s-{{stack_name}}T2P1"
  set srcintf "ssl.root"
  set dstintf "{{stack_name}}T2P1"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
  set comments "Reverse of {{stack_name}}T2P1-s"
end

config firewall policy
  edit 0
  set name "{{stack_name}}T1P1-h"
  set srcintf "{{stack_name}}T1P1"
  set dstintf "{{hubName}}"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
end

config firewall policy
  edit 0
  set name "h-{{stack_name}}T1P1"
  set srcintf "{{hubName}}"
  set dstintf "{{stack_name}}T1P1"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
end

config firewall policy
  edit 0
  set name "{{stack_name}}T2P1-h"
  set srcintf "{{stack_name}}T2P1"
  set dstintf "{{hubName}}"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
end

config firewall policy
  edit 0
  set name "h-{{stack_name}}T2P1"
  set srcintf "{{hubName}}"
  set dstintf "{{stack_name}}T2P1"
  set srcaddr "all"
  set dstaddr "all"
  set action accept
  set schedule "always"
  set service "ALL"
end

The script configures:

  • the 2 phases of the 2 ip tunnels,
  • creates the interface for it,
  • configures the bgp neighbors
  • creates the forward and reverse policies to:
    • the firewall itself
    • an ssl client vpn
    • the dc’s