7 minute read

값은 전부 예시(lab.local / 10.10.10.x / 192.168.219.x)로 치환함. 실제 환경과 무관함. 이미지 플레이스홀더는 ![desc](img) 형태로 삽입함.

0. 왜 DMZ를 “진짜로” 나누는가

기존 홈랩은 DC·ADFS·SQL·CA·WAP를 전부 한 평면망(192.168.219.0/24)에 올려둔 상태였음. WAP를 도메인 미가입으로만 두고 “DMZ”라 불렀지만, 사실 같은 서브넷이라 WAP가 마음만 먹으면 DC·SQL에 다 접근됨. 명목상 DMZ이지 격리가 아님.

실무 DMZ의 본질은 “WAP가 뚫려도 내부(AD)까지 못 넘어오게 폭발 반경을 봉쇄”하는 것임. 그러려면 네트워크를 물리적으로(가상 스위치 + 방화벽) 나눠야 함. 이번 글은 그걸 OPNsense로 구현하고 검증한 기록임.

핵심 개념 하나 — DMZ 격리에는 두 계층이 있음.

계층 역할 이번 글
① 네트워크 방화벽 서버 간 트래픽 격리 (L3/L4) OPNsense (이번 구축)
② 접근제어(PAM) 사람의 관리 접속 통제 개념만 (상용 PAM 영역)

여기선 ①(방화벽/격리)만 다룸. ②(PAM)는 실무에서 관리자가 서버에 접속할 때 거치는 관문인데, 이건 별개 계층이라 이번 범위 밖임.


1. 목표 아키텍처

        (외부 시뮬레이션 생략)
              │
 ┌────────── DMZ (10.10.10.0/24) ──────────┐
 │   WAP01  10.10.10.10 (workgroup)         │  ← 도메인 미가입
 │            gw → 10.10.10.1               │
 └───────────┬──────────────────────────────┘
             │ OPNsense LAN(hn1) = 10.10.10.1
        ┌────┴────┐
        │ OPNsense │  방화벽 VM (NIC 2개)
        └────┬────┘
             │ OPNsense WAN(hn0) = 192.168.219.250
 ┌────────── 내부망 (192.168.219.0/24) ─────┐
 │  ADFS NLB VIP .240 / ADFS01 .241 / .242  │
 │  DC01 .191 / SQL / CA .232               │  ← 신뢰 구역
 └──────────────────────────────────────────┘

 허용 규칙(내부 방화벽):
   DMZ → ADFS(.240) : 443     ADFS 프록시 중계 (본질)
   DMZ → ADFS(.240) : 49443   certauth (클라이언트 인증서 인증용)
   그 외 DMZ → 내부  전부 차단

설계 선택 — A안(기존 External을 내부망으로 재사용): 이미 완성한 ADFS 팜·SQL AG·NLB가 전부 기존 스위치에 물려 있어서, 이걸 옮기면(B안) 어렵게 세운 인프라가 깨질 side-effect가 큼. 그래서 WAP만 새 DMZ 세그먼트로 분리하고 나머지는 그대로 둠. 격리 학습 목적엔 이걸로 충분함.


2. OPNsense 방화벽 구축

2-1. 왜 pfSense/OPNsense인가 (Windows RRAS가 아니라)

Windows Defender 방화벽은 host-based임. 자기 호스트로 드나드는 트래픽만 거르고, 호스트를 통과(라우팅)하는 전송 트래픽은 못 막음. DMZ↔내부 사이를 지나가는 트래픽 필터링이 DMZ 격리의 본질인데, 이건 통과 트래픽이라 일반 Windows 방화벽 정책으론 안 됨.

RRAS 정적 패킷 필터로 억지로 가능은 하지만 구식이고 실무 표준이 아님. 그래서 전용 방화벽 OS(OPNsense/pfSense) 로 감. 실무 경계 방화벽도 Fortinet·Palo Alto 같은 전용 어플라이언스(non-Windows)라, 오히려 이게 실무와 일치함.

pfSense CE는 Netgate 스토어 경유라 계정·유료 혼동이 있어서, 계정 없이 ISO 직다운 가능한 OPNsense로 선회함. 규칙 개념은 pfSense와 사실상 동일.

pfSense 다운로드 페이지 — 계정·스토어 경유 확인

