본문 바로가기

DevOps/AWS

AWS Network Firewall & TCP 패킷만 캡처되는 이슈 트러블 슈팅

개요

오늘은 AWS의 Network Firewall에 대해서 알아보려고 한다. 저번 포스팅에 이어서 여전히 AWS의 서비스와 통신할 때 IGW를 통해 외부로 나가면 안 된다는 요구사항을 만족시키기 위해 사용하던 중 새롭게 알게 된 내용들과 삽질한 내용이 있어 이를 정리해두려고 한다.

AWS Network Firewall

AWS Network Firewall은 VPC 내에서 사용할 수 있는 네트워크 방화벽 겸 침입 탐지 및 방지 서비스이다. IGW, NAT, VPN 같은 AWS 서비스와 함께 사용할 수 있다.

https://docs.aws.amazon.com/network-firewall/latest/developerguide/how-it-works.html

이건 Network Firewall 작동방식을 소개한 공식 문서에 올라온 구성도인데, 구성도에 있는 것처럼 VPC 하위에 Firewall에서 사용할 전용 Subnet을 생성하고(Firewall subnet), 이 서브넷에는 하나의 AZ마다 하나의 Firewall Endpoint가 생긴다. 이 Endpoint는 자신이 위치한 서브넷 외의 모든 서브넷의 통신을 보호할 수 있다.

 

구성 요소

Network Firewall은 3가지 구성 요소를 가진다. Rule group, Firewall poilicy, Firewall인데 중요하니까 간단하게 설명하겠다.

  1. Rule group: 들어오는 트래픽을 검사하고 처리하기 위한 기준이 되는 그룹. 검사 기준에 따라 패킷을 삭제(drop)하거나 통과(pass)시킬 수 있다.
  2. Firewall policy: Rule group과 정책 수준에서의 동작을 어떻게 할지 정의한다.
  3. Firewall: Firewall policy를 VPC에 연결하며, 로깅이나 필터링을 어떻게 할 건지도 정할 수 있다.

Network Firewall의 구성 요소를 사용하다보면 Stateless니 Stateful이니 하는 단어들이 많이 나오는데, 이것 또한 중요한 개념이니 간단하게 짚고 넘어가도록 하겠다.

 

Stateless vs Stateful

Stateless는 말 그대로 상태가 없는, Stateful은 상태가 있는 이라는 뜻이다. 그래서 동작 방식도 직역한 그대로 동작하게 되는데, 예시를 하나 들어서 설명하도록 하겠다.

출발지(Source): 1.2.3.4
출발지(Source): 5.6.7.8
도착지(Destination): 9.0.1.2
방화벽(Firewall): 1.2.3.4에서 온 TCP 트래픽은 허용

1. Stateless

1.2.3.4에서 온 요청과 5.6.7.8에서 온 요청은 각각 어떻게 처리될까?

당연히 1.2.3.4에서 요청이 온 경우에는 Allow가 될 것이고, 5.6.7.8에서 요청이 온 경우에는 Deny 처리가 될 것이다. 그런데 문제는 요청이 온 뒤, 다시 응답을 보내는 경우인데 Stateless인 경우에는 둘 다 Deny 처리가 된다. 즉, 1.2.3.4 서버에서 요청은 가지만 응답이 오지 않게 되는 것이다.

왜 이런 일이 생기는걸까? Stateless는 말 그대로 “상태가 없기” 때문이다. 들어온 요청에 대해 따로 저장하지 않기 때문에 응답을 할 때도 명확한 규칙을 적용해야 하는 것이다. 만약 1.2.3.4에서 들어온 요청을 허용하고, 응답을 받으려면 아래와 같이 규칙을 추가해야 한다.

 

방화벽(Firewall): 1.2.3.4에서 온 TCP 트래픽은 허용 + 0.0.0.0으로 나가는 TCP 트래픽 허용

 

2. Stateful

이번에는 Stateful이다. 요청은 Stateless와 마찬가지로 1.2.3.4는 Allow, 5.6.7.8은 Drop 된다. 그러나 차이가 있다면 Stateful에는 세션 테이블이라는 것이 있고, 인바운드 패킷이 들어오게 되면 1.2.3.4에 관한 세션이 생성된다. 9.0.1.2에서 1.2.3.4에게 응답을 보내줄 때에는 아웃바운드 정책과 상관없이 세션 테이블에 응답 패킷과 일치하는 정보가 있다면 내보낸다.

이런 Stateless, Stateful 방식을 AWS에서도 찾아볼 수 있는데, NACL은 Stateless, SG는 Stateful하게 동작한다. 그리고 우리가 지금 포스팅에서 다루는 Network firewall은 Stateless, Stateful을 둘 다 지원한다.

 

 

