본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 06. 15. 04:39

AI 에이전트 시대의 공개 전 유출 방지 ― NG 워드 사전을 암호화하여 git push 전에 차단하는 neko-not-yoshi

요약

AI 에이전트 시대의 코드 유출 사고를 방지하기 위해, git push 전 개인정보와 기밀을 탐지하여 차단하는 스캐너 'neko-not-yoshi'의 설계와 운용 방식을 소개합니다. 기존 시크릿 스캐너가 놓치기 쉬운 문맥 의존적 기밀(고객명, IP 등)을 보호하는 데 중점을 둡니다.

핵심 포인트

  • AI 에이전트의 대량 코드 생성에 따른 육안 검토 한계 극복
  • 문맥 의존적 기밀(고객명, IP 등) 탐지를 위한 2층 분리 사전 설계
  • exit code를 활용하여 언어와 도구에 구애받지 않는 push 차단 메커니즘
  • 오탐 억제를 위한 downgrade 규칙 및 3가지 리스트(blacklist/whitelist/allowlist) 설계

개요

공개 리포지토리(Public Repository)로의 git push

전에, 개인정보·고객명·글로벌 IP를 기계적으로 탐지하여 push를 중단시키는 스캐너 neko-not-yoshi의 설계와 운용을 해설한다. 이름의 유래는 "고양이가 '요시(좋아/OK)'라고 말할 수 없는 것을 탐지한다"는 의미다.

주제는 세 가지다. NG 워드 사전의 2층 분리(공개 가능한 정규 표현식 패턴 / 암호화된 고객명 실체), 혼동하기 쉬운 blacklist·whitelist·allowlist의 3가지 리스트 설계, 그리고 보안 도구 특유의 난제인 IOC(위협 인텔리전스 지표)의 오탐(False Positive)을, 놓치는 것이 없는 방침을 유지하면서도 억제하는 downgrade 규칙이다.

OSS로서 이미 공개됨 (MIT). 지난번에 해설한 pii-mask-yoshi(AI에게 읽히기 의 방어)와 대조되는, 공개하기 의 방어 계층에 해당한다.

배경: 유출은 "공개하는 순간"에 발생한다

AI 코딩 에이전트의 보급으로, 한 세션에 수십 개의 파일이 생성·편집되며, 그중 상당수가 제대로 된 리뷰 없이 public 리포지토리로 push되는 시대가 되었다. 문제는 생성량에 비해 인간의 육안 확인이 따라가지 못한다는 점이다. 유출은 악의가 아니라 사고로 발생한다.

  • 조사 로그를 그대로 docs/에 붙여넣기 (사내 IP 주소나 이메일 주소가 남아 있음)
  • 실제 환경의 설정을 README 샘플로 유용하기 (글로벌 IP가 그대로 노출됨)
  • 고객명이 포함된 작업 메모가 커밋(Commit)에 섞여 들어감

gitleaks로 대표되는 기존의 secret scanner는 API 키나 토큰과 같이 "형식이 정해져 있는 기밀"의 탐지가 주 전장이다. 반면, "고객명", "일본어 개인 이름", "이 IP는 글로벌인가"와 같은 조직 고유의·문맥 의존적인 기밀은 방어 범위에서 벗어나기 쉽다. 이 부분을 메우는 것이 neko-not-yoshi다.

이단 방어: "읽히기 전"과 "공개하기 전"

구분pii-mask-yoshineko-not-yoshi
방어 타이밍AI에게 파일을 읽히기 전공개 리포로 push하기 전
구현 레이어MCP 도구 계층 (safe_read / unmask_file)CLI + push 전 게이트 (exit code 연동)
보호 대상LLM 서버로의 PII(개인식별정보) 전송public 리포로의 유출 혼입
방식가역 토큰 치환 (Masking)탐지하여 push를 차단 (Blocking)

입구(AI로의 입력 경로)와 출구(공개로의 출력 경로)에서 보호하는 장소가 다르기 때문에, 어느 한쪽만으로는 빈틈이 남는다. 본고는 출구 측에 관한 이야기다.

push를 차단하는 메커니즘: exit code 연동의 최후의 보루

scan은 block 탐지 시 exit 1을 반환한다. 쉘(Shell)의 &&로 연결하는 것만으로 push 게이트가 된다.

node src/cli.mjs scan <repo> && git push

메커니즘이 CLI와 exit code뿐이므로, AI CLI의 종류나 개발 언어에 의존하지 않는다. pre-push hook이나 CI 잡(Job)에도 그대로 통합할 수 있다.

