For this lab I have created a small topology with a FortiGate which will be configured with Terraform. There are two networks INSIDE
192.168.10.0/24
and DMZ
192.168.20.0/24
. The other interface is for OUTSIDE
this is connected into my home lab network and has an IP of 10.10.30.215
.
Terraform is used to configure the FortiGate firewall, only the DHCP address from the home lab network for the OUTSIDE
interface of 10.10.30.215
is not Terraform. There are two DHCP servers running on the Fortigate for each network segment INSIDE
and DMZ
. I have enabled internet access for the INSIDE
segment and a 1 to 1 NAT to allow traffic from the OUTSIDE
to reach the server in the DMZ
interface using IP 10.10.30.222
. Finally, there is a firewall rule to permit traffic from the INSIDE
segment to the DMZ
segment.
For this lab I have used FortiGate version 7.2.4
, this does have limitations with firewall rules, only allowing three rules. If a fourth is added, Terrform fails. This is just trial licence issues and wouldn’t be a problem if you were to buy a licence.
Full documentation for the FortiGate provider can be found here. And the Terraform script can be found in my GitHub.
Terraform Configuration Elements
- DHCP
- DNS
- Two physical interface IPs
- Create static route to internet
- PAT(NAT overload) for
INSIDE
DMZ
1:1 NAT10.10.30.222
- Firewall rule
INSIDE
toDMZ
Terraform Setup
The Terraform Setup for the FortiGate appliance is very simple.
– Create API admin in FortiGate
– Use API Token and initialise Terraform
Create FortiGate API Admin
This is the user that Terraform will use to issue all configuration commands. The following steps should get the API admin user setup.
The administrative profile must be created, I have manually created full_access
, to add a new profile just press select
.
I have disabled the PKI, as I don’t require any certificates to be correct for my lab.
After clicking ok
, the API key will be displayed. This is to be copied as it will be used inside the Terraform script.
Fortios Terraform Setup
This is the basics for setting up Terraform to use with the FortiGate appliances. Required is; IP, API token (from the username in previous step) and setting insecure for certificate checking.
Terraform will connect over HTTPS, but the certificates are self-signed so setting to insecure
will ignore this.
I have created a file called main.tf
in the directory C:\Users\Stef\Documents\Terraform\FortiGate>
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
terraform { required_providers { fortios = { source = "fortinetdev/fortios" } } } provider "fortios" { hostname = "10.10.30.215" token = "5qn3Gk0hH5fxpjN6jhsyzwrmwt0gyg" insecure = true } |
Once this is done, open a terminal and navigate to the directory C:\Users\Stef\Documents\Terraform\FortiGate>
, from here run terraform init
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
C:\Users\Stef\Documents\Terraform\FortiGate>terraform init Initializing the backend... Initializing provider plugins... - Finding latest version of fortinetdev/fortios... - Installing fortinetdev/fortios v1.16.0... - Installed fortinetdev/fortios v1.16.0 (signed by a HashiCorp partner, key ID 5ACA8FD248F63A0E) Partner and community providers are signed by their developers. If you'd like to know more about provider signing, you can read about it here: https://www.terraform.io/docs/cli/plugins/signing.html Terraform has created a lock file .terraform.lock.hcl to record the provider selections it made above. Include this file in your version control repository so that Terraform can guarantee to make the same selections by default when you run "terraform init" in the future. Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary. |
Terraform is now ready to go. Next is configuration of the FortiGate appliance.
Terraform Configuration
For the configuration, I have pulled these from the examples in the documentation. There are a lot of parameters that I have not used, and are left as defaults.
The examples can be found in the Terraform Fortios provider documentation. Here, you just search for what you want. Below, I have searched for interface
to configure a physical interface. There are multiple matching results, but the one that is required is fortios_system_interface
.
All configuration uses only single resource. Only the NAT for the DMZ server is another resource referenced. More on this below.
In the FortiGate configuration, a Virtual IP (VIP) is created that maps the OUTSIDE
IP to the DMZ
IP. This VIP is referenced by the firewall policy to perform the NAT. In the below config the VIP name is DMZ_NAT_IN
, this is referenced in the firewall policy under dstaddr
(second code block).
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#NAT DMZ IN VIP resource "fortios_firewall_vip" "DMZ_NAT_IN" { arp_reply = "enable" color = 0 dns_mapping_ttl = 0 extintf = "port1" extip = "10.10.30.222" extport = "0-65535" fosid = 0 http_cookie_age = 60 http_cookie_domain_from_host = "disable" http_cookie_generation = 0 http_cookie_share = "same-ip" http_ip_header = "disable" http_multiplex = "disable" https_cookie_secure = "disable" ldb_method = "static" mappedport = "0-65535" max_embryonic_connections = 1000 name = "DMZ_NAT_IN" nat_source_vip = "enable" outlook_web_access = "disable" persistence = "none" portforward = "disable" portmapping_type = "1-to-1" protocol = "tcp" type = "static-nat" mappedip { range = "192.168.20.102" } } |
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#NAT DMZ IN firewall policy resource "fortios_firewall_policy" "DMZ_NAT_IN" { action = "accept" logtraffic = "utm" name = "DMZ_NAT_IN" policyid = 3 schedule = "always" wanopt = "disable" wanopt_detection = "active" wanopt_passive_opt = "default" wccp = "disable" webcache = "disable" webcache_https = "disable" wsso = "enable" dstaddr { name = "DMZ_NAT_IN" } dstintf { name = "port3" } service { name = "ALL" } srcaddr { name = "all" } srcintf { name = "port1" } } |
The full terraform script is below
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
terraform { required_providers { fortios = { source = "fortinetdev/fortios" } } } provider "fortios" { hostname = "10.10.30.215" token = "5qn3Gk0hH5fxpjN6jhsyzwrmwt0gyg" insecure = true } #Interfaces resource "fortios_system_interface" "INSIDE" { algorithm = "L4" defaultgw = "enable" distance = 5 ip = "192.168.10.1 255.255.255.0" mtu = 1500 mtu_override = "disable" name = "port2" type = "physical" vdom = "root" mode = "static" snmp_index = 3 description = "Created by Terraform Provider for FortiOS" ipv6 { nd_mode = "basic" } } resource "fortios_system_interface" "DMZ" { algorithm = "L4" defaultgw = "enable" distance = 5 ip = "192.168.20.1 255.255.255.0" mtu = 1500 mtu_override = "disable" name = "port3" type = "physical" vdom = "root" mode = "static" snmp_index = 3 description = "Created by Terraform Provider for FortiOS" ipv6 { nd_mode = "basic" } } #DHCP resource "fortios_systemdhcp_server" "INSIDE_DHCP" { dns_service = "default" fosid = 1 interface = "port2" netmask = "255.255.255.0" status = "enable" # ntp_server1 = "192.168.52.22" # timezone = "00" default_gateway = "192.168.10.1" ip_range { end_ip = "192.168.10.200" id = 1 start_ip = "192.168.10.100" } } resource "fortios_systemdhcp_server" "DMZ_DHCP" { dns_service = "default" # fosid = 1 interface = "port3" netmask = "255.255.255.0" status = "enable" # ntp_server1 = "192.168.52.22" # timezone = "00" default_gateway = "192.168.20.1" ip_range { end_ip = "192.168.20.200" id = 1 start_ip = "192.168.20.100" } } #DNS resource "fortios_system_dns" "trname" { cache_notfound_responses = "disable" dns_cache_limit = 5000 dns_cache_ttl = 1800 ip6_primary = "::" ip6_secondary = "::" primary = "1.1.1.1" retry = 2 secondary = "8.8.8.8" source_ip = "0.0.0.0" timeout = 5 domain { domain = "fortitest.lab" } } #Static Route to Internet resource "fortios_router_static" "trname" { bfd = "disable" blackhole = "disable" device = "port1" distance = 10 dst = "0.0.0.0 0.0.0.0" dynamic_gateway = "disable" gateway = "10.10.30.1" internet_service = 0 link_monitor_exempt = "disable" priority = 22 seq_num = 1 src = "0.0.0.0 0.0.0.0" status = "enable" virtual_wan_link = "disable" vrf = 0 weight = 2 } #PAT INSIDE resource "fortios_firewall_policy" "PAT_INSIDE" { action = "accept" logtraffic = "utm" name = "PAT_INSIDE" policyid = 1 schedule = "always" wanopt = "disable" wanopt_detection = "active" wanopt_passive_opt = "default" wccp = "disable" webcache = "disable" webcache_https = "disable" wsso = "enable" nat = "enable" dstaddr { name = "all" } dstintf { name = "port1" } service { name = "ALL" } srcaddr { name = "all" } srcintf { name = "port2" } } #Add addresses as objects resource "fortios_firewall_address" "DMZ_Server" { allow_routing = "disable" associated_interface = "port3" color = 3 end_ip = "255.255.255.255" name = "DMZ_Server" start_ip = "192.168.20.102" subnet = "192.168.20.0 255.255.255.0" type = "ipmask" visibility = "enable" } resource "fortios_firewall_address" "DMZ_Server_NAT" { allow_routing = "disable" associated_interface = "port1" color = 3 end_ip = "255.255.255.255" name = "DMZ_Server_NAT" start_ip = "10.10.30.222" subnet = "10.10.30.222 255.255.255.0" type = "ipmask" visibility = "enable" } #DMZ NAT IP Proxy ARP resource "fortios_system_proxyarp" "DMZ_Server_NAT" { end_ip = "10.10.30.222" fosid = 1 interface = "port1" ip = "10.10.30.222" } #NAT DMZ IN VIP resource "fortios_firewall_vip" "DMZ_NAT_IN" { arp_reply = "enable" color = 0 dns_mapping_ttl = 0 extintf = "port1" extip = "10.10.30.222" extport = "0-65535" fosid = 0 http_cookie_age = 60 http_cookie_domain_from_host = "disable" http_cookie_generation = 0 http_cookie_share = "same-ip" http_ip_header = "disable" http_multiplex = "disable" https_cookie_secure = "disable" ldb_method = "static" mappedport = "0-65535" max_embryonic_connections = 1000 name = "DMZ_NAT_IN" nat_source_vip = "enable" outlook_web_access = "disable" persistence = "none" portforward = "disable" portmapping_type = "1-to-1" protocol = "tcp" type = "static-nat" mappedip { range = "192.168.20.102" } } #NAT DMZ IN firewall policy resource "fortios_firewall_policy" "DMZ_NAT_IN" { action = "accept" logtraffic = "utm" name = "DMZ_NAT_IN" policyid = 3 schedule = "always" wanopt = "disable" wanopt_detection = "active" wanopt_passive_opt = "default" wccp = "disable" webcache = "disable" webcache_https = "disable" wsso = "enable" dstaddr { name = "DMZ_NAT_IN" } dstintf { name = "port3" } service { name = "ALL" } srcaddr { name = "all" } srcintf { name = "port1" } } #Firewall Rule INSIDE to DMZ resource "fortios_firewall_policy" "INSIDE_DMZ" { action = "accept" logtraffic = "utm" name = "INSIDE_DMZ" policyid = 4 schedule = "always" wanopt = "disable" wanopt_detection = "active" wanopt_passive_opt = "default" wccp = "disable" webcache = "disable" webcache_https = "disable" wsso = "enable" dstaddr { name = "all" } dstintf { name = "port3" } service { name = "ALL" } srcaddr { name = "all" } srcintf { name = "port2" } } |
Deploying the Configuration
To deploy the configuration, I have used terraform plan
to first show the changes that will be made. Any line with a +
is a new configuration that will be added. Any line with a -
is a line that will be removed. As this is a new FortiGate there won’t be any config to be removed.
I have included a partial output below. The entire things is quite long.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
C:\Users\Stef\Documents\Terraform\FortiGate>terraform plan ... # fortios_systemdhcp_server.INSIDE_DHCP will be created + resource "fortios_systemdhcp_server" "INSIDE_DHCP" { + auto_configuration = (known after apply) + auto_managed_status = (known after apply) + conflicted_ip_timeout = (known after apply) + ddns_auth = (known after apply) + ddns_key = (sensitive value) + ddns_keyname = (known after apply) + ddns_server_ip = (known after apply) + ddns_ttl = (known after apply) + ddns_update = (known after apply) + ddns_update_override = (known after apply) + ddns_zone = (known after apply) + default_gateway = "192.168.10.1" + dhcp_settings_from_fortiipam = (known after apply) + dns_server1 = (known after apply) + dns_server2 = (known after apply) + dns_server3 = (known after apply) + dns_server4 = (known after apply) + dns_service = "default" + domain = (known after apply) + dynamic_sort_subtable = "false" + filename = (known after apply) + forticlient_on_net_status = (known after apply) + fosid = 1 + id = (known after apply) + interface = "port2" + ip_mode = (known after apply) + ipsec_lease_hold = (known after apply) + lease_time = (known after apply) + mac_acl_default_action = (known after apply) + netmask = "255.255.255.0" + next_server = (known after apply) + ntp_server1 = (known after apply) + ntp_server2 = (known after apply) + ntp_server3 = (known after apply) + ntp_service = (known after apply) + server_type = (known after apply) + status = "enable" + timezone = (known after apply) + timezone_option = (known after apply) + vci_match = (known after apply) + wifi_ac1 = (known after apply) + wifi_ac2 = (known after apply) + wifi_ac3 = (known after apply) + wifi_ac_service = (known after apply) + wins_server1 = (known after apply) + wins_server2 = (known after apply) + ip_range { + end_ip = "192.168.10.200" + id = 1 + start_ip = "192.168.10.100" + vci_match = (known after apply) } } Plan: 13 to add, 0 to change, 0 to destroy. ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now. |
Now I am happy with the configuration I will use terraform apply -auto-approve
, this will apply the configuration. As this is a lab, I do not require any interaction to confirm the application of the config.
Testing the Configuration
There is quite a lot to test, DHCP, DNS, NAT, PAT and the firewall rules. I’m going to test; the NAT for the DMZ server, the PAT for the INSIDE server and the firewall rules from the INSIDE server to the DMZ server.
The server named MetasploitableVM-1
is pingable as the IP 10.10.30.222
The Kali Linux server is also able to reach the internet and is NATed to the IP of the OUTSIDE
interface, port 1 of 10.10.30.215
.
The last test is for the connectivity between the two segments, INSIDE
and DMZ
. For this, I will simply run an Nmap port scan. The firewall rule from INSIDE
to DMZ
is a permit IP any any type policy.