본문

ALB와 AWS WAF를 이용한 x-api-key 기반 API 보안 강화하기

인프라를 만지다 보면 보안과 편의성 사이에서 참 고민이 많아지죠. 최근에 Multi Agent 사내 서비스를 구축하면서 "아차" 싶었던 순간이 있었습니다.

 

든든한 사내망(VPC) 안에 고이 모셔둔 서비스라 안전할 줄 알았는데, 알고 보니 방화벽이 허용된 사내 PC라면 누구나 Postman으로 API를 쿡쿡 찔러볼 수 있는 상태더라고요. "에이, 우리 식구들끼리 쓰는 건데 어때?"라는 안일한 생각이 자칫하면 보안의 '구멍'이 될 수도 있겠다는 직감이 왔습니다.

 

그래서 오늘은 큰 공사 없이도 인프라 레벨에서 슥~ 방어막을 칠 수 있는 방법을 가져왔습니다. AWS WAF를 활용해 x-api-key 검증 레이어를 한 겹 덧씌워, 더 안전하게 업그레이드해 본 과정을 가볍게 공유해 봅니다 :)


 

ALB + WAF + Secrets Manager

왜 ALB가 아닌 WAF인가?

ALB 리스너 규칙으로도 헤더 검사가 가능하지만, WAF를 사용하는 이유는 명확합니다.

  • 중앙 관리: 여러 ALB에 동일한 Web ACL을 쉽게 적용할 수 있습니다.
  • 가시성: CloudWatch를 통해 어떤 요청이 차단되었는지 상세 로깅과 모니터링이 가능합니다.
  • 확장성: 향후 IP 차단, Rate Limit, SQL Injection 방어 등 복잡한 보안 요구사항을 즉시 추가할 수 있습니다.

1. 추측 불가능한 x-api-key 생성

비밀 키를 생성할 때는 추측이 불가능하도록 충분한 엔트로피(High Entropy)를 확보해야 합니다.

  • UUID v4 권장: 가장 범용적이고 안전합니다.
    • 생성 방법: (macOS, Linux etc) 터미널에서 uuidgen 명령어를 사용합니다.

uuidgen

  • Prefix(접두사) 활용: company-nam-prod-...처럼 접두사를 붙여 관리 효율성을 높입니다.
  • Secrets Manager 활용: 키를 코드나 환경 변수에 직접 노출하지 않고 AWS Secrets Manager에 저장하여 관리합니다. 이는 나중에 Terraform(IaC) 자동화 시 보안 노출 위험을 줄여줍니다.

2. AWS WAF 설정

WAF 규칙(Rule) 생성

x-api-key 헤더를 검사하여 값이 정확히 일치할 때만 Allow 처리를 하는 사용자 지정 규칙을 생성합니다.

WAF 신규 생성

(UI / JSON) 규칙 추가
{
  "Name": "check-x-api-key-rule",
  "Priority": 1,
  "Statement": {
    "ByteMatchStatement": {
      "FieldToMatch": {
        "SingleHeader": {
          "Name": "x-api-key"
        }
      },
      "PositionalConstraint": "EXACTLY",
      "SearchString": "YOUR_SECURE_API_KEY",
      "TextTransformations": [
        {
          "Type": "NONE",
          "Priority": 0
        }
      ]
    }
  },
  "Action": {
    "Allow": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "check-x-api-key-rule"
  }
}

보호 팩(웹 ACL) 생성
보호 팩(웹 ACL) 동작 변경하기
허용 > 차단
'차단'으로 변경 후 , [저장] 버튼 클릭

 

보호 팩(Web ACL) 동작 변경 규칙 추가 후, Web ACL의 기본 동작(Default Action)을 허용(Allow) > 차단(Block)으로 변경하고 저장합니다.

💡 운영 팁: 장애 없는 배포 전략 갑작스러운 차단은 운영 리스크가 큽니다. 처음에는 Web ACL의 Default Action을 Allow로 두어 정상 요청이 잘 식별되는지 모니터링한 후, 검증이 완료되었을 때 '차단'으로 전환하는 것이 안전합니다.


[심화] 브라우저 접속은 허용하고, API(POST)만 검증하고 싶다면?