실제 탐지 사례를 본다. 다음 내용을 포함하는 디렉토리를 스캔한다.

# incident report
contact: taro@example.org
C2: 203.0.113.7
...
$ node src/cli.mjs scan ./demo
report.md:2: [warning][pii] taro@example.org (email)
report.md:3: [block][network] 203.0.113.7 (ipv4)
...

중요도는 두 단계로 나뉜다.

  • block: push를 중단함 (exit 1). 글로벌 IP 등
  • warning: 보고만 함 (exit 0). 이메일 주소, 프라이빗 IP 등

IPv4는 검증기(Validator)를 통해 글로벌과 프라이빗을 구분한다. 192.168.1.10은 내부 정보로서 보고에 남지만 push는 통과하며, 글로벌 IP는 block으로 차단된다. --warnings-as-errors를 붙이면 warning도 exit 1로 만들 수 있다.

스캔 범위 설계

기본 스캔 대상은 「git 추적 파일 + 미추적(.gitignore 대상 외) 파일」, 즉 git ls-files --cached --others --exclude-standard와 상당 부분 일치한다. 여기에는 두 가지 이유가 있다.

  • git add를 하기 의 신규 파일이야말로 유출될 수 있으므로, 미추적 파일도 검사한다.
  • .gitignore 처리된 로컬 생성물(실행 로그 등)은 push 되지 않으므로 제외한다.

--all을 붙이면 .gitignore 처리된 것을 포함하여 전체 스캔이 가능하다.

NG 워드 사전의 2층 분리

검출 패턴은 두 개의 파일로 분리되어 있다.

파일내용공개 여부
ngwords.public.json정규 표현식 패턴 (email / 전화 / IP / 홈 패스 / 로컬 패스)리포지토리 포함 가능
ngwords.private.json고객명·개인명의 실체.gitignore 대상, 로컬 한정

이러한 분리를 통해 "검출기 그 자체를 공개하더라도, 어떤 고객명·고유명사를 검출하고 있는지는 유출되지 않는다". public 측은 구조적 패턴만 포함하며 구체적인 명칭을 포함하지 않고, 고객명과 같은 고유명사는 private 측에 격리된다.

등록은 수동 입력과 AI 세션으로부터의 자동 축적이라는 두 가지 경로가 있다.

node src/cli.mjs add --private "고객명" # private (.gitignore)에 등록
node src/cli.mjs add --public "<regex>" --category pii # public 패턴 등록
node src/cli.mjs import words.csv # CSV/TXT/Markdown 일괄 투입
...

또한 export는 기본적으로 private 리스트를 출력하므로, 출력 파일 자체가 기밀이 된다. 출력 위치는 .gitignore 처리된 디렉토리로 한정한다.

3가지 「리스트」의 구분 사용

이런 종류의 툴에서 혼동하기 쉬운 것이 blacklist / whitelist / allowlist의 구분이다. neko-not-yoshi에서는 역할을 명확히 나누고 있다.

구분NG 워드 사전 (blacklist)whitelistallowlist
단위용어 단위 (글로벌)패턴 / 단어검출 단위 (경로 + 카테고리)
효과검출한다매치 대상에서 제외한다특정 검출을 억제 (allow) 또는 강등 (downgrade)
용도검출 대상의 정의애초에 기밀이 아닌 일반 IT 용어특정 파일의 기지(known) false positive

whitelist: 일반 IT 용어의 제외

NG 워드를 AI 세션으로부터 자동 축적하다 보면, "AWS", "Kubernetes", "OAuth"와 같은 일반 IT 용어가 사전에 혼입되어 오검출이 늘어난다. whitelist에 등록된 용어는 NG 워드 매치에서 제외된다 (대소문자 구분 없는 완전 일치. "API"를 등록해도 "API Gateway"는 스킵되지 않는다).

공개 버전인 ngwords-whitelist.json에는 큐레이션된 319개의 일반 IT 용어(AWS/Azure/GCP 서비스명, OSS 프로젝트명, 프로토콜, 규격명)가 포함되어 있으며, 환경 고유의 용어는 .local.json (.gitignore 대상)으로 분리하여 추가할 수 있다.

allowlist: 검출 단위의 핀포인트 억제

