사용자를 번거롭게 하지 않으면서 비밀 정보를 차단하는 로컬 AI Pre-Commit Hook
요약
정규 표현식의 오탐지와 미탐지 문제를 해결하기 위해 로컬 LLM을 결합한 Pre-Commit Hook 구축 방법을 소개합니다. Ollama를 활용해 스테이징된 변경 사항을 로컬에서 검사함으로써 보안과 속도, 데이터 프라이버시를 모두 확보합니다.
핵심 포인트
- 정규 표현식의 한계를 로컬 LLM(qwen2.5-coder)으로 보완
- Ollama를 사용하여 민감한 코드 데이터의 외부 유출 방지
- 스테이징된 콘텐츠(git show)를 스캔하여 정확한 커밋 검사 수행
- 종료 코드를 통한 자동화된 커밋 차단 및 허용 프로세스 구축
제가 사용하던 정규 표현식 (regex) 비밀 정보 스캐너는 한 번은 테스트 파일에 sk_test_EXAMPLE_KEY_DO_NOT_USE라는 문자열이 포함되어 있다는 이유로 커밋을 차단한 적이 있습니다. 같은 주에, 다른 프로젝트의 동료는 하드코딩된 URL 안에 실제 Etherscan API 키를 커밋했는데, 알려진 키 형식과 일치하지 않아 어떤 스캐너도 이를 잡아내지 못했습니다. 이 두 번의 실패는 정규 표현식 기반의 비밀 정보 스캐닝의 핵심을 보여줍니다. 중요하지 않은 곳에서는 시끄럽고, 정작 중요한 곳에서는 조용하다는 점입니다.
표준적인 해결책은 끝없이 늘어나는 허용 목록 (allowlist) 파일을 만드는 것이고, 그 결과 개발자들은 근육 기억으로 git commit --no-verify를 입력하는 법을 배우게 됩니다. 사람들이 습관적으로 훅 (hook)을 우회하기 시작하면, 스캐너는 그저 장식품에 불과해집니다.
그래서 저는 다른 방법을 시도했습니다. 정규 표현식 스캐너는 유지하되, 두 번째 의견을 제시할 작은 로컬 LLM (Large Language Model)을 추가하는 것입니다. 정규 표현식 단계에서 무엇을 살펴볼 가치가 있는지 결정합니다. 모델은 그것이 실제로 비밀 정보인지 결정합니다. 플래그가 지정된 파일만이 모델에 전달되므로 훅은 빠른 속도를 유지하며, 모델이 제 컴퓨터에서 실행되는 Ollama이기 때문에 스테이징된 디프 (diff)가 제 노트북을 절대 벗어나지 않습니다. 마지막 부분은 저에게 타협할 수 없는 조건입니다. 비밀 정보가 포함되었을지도 모르는 디프를 클라우드 API로 보내 확인하는 것은 그 자체로 농담이나 다름없기 때문입니다.
한 문단으로 요약한 아키텍처
1단계는 스테이징된 변경 사항에 대해 의도적으로 편집증적인 정규 표현식 패스 (pass)를 수행합니다: 높은 엔트로피 (entropy) 문자열, 알려진 키 접두사, PRIVATE KEY 블록, 의심스러운 변수 이름 등을 검사합니다. 2단계는 플래그가 지정된 각 덩어리 (hunk)를 주변 문맥 몇 줄과 함께 Ollama를 통해 qwen2.5-coder로 분류 프롬프트와 함께 보냅니다. 판결이 SECRET이면 커밋을 차단하고, FALSE_POSITIVE이면 통과시킵니다. 대부분의 커밋은 1단계를 전혀 트리거하지 않으므로, 대부분의 커밋은 지연 시간이 전혀 발생하지 않습니다.
훅 (The hook)
.git/hooks/pre-commit (또는 원하는 훅 매니저를 통해 연결):
#!/usr/bin/env bash
set -euo pipefail
...
그 안의 두 가지 세부 사항은 보이는 것보다 더 중요합니다.
작업 디렉터리(working tree)가 아닌 스테이징된 콘텐츠를 스캔하세요. git show :=$file은 인덱스(index)를 읽습니다. 만약 디스크에 있는 파일을 스캔한다면, 언스테이징된 임시 내용물로 인해 커밋을 차단하게 될 것이고, 비밀 정보가 스테이징되었지만 작업 복사본에서는 이미 삭제된 경우를 놓치게 됩니다.
종료 코드(exit-code) 계약이 전체 인터페이스입니다. 종료 코드 0은 커밋을 허용하고, 종료 코드 1은 차단하며, 모델의 자유 형식 텍스트는 스스로 어떤 결정도 내리지 않습니다. 저는 첫 번째 줄만 파싱하여 두 토큰 중 하나가 되도록 요구합니다. 작은 로컬 모델들은 가끔 완화하는 문단의 내용을 생성할 수 있는데, 기계가 읽을 수 있는 첫 줄을 강제하는 것이 이들을 파이프라인에서 사용 가능하게 만듭니다.
프롬프트는 대부분 비(非)비밀 정보 목록입니다
프롬프트가 무엇이 비밀인지에 할애하는 단어보다 무엇이 아닌지에 더 많은 단어를 사용하는 것에 주목하세요. 이것은 의도적입니다. 정규 표현식(regex) 단계에서 모델이 보는 모든 것이 비밀처럼 보이도록 보장하기 때문에, 모델의 실제 임무는 자리 표시자(placeholders), 고정 값(fixtures), 템플릿을 인식하는 것입니다. 이렇게 틀을 잡으니 오탐지 차단(false blocks)을 극적으로 줄일 수 있었습니다. 저는 spectr-ai를 구축하면서 이 패턴을 배웠습니다: 작은 모델들은
지연 시간 (Latency). 제 컴퓨터(WSL2, 중급 사양 GPU)에서, 모델이 예열(warm)된 상태의 7b 모델을 사용하면 탐지된 파일 하나당 약 2~4초가 소요됩니다. Ollama가 모델을 먼저 로드해야 하는 경우에는 더 오래 걸립니다. 깨끗한 커밋(clean commits)은 1단계에서 단락(short-circuit)되므로 비용이 전혀 들지 않습니다. .env.example이나 테스트 픽스처(test fixture)를 건드리는 커밋은 몇 초 정도 소요됩니다. 저는 이 정도면 수용 가능하다고 생각하지만, 그렇지 않을 수도 있습니다. 만약 팀이 한 시간에 40번씩 커밋한다면, 모델 단계를 비동기(async)로 유지하거나 권고(advisory) 모드로 운영해야 합니다.
1.5b vs 7b. 저는 먼저 거의 즉각적으로 응답하는 qwen2.5-coder:1.5b를 시도했습니다. 하지만 이 모델은 너무 의욕이 앞섰습니다. 명백히 가짜인 픽스처를 너무 자주 SECRET으로 분류해서, 제가 직접 만든 훅(hook)을 우회하고 싶어질 정도였습니다. 이는 도구의 목적 자체를 무색하게 만듭니다. 7b 모델은 "이것은 tests/fixtures/에 있고 변수명이 FAKE_KEY이다"와 같은 문맥(context)을 읽는 능력이 눈에 띄게 더 좋습니다. 이 작업의 경우 모델이 실행되는 빈도가 낮기 때문에, 저는 더 큰 모델을 사용하는 비용을 지불합니다.
양방향 모두 틀릴 수 있습니다. 로컬 7b 모델은 보안 경계(security boundary)가 아닙니다. 이상한 실제 키가 빠져나갈 수도 있으며, 이 훅은 플랫폼 측 스캐닝(GitHub push protection 및 유사 도구들)을 대체하는 것이 아니라 보완하는 용도입니다. 이 도구가 해결하는 것은 인간 계층(human layer)입니다. 훅이 아주 드물게 경고를 보내기 때문에, 경고가 발생했을 때 저는 반사적으로 --no-verify를 입력하는 대신 실제로 멈춰서 확인하게 됩니다. 사람들이 신뢰하고 따르는 스캐너가, 모두가 우회해 버리는 더 엄격한 스캐너보다 낫습니다.
결정론 (Determinism). LLM의 판정은 경계선에 있는 입력값에 대해 실행할 때마다 결과가 뒤집힐 수 있습니다. 차단형 훅(blocking hook)으로서 저는 이를 수용합니다. 경계선에 있는 사례들이야말로 제가 어차피 사람이 다시 검토하기를 원하는 바로 그 사례들이기 때문입니다.
저는 이것을 몇 달 동안 실행해 왔습니다. 정규 표현식(regex) 단계는 일주일에 몇 번 실행되지만, 모델이 거의 매번 이를 뒤집습니다. 그리고 모델이 SECRET이라고 말했던 단 한 번의 사례는 정확했습니다. 제가 다시 살려내고 있던 오래된 테스트 안에 API 키가 포함된 실제 RPC URL이 있었습니다.
여러분의 현재 비밀 정보 스캐닝(secret-scanning) 설정은 어떠한가요? 그리고 솔직하게 말해 주세요, 얼마나 자주 그것을 우회하시나요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기