어느 날 메일함에 이런 제목의 메일이 와 있었다.

GitHub 저장소에 시크릿이 노출되었습니다

가슴이 철렁했다. 그런데 발신 도메인을 보니 getgitguardian.com이었다. 내가 아는 GitGuardian의 공식 도메인과는 철자가 달랐다. 보안 알림을 가장한 피싱은 흔한 수법이라, 일단 메일 안의 링크는 누르지 않기로 했다.

이 글은 그다음에 벌어진 일을 정리한 점검 기록이다. 결론부터 말하면 이렇다.

"노출됐다"고 다 위험한 건 아니다. 그리고 진짜 위험은 경고가 가리킨 곳이 아니라 다른 곳에 있었다.


1. 메일은 믿지 말고, 저장소를 직접 확인한다

보안 경고 메일을 받았을 때 가장 먼저 할 일은 메일 속 링크를 누르지 않는 것이다. 링크는 로그인 페이지를 흉내 낸 피싱 사이트로 이어질 수 있고, 거기서 계정 정보를 넣는 순간 진짜 사고가 된다.

대신 이렇게 확인했다.

  • 브라우저에 주소를 직접 입력해 GitHub에 접속한다.
  • 저장소의 Security → Secret scanning 탭에 알림이 실제로 있는지 본다.
  • 메일의 진위와 상관없이, 우리 저장소에 민감한 값이 올라가 있는지는 우리가 직접 점검한다.

메일이 가짜여도 점검은 해볼 가치가 있다. 메일이 진짜여도 마찬가지다. 어느 쪽이든 "내 저장소에 뭐가 올라가 있는지"가 핵심이기 때문이다.

2. 점검 결과: 서버 시크릿은 깨끗했다

가장 걱정했던 건 서버 쪽 값이었다. DB 비밀번호, JWT 서명 키, 외부 서비스 API 시크릿 같은 것들이다. 이런 값이 공개 저장소에 올라갔다면 즉시 폐기하고 재발급해야 한다.

저장소를 훑어보니 다행히 서버 시크릿은 .env로만 관리되고 있었고, 커밋 이력에서도 발견되지 않았다.

그런데 알림이 가리킨 파일은 따로 있었다. 모바일 앱 쪽의 google-services.json이었다.

3. 범인은 google-services.json 안의 Firebase API 키

안드로이드 앱에 Firebase를 붙이면 google-services.json 파일이 생긴다. 이 파일 안에는 current_key라는 이름으로 Google API 키가 들어 있다. 이 파일은 관례적으로 앱 저장소에 커밋되는 경우가 많고, 그래서 시크릿 스캐너가 단골로 잡아낸다.

여기서 중요한 사실이 하나 있다.

Firebase 클라이언트 API 키는 서버 시크릿과 성격이 다르다. 이 키는 "누구의 프로젝트인지 식별"하는 용도에 가깝고, 앱 안에 그대로 들어가 배포된다. APK를 풀어보면 누구나 꺼내 볼 수 있다. Firebase 공식 문서도 이 키를 전통적인 의미의 비밀로 취급하지 않는다.

그렇다고 방치해도 된다는 뜻은 아니다. 점검해 보니 이 키의 애플리케이션 제한이 "없음" 상태였다.

상태 의미
애플리케이션 제한: 없음 키 값만 알면 어디서든 이 프로젝트의 API를 호출할 수 있다
애플리케이션 제한: Android 앱 지정한 패키지명 + SHA-1 인증서 지문을 가진 앱의 요청만 허용한다

저장소가 공개든 아니든, 키가 한 번 노출되면 누가 가져갔는지 알 수 없다. 제한이 없다면 그 키로 우리 프로젝트의 할당량과 비용을 쓰는 요청이 날아올 수 있다. 진짜 문제는 "키가 노출된 것"이 아니라 "노출돼도 아무 방어가 없는 상태"였다.

4. 조치 ① GCP에서 키에 앱 제한 걸기

Google Cloud 콘솔에서 해당 프로젝트로 들어가 설정했다.

  1. API 및 서비스 → 사용자 인증 정보로 이동한다.
  2. 문제가 된 API 키를 연다.
  3. 애플리케이션 제한사항에서 Android 앱을 선택한다.
  4. 패키지 이름과 SHA-1 인증서 지문을 추가한다.
  5. 가능하면 API 제한사항도 함께 걸어 앱이 실제로 쓰는 API만 허용한다.