2-2. VM 생성 (Hyper-V)

  • 가상 스위치 2개: 기존 스위치(내부망/WAN측) + 신규 DMZ-Switch(Internal)
  • OPNsense VM: Gen1, RAM 2GB, Disk 16GB, NIC 2개 (WAN→기존, LAN→DMZ-Switch)
  • 두 vNIC 모두 MAC 주소 스푸핑 ON + 각 MAC 메모(콘솔 인터페이스 할당 대조용)
New-VMSwitch -Name "DMZ-Switch" -SwitchType Internal
# VM 생성 후 NIC 2개 연결 + MAC 스푸핑
Set-VMNetworkAdapter -VMName "OPNsense-FW" -Name "WAN" -MacAddressSpoofing On
Set-VMNetworkAdapter -VMName "OPNsense-FW" -Name "LAN" -MacAddressSpoofing On

2-3. 설치 & 인터페이스 할당

설치 후 콘솔에서 인터페이스 할당 — hn0/hn1 순서가 애매하므로 MAC 끝자리로 대조:

  • hn0 (…74:19) → WAN (내부망측)
  • hn1 (…74:1a) → LAN (DMZ측)

IP 설정:

  • WAN = 192.168.219.250/24, gateway 192.168.219.1
  • LAN = 10.10.10.1/24, gateway 없음, DHCP 서버 끔

인터페이스·IP까지 잡히면 WAN IP로 웹 GUI에 접속해 로그인:

OPNsense 웹 GUI 로그인 화면

2-4. WAN 사설망 차단 해제 (필수 관문)

Interfaces → WAN에서 Block private networks / Block bogon networks 해제. WAN이 실제론 사설망(192.168.219.x)이라, 안 끄면 RFC1918을 전부 드롭해서 통신이 0이 됨.

OPNsense WAN — Block private/bogon networks 체크 해제

2-5. 방화벽 규칙 — 최소 권한

Firewall → Rules → LAN(=DMZ측)에서 허용 규칙만 명시하고 나머지는 기본 deny:

Action Proto Source Destination Port Desc
Pass TCP LAN net 192.168.219.240 /32 443 ADFS
Pass TCP LAN net 192.168.219.240 /32 49443 certauth

그리고 기본 “Default allow LAN to any” 규칙을 비활성화. first-match 방식이라 이 규칙이 맨 위에 살아있으면 모든 트래픽이 “다 허용”으로 걸려서 격리가 안 됨.

Firewall → LAN — Default allow 규칙 비활성화 상태

왜 Destination이 /24가 아니라 /32인가/24를 넣으면 내부망 전체(DC·SQL·CA 포함)의 443이 열려서 격리가 무너짐. WAP가 실제 필요한 건 ADFS 하나뿐이므로 딱 그 호스트만(/32) 여는 게 최소 권한임. Source(LAN net)는 넓게, Destination은 좁게 — 이게 격리 규칙의 정석.

왜 49443까지 열었나 — ADFS SSL 인증서 SAN에 certauth.sts 가 포함돼 있음. 이건 TLS 클라이언트 인증서 인증 전용 엔드포인트로 443과 포트를 분리해 운영됨. 지금은 폼 기반 SSO만 검증하므로 당장 필수는 아니지만, 인증서에 이미 경로가 준비돼 있어 미리 열어둠. 반면 pki CRL(80)은 최소 권한 원칙에 따라 생략 — CA 체인은 WAP에 직접 반입하고 CRL 오프라인을 허용할 것이므로 pki 접근 없이도 검증이 통과됨. “인증서가 준비된 경로는 열고, 실제로 안 쓰는 경로는 닫는다”는 선택.

ADFS(.240) 443 Pass 규칙 등록:

방화벽 규칙 — ADFS 443 허용 (규칙 목록) 방화벽 규칙 — ADFS 443 허용 (규칙 상세)

certauth(.240) 49443 Pass 규칙 등록:

방화벽 규칙 — certauth 49443 허용 (규칙 목록) 방화벽 규칙 — certauth 49443 허용 (규칙 상세)


3. WAP01 DMZ 이전 + 검증 준비

WAP01(WS2022, workgroup)을 처음부터 DMZ-Switch에 생성. DMZ엔 DHCP가 없으므로 고정 IP를 수동 설정:

# WAP01
New-NetIPAddress -InterfaceAlias "Ethernet" -IPAddress 10.10.10.10 -PrefixLength 24 -DefaultGateway 10.10.10.1

WAP01 — 고정 IP·게이트웨이(10.10.10.1) 설정