allowlist의 각 엔트리는 action을 가진다.

  • `

allowlist의 downgrade가 진가를 발휘하는 지점은 IOC (Indicator of Compromise)의 오검출이다.

보안 탐지 도구의 리포지토리는 공개 위협 인텔리전스의 C2 서버 IP를 README나 탐지 스크립트 본문에 직접 작성하는 경우가 있다. 이는 도구의 탐지 대상이지, 자체 리포지토리의 유출이 아니다. 하지만 스캐너는 이를 구분하지 못하고 글로벌 IP로서 block 처리한다.

이를 per-IP × 파일 한정의 downgrade 엔트리로 억제한다.

{"action":"downgrade","match":"<IOC IP>","category":["network"],"pathGlob":"**/scan.sh"}

제어 장치는 2개 층으로 구성된다.

  • match는 특정 IOC IP의 엄격한 리터럴 (서브넷이나 와일드카드는 사용하지 않음)
  • pathGlob은 해당 IOC가 나타나는 실제 파일로 한정

이 구성에서는, 나열되지 않은 실제 글로벌 IP(향후 유출 포함), 동일 파일 내의 다른 실제 IP, pathGlob 외에 나타난 동일한 IOC IP는 모두 block 상태로 유지된다. 게다가 downgrade이므로 해당 IOC도 warning으로서 보고에 계속 남는다.

운영 절차도 "인간의 확인"을 거치는 방식으로 고정되어 있다.

  • 대상 리포지토리를 scan하여, block되는 IOC IP와 출현 파일을 특정한다.
  • 해당 IP가 공개된 위협 인텔리전스의 IOC이며, 자체 리포지토리의 유출이 아님을 인간이 확인한다.
  • allowlist에 IP × 출현 파일만큼의 엔트리를 추가한다.
  • 다시 scan하여 block=0이며, 해당 IP가 warning으로 남아 있는지 확인한다.

여담이지만, 이 기사 자체도 공개 전에 동일한 게이트를 통과했다. 기사 중의 데모용 IP (RFC5737의 문서용 예약 범위)가 실제로 block 판정을 받았고, 바로 위의 절차대로 "IP × 본 기사 파일 한정"의 downgrade 엔트리를 적용했다. IOC 문제의 미니어처 버전이, IOC 문제를 해설하는 기사 위에서 재현된 격이다.

private 사전의 암호화 (AES-256-GCM)

여기서 아이러니한 문제가 발생한다. 고객명을 검출하기 위한 private 사전은 고객명 목록이라는 가장 농밀한 기밀 파일이 된다. .gitignore를 통한 격리만으로는 로컬에 평문으로 계속 놓여 있다는 사실에는 변함이 없다.

따라서 private 사전은 AES-256-GCM으로 저장 시 암호화할 수 있다.

node src/cli.mjs keygen # 키 생성 (.neko-keyfile, .gitignore 대상)
node src/cli.mjs encrypt --delete-source # 암호화 후 평문 삭제
node src/cli.mjs decrypt # 편집 시에만 복호화

키는 다음 우선순위에 따라 해결된다.

우선순위소스용도
1NEKO_ENCRYPT_KEY 환경 변수Base64 인코딩된 256비트 생키(raw key). CI/CD용
2NEKO_ENCRYPT_PASSPHRASE 환경 변수패스프레이즈 (scrypt로 키 유도)
3.neko-keyfile (프로젝트 루트)keygen으로 생성. 개인 머신용

실용상의 요점은 **투명한 복호화 (transparent decryption)**이다. scan은 평문인 ngwords.private.json이 없을 경우, 암호화된 ngwords.private.enc.json을 자동으로 검출하여 메모리 상에서 복호화한다. 스캔할 때마다 수동으로 복호화하여 평문을 만들 필요가 없다 (평문이 디스크에 나타나는 것은 사전을 편집할 때뿐이다). 또한 GCM은 인증된 암호 (Authenticated Encryption)이므로, 암호화 파일의 변조도 검출할 수 있다.

v0.1.4: false positive과의 싸움

도입 직후 마주치는 가장 큰 적은 오검출이었다. NG 워드를 AI 세션으로부터 자동으로 축적하는 설계는 포괄성을 높이는 한편, 일반 용어의 혼입으로 오검출이 늘어나 게이트가 "양치기 소년"화된다. v0.1.4는 이 대책이 중심이다.

  • whitelist 319개 단어 동봉: 앞서 언급한 큐레이션된 일반 IT 용어 -
  • 멀티 모델 LLM 크로스 체크 (Multi-model LLM Cross-check): NG 워드 후보 리스트를 로컬 Ollama의 여러 모델(기본 2개 모델)에 '기밀인지 일반 용어인지' 판정하게 하는 보조 도구. 판정의 통합은 1개 모델이라도 기밀로 판정하면 남기는 안전 우선 방식(Safety-side merge)을 채택하며, 판정을 내리지 못한 후보도 남기는 쪽으로 처리한다. 이는 오검출(False Positive)의 감소가 검출 누락(False Negative)을 늘리는 방향으로 작용하지 않도록 하기 위한 설계다. 로컬 LLM만으로 완결되며, 외부 API로는 전송하지 않는다 -
  • serve (Web UI): node src/cli.mjs serve를 실행하면 localhost:7307에 관리 화면이 구동되어, 브라우저에서 NG 워드 및 whitelist를 확인하고 편집할 수 있다.

여기서 중요한 구분이 있다. **LLM을 사용하는 것은 사전 유지보수뿐이며, 스캔 자체는 결정적인 패턴 매칭 (Pattern Matching)**이다. 스캔에 LLM을 섞으면 결과의 재현성이 상실되어, push 게이트로서의 신뢰성이 무너진다. 결정적인 스캔과 확률적인 유지보수 보조. 이 선긋기는 의도적인 것이다.

실전: block을 밟은 후 push가 통과하기까지

실제 운용은 'block 발생 → 분류 및 대처 → 재스캔'의 루프가 된다.

# 1. 스캔에서 block 검출, push 중단
$ node src/cli.mjs scan . && git push
report.md:2: [warning][pii] taro@example.org (email)
...

mask가 기본적으로 dry-run인 이유는, 게이트 도구가 임의로 파일을 수정해서는 안 되기 때문이다. 수정은 --write를 명시했을 때만 수행된다. 또한 mask가 마스킹(Masking) 처리를 하는 것은 block 검출에 한하며, warning은 대상이 아니다 (--include-warnings로 포함할 수 있다). 앞선 출력에서 email이 warning 상태로 남아 있는 이유가 바로 이것이다.

방지할 수 있는 것 / 방지할 수 없는 것

방지할 수 있는 것

리스크방어 방법
글로벌 IP가 포함된 채로 pushblock + exit 1로 push 중단
등록된 고객명·개인명 혼입private 사전 매칭으로 검출 (customer는 강등 불가)
git add 전의 신규 파일로부터의 유출추적되지 않은 파일(untracked files)도 스캔 대상
private 사전 자체의 유출.gitignore + AES-256-GCM 저장 시 암호화
오검출로 인한 운용 붕괴 (양치기 소년화)whitelist / allowlist / LLM 크로스 체크

방지할 수 없는 것 (한계)

리스크이유
사전에 등록되지 않은 고유명사패턴이나 단어 리스트에 일치하지 않음
...

명확히 해두고 싶은 점은, 본 도구가 유출 제로를 보장하지는 않는다는 것이다. scan의 PASS가 의미하는 것은 '기존 패턴과 사전에 대한 검출이 제로'라는 뜻이며, 그 이상은 아니다. 최종적인 공개 여부 판단은 인간의 리뷰를 전제로 한다 (README의 면책 조항에도 명시되어 있다). push 전 게이트의 역할은, 인간이 실수로 놓치는 것을 기계로 줄이고, 기계가 원리적으로 놓치는 것을 인간이 잡는—그 보완 관계의 한 축을 담당하는 것이다.

요약

  • git push 전의 exit code 연동 게이트를 통해, '공개하는 순간'에 마지막 기계 검사를 넣는다.
  • 사전의 2층 분리 (public = 패턴 / private = 실체)와 AES-256-GCM 암호화를 통해, '검출기의 공개'와 '검출 대상의 비닉(秘匿)'을 양립한다.
  • blacklist / whitelist / allowlist의 3가지 리스트와 IOC downgrade를 통해, 놓치는 것이 없는 것을 목표로 하는 방침을 유지하면서 오검출을 운용 단계에서 억제한다.
  • LLM은 스캔이 아니라 사전 유지보수에 사용한다 (결정적 스캔 + 확률적 유지보수 보조의 분리).

지난번 pii-mask-yoshi (읽기 전)와 이번 글의 neko-not-yoshi (공개 전)를 통해, AI 에이전트 운용의 입구와 출구에 방어층이 갖춰졌다. 다음에는 이 두 가지를 포함한 에코시스템 전체의 구성을 소개할 예정이다.

공개일: 2026-06-12

저자: aliksir

라이선스: MIT

Discussion

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0