https://docs.aws.amazon.com/network-firewall/latest/developerguide/firewall-rules-engines.html

기본적으로 요청이 들어오면 Stateless Rule Groups에서 먼저 평가하게 된다. 만약 패킷이 설정한 조건에 부합한다면 그대로 통과(pass)하고 부합하지 않는다면 삭제(drop)하거나 Stateful Rule Group으로 이동시킨다.(forward to stateful)

Stateful Rule Group에서도 마찬가지로 정해진 규칙에 따라 평가하게 되는데, 평가 기준에 따라 통과(pass)하거나 삭제(drop)할 수도 있고, 아니면 알림이 울리게만 할 수도 있다(alert)

그런데 Network Firewall 관련 가이드를 읽어보면 알 수 있지만 Stateless를 사용하면 복잡성이 늘어나기 때문에 (응답에 대한 규칙도 추가적으로 구성해야 하기 때문에) 웬만하면 Stateless는 아무 설정하지 않고 forward to stateful로 설정해 두고, 필요한 규칙을 Stateful rule group에 설정해서 사용하길 권고한다.

Stateless rules should be used very sparingly because they can easily cause asymmetric flow forwarding issues (where only one side of the flow is seen by the stateful inspection engine of the firewall) and they tend to make the overall firewall ruleset more complex to understand and troubleshoot. For the large majority of use cases we recommend the stateless engine’s default action be set to “Forward to stateful rule groups” and we recommend not having any stateless rules configured since they take precedence over stateful rules.

 

인프라 구성

https://docs.aws.amazon.com/ko_kr/network-firewall/latest/developerguide/arch-igw-ngw.html

이건 Firewall을 사용하게 되면 인프라 구성이 어떻게 되어야 하는지에 대한 내용이다. 단일 AZ 기준으로 설명이 되어 있긴 하지만 결국에 다중 AZ도 서브넷 구성은 비슷하게 하기 때문에 이 내용을 기준으로 설명하면 위 구성도 같이 라우트 테이블이 구성된다. 내가 실제로 구성할때는 ALB가 있기 때문에 서브넷이 조금 다르게 구성이 되긴 했지만 IGW ↔ Firewall Subnet ↔ NAT ↔ Private Subnet이라는 큰 틀은 같았다. 그리고 난 아웃바운드 통신을 제어하는 것만이 목적이었기 때문에 처음에는 NAT와 Firewall subnet의 위치를 바꿔서 구성했다가 엄청난 삽질을 했다 (…) 이 내용은 후술하도록 하겠다.

 

Stateful Rule Group

인프라 구성을 얘기하다가 갑자기 유턴하면서 Stateful Rule Group 얘기로 돌아왔는데, Rule group을 만들 때 format을 Standard stateful rule, Domain list, Suricata compatible rule string 중 선택할 수 있다.

 

처음에 WhitelistDomain을 적용하는게 목적이어서 Domain list 방식을 선택했는데, 결과적으로는 Suricata compatible rule string format을 사용하게 되었다. 기본적으로 제공해 주는 로그의 내용이 빈약하기도 하고, 원하는 조건을 상세하게 적용하려면 결국에는 Suricata를 사용해야 한다. 애초에 Suricata 기반으로 Rule group을 만들어서 그런지 호환도 잘된다. Network firewall best practice guide를 봐도 Suricata를 사용하길 권장한다.

Use Custom Suricata rules instead of UI generated rules

These are configurable under the Stateful rule group options and are a free-form text that you to have full control. They allow you to more easily leverage the full flexibility of Suricata. Here are example Suricata rules that customers have found helpful when getting started.

공식문서에 사용법도 잘 나와 있기도 하고 사용법도 직관적이다. Suricata가 뭔지까지 설명하기엔 포스팅이 너무 길어질 것 같으니 관련 링크만 몇개 달아두고 나중에 별도 포스팅으로 다룰 예정이다.

 

실습

Network Firewall을 사용하려면 기본적으로 VPC, Subnet을 만들어야 하는데 콘솔로 하나하나 만들기에는 너무 길어질 것 같고, aws-samples 프로젝트에 테라폼으로 잘 만들어둔 코드가 있어서 이를 사용해서 실습을 해보려고 한다.

https://github.com/aws-samples/aws-network-firewall-egress-domain-filter

깃허브에 나와있긴 하지만, 테라폼을 적용하면 위 구성도에 따라 구성이 된다. VPC와 Internet Gateway가 1개씩 생기고, Subnet이 AZ 2개로 구성된다. 각각의 Subnet에는 Firewall, NAT Subnet, EC2 Subnet이 포함된다. 그거 말고도 Log를 저장하는 CloudWatch Log Group도 몇 개 추가된다.

