WAF 기초 워크숍을 해보았습니다.(Introduction to WAF)

2023.08.21

안녕하세요, 클래스메소드의 서은우 입니다.

AWS에는 보안과 관련된 여러 서비스 중, AWS WAF 서비스를 이용하면 손 쉽게 웹 어플리케이션을 보호할 수 있습니다.

AWS에서는 AWS WAF에 관한 지식을 배울 수 있는 워크숍을 제공하고 있는데요, 본 블로그에서는 해당 워크숍을 제가 직접 진행해보고 그 내용을 여러분에게 소개 시켜드리고자합니다.

더욱 자세한 내용은 아래의 원문을 통해 확인하실 수 있습니다.

목차

워크숍은 다음과 같은 순서로 진행됩니다.

  • Introduction
  • Web ACLs and Managed Rules
  • Custom Rules
  • Advanced Custom Rules
  • Testing New Rules
  • Logging
  • Cleanup

Introduction

워크숍에 대한 개요를 설명하는 파트입니다.

워크숍을 진행하기 위해서 다음과 같은 리소스가 필요합니다. 미리 제공되는 CloudFormation 스택을 이용하여 빠르게 워크숍 환경을 구축할 수 있습니다.

  • AWS 계정
  • 워크숍 진행 환경
    • Mac 과 리눅스 OS의 경우
      • 최신 버전의 AWS CLI
      • 네트워크 가시성을 제공하는 브라우저(ex: Chrome)
      • curl
    • Windows의 경우
      • AWS Cloud9 추천
      • 혹은 curl
  • 샘플 웹 어플리케이션 배포
    • 미리 제공된 CloudFormation 스택을 이용하여 배포
    • 웹 어플리케이션은 OWASP Juice Shop를 이용
    • 환경 변수 설정

Web ACLs and Managed Rules

해당 파트에서는 파트A 와 파트B, 두개의 파트를 통해 Managed Rules의 Web ACL을 배포합니다. Managed Rules는 AWS 혹은 타사로부터 생성되고 관리되는 규칙입니다.

파트 A

파트 A에서는 Web ACL을 생성합니다.

AWS WAF 콘솔 화면으로 이동하여 WebACL 항목으로 이동합니다.

Web ACL을 생성하고자하는 리전을 선택하고 WebACL 생성 버튼을 눌러 주세요. 워크숍에서는 CloudFront에 WAF를 설정하기 때문에 Global(CloudFront) 를 선택합니다.

WebACL 이름과 설명을 입력하면 CloudWatch 지표 이름이 자동으로 생성되는 것을 확인할 수 있습니다. 그리고 리소스 타입이 Amazon CloudFront distributions 임을 확인해주세요.

스트롤을 내려 WAF를 연결시킬 AWS 리소스를 선택해줍니다. CloudFormation 으로 미리 생성된 리소스를 선택한 후 다음으로 넘어가주세요.

이것으로 WebACL을 생성하였지만, 해당 WebACL에는 아직 아무런 룰이 설정되어 있지 않은 상태입니다. 룰에 관해서는 파트 B에서 이어집니다.

파트 B

파트 B 에서는 WebACL에 관리형 룰을 추가합니다. 해당 워크숍에서는 다음과 같은 룰을 설정해야합니다.

AWS Managed Rule Groups

  • Core rule set: 웹 애플리케이션에 공통적인 광범위한 취약성을 다루는 규칙
  • SQL database: SQL 인젝션과 같은 SQL 데이터베이스 공격으로부터 보호하기 위한 규칙

Add manged rule groups 버튼을 눌러 주세요. AWS Managed Rule Groups - Free rule groups 에서 해당 룰 그룹을 추가하고 설정을 완료합니다.

룰 설정이 완료되었다면 터미널을 열어 테스트를 진행할 수 있습니다. 테스트 요청이 Block 당했다면 설정대로 WAF가 제대로 작동하고 있다는 것입니다.

# Request XSS Attack
 curl -X POST  $JUICESHOP_URL -F "user='<script><alert>Hello></alert></script>'"
 # Request SQL Injection
 curl -X POST $JUICESHOP_URL -F "user='AND 1=1;"

# Request Blocked
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>403 ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
Request blocked.

Custom Rules

자신이 원하는 대로 설정한 룰을 Custom Rules 이라고 합니다.

