데스크톱 AI 비서용 비밀 스크러버 구축기: 7가지 패턴, 엔트로피 게이트, 그리고 모델 호출 제로
요약
본 글은 데스크톱 AI 비서 애플리케이션에 비밀 정보 유출을 방지하는 '비밀 스크러버(secret scrubber)' 구축 과정을 다룹니다. 이 모듈은 디스크에 기록되는 모든 영속성 경계에서 작동하며, 모델 호출 없이 다양한 패턴과 엔트로피 분석을 통해 민감한 정보를 탐지하고 마스킹합니다.
핵심 포인트
- 비밀 스크러버는 앱의 모든 영속성 지점에서 동작하여 데이터 유출을 방지합니다.
- 엔트로피 게이트와 정규식을 결합해 일반적인 비밀 패턴(api_key=)과 높은 무작위성을 가진 값을 포착합니다.
- Base64 등 인코딩된 래퍼도 최대 재귀 깊이 2까지 디코드하여 비밀 여부를 검사합니다.
- 마스킹은 쓰기 경로(`redactValue`)에서 실행되어 원본 모델 메시지를 변경하지 않습니다.
데스크톱 AI 비서용 비밀 스크러버 구축기: 7가지 패턴, 엔트로피 게이트, 그리고 모델 호출 제로
Ankita는 저의 오픈 소스 데스크톱 AI 동반자입니다. “작은 친구지만, 훨씬 더 많은 가능성을 가진 존재”죠. 이 앱은 제 데스크톱에 상주하며, 제가 자리를 비운 동안 예약된 작업을 실행하고, 전사(transcript)를 보관하며, Telegram 알림을 발생시킵니다. 이 모든 것은 한 가지를 의미합니다. 즉, 제가 채팅창에 토큰 하나를 붙여넣는 순간, 그 토큰이 디스크의 세션 JSON, 데몬 로그, 알림 메시지, 메모리 파일 등으로 유출되기를 원한다는 것입니다.
그래서 저는 비밀 스크러버(secret scrubber)를 만들었습니다. 앱 내 모든 영속성 경계(persistence boundary)에서 무언가가 디스크에 기록되기 전에 호출하는 모듈 하나인 src/security/secret-scrubber.mjs (116줄)입니다. 그리고 이 과정에서 모델을 한 번도 호출하지 않습니다: 헤더 주석이 명확하게 말해줍니다. “영속성 경계 감지: 컴파일된 패턴, 네트워크 또는 모델 호출 없음.” 모델에게
2. 엔트로피로 판단되는 레이블 토큰. 실제 세계의 비밀 정보는 항상 배지를 달고 다니지 않습니다. 따라서 LABEL 정규식(regex)은 api_key=, token:, secret "…" 등, 일반적인 용의자 패턴을 감시합니다. 레이블된 값이 비밀로 간주되려면 최소 12자 이상이어야 하며, 해당 레이블이 문자 그대로 password/passwd/pwd가 아닌 경우 샤논 엔트로피(Shannon entropy)가 문자당 4.2 비트보다 높아야 합니다. 이 단 하나의 규칙 덕분에 "토큰 개수를 사용하여 비용을 추정하세요"라는 문구는 마스킹되지 않으면서도 api_key=aB3cD4…와 같은 값은 포착됩니다. CONTEXT_KEY 정규식은 JSON 객체 키에 대해 동일한 역할을 수행하여, 중첩된 객체, 배열, 심지어 JSON 문자열 내에서도 {"private_key": "…"}가 마스킹됩니다.
3. 인코딩된 래퍼(Encoded wrappers), 깊이 ≤ 2. AWS 키를 숨긴 base64 블롭도 여전히 AWS 키입니다. 길이가 16자 이상인 모든 후보 값은 URL 디코드 및 base64 디코드 과정을 거치며 (최대 16,384자로 제한, 최대 재귀 깊이는 2), 디코딩된 형태에 비밀 정보가 포함되어 있으면, 이 래퍼 자체는 encoded_secret으로 플래그 지정되고 제자리에 마스킹됩니다. Data URI (data:image/…)는 예외입니다. 모든 붙여넣은 스크린샷을 디코드하는 것은 성능과 안정성 측면에서 재앙이 될 것이기 때문입니다.
겹치는 탐지 결과(Overlapping hits)는 시작 지점별로 정렬되어 단일 범위로 병합되며, 마스킹 과정은 위상 불변적(idempotent)입니다: redact(redact(x)) === redact(x)가 성립합니다. 이는 마커 형식인 [REDACTED:type] 자체가 다음 처리 단계에서 인식되고 건너뛰어지기 때문입니다.
스크러빙은 두 방향으로 실행됩니다
탐지는 설계의 절반에 불과합니다. 이 모듈은 다섯 가지 함수—detect, redact, containsSecret, secretValues, 그리고 redactValue—를 내보내며, 그중 마지막 두 개가 가장 중요합니다.
redactValue는 쓰기 경로(write path)이며, 해당 문서 주석에는 전체 철학이 담겨 있습니다: "디스크/로그/내보내기를 위한 새 사본을 만드세요. 라이브 모델의 메시지를 절대 변경하지 마십시오." 모든 영속성 지점—세션 기록, 프로젝트 파일, 프로필 메모리, 데몬 경고 메시지—은 기본적으로 이 함수를 호출합니다. src/core/sessions.mjs에서는 saveSession(file, data, { transform = redactValue }): 마스킹이 선택 사항이 아니라 기본값입니다.
SecretHistory (데스크톱 프로세스 내)는 반대 방향을 처리합니다.
사용자가 "이것을 내 openai 키로 저장해"라고 입력하면, save-intent 정규식과 명명 패턴이 실제 값을 슬러그화된 이름(최대 80자, 다중 비밀번호 붙여넣기의 경우 -1, -2 접미사 사용)으로 OS 키체인에 전송합니다. 그 이후로는 해당 값이 어디에서 복사되든 [STORED:keychain:openai-key]로 대체됩니다. 이 마커는 부분적인 중첩이 대체품을 손상시키지 못하도록 가장 긴 순서(longest-first)로 처리되며, 기존 마커는 보존됩니다. 원본 값은 메모리 또는 볼트 암호문 내에만 존재합니다. 이것이 에코 보호 기능입니다: 비밀 값이 초안, 내보내기, 또는 붙여넣어진 모델 응답에 놓이더라도 확산되기 전에 참조 마커로 대체됩니다.
적대자처럼 테스트하기
테스트 스위트는 50개의 샘플 코퍼스를 던집니다: 모양화된 토큰(shaped tokens), URL 인코딩 형태, base64 형태, 그리고 passwd=hunter2와 같은 레이블링된 가짜 값들입니다. 모든 샘플은 감지되어야 하고, 모든 검열 작업은 항등원적(idempotent)이어야 하며, 어떤 샘플도 정화 과정을 생존해서는 안 됩니다. 그다음은 오탐지 테스트: "비밀번호를 재설정하려면 어떻게 하나요?", "비밀스러운 생일 깜짝 이벤트를 해야 해요.", "토큰 수를 사용하여 비용을 추정해 주세요." — 이들 중 어느 것도 건드려서는 안 되며, 일반 메시지는 평균 1밀리초 미만이어야 합니다. 비밀 감지기는 아무도 눈치채지 못하는 키 입력 근접 경계마다 실행될 만큼 충분히 빨라야 합니다.
제가 여전히 스스로 논쟁 중인 질문
redactValue는 의도적으로 실제 대화에는 전혀 영향을 미치지 않습니다. 실행 중인 채팅은 실제 비밀을 유지하는데, 그 이유는 동반자가 실제로 작업을 수행하는 데 그것이 필요하기 때문입니다 — 검열된 모델로는 API를 호출할 수 없습니다. 디스크, 로그, 그리고 내보내기 파일만 깨끗한 사본을 받습니다.
하지만 그것이 올바른 경계일까요? 대안은 금고 참조 모델(vault-reference model)입니다. 즉, 라이브 채팅은 항상 [STORED:keychain:openai-key]만 보게 되고, 실제 값은 모델이 이를 참조하여 요청한 후 도구 실행 경계(tool-execution edge)에서 주입됩니다. 이는 훨씬 더 완벽합니다—맥락 창(context window)에 비밀 정보가 전혀 없습니다—하지만 모든 도구 경계마다 복잡성을 요구하며, 원본 값을 가지고 있을 때 오히려 모델을 더 똑똑하게 만드는 바로 그 지점에서 모델의 흐름을 깨뜨립니다.
현재 저는 비밀 정보를 라이브 메모리(live memory)에 유지하고 사본들을 스크러빙합니다. 여러분 생각은 어떠신가요—모든 것을 스크러빙할까요, 아니면 에이전트의 작업 기억(working memory)을 온전히 보존할까요?
Ankita는 https://github.com/akyourowngames/A.N.K.I.T.A에서 오픈 소스(MIT)로 공개되었습니다—탐지기는 src/security/secret-scrubber.mjs에 있고 에코 보호 측면은 desktop/electron/secret-history.mjs에 있습니다. 속이려 노력해 보세요. 만약 누출을 발견한다면, 그것이야말로 남길 수 있는 가장 유용한 댓글일 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기