웹 서비스(UI)를 함께 운영하는 경우, 모든 접속에 키를 요구하면 브라우저 접속이 불가능해집니다. 이럴 땐 "POST 메서드이면서 키가 없는 경우만 차단"하는 설정을 참고자료로 활용해 보세요. (이 경우 Default Action은 Allow로 설정합니다.)

{
    "Name": "enforce-api-key-on-all-post-requests",
    "Priority": 1,
    "Statement": {
        "AndStatement": {
            "Statements": [
                {
                    "ByteMatchStatement": {
                        "SearchString": "POST",
                        "FieldToMatch": {
                            "Method": {}
                        },
                        "TextTransformations": [
                            {
                                "Priority": 0,
                                "Type": "NONE"
                            }
                        ],
                        "PositionalConstraint": "EXACTLY"
                    }
                },
                {
                    "NotStatement": {
                        "Statement": {
                            "ByteMatchStatement": {
                                "SearchString": "YOUR_SECURE_API_KEY",
                                "FieldToMatch": {
                                    "SingleHeader": {
                                        "Name": "x-api-key"
                                    }
                                },
                                "TextTransformations": [
                                    {
                                        "Priority": 0,
                                        "Type": "NONE"
                                    }
                                ],
                                "PositionalConstraint": "EXACTLY"
                            }
                        }
                    }
                }
            ]
        }
    },
    "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "enforce-api-key-on-all-post-requests"
    },
    "Action": {
        "Block": {}
    }
}

 

 


3. Python 호출 로직 샘플 (verify_waf_auth.py)

Secrets Manager에서 키를 동적으로 가져와 API를 호출하는 코드입니다.

import boto3
import json
import requests
from botocore.exceptions import ClientError

def get_secret():
    secret_name = "YOUR_SECRET_NAME" 
    region_name = "ap-northeast-2"
    session = boto3.session.Session()
    client = session.client(service_name='secretsmanager', region_name=region_name)

    try:
        response = client.get_secret_value(SecretId=secret_name)
        secrets_dict = json.loads(response['SecretString'])
        return secrets_dict.get("API_KEY") 
    except ClientError as e:
        print(f"Secret Manager 에러: {e}")
        raise e

def call_protected_api(url, api_key):
    headers = {'x-api-key': api_key, 'Content-Type': 'application/json'}
    try:
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.HTTPError as err:
        print(f"HTTP 호출 에러: {err}")

if __name__ == "__main__":
    TARGET_URL = "http://internal-your-alb-address.ap-northeast-2.elb.amazonaws.com/health"
    retrieved_key = get_secret()
    if retrieved_key:
        result = call_protected_api(TARGET_URL, retrieved_key)
        print(f"호출 결과: {result}")

4. 테스트

구성을 마친 후, WAF 방어선이 정상 작동하는지 확인합니다.

 

실패 예시 (키 미포함 시)

 

curl -I "http://internal-your-alb-address.elb.amazonaws.com/health"
# 결과: HTTP/1.1 403 Forbidden (WAF에서 즉시 차단)

 

 

성공 예시: verify_waf_auth.py 실행 시 정상 데이터 수신

API KEY를 포함한 샘플로직(verify_waf_auth.py) 실행 결과
 

마치며

지금까지 WAF와 Secrets Manager를 조합해 사내 API에 '자물쇠'를 하나 채워봤습니다. 사실 사내 서비스라고 방심하기 쉽지만, 이런 최소한의 인증 장치 하나가 의외로 든든한 버팀목이 되곤 합니다.

기존 코드를 건드리지 않고 인프라 설정만으로 보안을 챙길 수 있다는 점이 이번 작업의 가장 큰 매력이 아닐까 싶네요. 여기에 주기적으로 키만 잘 갈아준다면 그야말로 금상첨화겠쥬~?

 

물론 지금 구성에도 약간의 숙제는 남아있습니다. 키가 변경될 때마다 연계된 컨테이너들이 Secrets Manager의 최신값을 반영하기 위해 재기동되어야 하는 번거로움이 있거든요.

 

이 부분은 향후 애플리케이션이 구동 중에 API 서버를 통해 실시간으로 키를 호출하는 방식으로 개선해본다면, 중단 없는 더 유연한 시스템이 될 것 같습니다 :)

 

공유

댓글

Cloud & AI Engineering | 임승한

design by tokiidesu. powerd by AXZ.