본 워크숍에서는 X-TomatoAttack 값이 있는 특정 헤더를 차단하는 커스텀 룰을 생성합니다.

생성한 WebACL의 Rules 항목에서 각종 룰을 추가할 수 있습니다. 여기서 Add my own rules and rule groups 를 통해 Custom Rules을 생성할 수 있습니다.

X-TomatoAttack 값이 포함되어 있는 헤더를 차단하기 위해 다음과 같은 값을 설정할 수 있습니다.

  • Inspect: All headers
  • Headers match scope: Keys
  • Content to inspect: Include headers that have specified keys
  • Keys to include: X-TomatoAttack
  • Match type: Size greater than or equal to
  • Size in bytes: 0
  • Oversize handling: Match (WAF가 검사가능한 크기 이상의 콘텐츠에 대한 설정으로, 본 워크숍에서는 요청을 룰과 일치하는 것으로 처리하는 Match를 설정하였습니다. )

설정 완료 후 터미널을 열어 테스트를 진행합니다. 커스텀 룰이 제대로 설정되었다면 요청이 거부됩니다.

# X-TomatoAttack 값이 있는 헤더를 담아 요청
curl -H "X-TomatoAttack: Red" "${JUICESHOP_URL}"

# Request Blocked
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>403 ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
Request blocked.

Advanced Custom Rules

앞선 파트에서는 간단한 커스텀 룰을 작성해 보았는데요, Advanced Custom Rules 파트에서는 JSON을 이용하여 조금 더 복잡한 커스텀 룰을 작성합니다.

본 워크숍에서는 다음과 같은 요청을 거부하는 룰을 JSON을 이용하여 추가합니다.

다음 중 하나에 해당하는 모든 요청 차단

  • 다음 헤더를 포함: x-milkshake: chocolate AND x-favourite-topping: nuts
  • 다음 파라미터를 포함: milkshake=banana AND favourite-topping=sauce
{
    "Name": "AdvancedCustomRules", // Rule name
    "Priority": 0,
    "Action": {
        "Block": {}
    },
    "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "AdvancedCustomRules" // CloudWatch Metric name
    },
    "Statement": {
        "OrStatement": { // OR문 둘 중에 하나만 일치해도 일치
            "Statements": [
                {
                    "AndStatement": { // AND문 둘 다 일치해야 일치
                        "Statements": [
                            {
                                "ByteMatchStatement": {
                                    "FieldToMatch": {
                                        "SingleHeader": { // 헤더
                                            "Name": "x-milkshake"
                                        }
                                    },
                                    "PositionalConstraint": "EXACTLY",
                                    "SearchString": "chocolate",
                                    "TextTransformations": [
                                        {
                                            "Type": "NONE",
                                            "Priority": 0
                                        }
                                    ]
                                }
                            },
                            {
                                "ByteMatchStatement": {
                                    "FieldToMatch": {
                                        "SingleHeader": {
                                            "Name": "x-favourite-topping"
                                        }
                                    },
                                    "PositionalConstraint": "EXACTLY",
                                    "SearchString": "nuts",
                                    "TextTransformations": [
                                        {
                                            "Type": "NONE",
                                            "Priority": 0
                                        }
                                    ]
                                }
                            }
                        ]
                    }
                },
                {
                    "AndStatement": { // 둘 다 일치해야 일치
                        "Statements": [
                            {
                                "ByteMatchStatement": {
                                    "FieldToMatch": {
                                        "SingleQueryArgument": { // 쿼리 파라미터
                                            "Name": "milkshake"
                                        }
                                    },
                                    "PositionalConstraint": "EXACTLY",
                                    "SearchString": "banana",
                                    "TextTransformations": [
                                        {
                                            "Type": "NONE",
                                            "Priority": 0
                                        }
                                    ]
                                }
                            },
                            {
                                "ByteMatchStatement": {
                                    "FieldToMatch": {
                                        "SingleQueryArgument": {
                                            "Name": "favourite-topping"
                                        }
                                    },
                                    "PositionalConstraint": "EXACTLY",
                                    "SearchString": "sauce",
                                    "TextTransformations": [
                                        {
                                            "Type": "NONE",
                                            "Priority": 0
                                        }
                                    ]
                                }
                            }
                        ]
                    }
                }
            ]
        }
    }
}

작성 완료후 테스트를 진행합니다.

# 허용되는 요청
curl -H "x-milkshake: chocolate" "${JUICESHOP_URL}"
curl  "${JUICESHOP_URL}?milkshake=banana"