내부 노드가 DMZ(10.10.10.x)로 돌아가는 길을 알도록 ADFS 노드에 return-route 추가:

# ADFS01
route add 10.10.10.0 mask 255.255.255.0 192.168.219.250 -p

ADFS01 — DMZ 대역 return-route 추가

실제로는 OPNsense가 Automatic Outbound NAT를 걸어서 WAP 소스를 WAN IP(.250)로 변환하므로 return-route 없이도 됐지만, 명시적으로 걸어두면 경로가 명확해짐.


4. 트러블슈팅 — 여기가 이 글의 본론

격리 규칙까지 다 맞췄는데도 WAP01 → ADFS:443이 안 됨. 구간을 하나씩 끊어가며 원인을 추적한 기록임.

트러블 #1 — WAN IP가 DHCP로 되돌아감

증상: 콘솔에서 WAN을 .250 고정으로 잡았는데, Interfaces → Overview.166(DHCP값)으로 표시. OPNsense ping에서 bind: Can't assign requested address (OPNsense가 .250을 자기 IP로 인식 못 함).

원인: DHCP→고정 전환 직후 인터페이스 재초기화 과정에서 이전 DHCP lease가 다시 잡힘.

조치: Interfaces → [WAN]에서 static .250 재저장 + Apply, 반영 안 되면 재부팅. 재부팅 후 Overview에서 .250 고정 확정.

트러블 #2 — NLB VIP(.240)가 “Host is down”

증상: OPNsense에서 .241(ADFS01 실제 IP)은 ping loss 0%인데, .240(NLB VIP)만 sendto: Host is down (ARP 단계 실패).

원인: NLB가 Multicast 모드라 VIP를 멀티캐스트 MAC(03-bf-...)으로 announce함. L3 장비(OPNsense/라우터)는 “유니캐스트 IP에 멀티캐스트 MAC” 매핑을 ARP로 못 배워서 VIP에 못 닿음. 같은 L2의 Windows 노드끼리는 되지만 라우터 너머에서 막히는 전형적 케이스.

조치: NLB VIP의 멀티캐스트 MAC을 계산 후 OPNsense에 정적 ARP 등록.

# NLB VIP 192.168.219.240 → 16진수 C0.A8.DB.F0
# Multicast NLB MAC = 03-bf-<VIP hex> = 03-bf-c0-a8-db-f0
# (nlb display의 ClusterNetworkAddress로 검증)

# OPNsense 쉘
arp -s 192.168.219.240 03:bf:c0:a8:db:f0

결과 — ICMP 관통 성공:

ping -c 3 192.168.219.240
64 bytes from 192.168.219.240: ... time=0.383 ms
64 bytes from 192.168.219.240: ... (DUP!)   ← NLB 양 노드 응답(정상)
0.0% packet loss

(DUP!)는 에러가 아니라 멀티캐스트 NLB에서 두 노드가 다 응답해서 나오는 정상 현상. 오히려 NLB가 도는 증거.

트러블 #3 — ICMP는 뚫렸는데 TCP 443은 여전히 안 됨 (핵심)

정적 ARP로 ping은 됐는데, WAP01 → .240:443은 계속 실패. 여기서 tcpdump로 전 구간을 캡처함.

① WAP → OPNsense 도착 확인 (hn1):

tcpdump -ni hn1 port 443
IP 10.10.10.10.50420 > 192.168.219.240.443: Flags [S]   ← SYN 도착
IP 10.10.10.10.50420 > 192.168.219.240.443: Flags [S]   ← 재전송(응답 없음)

→ WAP의 SYN은 OPNsense에 잘 도착함.

② OPNsense → WAN 송출 확인 (hn0):

tcpdump -ni hn0 host 192.168.219.240 and port 443
IP 192.168.219.250.57444 > 192.168.219.240.443: Flags [S]   ← NAT돼서 송출됨
IP 192.168.219.250.57444 > 192.168.219.240.443: Flags [S]   ← 재전송(응답 없음)

→ OPNsense는 NAT(.250)해서 WAN으로 정상 송출함. 방화벽·NAT·라우팅 전부 정상. SYN-ACK만 안 돌아옴.

③ Real IP(.241)로 비교 (hn0):

IP 192.168.219.250.8594 > 192.168.219.241.443: Flags [S]     ← SYN
IP 192.168.219.241.443 > 192.168.219.250.8594: Flags [S.E]   ← SYN-ACK (ADFS01 응답!)
IP 192.168.219.250.8594 > 192.168.219.241.443: Flags [.]     ← ACK (연결 성립)
→ TcpTestSucceeded : True

