왜 사후 대응이 병목인가
자격증명(API 키, DB 비밀번호, 토큰)이 커밋 히스토리나 로그로 유출되면, 탐지보다 대응 속도가 피해 규모를 결정한다. GitHub Secret Scanning이나 truffleHog가 알림을 주더라도, 사람이 키를 찾아 폐기(revoke)하고 재발급하고 배포까지 손으로 하면 평균 대응 시간(MTTR)이 수 시간을 넘긴다. 공격자에게는 그 사이가 곧 침투 창구다. 핵심은 "누가 유출했나"보다 "얼마나 빨리 무력화했나"이며, 이 단계는 결정론적이라 자동화하기 좋다.
파이프라인 전체 흐름
대응은 네 단계로 나눈다. 탐지 → 격리(폐기) → 순환(재발급/배포) → 감사. 탐지는 도구에 맡기고, 나머지 세 단계를 웹훅 트리거로 이어 붙인다.
| 단계 | 목표 | 수동 리스크 |
|---|---|---|
| 격리 | 유출 키 즉시 폐기 | 지연 시 악용 |
| 순환 | 새 키 발급·시크릿 저장소 갱신 | 서비스 다운 |
| 감사 | 영향 범위 기록·알림 | 재발·규정 위반 |
탐지 훅: 검증된 이벤트만 받기
GitHub Secret Scanning은 유출 의심 키를 파트너 API로 검증한다. 이 웹훅을 수신하되, 서명 검증을 반드시 거쳐 위조 이벤트로 인한 오폐기를 막는다.
@app.post("/webhook/secret-alert")
async def handle_alert(req: Request):
body = await req.body()
sig = req.headers.get("X-Hub-Signature-256", "")
expected = "sha256=" + hmac.new(
SECRET.encode(), body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(sig, expected):
raise HTTPException(401, "invalid signature")
alert = json.loads(body)
if alert["action"] == "created":
enqueue_revoke(alert["alert"]["secret_type"],
alert["alert"]["secret"])
return {"ok": True}
격리와 순환 자동화
폐기는 벤더별 API가 다르므로 타입별 핸들러로 분기한다. 새 키를 발급한 뒤 먼저 시크릿 저장소(Vault, AWS Secrets Manager)를 갱신하고, 배포가 성공한 것을 확인한 다음에 옛 키를 폐기하는 순서가 안전하다. 순서를 뒤집으면 재발급 실패 시 전면 장애가 난다.
rotate_aws_key() {
local user="$1"
# 1) 새 키 발급
new=$(aws iam create-access-key --user-name "$user")
new_id=$(echo "$new" | jq -r '.AccessKey.AccessKeyId')
new_secret=$(echo "$new" | jq -r '.AccessKey.SecretAccessKey')
# 2) 시크릿 저장소 갱신 후 배포
aws secretsmanager put-secret-value \
--secret-id "prod/$user" \
--secret-string "{\"id\":\"$new_id\",\"key\":\"$new_secret\"}"
kubectl rollout restart deployment/api && \
kubectl rollout status deployment/api --timeout=120s || exit 1
# 3) 배포 성공 후에만 구 키 비활성화 → 폐기
aws iam update-access-key --user-name "$user" \
--access-key-id "$OLD_KEY_ID" --status Inactive
}
감사 로그와 영향 범위 기록
모든 대응을 불변 로그로 남긴다. 사후 조사와 규정 대응에 필요하며, 같은 키가 다시 올라오는지 추적하는 근거가 된다.
INSERT INTO credential_incidents
(secret_hash, secret_type, source_repo, detected_at,
revoked_at, rotated_at, status)
VALUES
(encode(digest(:secret, 'sha256'), 'hex'),
:type, :repo, now(), :revoked_at, :rotated_at, 'closed');
원문 키는 절대 저장하지 않고 해시만 남긴다. 로그 자체가 2차 유출원이 되지 않도록 하는 최소 원칙이다.
주의점
첫째, 자동 폐기는 오탐 시 서비스 장애로 직결된다. 프로덕션 키는 폐기 전 자동 비활성화 후 짧은 유예(예: 5분)를 두고, 트래픽 이상이 없을 때만 완전 폐기하는 2단계 방식이 안전하다. 둘째, 대응 파이프라인 자체가 강력한 권한(IAM 삭제, 시크릿 쓰기)을 가지므로, 이 파이프라인의 자격증명이 최우선 보호 대상이다. OIDC 기반 단기 토큰으로 실행하고 장기 키를 두지 않는다. 셋째, git 히스토리에 남은 키는 폐기로만 무력화되며 git filter-repo로 히스토리를 재작성해도 이미 유출된 값은 반드시 순환해야 한다. 커밋 삭제는 대응이 아니라 정리다.