사전 비밀값 커밋
문제

비밀값 커밋

gabury1고친 사람 github-actions[bot]

비밀값 커밋은 비밀번호처럼 남이 알면 안 되는 값이 코드 기록에 실려 새어 나가는 문제입니다. 버전관리 도구는 바꾼 코드를 기록에 새겨 둡니다. 이 일을 커밋이라고 합니다. 이 문제는 코드와 함께 비밀값까지 커밋해서 생깁니다. 파일에서 지운 뒤에도 지난 커밋에는 남습니다. 지우는 것으로는 못 막습니다. 값 자체를 새로 바꿔야 합니다.

쉽고 빠른 이해

무슨 일인가 — 남이 알면 안 되는 값이 코드 기록에 남는 일입니다. 데이터베이스 비밀번호를 설정 파일에 적은 채 커밋하면 그 코드를 받는 사람 누구나 비밀번호를 봅니다.

왜 곤란한가 — 그 값을 아는 사람은 주인과 같은 권한을 얻습니다. 지우는 커밋을 하나 더 만들어도 옛 커밋에서 값을 꺼낼 수 있습니다. 코드를 받아 간 사본마다 그 값이 들어 있습니다.

어떻게 터지나

  1. 비밀번호가 적힌 파일을 코드와 함께 커밋합니다
  2. 그 커밋을 여럿이 함께 쓰는 서버에 올립니다
  3. 동료와 빌드 서버가 코드를 받아 갈 때마다 비밀번호도 따라갑니다

막는 대가 — 비밀값을 코드 밖에 두면 기록에 안 실립니다. 대신 프로그램을 띄울 때 값을 넣어 줄 길을 따로 마련해야 합니다. 그 길이 없으면 새로 받은 코드가 바로 돌지 않습니다.

상세

이 절은 비밀값이 어떻게 버전관리 기록에 들어가는지, 들어간 뒤에는 왜 쉽게 안 빠지는지를 다룹니다. 설정 파일 하나를 커밋했다가 지우는 장면을 따라가며 값이 어디에 남는지 봅니다.

버전관리는 파일이 바뀌어 온 기록을 남기는 도구입니다. 바꾼 내용을 기록에 한 번 새기는 일이 커밋입니다. 파일을 고쳐 커밋하면 기록에 한 줄이 쌓입니다.

이렇게 커밋이 쌓인 기록을 이력이라고 부릅니다. 이력을 담아 두는 폴더가 저장소입니다. 비밀값이 끝내 남는 곳이 바로 이 저장소의 이력입니다.

비밀값

비밀값은 알아낸 사람이 곧 권한을 얻는 값입니다. 데이터베이스 비밀번호가 대표입니다. 이 값을 아는 사람은 누구든 그 데이터베이스에 들어가 읽고 지울 수 있습니다.

이처럼 신원을 증명하는 값을 묶어 자격증명이라고 부릅니다. 비밀번호 말고도 커밋에 자주 섞이는 자격증명이 몇 가지 있습니다.

비밀값은 복사해 가도 원래 값이 줄지 않습니다. 누가 가져갔는지 값만 봐서는 알 수 없다는 뜻입니다. 한 번 남의 눈에 닿았을 수 있으면 새었다고 보고 다뤄야 합니다.

코드에 들어가는 경로

가장 흔한 경로는 값을 코드에 바로 적는 것입니다. 이를 하드코딩이라고 부릅니다. 처음 기능을 짤 때는 동작부터 확인하려고 이렇게 적기 쉽습니다.

Python
DB_HOST = "db.example.com"
DB_PW = "hunter2"   # 비밀값이 코드에 박힘

이 파일을 그대로 커밋하면 비밀번호가 이력에 들어갑니다. 코드 리뷰에서도 눈에 잘 안 띕니다. 리뷰하는 사람은 대개 로직을 보지 값 한 줄을 보지 않습니다.

값을 코드에서 빼 설정 파일로 옮겨도 사정은 같을 수 있습니다. 많이 쓰는 꼴이 .env 파일입니다. 프로그램이 실행될 때 받을 이름과 값을 한 줄씩 적어 두는 파일입니다. 이 파일을 저장소가 추적하면 코드에 적은 것과 다르지 않습니다.

git add . 처럼 폴더 안 파일을 한꺼번에 담는 명령이 이 실수를 부릅니다. 무엇이 담겼는지 안 보고 커밋하면 .env 도 함께 들어갑니다. 디버깅하며 남긴 로그 파일이나 값이 박힌 테스트 데이터도 같은 길로 들어갑니다.

지워도 이력에 남는다

실수로 커밋한 .env 를 지우는 장면을 따라갑니다. 아래 코드에서 git rm --cached 는 파일을 내 폴더에 둔 채 저장소의 추적만 끊는 명령입니다. git log -p 는 커밋마다 바뀐 내용을 보여 주는 명령입니다.

