- FortiGate-Azure VPN Gateway IPsec 연동, BGP EBGP 멀티홉 설정, SD-WAN Health Check가 정상 작동하도록 만드는 법
- 적용 환경: FortiOS 7.x / Terraform 1.x / Azure Virtual WAN S2S VPN
- GitHub 저장소: https://github.com/20eung/terraform-fortios-bgp-sdwan
1. SD-WAN Health Check가 실패하던 진짜 이유
FortiGate 두 VPN 터널로 Azure Virtual WAN에 연결한 후 SD-WAN Health Check를 켰는데, 한쪽 터널에서 보낸 probe가 반대편에서 응답하는 현상을 만난 적 있으신가요. tunnel 1에서 보낸 ping이 tunnel 2의 source IP로 reply되고, SD-WAN은 tunnel 1을 down으로 판정합니다. 직관과 정반대라 처음엔 ISP나 Azure 쪽 문제라고 의심하기 쉽습니다.
진짜 원인은 Azure VPN Gateway가 active-active 인스턴스 2개를 1개로 라우팅 처리하기 때문입니다. FortiGate의 tunnel 2에서 보낸 probe는 Azure에서 instance 0으로 들어와 같은 instance 0으로 응답하고, 그 응답이 FortiGate의 tunnel 1로 들어옵니다. FortiGate 입장에선 “tunnel 2에서 보냈는데 tunnel 1에서 받았다” → “tunnel 2 down” 으로 기록됩니다.
BGP로 tunnel IP와 내부 네트워크를 양쪽 인스턴스에 모두 광고하면 Azure가 instance 0과 instance 1을 각각 다른 prefix로 라우팅해 응답이 정상적으로 돌아옵니다. 이 글은 그 BGP 설정과 SD-WAN Health Check를 Terraform으로 한 번에 묶는 과정을 정리합니다.