# 차단되는 요청
curl -H "x-milkshake: chocolate" -H "x-favourite-topping: nuts" "${JUICESHOP_URL}"
curl  "${JUICESHOP_URL}?milkshake=banana&favourite-topping=sauce"

이렇듯 JSON을 통해 조금 더 복잡한 룰을 생성할 수 있습니다.

Testing New Rules

새로운 룰을 생성하고 적용 시키기 전에 생성한 룰이 제대로 동작하는지 테스트를 진행할 필요가 있습니다. 때문에 실제 요청을 차단하지 않고 동작을 확인할 수 있도록 ActionBlock -> Count 로 변경합니다.

Action이 Count인 룰을 새롭게 작성합니다.

{
    "Name": "count-von-count",
    "Priority": 0,
    "Action": {
        "Count": {} // 일치하는 요청을 차단하지 않고 카운트함
    },
    "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "count-von-count"
    },
    "Statement": {
        ...
    }
}

새로운 룰 설정 후 WebACL의 Logging 설정을 유효로합니다.

로깅 옵션으로 CloudWatch Logs log group을 선택하고 로그 그룹을 지정합니다. (CloudWatch 로그 그룹이 없다면 생성해주세요)

테스트 요청을 보낸 후 CloudWatch Logs를 확인 합니다.

# Test request
curl "$JUICESHOP_URL?username=admin"

Logging

 ※ 현재는 Logging 데이터를 S3로 직접 보낼 수 있기때문에 S3에 로깅 데이터를 저장하기 위해 Kinesis Data Firehose 서비스를 이용하지 않아도 됩니다. 저는 워크숍의 내용을 따르기 위해 그대로 진행하였으며, 이 부분은 참고 정도라고만 생각하시면 될 것 같습니다.

Logging 파트에서는 민감한 정보를 로깅하지 않도록 설정하고 Amazon Kinesis Data Firehose를 이용하여 로그를 수집하고 S3에 저장합니다.

로깅하고 싶지 않는 정보가 있을 경우, Redacted fields 를 설정하여 해당 정보를 감추는 것이 가능합니다.

앞선 파트에서 CloudWatch Logs를 통해 로그를 수집했습니다. CloudWatch Logs로 수집한 로그를 영구 보존할 수 있긴 하지만 요금이 1 GB당 0.0314 USD로, 1 GB당 0.025 USD인 S3 보다는 비싸기 때문에 로그를 장기 보존할 필요가 있는 경우 S3에 보존하는 것이 비용적으로는 유리할 수 있습니다.

로그 보존을 위한 버킷과 Kinesis Data Firehose 전송 스트림을 생성합니다.(버킷 생성은 생략하겠습니다.)

WebACL Logging 설정 콘솔 화면에서 기존의 로깅 설정을 수정합니다.

Logging destination을 Kinesis Data Firehose stream 으로 수정하고 생성해둔 Kinesis Data Firehose 전송 스트림을 선택합니다.

숨기고 싶은 로깅 항목을 설정하기 위해 Redacted fields에서 Single header를 선택하고 그 값으로 Cookie를 추가한 후 설정을 마칩니다.

다음 curl 명령을 통해 테스트 요청을 보냅니다.

curl "$JUICESHOP_URL?username=admin"
curl "${JUICESHOP_URL}?milkshake=banana&favourite-topping=sauce"
curl -H "x-milkshake: chocolate" "${JUICESHOP_URL}"
curl -H "cookie: TheJuiceShopCookie" "${JUICESHOP_URL}" # cookie 헤더

S3 버킷에서 로그 파일을 다운 받고 Cookie 헤더를 확인 하면 아래와 같이 Cookie 헤더의 원래 값은 보이지 않고 REDACTED 로 바뀌어져 출력되는 것을 알 수 있습니다.

"name":"Cookie","value":"REDACTED"

Cleanup

끝으로 워크숍에서 사용한 리소스를 삭제하는 것으로 워크숍을 종료합니다.

끝으로

워크숍을 통해 AWS WAF의 기초적인 부분부터 직접 해보고 배울 수 있어서 매우 좋았습니다.

특히 지금껏 JSON을 이용하여 룰을 작성해 본 적이 없었는데, 이번 워크숍을 통해 배우고 연습해 볼 수 있었던 점이 가장 좋았던 것 같습니다.