터미널
git add .              # .env 도 담긴다
git commit -m "설정"
git rm --cached .env   # 지금 판에서만 뺀다
git commit -m "빼기"
git log -p             # 옛 값이 보인다

마지막 커밋에는 .env 가 없습니다. 그래도 그 앞의 "설정" 커밋은 파일을 품은 채 남아 있습니다. git log -p 를 치면 비밀번호가 적힌 줄이 그대로 나옵니다.

커밋은 앞 커밋 위에 쌓입니다. 새 커밋을 만들어도 옛 커밋은 고쳐지지 않습니다. 지우는 커밋은 지금 판에서 파일을 뺄 뿐입니다.

사본마다 퍼진다

한 번 올린 커밋은 받아 가는 사람마다 한 벌씩 복사됩니다. Git 같은 분산 버전관리 도구는 저장소를 받아 갈 때 이력 전체를 가져갑니다. 지금 판만 받는 것이 아닙니다. 비밀번호가 든 옛 커밋도 받는 사람마다 따라갑니다.

받아 가는 곳은 여럿이 함께 쓰는 서버의 저장소입니다. 이를 원격 저장소라고 합니다. 팀은 저마다의 커밋을 이곳에 모읍니다. 내 커밋을 거기 올리는 일이 푸시입니다.

원격 저장소를 통째로 받아 오는 일은 클론입니다. 새로 합류한 동료가 처음 하는 일이 대개 이것입니다. 클론한 사람의 컴퓨터에는 이력 전체가 한 벌 생깁니다.

받아 가는 쪽은 사람만이 아닙니다. 지속적 통합(CI, Continuous Integration)은 코드가 올라올 때마다 서버가 저장소를 받아 빌드와 테스트를 돌리는 일입니다. 이 빌드 서버도 사본을 하나씩 갖습니다.

저장소를 통째로 복사해 따로 키우는 포크도 사본입니다. 만일을 위해 떠 둔 백업도 마찬가지입니다.

아래 그림은 한 번 푸시한 커밋이 어디까지 번지는지 보입니다. 원격 저장소에서 나중에 지워도 화살표 끝의 사본에는 비밀번호가 그대로 남습니다.

flowchart TD
    A["내 커밋 · 비밀번호 포함"] -->|푸시| R["원격 저장소 · 나중에 지움"]
    R -->|클론| B["동료의 사본 · 비밀번호 남음"]
    R -->|클론| C["빌드 서버의 사본 · 비밀번호 남음"]
    R --> D["포크 · 비밀번호 남음"]
    R --> E["백업 · 비밀번호 남음"]

저장소가 공개돼 있으면 받아 가는 사람을 셀 수도 없습니다. 공개 저장소를 계속 훑으며 키처럼 생긴 문자열을 찾는 프로그램도 돕니다. 비공개 저장소라도 접근 권한이 있는 사람은 모두 값을 봅니다. 나중에 저장소를 공개로 돌리면 옛 이력도 함께 공개됩니다.

이력을 고쳐 써도 끝나지 않는다

이력에서까지 값을 걷어내려면 그 값이 든 커밋들을 모두 다시 만들어야 합니다. 커밋을 다시 만들면 커밋마다 붙은 식별 번호가 바뀝니다. 원격 저장소의 이력과 어긋나므로 고친 이력을 억지로 덮어 올려야 합니다. 이 덮어쓰기는 이력 강제 덮어쓰기에서 다룹니다.

덮어써도 이미 받아 간 사본은 바뀌지 않습니다. 동료의 노트북과 빌드 서버와 포크에는 옛 커밋이 그대로 있습니다. 그래서 이력 정리는 값을 덜 퍼지게 할 뿐입니다. 이미 새어 나간 값을 되돌리지는 못합니다.

수습은 값을 바꾸는 데서 시작합니다. 순서는 대개 이렇습니다.

  1. 새 값을 만들어 쓰는 곳마다 바꿔 넣습니다. 옛 값은 못 쓰게 막습니다. 이 일을 회전이라고 부릅니다
  2. 옛 값이 새어 있던 동안 누가 그 값으로 들어왔는지 살핍니다. 누가 언제 무엇에 접속했는지 남긴 기록인 감사 로그를 봅니다
  3. 그다음에 필요하면 이력에서 값을 걷어냅니다

옛 값이 이미 죽었다면 이력에 남아 있어도 쓸 데가 없습니다. 이력을 고쳐 쓰는 일이 맨 뒤에 오는 까닭입니다.

터지는 조건

아래 네 조건이 모두 겹치면 터집니다. 하나라도 빠지면 피해가 없거나 작게 끝납니다.

