외부에서 API 를 함부로 호출(비용 발생 enqueue 등)하지 못하도록 정적 키 guard 를 추가한다. '키의 존재'가 토글 — 개발(local/dev)은 env 를 비워 개방 모드(기동 시 WARN), prod 만 키를 주입한다(협의 결정). - router/v1/validator/auth.py: X-API-Key 의존성 — secrets.compare_digest 상수시간 비교, 콤마 구분 복수 키(무중단 키 교체), 매 요청 env 조회 (재기동 없이 테스트 가능). /v1 라우터 전체에 적용. - /healthz·/readyz 는 라우터 밖이라 항상 개방(LB 프로브). - negodata lps_sync_service: LPS_API_KEY env 있으면 헤더 자동 첨부(한 곳). - compose(lps-api·negodata-backend) LPS_API_KEY 패스스루 + .env.example. - prod 체크리스트(operations.md): 키 주입 + lps-api 포트 비공개 + 기동 로그 'API guard ON' 확인. api.md 인증 섹션 추가. - 라이브 스모크: 무헤더/오키 401 · 정키 2종 200 · healthz 200 확인. - 테스트 6건 추가, 전체 141 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
29 lines
1.5 KiB
Python
29 lines
1.5 KiB
Python
"""API 키 guard — LPS_API_KEY 가 설정된 경우에만 /v1 라우터 전체를 보호한다.
|
|
|
|
개발(local/dev)은 env 를 비워 **개방 모드**로 쓰고, prod 에서만 키를 주입한다(협의 결정
|
|
2026-07-13). '키의 존재'가 토글이라 APP_ENV=local 고정 운영 전제와 충돌하지 않는다.
|
|
- 키는 콤마 구분 복수 허용 — 무중단 키 교체(새 키 추가 → 호출자 전환 → 옛 키 제거).
|
|
- 비교는 secrets.compare_digest(상수시간) — 타이밍 공격 방지.
|
|
- /healthz·/readyz 는 라우터 밖이라 guard 대상이 아니다(LB/오케스트레이터 프로브).
|
|
- prod 는 여기에 더해 lps-api 포트 비공개(내부 네트워크만)를 권장 — docs/operations.md.
|
|
"""
|
|
|
|
import os
|
|
import secrets
|
|
|
|
from fastapi import Header, HTTPException
|
|
|
|
|
|
def configured_keys() -> set[str]:
|
|
"""유효 API 키 집합. 매 호출 env 를 읽는다 — 프로세스 재기동 없이 테스트 가능, 비용은 무시 수준."""
|
|
return {k.strip() for k in os.environ.get("LPS_API_KEY", "").split(",") if k.strip()}
|
|
|
|
|
|
async def require_api_key(x_api_key: str | None = Header(None, alias="X-API-Key")):
|
|
"""/v1 공통 의존성. 키 미설정=개방 모드(무검증), 설정 시 X-API-Key 불일치는 401."""
|
|
keys = configured_keys()
|
|
if not keys:
|
|
return
|
|
if not x_api_key or not any(secrets.compare_digest(x_api_key, k) for k in keys):
|
|
raise HTTPException(status_code=401, detail="invalid or missing X-API-Key")
|