💼 이 글은 FortiOS 7.x + Azure Virtual WAN (vWAN) S2S VPN 환경을 기준으로 작성됐습니다. classic Azure VPN Gateway에서도 동작은 같지만 리소스 구조가 다르므로 azureurerm_virtual_wan/virtual_hub 대신 azureurerm_virtual_network_gateway를 쓰면 됩니다.
2. 모듈 구조 — vpn / route / sdwan / interface 분리
레포는 단일 모듈 안에 책임별로 .tf 파일을 나눕니다. main.tf는 이 모듈을 호출하면서 변수만 전달합니다.
modules/ ├── vpn.tf # Phase 1/Phase 2 IPsec 제안 ├── firewall_policy.tf # SD-WAN 통과 정책 + tcp-mss ├── system_interface.tf # us_site_vlan (10.92.129.1/24) + tunnel IP ├── route.tf # BGP neighbor + access-list + network └── sdwan.tf # SD-WAN member + Performance SLA Health-Check
main.tf는 모듈 호출 한 줄과 변수 정의만 둡니다.
module "azure_vpn_bgp_sdwan" {
source = "./modules"
# Azure VPN 정보
azure_vpn_instance0_public_ip = var.azure_vpn_instance0_ip
azure_vpn_instance1_public_ip = var.azure_vpn_instance1_ip
azure_vpn_asn = var.azure_vpn_asn
# US Site 정보
us_site_bgp_asn = var.us_site_bgp_asn
us_site_router_id = var.us_site_router_id
us_site_user_subnet = "10.92.129.0/24"
# SD-WAN SLA 측정용 Azure 서버 IP
sla_server_ips = var.azure_sla_servers
}
💼 루트의 provider.tf와 모듈 안의 required_providers 블록은 동시에 정의합니다. 모듈을 단독으로 terraform init하는 경우가 잦기 때문입니다. post-02 6.3 절 참고.
3. IPsec VPN Phase 1/2 — Azure와 호환되는 권장값
Azure VPN Gateway는 자체 알고리즘만 받기 때문에 Phase 1/2 제안값이 틀리면 터널이 up 되어도 traffic이 안 흐릅니다. Fortinet cookbook의 권장값은 다음과 같습니다.
| 설정 | Phase 1 권장 값 |
|---|---|
| IKE Version | 2 |
| dpd | on-idle |
| keylife | 28800 |
| proposal | aes128-sha1 3des-sha1 aes256-sha256 |
| dhgrp | 2 |
| dpd_retryinterval | 10 |
| nattraversal | disable |
| 설정 | Phase 2 권장 값 |
|---|---|
| proposal | aes128-sha1 3des-sha1 aes256-sha256 |
| pfs | Disable |
| keylife | 27700 |
| 설정 | Firewall Policy 권장 값 |
|---|---|
| tcp-mss-sender | 1350 |
| tcp-mss-receiver | 1350 |
또는
| 설정 | VPN Interface 권장 값 |
|---|---|
| tcp-mss | 1350 |
FortiOS Cookbook 바로가기
nattraversal=disable이 핵심입니다. Azure VPN Gateway는 NAT-traversal을 자동으로 처리하므로 FortiGate에서 enable로 두면 양쪽이 모두 NAT 헤더를 추가해 tunnel은 up이지만 traffic이 안 흐르는 silent fail이 생깁니다.
💼 Phase 1 keylife(28800) ≠ Phase 2 keylife(27700). 일부러 다르게 둡니다. 동일하게 두면 양쪽이 동시에 재협상하면서 traffic loss가 동시에 발생합니다.
3.1 tcp-mss 1350 — 누락하면 큰 파일 전송이 hang 되는 이유
Azure VPN은 MTU 1400 환경을 가정합니다. FortiGate 기본 MSS 1460 그대로 두면 큰 패킷이 fragmentation으로 인해 성능이 떨어지고, ICMP fragmentation-needed가 차단된 환경에선 더 큰 문제가 됩니다. firewall policy 또는 interface 양쪽에서 MSS를 1350으로 강제합니다.
firewall policy 방식:
resource "fortios_firewall_policy" "sdwan_to_azure" {
name = "sdwan-to-azure"
srcintf = ["virtual-wan-link"]
dstintf = ["azure-vpn"]
srcaddr = ["us_site_vlan"]
dstaddr = ["all"]
action = "accept"
schedule = "always"
tcp_mss_sender = 1350
tcp_mss_receiver = 1350
}
interface 방식 (동일 효과, 정책이 짧을 때 권장):
resource "fortios_system_interface" "vpn_tunnel1" {
name = "azure-vpn-tunnel1"
ip = "169.254.0.1 255.255.255.252"
type = "tunnel"
tcp_mss = 1350
}
4. BGP EBGP 멀티홉 — Azure VPN의 진짜 load balancing
Azure VPN Gateway의 active-active 인스턴스 0과 인스턴스 1이 모두 FortiGate의 tunnel 1, tunnel 2에 연결됩니다. 일반적인 EBGP는 한 peer만 best path로 선택해 load balancing이 안 됩니다. EBGP 멀티홉은 이 제약을 푸는 표준 트릭입니다.
💼 멀티홉은 EBGP 전용입니다. IBGP는 멀티홉이 기본 동작이라 별도 설정이 없습니다.
4.1 BGP 기본 + neighbor
resource "fortios_router_bgp" "us_site" {
as = var.us_site_bgp_asn
router_id = var.us_site_router_id
ebgp_multipath = "enable"
}
resource "fortios_router_bgp_neighbor" "azure_tunnel0" {
ip = var.azure_vpn_instance0_public_ip
remote_as = var.azure_vpn_asn
update_source = "azure-vpn-tunnel1"
ebgp_enforce_multihop = "enable"
distribute_list_out = "tunnel1-out"
}
resource "fortios_router_bgp_neighbor" "azure_tunnel1" {
ip = var.azure_vpn_instance1_public_ip
remote_as = var.azure_vpn_asn
update_source = "azure-vpn-tunnel2"
ebgp_enforce_multihop = "enable"
distribute_list_out = "tunnel2-out"
}
ebgp_enforce_multihop = "enable"이 두 peer를 동시에 best path 후보로 올리는 핵심입니다. 멀티홉이 없으면 BGP는 한 peer만 best path로 선택해 나머지는 inactive 상태로 남습니다.
4.2 Access-list + Network 선언
tunnel별로 광고할 prefix를 분리해 두면 Azure 쪽 라우팅 테이블에서 instance 0/1이 각각 다른 prefix를 받습니다. 이게 Health Check가 정상 작동하는 비결입니다.
resource "fortios_router_accesslist" "tunnel1_out" {
name = "tunnel1-out"
comments = "advertise via tunnel 1"
rule {
id = 1
action = "permit"
prefix = "169.254.0.0/30" # tunnel 1 IP
}
rule {
id = 2
action = "permit"
prefix = "10.92.129.0/24" # us_site_vlan
}
}
resource "fortios_router_bgp_network" "tunnel1" {
prefix = "169.254.0.0/30"
}
resource "fortios_router_bgp_network" "tunnel2" {
prefix = "169.254.0.4/30"
}
resource "fortios_router_bgp_network" "us_subnet" {
prefix = "10.92.129.0/24"
}
💼 Access-list로 prefix를 정확히 분리해 두지 않으면 Azure가 instance 0/1에 같은 prefix를 받아 다시 1개로 라우팅 처리합니다. 결과적으로 Health Check 실패가 그대로 발생합니다.
5. SD-WAN Health Check — BGP가 끝나야 비로소 정상
BGP neighbor가 up이고 Azure vHub BGP Dashboard에서 instance 0/1 양쪽에 prefix가 보이는 상태가 되어야 SD-WAN Health Check가 정상 작동합니다.
5.1 SD-WAN member + Performance SLA
resource "fortios_system_sdwan_members" "tunnel1" {
interface = "azure-vpn-tunnel1"
status = "enable"
gateway = "169.254.0.2"
}
resource "fortios_system_sdwan_members" "tunnel2" {
interface = "azure-vpn-tunnel2"
status = "enable"
gateway = "169.254.0.6"
}
resource "fortios_system_sdwan_healthcheck" "azure_sla" {
name = "azure-vpn-sla"
server = var.azure_sla_servers[0]
protocol = "ping"
interval = 500
failtime = 3
recoverytime = 5
members = "1 2"
health_check_type {
type = "latency"
}
}
5.2 Azure 측 검증 — vHub Site / Link / BGP Dashboard / Effective Routes
Terraform이 끝나도 Azure 쪽에서 4가지를 확인해야 합니다.
Site 설정: Azure Virtual WAN > Virtual HUB > S2S VPN > Site.

