본문

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로 두어 정상 요청이 잘 식별되는지 모니터링한 후, 검증이 완료되었을 때 '차단'으로 전환하는 것이 안전합니다.

 

👉 GET 방식은 허용하고, POST 방식만 검증하고 싶다면?

더보기

웹 서비스(UI)를 함께 운영하는 환경이라면 고민이 더 깊어집니다. 모든 접속에 API Key를 요구할 경우, 정작 일반 사용자가 브라우저로 접속하는 것조차 불가능해지기 때문입니다.

 

이럴 때는 "데이터 변경이 일어나는 POST 메서드이면서, 동시에 키가 없는 요청만 골라 차단"하는 전략이 효율적입니다. 아래 JSON 설정을 참고해 보세요. (이 경우, 인증이 필요 없는 일반 GET 요청들을 통과시키기 위해 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": {}
    }
}

 

 

👉 [심화] 특정 경로(예: 로그인, 헬스체크)를 제외한 모든 POST 요청 방어하기

더보기

전체 POST 요청을 방어하고 싶지만, 이미 다른 인증 체계를 사용 중이거나 공개가 필요한 "/api/*" 경로는 자유롭게 열어두고 싶을 때 아주 유용한 설정입니다. 특정 경로를 제외한 모든 데이터 변경(POST) 요청에 대해서만 API Key 검증을 강제하는 로직을 확인해 보세요 :)

 

💡 운영 팁: 이 규칙은 조건에 맞는 '나쁜 요청'만 골라 막는 방식이므로, Web ACL 전체의 Default Action은 Allow로 설정해야 일반적인 페이지 접속이 차단되지 않습니다.

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

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.