조건 빠지면 어떻게 되나
비밀값이 저장소가 추적하는 파일 안에 글자로 적혀 있다 값을 코드 밖에서 넣으면 커밋할 것이 없다
그 파일이 커밋된다 커밋 전이면 파일만 고치면 끝난다
그 커밋이 원격 저장소에 올라간다 내 컴퓨터에만 있으면 그 커밋을 고쳐 쓰고 끝낼 수 있다
값이 아직 쓸 수 있는 상태다 이미 막은 값은 알아도 쓸 데가 없다

셋째 조건이 수습 비용을 가릅니다. 푸시하기 전에 알아챘다면 그 커밋은 내 저장소에만 있습니다. 고쳐 써도 어긋날 사본이 없습니다. 푸시한 뒤라면 사본이 이미 퍼졌다고 보고 값부터 바꿉니다.

막는 방법

비밀값이 저장소에 안 들어가게 하는 방법은 넷입니다. 바탕에 깔린 생각은 둘입니다. 비밀값을 저장소가 보는 곳에 두지 않습니다. 실수로 들어가면 올리기 전에 잡습니다.

첫째는 값을 코드 밖에서 넣는 것입니다. 환경 변수는 프로그램을 띄울 때 운영체제가 넘겨주는 이름과 값의 짝입니다. 코드는 이름만 압니다. 값은 실행할 때 받습니다.

Python
import os
pw = os.environ["DB_PW"]  # 실행 때 받는다

둘째는 값을 적은 파일을 커밋에서 빼는 것입니다. .gitignore는 커밋에서 뺄 파일 이름을 적어 두는 파일입니다. 여기에 .env 를 적어 두면 git add . 를 쳐도 그 파일은 안 담깁니다.

셋째는 커밋하기 직전에 검사하는 것입니다. 프리 커밋 훅은 커밋할 때마다 저절로 도는 검사 스크립트입니다. 담긴 내용에서 키처럼 생긴 문자열을 찾으면 커밋을 멈춥니다.

넷째는 원격 저장소 쪽에서 검사하는 것입니다. 비밀값 스캔은 올라온 커밋에서 키처럼 생긴 문자열을 찾아 알리는 기능입니다. 내 컴퓨터의 검사를 건너뛴 커밋도 여기서 걸립니다.

넷은 막는 시점이 다릅니다. 치르는 비용도 다릅니다.

방법 막는 시점 대가
환경 변수 코드를 쓸 때 실행하는 곳마다 값을 넣어 줄 길이 따로 있어야 한다
.gitignore 파일을 담을 때 이미 커밋한 파일은 못 막는다
프리 커밋 훅 커밋할 때 사람마다 설치해야 한다. 건너뛸 수도 있다
비밀값 스캔 올린 뒤 알아챘을 때는 이미 올라간 뒤다

뒤의 두 검사는 모양으로 찾습니다. API 키는 서비스마다 늘 같은 글자로 시작하는 일이 많습니다. 어떤 서비스의 키가 모두 abc_ 로 시작한다고 해 봅시다. 검사기는 그 글자만 찾으면 됩니다.

평범한 낱말로 지은 비밀번호는 사정이 다릅니다. 다른 문자열과 구별이 안 돼 놓치기 쉽습니다. 그래서 검사만 믿지 않고 값을 코드 밖에 두는 첫째 방법과 함께 씁니다.

값을 넣어 줄 길이 여럿 늘면 그것도 관리할 거리가 됩니다. 비밀값을 한곳에 모아 두고 권한 있는 프로그램에만 내주는 일을 시크릿 관리라고 부릅니다.

관련 항목

비밀값 커밋과 함께 터지는 버전관리 문제

병합 충돌 · 이력 강제 덮어쓰기 · 큰 이진 파일 · 분리된 HEAD · 장수 브랜치

비밀값 커밋으로 새어 나가는 자격증명

비밀번호 · API 키 · 액세스 토큰 · 개인 키 · 인증서 · 자격증명

비밀값 커밋을 막는 수단

환경 변수 · .gitignore · 프리 커밋 훅 · 비밀값 스캔 · 시크릿 관리 · 키 관리

비밀값 커밋 뒤 수습에 쓰는 작업

회전 · 폐기 · 감사 로그 · 커밋 고쳐 쓰기 · 사고 대응

비밀값이 사본으로 퍼지는 경로

원격 저장소 · 푸시 · 클론 · 포크 · 백업 · 지속적 통합

비밀값 커밋이 속하는 상위 분류

버전관리 · Git · 분산 버전관리 · 하드코딩 · 최소 권한

다른 이름: committed secrets · secrets in version control · 비밀값 유출 · 시크릿 커밋 · 비밀번호 커밋