SHA-1은 아래 명령으로 확인할 수 있다.

 
 
 
 
 
 
 
 
# 디버그 키스토어
keytool -list -v -keystore ~/.android/debug.keystore \
-alias androiddebugkey -storepass android -keypass android
 
# 안드로이드 프로젝트에서 Gradle로 확인
./gradlew signingReport
 
 

여기서 놓치기 쉬운 함정이 있다.

  • 디버그용과 릴리스용 SHA-1은 서로 다르다. 둘 다 등록해야 개발 중에도, 배포 후에도 정상 동작한다.
  • Play App Signing을 쓴다면 스토어에 올라간 앱은 Google이 관리하는 키로 다시 서명된다. Play Console의 앱 서명 메뉴에 있는 SHA-1도 등록해야 한다.
  • 제한이 반영되기까지 몇 분 걸릴 수 있다. 설정 직후 앱에서 푸시나 인증이 실패한다면 지문 누락을 먼저 의심한다.

5. 조치 ② GitHub 알림은 Won't fix로 닫는다

GitHub Secret scanning 알림이 열려 있었다. 키는 앱에 포함되어 배포되는 값이고, 이미 앱 제한으로 방어를 걸었으므로 폐기(revoke)할 필요가 없다고 판단했다. 알림은 "Won't fix"로 닫았다.

중요한 점은 순서다. 앱 제한을 먼저 걸고, 그다음에 닫았다. 방어 없이 알림만 닫으면 위험은 그대로 남은 채 경고만 사라진다.

닫을 때는 사유를 남기는 편이 좋다. 나중에 같은 알림을 본 팀원이 "이거 왜 닫혀 있지?" 하고 다시 헤매지 않는다.

6. 조치 ③ 다음번을 위해 Secret Scanning과 Push Protection 켜기

이번 일의 가장 큰 수확은 이거였다. 저장소 설정에서 두 기능을 켰다.

  • Secret scanning: 저장소에 올라간 시크릿 패턴을 탐지해 알려준다.
  • Push protection: 시크릿이 포함된 커밋이 push되는 순간 차단한다.

알림은 이미 올라간 뒤에 알려주지만, Push protection은 올라가기 전에 막는다. 한 번 공개된 값은 커밋 이력에서 지워도 이미 복사됐을 수 있으니, 사고를 막는 쪽이 훨씬 싸다.

7. 한 가지 더: 앱 제한은 만능이 아니다

솔직히 짚고 넘어갈 게 있다. 패키지명과 SHA-1 제한은 값을 아는 누군가가 요청 헤더를 흉내 내면 우회될 여지가 있다. 그래서 이 제한을 "완벽한 잠금"으로 믿으면 안 된다. 어디까지나 아무 방어도 없는 상태에서 한 겹을 올린 것이다.

진짜 데이터를 지키는 건 다른 층이다.

  • Firebase 쓰는 경우 Security Rules를 제대로 작성한다.
  • App Check로 정상 앱에서 온 요청인지 검증한다.
  • 서버 시크릿은 클라이언트에 절대 넣지 않는다.

키 하나를 가리는 것보다, 키가 털려도 데이터가 안전하게 설계돼 있는지가 더 중요하다.


정리

확인한 것 결과
메일 속 링크 누르지 않고 GitHub에 직접 접속해 확인
서버 시크릿 .env 관리, 노출 없음
진짜 문제 Firebase Android API 키의 앱 제한 "없음"
조치 패키지명 + SHA-1 제한, 알림 Won't fix 처리
재발 방지 Secret scanning + Push protection 활성화

이 글의 핵심 메시지

  1. 노출됐다고 다 위험한 건 아니다. 식별용으로 설계된 키까지 전부 폐기하며 허둥댈 필요는 없다.
  2. 하지만 "안전한 값"이라는 이유로 방치하면 안 된다. 노출돼도 괜찮으려면 방어 설정이 있어야 한다.
  3. 진짜 위험은 경고가 가리킨 곳이 아니라, 경고를 계기로 들여다본 곳에 있다. 알림 하나가 점검의 출발점이 된다.
  4. 의심스러운 보안 메일은 링크 대신 직접 접속으로 확인한다.

혹시 지금 google-services.json이 커밋된 저장소가 있다면, 오늘 GCP 콘솔에서 해당 키의 애플리케이션 제한이 걸려 있는지 한 번만 열어보길 권한다. 5분이면 된다.