Zettelkasten

Cloud Monitoring consumed_api 메트릭으로 GCP API 키 사용 패턴을 추적한다

·수정 2026.05.13·수정 1

요약

  • consumed_api::serviceruntime.googleapis.com/api/request_count 메트릭은 API 키별·메서드별·응답코드별 호출 시계열을 담고 있다
  • MQL(Monitoring Query Language)로 쿼리하면 호출자 IP만 빼고 거의 모든 정보 추출 가능
  • 윈도우를 1d → 1h → 10m으로 점진 확장하며 폭증 시점을 분 단위까지 좁힐 수 있음

본문

메트릭 구조

리소스 타입: consumed_api
메트릭: serviceruntime.googleapis.com/api/request_count
보관: 약 2년 (730일)

라벨 (모두 필터/group_by 가능):

라벨 종류 예시
resource.credential_id 어떤 키 apikey:8783620425315600619, serviceaccount:115030646863660571478
resource.service 어떤 API generativelanguage.googleapis.com
resource.method 어떤 메서드 google.ai.generativelanguage.v1beta.GenerativeService.GenerateContent
resource.version API 버전 v1, v1beta
resource.location 호출 리전 us-central1, asia-southeast1
resource.project_id 프로젝트 connecting-208115
metric.response_code HTTP 응답 200, 400, 404, 429, 503
metric.response_code_class 응답 클래스 2xx, 4xx, 5xx
metric.protocol 프로토콜 grpc, http, grpc_loas2
metric.grpc_status_code gRPC 상태 0, 8, 14

기본 쿼리 패턴

ACCESS_TOKEN=$(gcloud auth print-access-token)
curl -s "https://monitoring.googleapis.com/v3/projects/PROJECT_ID/timeSeries:query" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "<MQL>"}'

핵심 MQL 템플릿

1) 특정 키의 서비스/메서드별 사용량 (가장 자주 씀)

fetch consumed_api::serviceruntime.googleapis.com/api/request_count
| filter (resource.credential_id == 'apikey:KEY_UID')
| within 90d
| every 1d
| group_by [resource.service, resource.method, metric.response_code], sum(value.request_count)
| top 100

2) 프로젝트 내 특정 API를 호출하는 모든 키 (유출 키 색출)

fetch consumed_api::serviceruntime.googleapis.com/api/request_count
| filter (resource.service == 'generativelanguage.googleapis.com')
| within 730d
| every 30d
| group_by [resource.credential_id], sum(value.request_count)

3) 폭증 시점 분 단위 추적

fetch consumed_api::serviceruntime.googleapis.com/api/request_count
| filter (resource.credential_id == 'apikey:KEY_UID' && resource.service == 'TARGET_SERVICE')
| within 2d
| every 10m
| group_by [metric.response_code], sum(value.request_count)

4) 리전 분포 (공격자 위치 변화 추적)

fetch consumed_api::serviceruntime.googleapis.com/api/request_count
| filter (resource.credential_id == 'apikey:KEY_UID')
| within 120d
| every 1d
| group_by [resource.location], sum(value.request_count)

시간 윈도우 점진 확장 전략

1단계: within 30d, every 1d   → 큰 그림, 폭증 여부
2단계: within 7d,  every 1h   → 폭증 일자 정밀화
3단계: within 2d,  every 10m  → 분 단위 시작/종료 시각
4단계: within 730d, every 30d → 보관 한계까지 봐서 첫 호출 시점 확인

MQL 주의사항

  • every <interval> 의 단위가 align 윈도우보다 작으면 에러: rate(1m) + every 1d 같이 쓰면 "alignment period is 1d"라며 거부됨
  • top N은 group_by 후 합계 상위 N개 시계열만 반환. 누락 우려 있으면 top 200 이상으로
  • filter 안에서 문자열 비교는 == (single equal)이 아니라 == 맞음. &&로 AND, ||로 OR
  • int64Value 또는 doubleValue 둘 다 들어올 수 있어서 응답 파싱 시 양쪽 모두 처리

어뷰징 패턴 시그니처

이 메트릭만으로 식별 가능한 유출 신호:

  1. 메서드 불일치: Firebase Android 키인데 generativelanguage 호출
  2. 시간 패턴 이상: 오랜 기간 0 → 갑자기 폭증 (정상 앱은 점진 증가)
  3. 응답코드 분포: 200 비율 < 80%, 특히 429(rate limit) 폭증 = 한계까지 긁기
  4. 리전 jumping: resource.location 가 자주 바뀌면 VPN/프록시 의심
  5. 정찰 메서드: ListModels, ListCollectionIds, ListFiles 같은 열거 메서드는 정상 SDK가 거의 안 부름

한계

실전 활용 흐름

  1. 유출 의심: 광범위 윈도우(730d, 30d 단위)로 어떤 키가 의심 API 호출하는지 색출
  2. 타임라인 구축: 점진 좁히기로 첫 호출 시점 → 첫 성공 시점 → 폭증 시점 추적
  3. 패턴 분류: 메서드/응답코드/리전 분포로 정상 vs 어뷰징 판별
  4. 차단: gcloud services api-keys update 로 apiTargets 화이트리스트 적용
  5. 차단 후 검증: 동일 쿼리로 어뷰징 트래픽 사라졌는지 / 정상 트래픽 유지되는지 확인

참고