Link 설정: 같은 Site 안의 Link 탭에서 tunnel 0/1 양쪽이 별도 Link로 등록돼 있는지 확인.

BGP Dashboard: instance 0과 instance 1 양쪽에 BGP 상태가 Established로 보여야 합니다. 한쪽만 Established면 prefix가 한쪽으로만 광고된 상태이므로 Health Check가 다시 깨집니다.

Effective Routes: US Site의 내부 prefix(10.92.129.0/24)가 instance 0과 instance 1 양쪽 next-hop으로 등록되어 있어야 합니다.

5.3 FortiGate 측 검증 — BGP summary / routing-table
FortiGate에서 다음 두 명령으로 BGP 상태와 라우팅 테이블을 확인합니다.
FortiGate # get router info bgp summary

FortiGate # get router info routing-table bgp

BGP summary에서 두 neighbor가 Established이고, routing-table bgp에서 10.92.129.0/24가 두 경로 모두 active면 SD-WAN Health Check가 곧바로 정상화됩니다.
6. 자주 발생하는 오류와 해결법
| 증상/오류 메시지 | 원인 | 해결 방법 |
|---|---|---|
| tunnel은 up인데 traffic 안 흐름 | nattraversal=enable |
Phase 1 nattraversal=disable로 변경 |
| tunnel은 up, Phase 2 제안 mismatch | Azure와 proposal 불일치 | Phase 1/2 모두 aes128-sha1 3des-sha1 aes256-sha256 사용 |
| 큰 파일 전송 시 hang | tcp-mss 미설정 | firewall policy 또는 interface에 tcp_mss=1350 |
| Health Check 항상 tunnel 2 down | BGP access-list 미분리 | tunnel 1/2 별도 access-list + distribute_list_out |
| BGP neighbor Established인데 Health Check 실패 | ebgp_multipath 미설정 | fortios_router_bgp에 ebgp_multipath=enable |
| Azure Effective Routes에 한쪽만 보임 | BGP network에 한 쪽 tunnel IP만 선언 | fortios_router_bgp_network에 tunnel 1/2 IP 모두 추가 |
ebgp_enforce_multihop unknown attribute |
provider 버전이 너무 낮음 | fortinetdev/fortios provider ~> 1.20 이상 |
7. 마치며
FortiGate-Azure VPN에서 SD-WAN Health Check를 Terraform으로 정상화하려면 IPsec Phase 1/2 제안값, tcp-mss, BGP EBGP 멀티홋, BGP access-list 분리, SD-WAN Health Check가 한 모듈 안에서 한꺼번에 묶여야 합니다. BGP neighbor만 만들고 SD-WAN을 별도로 두면, Azure VPN active-active의 응답 비대칭 때문에 Health Check가 항상 실패합니다. 이 글의 모듈 구조(vpn/firewall_policy/system_interface/route/sdwan)는 그 한계를 Terraform 코드 한 곳에서 전부 표현하도록 설계됐습니다.
🎯 핵심 요약
- Azure VPN active-active의 응답 비대칭 = Health Check가 실패하는 진짜 원인, BGP 멀티홉 + access-list 분리로 해결
- Phase 1
nattraversal=disable+ Phase 1/2 keylife를 다르게 두는 것이 silent fail 방지 - tcp-mss 1350 = firewall policy 또는 interface 한 곳에서라도 설정
- EBGP 멀티홉 = 멀티홉은 EBGP 전용, 두 peer를 동시에 best path 후보로 올림
- Access-list 분리 = tunnel 1/2 별도 distribute_list_out, Azure가 instance 0/1에 다른 prefix를 받게 함
- Azure 측 검증 = Site/Link/BGP Dashboard (양쪽 Established) + Effective Routes (양쪽 next-hop) 필수