$ git clone git@github.com:aws-samples/aws-network-firewall-egress-domain-filter.git
$ cd aws-network-firewall-egress-domain-filter

프로젝트 폴더로 이동한 뒤, region을 설정해준 뒤 terraform init - terraform apply를 순서대로 해준다 (자세한 내용은 README.md에 나와있으니 참고) 구성하고 난 뒤에 테스트를 해보려면 EC2를 직접 띄우면 된다.

EC2와 Session Manager로 접근할 수 있도록 IAM Role도 생성하고 Launch instance를 해준다. Amazon Linux 2에는 AWS SSM Agent가 이미 깔려있기 때문에 별도로 설치할 필요는 없고, 조금 기다린 뒤 인스턴스 Connect를 하면 Session Manager가 활성화된다.

$ curl https://www.amazon.com

 

curl로 amazon.com에 요청을 보내보면 응답이 잘 오는 것을 확인할 수 있다.

$ curl https://www.google.com

 

그러나 amazon.com 이외의 다른 사이트에 요청을 해보면 요청이 제대로 가지 않는 것을 확인할 수 있다. 그 이유는 Network firewall rule group에 구성된 내용 때문인데,

 

코드를 보면 allowed_domains.yml 에 명시된 도메인은 ALLOWLIST로 설정이 되어 있다. 이 파일을 보면 www.amazon.com만 명시가 되어 있기 때문에 www.amazon.com 이외의 도메인은 모두 Drop 된 것이다. 콘솔로 확인해도 확인이 가능한데, 캡처를 안 해놓고 리소스를 삭제해서 따로 캡처한 것은 없다 … 😅

아무튼 이 로그는 CloudWatch Log group에서 확인이 가능하다. firewall_alert_logs Log Group에 들어가면 로그가 생성된 것을 확인할 수 있다.

 

 

로그의 내용에서 여러 정보를 확인할 수 있다.

{
  "firewall_name": "network-firewall",     // 방화벽 이름
  "availability_zone": "ap-northeast-2b",  // AWS 가용 영역(서울 리전)
  "event_timestamp": "~",                  // 이벤트 타임스탬프
  "event": {
    "timestamp": "2025-04-27T08:48:08.126655+0000",  // 이벤트가 발생 시간
    "flow_id": 1019585680161813,                     // 트래픽 흐름 식별자
    "event_type": "alert",                          // 이벤트 유형(알림)
    "src_ip": "10.0.2.182",                         // 출발지 IP(내부 네트워크)
    "src_port": 59601,                              // 출발지 포트
    "dest_ip": "172.217.26.4",                      // 목적지 IP(Google 서버)
    "dest_port": 443,                               // 목적지 포트(HTTPS)
    "proto": "TCP",                                 // 사용된 프로토콜
    "alert": {
      "action": "blocked",                          // 수행된 조치(차단됨)
      "signature_id": 4,                            // 시그니처 ID
      "rev": 1,                                     // 규칙 리비전
      "signature": "not matching any TLS allowlisted FQDNs",  // 차단 이유(허용 목록에 없는 도메인)
      "category": "",                               // 알림 카테고리
      "severity": 1                                 // 심각도
    },
    "tls": {
      "sni": "www.google.com",                      // 접속하려던 도메인 이름
      "version": "UNDETERMINED",                    // TLS 버전
      "ja3": {},                                    // JA3 지문
      "ja3s": {}                                    // JA3S 지문
    },
    "app_proto": "tls"                              // 애플리케이션 프로토콜
  }
}

이 Alert 로그를 보고 차단된 도메인을 Allow List에 추가하는 등의 조치를 취해볼 수 있다. 그런데 내가 원한 것은 차단이 아닌 Alert 로그만 쌓인 뒤 Pass를 하도록 하는 것이고, 그러려면 Rule Group 수정이 필요하다. 이때 Suricata로 변경하고 아래와 같은 Rule을 사용했다.

 