결론: .241(Real IP)은 3-way 핸드셰이크 정상, .240(NLB VIP)은 SYN 도달/SYN-ACK 미반환. 경로는 완벽하고, 순수하게 NLB VIP가 NAT된 세션의 응답(SYN-ACK)을 반환하지 못하는 문제로 확정.

원인 규명: Multicast NLB VIP는 L3 방화벽 + NAT 너머로 들어온 TCP 세션의 응답 경로에서 비대칭이 발생함. ICMP(무연결)는 정적 ARP로 관통되지만, TCP(연결 상태)는 NLB의 노드 배정·응답 반환 과정에서 SYN-ACK이 유실됨. Real IP는 노드가 직접 받고 직접 응답하므로 정상.

조치(이번 회차): 격리 검증 자체는 NLB와 무관하므로, 방화벽 규칙 Destination을 .241(Real IP)로 두고 검증 완료. NLB VIP의 L3+NAT 응답 비대칭은 별도 재검증 과제로 분리(affinity None / Unicast 모드 실험은 ADFS 팜 영향을 봐야 하므로 신중히).

실무 관점: DMZ의 WAP가 내부 ADFS 팜에 붙을 때 NLB VIP로 가는 게 정석이 맞음. 다만 가상화(Hyper-V) + L3 NAT 환경에선 Multicast NLB의 응답 비대칭이 걸림돌이 될 수 있어, 실무에서도 이 구간은 로드밸런서 앞단 설계나 노드 직접 지정 등으로 우회하는 경우가 있음. 이번엔 원인을 tcpdump로 정확히 규명한 것에 의미를 둠.


5. 격리 검증 — 오늘의 결과

WAP01에서 “열려야 할 것 / 막혀야 할 것”을 한 번에 확인:

# 열려야 하는 것
Test-NetConnection 192.168.219.241 -Port 443   # ADFS  → True

# 막혀야 하는 것 (격리 증명)
Test-NetConnection 192.168.219.191 -Port 389   # LDAP  → False
Test-NetConnection 192.168.219.241 -Port 3389  # RDP   → False
Test-NetConnection 192.168.219.191 -Port 445   # SMB   → False
Test-NetConnection 192.168.219.232 -Port 80    # pki   → False

결과:

RemoteAddress    RemotePort  TcpTestSucceeded
192.168.219.241  443         True    ← 허용한 것만 열림
192.168.219.191  389         False   ← LDAP 차단
192.168.219.241  3389        False   ← RDP 차단
192.168.219.191  445         False   ← SMB 차단
192.168.219.232  80          False   ← pki 차단

“허용한 443만 열리고 나머지 내부 접근은 전부 차단” — DMZ 격리가 의도대로 동작함을 확인함.

격리 검증 결과 — 443만 열리고 나머지 내부 접근은 전부 차단


6. 정리 & 다음

오늘 완성

  • OPNsense 방화벽 VM으로 DMZ 세그먼트(10.10.10.0/24) 물리적 분리
  • WAP(DMZ) → ADFS 443만 허용, 내부 나머지 전부 차단
  • 전 구간 tcpdump 검증 (도착 / NAT 송출 / 핸드셰이크)

재검증 과제 (다음 회차)

  • NLB VIP(.240) L3+NAT 응답 비대칭 — affinity None / Unicast 모드 실험
  • 정적 ARP 영구화 — 재부팅 시 arp -s가 사라지므로 부팅 스크립트 자동화
  • Part C — WAP 역할 설치 + Install-WebApplicationProxy (격리된 상태 위에서 ADFS 프록시 구성)

이번 회차 배운 것

  1. DMZ 격리는 host-based 방화벽으론 불가 — 전송 트래픽 필터가 필요, 전용 방화벽 OS의 존재 이유
  2. 최소 권한은 /32로 — 필요한 호스트 하나만 열기
  3. NLB VIP + L3 + NAT는 ICMP와 TCP가 다르게 동작 — ping 뚫렸다고 통신 되는 게 아님. tcpdump로 SYN/SYN-ACK을 봐야 진짜 원인이 보임
  4. 문제 구간은 끊어서 좁힌다 — WAP→OPN→WAN→ADFS를 단계별 캡처로 격리하니 원인이 명확해짐

본 글의 IP/도메인/MAC은 예시값으로 치환함. 실제 운영 환경과 무관함.

Comments