API 키 유출 방지 체크리스트: 깃허브·앱·로그 점검 10가지
API 키 유출이 주로 생기는 Git 히스토리, 모바일 앱 번들, 에러 로그를 짚고, 출시 전에 점검할 10가지와 유출 직후 폐기·교체 순서를 체크리스트로 정리했습니다.
API 키는 대부분 세 군데로 샙니다. Git 저장소(특히 커밋 히스토리), 사용자에게 배포되는 앱·프론트엔드 번들, 에러 로그와 CI 로그입니다. 실제로 유출됐다면 히스토리 정리보다 키 폐기와 재발급을 먼저 하고, 그다음 해당 키의 사용 기록을 확인하는 순서로 움직여야 합니다. 코드에서 키를 지우는 것만으로는 이미 복제된 사본이나 포크에 남은 기록까지 없앨 수 없기 때문입니다.
평소에 할 일도 정리하면 단순합니다. 키를 발급할 때 권한을 좁히고, 저장 위치를 한곳으로 모으고, 노출 경로마다 자동 탐지를 걸어 두는 것입니다. 이 글은 특정 프레임워크의 환경변수 설정법이 아니라 키 수명주기 전체(발급 → 저장 → 노출 경로 → 탐지 → 폐기·교체)를 기준으로, 출시 전 점검 10가지와 유출 직후 대응 순서를 바로 쓸 수 있는 형태로 묶었습니다.
OWASP Secrets Management Cheat Sheet도 비슷한 관점을 제시합니다. 시크릿의 저장·발급·감사·교체를 중앙에서 관리해야 유출 범위를 줄일 수 있고, 소스 코드에 평문으로 하드코딩하는 것을 대표적인 문제로 꼽습니다.
시크릿 수명주기로 보면 점검 포인트가 보인다
"키가 새지 않게 조심하자"는 다짐만으로는 점검이 되지 않습니다. 단계별로 질문을 나누면 빠진 곳이 보입니다.
- 발급: 이 키는 필요한 권한만 갖고 있는가? 누가, 어떤 용도로 쓰는가?
- 저장: 키의 원본은 어디에 있는가? 사본은 몇 군데에 퍼져 있는가?
- 노출 경로: 코드, 히스토리, 빌드 결과물, 로그 중 키가 흘러갈 수 있는 곳은 어디인가?
- 탐지: 누군가 실수로 커밋하면 누가, 언제 알게 되는가?
- 폐기·교체: 지금 이 키를 끄면 무엇이 멈추는가? 교체 절차가 문서로 있는가?
아래 10가지 항목은 이 순서를 그대로 따릅니다.
1~3. 발급과 저장: 새기 전에 피해 범위부터 줄인다
1. 키마다 권한과 사용처를 제한했는가
유출은 언젠가 일어날 수 있다고 보고, 유출됐을 때 할 수 있는 일을 줄여 두는 것이 첫 단계입니다. Google Cloud 문서도 API 키는 기본적으로 제한이 없는 상태로 만들어지므로, 프로덕션에서는 애플리케이션 제한(허용 웹사이트, IP, 앱)과 API 제한(호출 가능한 API 목록)을 함께 걸라고 권장합니다.
판단 기준은 이렇습니다.
- 읽기만 필요한 곳에 쓰기 권한 키를 쓰고 있지 않은가
- 결제 취소, 사용자 삭제처럼 영향이 큰 권한이 일반 키에 포함돼 있지 않은가
- 서버 키에 IP 제한, 앱 키에 패키지명·번들 ID 제한을 걸 수 있는데 비워 두지 않았는가
2. 키를 용도·환경별로 분리했는가
개발·스테이징·운영이 같은 키를 쓰면, 개발자 노트북에서 새는 키가 곧 운영 키가 됩니다. 팀원별·서비스별로 키를 나누면 유출 시 어느 키를 끄면 되는지 바로 알 수 있고, 하나를 폐기해도 전체 서비스가 멈추지 않습니다. Google도 팀원과 애플리케이션마다 별도 키를 쓰고 사용량을 모니터링하라고 안내합니다.
3. 원본 저장 위치가 한곳인가
키가 .env 파일, 사내 메신저, 노션 문서, CI 설정, 개인 메모장에 흩어져 있으면 교체할 때 반드시 하나를 빠뜨립니다. 원본은 클라우드의 시크릿 매니저나 Vault 같은 전용 저장소 한곳에 두고, 애플리케이션은 실행 시점에 거기서 읽어 오게 하는 구조가 기본입니다. 메신저로 키를 주고받는 관행이 있다면 이 항목에서 함께 정리합니다.
4~5. Git 저장소와 히스토리: 지운 커밋도 남아 있다
4. 현재 코드뿐 아니라 히스토리 전체를 스캔했는가
가장 흔한 착각은 "최신 커밋에서 지웠으니 괜찮다"입니다. Git은 과거 커밋을 모두 보관하므로, 3개월 전에 한 번 커밋됐다가 다음 날 지워진 키도 히스토리에는 그대로 남아 있습니다.
// ❌ 취약: 키를 코드에 직접 작성 (나중에 지워도 히스토리에 남음)
const client = new PaymentClient({
secretKey: "sk_live_EXAMPLE_DO_NOT_USE_1234",
});
// ✅ 개선: 실행 환경에서 주입, 값이 없으면 기동 실패
const secretKey = process.env.PAYMENT_SECRET_KEY;
if (!secretKey) throw new Error("PAYMENT_SECRET_KEY is not set");
const client = new PaymentClient({ secretKey });
출시 전에는 gitleaks 같은 도구로 전체 히스토리를 한 번 스캔해 두는 것이 좋습니다. 공개 전환을 앞둔 저장소라면 특히 필수입니다.
5. .gitignore와 예시 파일 규칙이 있는가
.env, *.pem, serviceAccount*.json, google-services.json처럼 시크릿이 들어갈 수 있는 파일은 저장소 생성 시점부터 .gitignore에 넣습니다. 대신 값이 비어 있는 .env.example을 커밋해 어떤 변수가 필요한지만 공유합니다.
# .gitignore
.env
.env.*
!.env.example
*.pem
*.p12
.gitignore는 이미 추적 중인 파일에는 효과가 없다는 점도 기억해야 합니다. 한 번 커밋된 파일은 git rm --cached로 추적을 해제해야 하고, 이미 들어간 키는 4번 항목처럼 히스토리 문제로 봐야 합니다.
6~7. 모바일 앱·프론트엔드 번들: 배포한 순간 공개된 것으로 본다
6. 클라이언트에 비밀 키가 들어가 있지 않은가
APK, IPA, 웹 번들은 사용자 기기에 그대로 내려갑니다. OWASP MASWE-0005(API Keys Hardcoded in the App Package)는 앱 소스 코드나 설정·매니페스트·리소스 파일에 포함된 키를 리버스 엔지니어링으로 쉽게 추출할 수 있다고 설명하고, 종량제 API(예: AI·ML 서비스) 오남용에 따른 금전 피해를 대표적인 영향으로 꼽습니다. 난독화는 추출에 드는 시간을 늘릴 뿐이고, 올바른 키 관리를 대신할 수 없다는 점도 함께 짚습니다.
해결책은 비밀 키가 필요한 호출을 서버로 옮기는 것입니다.
// ❌ 취약: 앱/브라우저에서 외부 AI API를 직접 호출
const res = await fetch("https://api.example.com/v1/generate", {
headers: { Authorization: "Bearer sk-EXAMPLE-SECRET" },
body: JSON.stringify({ prompt }),
method: "POST",
});
// ✅ 개선: 자체 서버가 사용자 인증·요청 제한 후 대신 호출
const res = await fetch("https://app.example.com/api/generate", {
headers: { Authorization: `Bearer ${userSessionToken}` },
body: JSON.stringify({ prompt }),
method: "POST",
});
서버 쪽 프록시에는 사용자 인증, 사용자별 호출 한도, 허용된 요청 형태 검사를 함께 둬야 합니다. 그렇지 않으면 키만 숨겼을 뿐 프록시 자체가 공짜 API 창구가 됩니다.
7. "공개용 키"는 공개를 전제로 설계했는가
지도 SDK 키, Firebase 설정값, Supabase anon 키처럼 클라이언트에 포함되도록 설계된 키도 있습니다. 이런 키는 숨기는 것보다 노출돼도 안전하게 만드는 것이 핵심입니다. 도메인·앱 제한을 걸고, 데이터 접근은 서버 측 권한 정책으로 막아야 합니다. Supabase를 쓴다면 Supabase RLS 보안 가이드에서 anon 키 노출을 전제로 한 정책 설정을 참고할 수 있습니다.
빌드 결과물에 무엇이 들어갔는지는 직접 확인하는 것이 가장 확실합니다. 웹이라면 배포된 JS 번들을, 앱이라면 릴리스 빌드를 대상으로 sk_, secret, private_key 같은 문자열을 검색해 보세요. 프레임워크마다 클라이언트에 노출되는 환경변수 접두사 규칙이 있으니, 비밀 값에 그 접두사가 붙어 있지 않은지도 함께 봅니다.
8~9. 에러 로그와 CI 로그: 가장 늦게 발견되는 경로
8. 애플리케이션 로그에 토큰·헤더가 찍히지 않는가
OWASP Logging Cheat Sheet는 로그에 직접 기록하지 말아야 할 데이터로 비밀번호, 세션 식별자, 액세스 토큰, 데이터베이스 연결 문자열, 암호화 키 등을 들고, 필요하면 제거·마스킹·해시 처리하라고 권합니다. 문제는 개발자가 의도해서가 아니라 디버깅용 전체 요청 덤프나 외부 API 에러 객체 통째 출력으로 새는 경우가 많다는 점입니다.
// ❌ 취약: 요청 헤더와 에러 객체를 통째로 기록
logger.error("upstream failed", { headers: req.headers, error: err });
// err.config.headers.Authorization 에 키가 포함될 수 있음
// ✅ 개선: 필요한 필드만 고르고 민감 키는 마스킹
const SENSITIVE = ["authorization", "x-api-key", "cookie"];
const safeHeaders = Object.fromEntries(
Object.entries(req.headers).map(([k, v]) =>
SENSITIVE.includes(k.toLowerCase()) ? [k, "[REDACTED]"] : [k, v]
)
);
logger.error("upstream failed", {
headers: safeHeaders,
status: err.response?.status,
message: err.message,
});
같은 이유로 API 키를 URL 쿼리 파라미터로 보내는 방식은 피합니다. 쿼리 문자열은 웹 서버 액세스 로그, 프록시 로그, 브라우저 기록에 남기 쉽습니다. Google도 키를 URL 대신 헤더로 전달하라고 안내합니다. 로그 수집 서비스(APM, 에러 트래커)에 이미 쌓인 데이터도 점검 대상입니다.
9. CI 로그의 마스킹을 맹신하지 않는가
GitHub Actions는 등록된 시크릿 값을 로그에서 가려 주지만, 공식 문서는 이것이 최선의 노력(best-effort)일 뿐 보장이 아니라고 명시합니다. 정확히 일치하는 값과 일부 인코딩만 가려지기 때문에 다음을 지켜야 합니다.
- JSON처럼 구조화된 데이터를 하나의 시크릿으로 저장하지 말고 값별로 나눈다
- 시크릿으로 만든 파생 값(서명된 JWT 등)도 별도로 마스킹 등록한다
- 정상·비정상 입력으로 테스트한 뒤 실행 로그를 직접 열어 확인한다
- 워크플로 기본 토큰 권한은 읽기 전용으로 두고, 필요한 작업에서만 올린다
저장소에 쓰기 권한이 있는 사람은 워크플로를 통해 시크릿을 사용할 수 있다는 점도 권한 설계 시 고려합니다.
10. secret scanning을 커밋 전과 푸시 시점에 건다
사람의 주의력만으로는 막기 어렵기 때문에 자동 탐지를 두 겹으로 겁니다.
① 로컬 커밋 전(pre-commit): gitleaks는 pre-commit 훅으로 붙일 수 있습니다.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: vX.Y.Z # 최신 릴리스 태그로 고정
hooks:
- id: gitleaks
pre-commit install로 활성화하고, pre-commit autoupdate로 버전을 갱신합니다. 다만 로컬 훅은 개발자가 건너뛸 수 있으니 이것만으로 끝내지 않습니다.
② 원격 저장소(push protection): GitHub의 secret scanning과 push protection은 시크릿이 포함된 푸시를 차단하고, 누군가 차단을 우회하면 알림을 생성합니다. 공개 저장소와 GitHub Secret Protection이 활성화된 조직 저장소에서 사용할 수 있으니 플랜과 설정을 확인해 보세요. 우회 알림이 왔을 때 누가 확인하고 판단하는지 담당자를 정해 두는 것까지가 이 항목입니다.
유출 직후 대응 순서: 폐기가 먼저, 정리는 나중
키 유출을 발견했다면 아래 순서로 움직입니다. 핵심은 "지우기"보다 "끄기"가 먼저라는 점입니다.
- 폐기·교체: 새 키를 발급해 서비스에 반영하고, 유출된 키를 즉시 비활성화합니다. 교체 중 장애가 걱정된다면 권한을 먼저 최소화하거나 IP 제한을 거는 것으로 시간을 법니다.
- 사용 기록 확인: 해당 키의 호출 로그, 과금 내역, 클라우드 감사 로그에서 노출 시점 이후 비정상 사용이 있었는지 확인합니다.
- 영향 범위 판단: 그 키로 접근 가능한 데이터에 개인정보가 포함됐다면 법적 신고 판단이 필요할 수 있습니다. 절차는 개인정보 유출 신고 절차에서 시간대별로 정리해 두었습니다.
- 히스토리 정리: GitHub 문서는 git-filter-repo(2.47 이상,
--sensitive-data-removal옵션)로 로컬 히스토리를 다시 쓴 뒤 강제 푸시하는 절차를 안내합니다.
# 예시: 특정 파일을 전체 히스토리에서 제거 (백업 후 진행)
git filter-repo --sensitive-data-removal --invert-paths --path config/secrets.json
같은 문서는 히스토리를 다시 써도 다른 사람의 클론, 포크, 해당 커밋을 참조하는 PR에는 남을 수 있다고 설명합니다. 그래서 히스토리 정리는 재발 방지와 정돈을 위한 단계이지, 1번 폐기를 대신하지 못합니다.
- 재발 방지: 원인이 된 경로(1~10번 중 어디였는지)를 기록하고, 해당 항목의 자동 탐지를 추가합니다. 교체 과정에서 키가 몇 군데에 퍼져 있었는지가 드러났다면 3번 항목을 다시 정리할 때입니다.
자주 묻는 질문
비공개(private) 저장소라면 키를 커밋해도 괜찮지 않나요?
권하지 않습니다. 비공개 저장소도 접근 권한이 있는 모든 사람, 연동된 외부 서비스, 개발자 노트북의 클론으로 키가 퍼집니다. 나중에 저장소를 공개로 바꾸거나 외주사에 권한을 주는 순간 과거 히스토리까지 함께 넘어갑니다.
키를 난독화하거나 암호화해서 앱에 넣으면 안전한가요?
추출을 어렵게 만들 수는 있지만 막지는 못합니다. 앱이 실행 중에 키를 복호화해 써야 하므로 결국 기기 안에서 평문이 됩니다. 비밀이어야 하는 키는 서버로 옮기고, 클라이언트에는 노출돼도 괜찮도록 제한된 키만 둡니다.
키 교체는 얼마나 자주 해야 하나요?
일괄적인 정답보다 교체가 가능한 상태인지가 더 중요합니다. 교체 절차를 문서로 만들고 한 번 실제로 해 보면, 유출 시 몇 분 안에 대응할 수 있는지 알 수 있습니다. 퇴사자 발생, 외주 계약 종료, 의심 정황이 있을 때는 주기와 관계없이 교체합니다.
정리
- API 키 유출은 주로 Git 히스토리, 앱·프론트엔드 번들, 에러·CI 로그에서 일어납니다.
- 발급할 때 권한과 사용처를 좁히고, 원본 저장 위치를 한곳으로 모으면 유출 피해와 교체 비용이 함께 줄어듭니다.
- secret scanning은 커밋 전(pre-commit)과 푸시 시점(push protection) 두 겹으로 겁니다.
- 유출을 발견하면 히스토리 정리보다 키 폐기·교체와 사용 기록 확인이 먼저입니다. 직접 점검이 어렵다면 무료 분석 신청으로 현재 상태를 함께 확인해 볼 수 있습니다.
참고 자료
- OWASP Secrets Management Cheat Sheet
- OWASP MASWE-0005: API Keys Hardcoded in the App Package
- GitHub Docs: Working with secret scanning and push protection
- GitHub Docs: Removing sensitive data from a repository
- GitHub Docs: Security hardening for GitHub Actions
- Google Cloud: Best practices for managing API keys
- Gitleaks README (pre-commit 설정)
함께 읽으면 좋은 글
AWS 보안 설정 가이드: 초기 스타트업이 하루에 잠글 5가지
AWS 보안 설정을 루트 계정, IAM, S3, 보안 그룹, 감사 로그 순으로 정리했다. 소규모 팀이 하루 안에 점검하고 고칠 수 있는 명령어와 수정 전후 예시, 판단 기준을 담았다.
취약점 점검 리포트 읽는 법: 위험도 해석부터 조치 우선순위까지
취약점 점검 리포트의 위험도를 서비스 맥락으로 해석하고, 조치 순서 결정, 오탐 확인, 재점검 요청, 수정 완료 근거 정리까지 개발팀의 처리 흐름을 안내합니다.
개인정보 유출 사고 사례: 개인정보위 처분에서 반복된 원인 5가지
개인정보 유출 사고 사례를 개인정보위 처분 기준으로 분석해 반복된 원인 5가지를 정리하고, 개발팀이 바로 쓸 코드 예시와 점검 항목으로 바꿔 줍니다.