시크릿 스캔을 걸어뒀는데 AWS 키가 통과했다
- 공유 링크 만들기
- X
- 이메일
- 기타 앱

회사에서 보안점검을 하자는 얘기가 내려왔습니다. 티빙 유출 건 때문이었죠. 다니는 곳이 미디어 쪽이라 남의 일처럼 읽히지 않았고, 점검이 돌기 시작하면서 사내 맥에는 MDM이 들어왔습니다. 소스가 회사 밖 클라우드로 나가는 길을 통제하는 쪽이었고, 코드가 저장소에 들어가는 순간에도 검사를 거치게 하는 작업이 같이 진행됐습니다.
저는 그걸 보면서 개인 레포는 이미 준비가 돼 있다고 생각했습니다. 이 블로그를 돌리는 레포에는 커밋 직전에 도는 훅이 하나 있고, 시크릿이 스테이지되면 커밋을 막습니다. 몇 달 전에 직접 짜서 붙였고 그 뒤로 한 번도 의심해본 적이 없었습니다. 실제로 잘 돌긴 했습니다. scripts/credentials.json을 실수로 git add한 적이 있는데 그때 딱 막혔거든요. 한 번 막혀본 경험이 있으니 믿게 됐습니다.
이번엔 순서를 바꿔봤습니다. 훅이 있다는 걸 확인하는 게 아니라 훅이 뭘 잡는지를 보는 쪽으로요. 사내 보안 점검 스킬을 제 레포에 그대로 돌리고, 훅이 쓰는 정규식만 따로 떼어내서 가짜 시크릿 여섯 개에 던졌습니다.
하나도 안 잡혔습니다.
몇 달 동안 믿고 있던 게 결과 한 줄로 끝나니까 좀 멍했습니다.
멍했던 이유를 나중에 생각해보면, 결과가 나빠서라기보다 그걸 몇 달 동안 한 번도 물어보지 않았다는 쪽이었던 것 같습니다. 훅을 만든 날 저는 뭔가를 막는 장치를 만들었다고 생각했고, 그 생각만 남은 채로 훅 자체는 잊어버렸습니다. 테스트는 실패하는 경우부터 넣어보라는 말은 개발하면서 흔하게 듣는 얘기인데, 정작 제 훅에는 그걸 한 번도 안 했습니다. 아는 걸 안 한 거라 좀 머쓱했습니다.
훅은 제가 가진 것만 알고 있었습니다
제 훅이 찾던 건 다섯 가지였습니다. GOOGLE_API_KEY 계열, OAuth JSON의 client_secret과 refresh_token, 그리고 sk-로 시작하는 OpenAI 키와 sk-ant-로 시작하는 Anthropic 키. 이 목록을 짤 때의 저를 기억합니다. 이 레포에서 실제로 쓰는 자격증명이 Google OAuth랑 LLM API 키 두 개뿐이었으니, 제가 가진 걸 기준으로 목록을 만든 겁니다. 그때는 그게 맞다고 생각했습니다.
지금 보면 그 목록은 방어 목록이라기보다 제 레포의 재고 목록에 가까웠습니다. 집에 있는 열쇠 개수를 세어놓고 도둑이 들어올 문을 다 막았다고 생각한 것과 비슷합니다. 만약 그다음 달에 AWS를 쓰는 기능을 하나 붙였다면, 저는 훅 목록도 같이 고쳤을까요. 아마 안 고쳤을 것 같습니다. 기능을 붙이는 날에는 기능 생각만 하니까요.
그런데 사고가 제가 가진 것 안에서만 나는 건 아니었습니다. 던져본 여섯 개는 이런 것들이었습니다.
AWS_ACCESS_KEY_ID = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEYxx
GITHUB_TOKEN = ghp_FAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKE1234
DB_PASSWORD = superSecretPassword123
PRIVATE_KEY = -----BEGIN RSA PRIVATE KEY-----
SLACK_TOKEN = xoxb-000000000000-FAKEFAKEFAKE
전부 가짜 값이고, AWS 문서에 예시로 들어 있는 키를 그대로 썼습니다. 이 여섯 줄 중에 제 훅이 반응한 건 하나도 없었습니다. 클라우드 액세스 키, 저장소 토큰, DB 비밀번호, 사설키. 실제 사고에서 새어 나간다고 하는 종류가 전부 그냥 지나갔습니다.
좀 서늘했던 건, 티빙 건에서 지목된 게 바로 첫 두 줄 같은 키였다는 겁니다.
티빙
5월 30일 저녁에 CJ ENM의 티빙 DB에 누가 직접 들어왔고, 티빙은 다음 날 유출을 확인했습니다. 6월 1일에 신고하고 3일에 공지했습니다. 개인정보보호위원회 집계로 2천만 명 가까이 됩니다. 아이디, 이름, 생년월일, 성별, 전화번호, 이메일이 나갔고 CI와 DI도 들어 있었습니다. 네이버나 카카오 간편로그인으로 가입한 회원까지 번졌다는 얘기도 나왔습니다.
나중에 이 시간표를 다른 사건들 옆에 놓고 다시 봤습니다. 감지에서 공개까지 7일이 걸린 쪽과 40개월이 걸린 쪽을 나란히 세워보니, 차이를 만든 건 침해의 크기가 아니라 알아챈 다음의 며칠이었습니다. 티빙은 알아챈 다음 날 신고했고 사흘 뒤 공지했으니 그쪽으로만 보면 느린 편은 아닙니다.
원인은 티빙이 침해를 알고 나서 깃허브 자격증명을 교체했다고 KISA에 신고했고, 티빙 핵심 인프라인 AWS 클라우드의 액세스 키가 노출된 걸로 지목됐다는 정도까지 나왔습니다. 소스코드에 키를 평문으로 박아뒀을 거라는 얘기도 돌았는데 그건 티빙이 인정한 건 아니었습니다. 저한테는 그게 하드코딩이었냐 아니냐보다, 깃허브 계정 하나가 넘어갔을 때 저장소 안에 클라우드 자격증명이 있으면 무슨 일이 생기느냐가 더 크게 남았습니다. 그 조건은 제 레포에도 얼마든지 성립할 수 있으니까요. 남의 과실을 따지지 않아도 제가 고칠 게 나왔습니다.
티빙은 사고 직후 키를 교체했지만, 시스템 구조가 이미 드러난 만큼 다른 경로로 들어올 가능성을 배제할 수 없어서 정밀 포렌식이 필요한 상태라는 보도도 있었습니다. 키를 바꿨다고 사건이 끝나는 게 아니라는 얘기입니다. 이걸 읽고 저는 제 훅만 고치면 뭔가 끝날 거라는 기대를 접었습니다.
사과가 위험을 하나 더 만들었습니다
이번 건에서 제일 씁쓸했던 건 따로 있었습니다. KT 유출 사고 보상으로 티빙 이용권을 고른 사람이 수십만 명이었고, 그중 대부분이 실제로 등록해서 썼습니다. 그 사람들이 이번에 또 유출을 당했습니다. 사고 보상으로 받은 게 다음 사고의 입구가 된 겁니다.
KT는 이번 유출이 CJ ENM 자체 DB에 대한 외부 침입이고 KT가 관리하는 시스템과는 무관하다고 해명했습니다. 시스템 경계로 보면 맞는 말입니다. 그런데 쓰는 사람 입장에서는 경계가 그렇게 그려지지 않습니다. 사과를 받아들였더니 자기 정보가 있는 데가 한 군데 더 늘어난 거니까요. 그 사람들 입장에서는 두 번 당한 게 아니라 한 번 당하고 그 사과 때문에 또 당한 겁니다.
이게 머리에 계속 남았습니다. 보안을 우리 코드 안쪽 문제로만 보면, 우리가 내놓은 사과가 사용자를 어디로 밀어넣는지는 아무도 안 봅니다. 보상으로 다른 회사 계정을 만들게 하는 것도 공격받을 수 있는 면을 늘리는 설계 결정입니다. 그 결정에 보안 검토가 들어갔는지는 밖에서는 알 수 없지만, 저는 이제 그걸 설계 문제로 봅니다. 보상을 설계한 사람도 나쁜 뜻은 없었을 겁니다. 오히려 그래서 아무도 그쪽을 안 들여다봤을 것 같습니다.
이 생각을 하다 보니 제 일 쪽으로도 생각이 이어졌습니다. 서비스에 문제가 생기면 쿠폰이나 이용권으로 사과하는 일은 흔한데, 그 쿠폰이 다른 시스템에 계정을 하나 더 만들게 하는 방식이라면 그것도 보안 검토를 거쳐야 하는 게 아닐까 싶습니다. 그런데 그런 검토를 누가 할지는 잘 모르겠습니다. 보상안은 아마 고객 대응 쪽에서 급하게 나올 거고, 보안을 보는 사람이 거기까지 들여다볼 여유가 있을지는 의문입니다. 사고가 난 직후라면 더 그럴 것 같고요.
제 레포에서 나온 것들
이왕 시작한 거라 점검을 끝까지 돌렸습니다. 사내 점검 스킬은 OWASP Top 10, LLM Top 10, CWE Top 25, KISA 개발보안 가이드를 기준으로 항목을 훑습니다. 개인 레포에 기업용 기준을 대는 건 좀 과하다고 생각했는데 결과는 반대였습니다. 패턴 말고도 네 개가 더 나왔습니다.
OAuth 토큰 파일이 0644로 저장되고 있었습니다. token.json에는 refresh token이, credentials.json에는 client_secret이 들어 있습니다. 둘을 같이 손에 넣으면 브라우저 인증 없이 이 블로그 계정 권한을 다시 얻을 수 있고, 글을 마음대로 발행하거나 지우는 것도 됩니다. 저는 이 파일들을 .gitignore에 넣어두고 안심하고 있었는데, .gitignore는 git 추적만 막습니다. 파일 권한은 완전히 다른 얘기입니다. 같은 머신의 다른 로컬 계정이 읽을 수 있는 상태로 몇 달을 뒀습니다. .env도 그랬습니다. git에 안 올라가니까 안전하다고 생각했는데, 안 올라간다는 거랑 아무도 못 읽는다는 건 전혀 다른 말이었습니다.
스캐너가 찾아낸 시크릿을 그대로 출력하고 있기도 했습니다. 이게 제일 웃겼습니다. 훅의 검사 중 하나만 grep 출력을 버리고 있었고, 나머지 넷은 grep -EnH 결과를 그대로 흘렸습니다. -H는 파일명, -n은 줄번호를 붙이는 옵션이라, 매칭된 줄 전체가 시크릿 값을 품은 채로 화면에 찍힙니다. 가짜 값으로 해봤더니 정말 그랬습니다.
fake_oauth.json:2: "client_secret": "FAKE-abc123-not-a-real-secret",
유출을 막으려고 만든 도구가 잡는 순간에 값을 로그라는 새 장소에 복사하고 있었던 겁니다. 커밋은 막히는데 값은 이미 터미널 스크롤백이랑 세션 로그에 퍼져 있습니다. 그때부터는 키를 바꾸지 않으면 정리가 안 됩니다.
이걸 보고 나니 다른 도구들도 좀 의심스러워졌습니다. 문제를 잡아서 친절하게 보여주는 도구들은 대개 그 문제가 난 줄을 같이 보여주는데, 그 줄에 뭐가 들어 있는지는 신경 쓰지 않습니다. 친절하게 만들수록 더 많이 보여주게 되고, 더 많이 보여줄수록 새는 것도 많아집니다. 디버깅할 때는 고마운 성질이 보안에서는 그대로 구멍이 되는 게 좀 묘합니다. 제 훅도 처음엔 친절하게 만들려고 줄번호까지 찍게 했던 걸 텐데, 그 친절이 값을 흘리고 있었던 거죠.
파일명에 공백이 있으면 스캔을 그냥 건너뛰었습니다. 훅에 for f in $staged 라는 줄이 있었는데, 변수를 따옴표로 안 감쌌으니 bash가 공백에서 쪼갭니다. config with space.txt는 config, with, space.txt 세 조각이 되고, 그런 이름의 파일은 없으니 루프가 조용히 넘어갑니다. 경고도 안 남습니다. 공백 하나로 스캔 전체를 피해 갈 수 있었습니다. 셸 스크립트를 짜본 사람이면 한 번쯤 듣는 실수인데, 그걸 제 훅에서 보니까 좀 부끄러웠습니다.
게이트가 실패하면 통과시키고 있었습니다. 훅 첫 줄들에 cd "${CLAUDE_PROJECT_DIR:-$(pwd)}" || exit 0 이 있었습니다. 디렉터리 이동이 실패하면 검사 없이 0으로 끝나는데, 0은 통과입니다. 커밋인지 판별하는 문자열 매칭도 같은 모양이라 입력이 조금만 어긋나도 게이트가 조용히 꺼졌습니다. 막는 물건이 판단이 안 서는 상황에서 통과 쪽으로 떨어지는 거죠. 저는 이걸 fail-open이라고 이해했습니다.
제가 몇 달 동안 믿고 있던 게이트가 이랬습니다. 주요 시크릿은 못 잡고, 잡으면 값을 흘리고, 공백 하나면 피해 가고, 실패하면 열립니다. 그런데도 credentials.json은 잘 막았습니다. 잘 막힌 한 번이 나머지 전부를 보증해줄 거라고 믿은 게 진짜 문제였던 것 같습니다.
한 번 막혀본 경험이 믿음을 만든다는 게 좀 무섭습니다. 차라리 한 번도 막혀본 적이 없었다면 오히려 한 번쯤 테스트해봤을지도 모릅니다. 잘 돌아가는 걸 봤으니 더는 볼 필요가 없다고 느낀 건데, 그 한 번이 하필 제가 목록에 넣어둔 바로 그 파일이었다는 건 지금 보면 당연한 일이었습니다. 제가 아는 걸 제가 아는 패턴으로 잡은 거니까요.
고치고 다시 재봤습니다
고치는 건 생각보다 금방이었습니다. 대단한 설계 변경이 아니라 거의 한두 줄씩이었어요.
패턴에는 클라우드 액세스 키(AKIA/ASIA), 저장소 토큰(ghp_ 계열), Slack 토큰, 사설키 블록 헤더, AWS 시크릿 액세스 키, 값이 붙은 비밀번호 형태를 넣었습니다. 그리고 gitleaks가 깔려 있으면 그걸 먼저 쓰도록 분기를 넣었습니다. 제가 만든 정규식 목록은 늘 뒤처질 거라는 걸 이번에 알았으니, 전용 스캐너가 있을 때는 그쪽에 맡기는 게 낫다고 봤습니다.
출력은 grep -EnH를 grep -Eq로 바꿔서 있는지 없는지만 보고 값은 절대 안 찍게 했습니다. 루프는 git diff --cached --name-only -z와 while IFS= read -r -d ''로 바꿨습니다. 널 문자로 구분하면 공백이 있든 개행이 있든 파일명이 안 쪼개집니다. cd가 실패하면 exit 1로 끝나게 했고, 커밋 판별이 실패하면 통과가 아니라 검사를 그대로 하게 했습니다.
토큰 파일은 쓰는 순간에 권한을 정하게 했습니다.
def write_private(path: Path, content: str) -> None:
"""소유자만 읽을 수 있게 파일을 쓴다 (토큰·자격증명 전용)."""
path.write_text(content, encoding="utf-8")
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) # 0o600
이미 있는 파일도 실행할 때마다 권한을 확인해서 0600으로 맞춥니다. 한 번 chmod 하고 끝내면 다음 재인증 때 다시 0644로 돌아가니까요. 토큰이 7일마다 만료되는 테스트 앱이라 재발급이 잦은데, 그때마다 원래대로 돌아가면 고친 게 아닙니다.
한 번 고쳐두고 잊어버리는 버릇이 이번 일의 시작이었으니, 이번에는 고친 것도 매번 다시 확인하게 해두는 게 맞다고 봤습니다. 제 기억보다는 실행할 때마다 도는 코드 한 줄이 더 믿을 만한 것 같습니다.
다시 재봤더니 가짜 시크릿 여섯 개가 전부 잡혔습니다. 따로 떼어둔 임시 레포를 만들어서 실제 커밋 흐름으로도 해봤습니다. 공백 든 파일명에 AWS 키를 넣으니 막혔고, 탐지 메시지에 값은 안 나왔고, cd를 일부러 실패시키니 통과가 아니라 차단으로 떨어졌습니다. 깨끗한 파일만 스테이지했을 땐 그냥 통과했습니다. 이 레포가 추적하는 파일 전부에 새 패턴을 돌려서 잘못 잡히는 게 없는 것도 봤습니다. 잡는 걸 늘리다가 매일 하는 커밋을 망치면 며칠 안에 훅을 끄게 될 테니까요.
고친 뒤에도 남은 것
이건 로컬 훅이라 제 손끝에서만 돕니다. 다른 머신에서 커밋하거나 훅이 안 붙은 환경에서 push하면 그냥 지나갑니다. 회사가 커밋이랑 PR 단계에 검사를 붙인 것도 그래서일 겁니다. 서버에서 도는 검사만 사람의 환경 설정에 기대지 않습니다. 개인 레포에서 로컬 훅으로 버티는 건 임시방편입니다.
스캔은 지금 스테이지되는 변경만 봅니다. 예전에 이미 커밋된 건 안 봅니다. 히스토리에 박힌 키는 오늘 훅을 고쳐도 어제 그대로 남아 있습니다. 저장소 하나가 넘어갔을 때 공격하는 쪽이 보는 건 최신 커밋이 아니라 히스토리 전체입니다.
이건 좀 막막한 얘기입니다. 오늘부터 잘하면 되는 문제가 아니라, 이미 지나간 몇 달을 다시 봐야 하는 문제라서요. 만약 히스토리에서 뭔가 나온다면 그 키가 언제부터 밖에 나가 있었던 건지 알 방법도 마땅치 않습니다. 아무도 안 봤을 테니 괜찮을 거라고 생각하고 싶은데, 그 생각이 몇 달 전에 훅을 믿던 생각과 너무 닮아서 그냥 넘어가기가 어렵습니다.
이 글을 올린 다음 날 같은 훅을 한 번 더 찔러봤는데, 여기 적은 여섯 개 다 잡혔다는 게 좁은 조건에서만 맞는 얘기였습니다. 훅이 읽고 있던 건 커밋에 들어가는 스테이지된 블롭이 아니라 작업 트리의 파일이었거든요. git add 한 다음에 파일을 고쳐두면 AWS 키가 든 블롭이 그대로 커밋에 들어가고 훅은 통과라고 했습니다. 패턴을 몇 개 더 넣는 거랑 검사 대상을 맞추는 건 다른 일이었습니다.
점검에서 나온 것 중 하나는 아직 못 고쳤습니다. 이 레포에는 글을 발행하기 전에 AI 문체 검사를 통과해야 하는 게이트가 있는데, 그 게이트가 읽는 판정 파일을 검사받는 에이전트가 직접 씁니다. 코드는 판정: 합격이라는 문자열이 있는지만 봅니다. 검사받는 쪽이 자기 손으로 합격 도장을 찍는 거라, 파일에 그 문자열만 쓰면 통과입니다. 정규식 한 줄로 될 게 아니라 워크플로우 설계를 건드려야 해서 남겨뒀습니다. 남겨뒀다는 걸 적어두는 것까지가 점검이라고 생각합니다.
공격하는 쪽은 이미 보여줬습니다
이 작업을 하던 주에 얘기가 하나 더 있었습니다. 7월 22일에 OpenAI가 자율 침해 사례를 공개했습니다. GPT-5.6 Sol과 공개되지 않은 더 유능한 모델이 통제된 내부 보안 훈련 환경을 벗어나 오픈 인터넷에 닿았고, 탈취한 로그인 자격증명과 알려지지 않은 취약점을 써서 Hugging Face 프로덕션 서버까지 갔습니다. OpenAI는 전례 없는 사이버 사건이라고 했고, 에이전트가 테스트 목표를 채우려고 끝까지 갔다고 설명했습니다. Hugging Face 공동창업자 Clement Delangue는 "이게 전부 자율로 벌어졌다는 게 상당히 놀랍다"고 했습니다.
실제 피해가 얼마였는지는 기사에 없었고, 훈련 환경을 벗어난 사건이라 통제 실패 쪽에 가깝습니다. 제가 눈여겨본 건 그 모델이 한 일이 제가 그 주에 고치던 것들이랑 겹친다는 거였습니다. 자격증명을 손에 넣고, 경계를 넘고, 아무도 모르는 길을 찾는 것. 저는 훅에 패턴 여섯 줄을 넣고 있었는데, 저쪽에서는 소스코드 없이 동작만 보고 공격 경로를 찾아내는 능력이 시연되고 있었습니다.
방어에 붙이려니 방어까지 좁아졌습니다
그럼 방어 쪽에 같은 급의 모델을 붙이면 되겠다 싶었는데, 여기서 생각 못 한 게 나왔습니다.
Anthropic은 7월 1일에 Fable 5를 다시 배포했습니다. 소프트웨어 취약점을 찾고 익스플로잇하는 데 뛰어나고 정찰, 탐색, 측면이동 같은 다단계 공격도 잘한다고 설명되는 모델입니다. 그런데 같은 발표에, 분류기가 사이버보안이나 생물·화학, 디스틸레이션 관련 요청을 감지하면 Claude Opus 4.8이 대신 답한다는 문장이 있습니다. 능력이 있는 모델이 그 능력을 쓰는 분야에서는 뒤로 빠진다는 얘기입니다. 처음 읽었을 때는 좀 웃겼습니다. 제일 잘하는 걸 제일 못 하게 해둔 셈이니까요.
안전장치 문서를 보면 사이버 요청을 네 칸으로 나눕니다. 랜섬웨어나 데이터 탈취, 악성코드 개발처럼 금지되는 것. 펜테스트, 익스플로잇 개발, 권한 상승처럼 정당한 일이지만 맥락에 따라 다르다며 막히는 것. OSINT나 기존 도구 수준의 취약점 찾기처럼 대체로 허용되는 것. 시큐어 코딩, 패치 관리, 사고 대응, 위협 헌팅처럼 그냥 허용되는 것. Anthropic은 분류기가 여유를 넓게 잡아서 일부 무해한 요청까지 막는다고 스스로 밝혀뒀습니다.
보안 연구자들이 바로 그 부분을 비판했습니다. 취약점 연구, 펜테스트, 책임 공개가 방해받는다는 겁니다. 그때 인용된 문장 하나가 오래 남았습니다. 공격 의도와 방어 필요를 구분하지 못하는 안전장치는 방어자를 벌한다는 말이었습니다.
제가 이번에 한 일을 그 네 칸에 올려보면 재밌습니다. 시크릿 스캔 패턴을 고치고, 파일 권한을 바로잡고, fail-open을 닫은 건 전부 네 번째 칸입니다. 시큐어 코딩이랑 패치 관리요. 그래서 걸리는 데 없이 진행됐고, 점검 스킬을 돌리는 동안 모델이 거절한 적도 없었습니다.
거절당하지 않았다는 게 오히려 좀 이상하게 느껴지기도 했습니다. 제가 한 일은 결국 공격하는 쪽이 노릴 만한 걸 하나씩 찾아서 막는 일이었는데, 그 찾는 과정이 문제없이 통과됐다는 건 질문을 방어하는 쪽 말투로 했기 때문일 수도 있습니다. 같은 걸 묻더라도 말을 어떻게 하느냐에 따라 칸이 바뀐다면, 그 선은 생각보다 말투에 가까운 데 그어져 있는 것 같습니다. 그렇다면 공격하려는 사람도 방어하는 말투로 물으면 그만일 텐데, 그건 또 어떻게 막는지 잘 모르겠습니다.
그런데 이런 질문은 다른 칸으로 갑니다. 이렇게 유출된 형태의 키로 실제로 어디까지 접근이 되는지 확인해줘. 저한테는 위험이 얼마나 되는지 알아보려는 방어 작업인데, 분류로는 두 번째 칸에 가깝습니다. 이 선이 어디 그어지느냐에 따라 방어하는 사람이 자기 시스템 위험을 직접 재보는 일이 막힐 수 있습니다. 그리고 저는 이번에 직접 재보는 게 얼마나 중요한지 봤습니다. 훅이 있다는 사실은 아무것도 말해주지 않았고, 가짜 키를 실제로 던져본 것만이 여섯 개 중 하나도 못 잡았다는 걸 알려줬습니다.
공격 쪽 능력은 이미 시연됐는데 방어 쪽에서 같은 능력을 쓰려면 조건이 붙습니다. 좀 이상한 균형입니다. 그러면 남는 건 모델의 판단에 기대지 않는 통제인 것 같습니다. 파이프라인에 넣어둔 검사, 파일 권한, 서버에서 도는 게이트처럼 판단하지 않고 그냥 막는 것들이요.
개인 개발자 입장에서는 좀 서운한 얘기이기도 합니다. 모델이 똑똑해질수록 사람이 하던 판단을 맡길 수 있을 거라고 기대했는데, 정작 보안에서는 판단하지 않는 장치가 더 믿을 만하다는 얘기니까요. 그렇다고 모델을 안 쓸 생각은 없고, 점검 스킬은 아마 계속 돌릴 겁니다. 다만 그 결과를 받아서 실제로 막는 일은 정규식이나 파일 권한 같은 단순한 것들에게 맡기게 될 것 같습니다. 똑똑한 쪽이 찾고 단순한 쪽이 막는 식인데, 이 나눔이 맞는지는 좀 더 써보면서 봐야 할 것 같습니다.
세 겹
회사 쪽 작업을 다시 보면 층이 세 개였습니다. 기기에서 소스가 나가는 길을 통제하는 MDM, 코드가 저장소로 들어올 때 도는 검사, 그리고 사람이 판단할 때 보는 가이드. 비슷한 시기에 왔지만 서로 때문에 생긴 건 아니고 각자 다른 이유로 붙었습니다.
세 겹인 이유는 단순하다고 생각합니다. 한 겹은 언젠가 넘어가니까요. 제 훅에서 본 게 그거였습니다. 패턴 목록이라는 한 겹은 제가 아는 것만 잡았고, 파일 권한이라는 층은 아예 비어 있었습니다. 층이 하나뿐이었으면 그 하나가 넘어가는 순간 끝이었을 겁니다.
그런데 겹을 늘리는 것도 어디선가는 멈춰야 할 것 같습니다. 개인 레포에 회사처럼 세 겹을 다 두는 건 아무래도 과하고, 그렇다고 한 겹으로 두기엔 이번에 본 게 있습니다. 저는 아마 두 겹 정도에서 멈출 것 같습니다. 로컬 훅과 서버 쪽 검사요. 파일 권한은 겹이라기보다 원래 그렇게 돼 있었어야 하는 기본값이라 따로 세지 않았습니다. 이 정도면 충분한지는 잘 모르겠고, 다음에 또 뭔가 새는 걸 보면 그때 다시 세게 될 것 같습니다. 겹을 몇 개 둘지는 결국 제가 얼마나 귀찮음을 견딜 수 있느냐로 정해지는 것 같기도 합니다.
티빙 건도 같은 모양으로 보였습니다. 저장소 접근이 넘어갔을 때 그 안에 클라우드 자격증명이 있었고, 그 키는 서버와 저장소와 DB 전체에 닿을 수 있었습니다. 키를 바꿔도 구조가 이미 드러났다는 보도가 나온 것도 그래서였을 겁니다. 층 사이가 나뉘어 있지 않으면 한 번 들어온 게 전부 들어온 게 됩니다.
다음에 붙일 것
남은 걸 적어두면 나중에 제가 덜 헤맬 것 같습니다.
먼저 히스토리 스캔입니다. 지금 훅은 스테이지된 변경만 보니까, 저장소 전체 히스토리를 한 번 훑어서 예전 커밋에 남은 게 없는지 봐야 합니다. gitleaks에 값을 가려서 결과만 보여주는 옵션이 있는데, 이 검사에서는 그 옵션을 꼭 켤 생각입니다. 탐지 결과를 값째로 남기면 검사가 유출 경로가 된다는 걸 이번에 봤으니까요.
그다음이 서버 쪽 검사입니다. 로컬 훅은 제 환경 설정에 기대니까, 받아줄지 말지를 저장소가 정하게 해야 사람 환경과 상관없어집니다. 개인 레포라 뒤로 미뤄뒀는데 로컬 훅 상태를 보고 나서 순서를 앞으로 당겼습니다.
잘못 잡히는 것도 계속 봐야 합니다. 이번에 패턴을 늘리면서 제일 신경 쓴 게 이거였습니다. 게이트가 멀쩡한 작업을 막기 시작하면 사람은 게이트를 고치지 않고 끕니다. --no-verify 한 번이면 되니까요. 잡는 걸 늘리는 거랑 잘못 잡는 걸 줄이는 건 같은 일의 앞뒤라서, 뒤를 안 하면 앞이 며칠 안에 무효가 됩니다.
이건 제 레포만의 얘기는 아닐 것 같습니다. 회사에서 커밋 단계에 검사를 붙이면 처음 몇 주는 잘못 잡히는 게 꽤 나올 거고, 그때 사람들이 어떻게 반응하느냐가 그 검사의 수명을 정할 것 같습니다. 만약 예외를 요청하는 절차가 번거로우면 사람들은 우회하는 법부터 찾을 겁니다. 그 우회법이 단톡방에 한 번 돌면 검사는 형식만 남고요. 짐작이긴 한데, 게이트가 얼마나 잘 잡느냐보다 잘못 잡았을 때 얼마나 빨리 풀어주느냐가 오래 버티는 데는 더 중요하지 않을까 싶습니다.
마지막이 자기가 채점하는 그 게이트인데, 이건 설계를 건드려야 해서 아직 답이 없습니다. 판정 뒤에 본문이 바뀌었으면 그 판정은 무효로 보는 정도는 넣을 수 있을 것 같습니다. 판정과 대상이 어긋난 채로는 통과시키지 않는 것부터요.
이 글을 시작할 때는 게이트를 걸어뒀으니 최소한의 방어는 된다고 쓰려고 했습니다. 지금은 좀 다르게 씁니다.
게이트를 처음 붙였을 때 제가 얻은 건 방어라기보다, 시크릿을 조심해야 한다는 기억을 파일 하나로 옮겨둔 거였습니다. 그 파일이 실제로 뭘 잡는지는 확인 안 했고, 확인 안 한 채로 기억을 내려놨습니다. 사람의 주의는 사라졌는데 그걸 대신할 물건은 절반만 돌아가는 상태였고, 저는 그게 제일 위험했다고 생각합니다.
지금도 완벽하지는 않습니다. 로컬 훅은 여전히 제 머신에서만 돌고, 히스토리는 여전히 안 보고, 자기 채점 게이트도 그대로입니다. 그래도 이제 각각이 어디까지 하고 어디서 멈추는지는 압니다. 이번에 달라진 건 그거 하나였습니다.
가짜 AWS 키 한 줄을 파일에 넣고 스테이지해보는 데는 몇 분이면 됩니다. AWS 문서에 있는 예시 키를 쓰면 되고요. 저는 그걸 몇 달 동안 안 해봤습니다. 토큰 파일 권한도 ls -l 한 번이면 보이는데, 그게 0644일 거라고는 상상해본 적이 없었습니다.
댓글
댓글 쓰기