resource "aws_networkfirewall_rule_group" "default_stateful_group" {
  capacity = 10000
  name     = "DefaultStatefulGroup"
  type     = "STATEFUL"
  rule_group {
    rules_source {
      rules_string = <<EOF
pass tls any any -> any any (tls.sni; content:".amazon.com"; flow:established,to_server; noalert; sid:1000;)
alert tls $HOME_NET any -> any any (msg:"Non-allowlisted domain accessed"; sid:9999;)
EOF
    }

이렇게 Rule을 적으면 .amazon.com 도메인으로 가는 TLS 트래픽은 첫 번째 규칙에 의해 통과(pass)되고, 그 외 모든 TLS 트래픽은 두 번째 규칙에 의해 알림이 생성되나 차단은 하지 않게 된다.

 

아까와 다르게 curl로 요청하면 응답도 잘 오고 Cloud Watch로 확인해도 event_typ만 alert로 바뀌고 로그가 잘 쌓이는 것을 확인할 수 있다.

 

그런데 문제가 하나의 요청에도 너무 많은 로그가 쌓이는 것이다. 아무래도 패킷이 하나가 아니다 보니 확인하는 패킷마다 로그가 쌓여서 그런 것 같다.

pass tls any any -> any any (tls.sni; content:".amazon.com"; flow:established,to_server; noalert; sid:1000;)
alert tls $HOME_NET any -> any any (msg:"Non-allowlisted domain accessed**"; flowbits:isnotset,alerted; flowbits:set,alerted; flow:established,to_server; threshold:type limit, track by_src, count 1, seconds 60; sid:9999;)

그래서 Rule을 아까와 다르게 조금 바꾸었는데, 이렇게 설정하면서 flowbits 덕분에 각 TLS 연결마다 한 번씩만 알림이 생성되고, threshold를 설정하면서 각 클라이언트(출발지 IP)마다 60초 동안 최대 한 번의 알림만 발생하게 변경했다.

 

그 뒤에는 하나의 요청에 하나의 로그만 남는 것을 확인할 수 있었다. 😄

 

가격이 비싸니 실습 이후에는 꼭 리소스를 삭제하자 … 그렇지 않으면 돈이 숭숭 나간다

 

https://aws.amazon.com/network-firewall/pricing/?nc1=h_ls

 

트러블 슈팅 - TCP 패킷만 캡처되는 이슈

이건 실습 이후에 회사에서 적용하면서 겪은 이슈를 같이 기록해 두면 좋을 것 같아서 적어두려고 한다. 리소스를 생성하고 적용하는 과정에는 문제가 없어 보였는데 남는 Alert 로그를 보면 TCP 로그만 남는 이슈가 있었다.

alert tcp any any -> any any (msg:"ALERT: TCP Traffic Logged"; sid:9999991;)
alert udp any any -> any any (msg:"ALERT: UDP Traffic Logged"; sid:9999992;)
alert icmp any any -> any any (msg:"ALERT: ICMP Traffic Logged"; sid:9999993;)
alert dns any any -> any any (msg:"ALERT: DNS Traffic Logged"; sid:9999994;)
alert http any any -> any any (msg:"ALERT: HTTP Traffic Logged"; sid:9999995;)
alert http any any -> any any (msg:"ALERT: HTTP Traffic Detected"; flow:to_server; sid:9999997;)
alert tls any any -> any any (msg:"ALERT: TLS Traffic Logged"; sid:9999996;)
alert tls $HOME_NET any -> any any (tls.sni; content:"www.example.com"; startswith; nocase; endswith; flow:to_server; msg:"TLS SNI alert example"; sid:202501052;)

Suricata 식으로 이렇게 적용을 했었는데, 남는 로그를 보면 TCP 로그 외에는 아무것도 남지 않았다.

 

우선순위도 조정해 보고, 구성도 조정해 가면서 테스트해 봤는데, 결론적으로는 인프라 구성의 문제였다.

 

문제가 됐던 인프라는 이렇게 구성되어 있었고,

 

https://docs.aws.amazon.com/network-firewall/latest/developerguide/arch-igw-ngw.html

이건 AWS에서 IGW, NAT를 사용했을 때 보여주는 인프라 구성이다. 차이라고는 Firewall Subnet과 NAT의 위치가 다른 것 밖에 없는데 이게 문제가 됐었다. 왜냐하면 NAT Gateway를 거쳐야 Destination 서버와 TLS 커넥션을 맺고 Client Hello를 받으며 SNI 정보를 확인할 수 있는데, 기존 인프라 구성에서는 NAT가 Firewall 이후에 배치되어 있기 때문에 TLS 커넥션을 맺기 전 TCP 패킷만을 검사할 수 있기 때문에 TLS 패킷이 캡처되지 않았던 것이다. 이를 해결하려면 Layer 6 이후에서 Firewall에서 패킷을 캡처해야 해야 하는데, 그러려면 NAT가 Firewall 이전에 배치되어 있어야 하기 때문이다.

 

근데 이 내용을 어디에서도 찾지 못해서 이게 100% 확실한 이유라고는 장담하지 못하지만 위 사진처럼 NAT와 Firewall Subnet을 위치를 바꾸니까 (기존 구성에서는 NAT와 LB가 같이 있어서 이것도 분리했다) 처음에 실습했던 것처럼 로그가 잘 나왔다!

누군가에게 도움이 되길 … 🤓👍