내 노트북을 스캔하여 유출된 비밀 정보를 찾아냈고, 무려 62,311개를 발견했다
요약
AI 코딩 도구와 에이전트가 실행 과정에서 생성하는 로그 및 트랜스크립트에 민감한 정보가 평문으로 노출되는 보안 문제를 다룹니다. 이를 해결하기 위해 실제 값 대신 별칭(alias)을 사용하고 실행 시점에만 값을 주입하는 오픈 소스 도구 'keynv'를 소개합니다.
핵심 포인트
- AI 에이전트(Claude Code, Cursor 등)의 세션 로그에 민감한 시크릿이 대량 노출될 수 있음
- 시크릿 매니저는 저장 시점의 보안은 책임지지만, 런타임 텍스트 표면 노출은 막지 못함
- keynv는 별칭 우선 해결(Alias-first resolution) 방식으로 보안 취약점 해결
- 실제 값을 프로세스 트리에서 격리하여 에이전트가 시크릿을 읽지 못하도록 차단
지난주에 저는 제 AI 코딩 도구들이 디스크에 얼마나 많은 민감한 데이터를 쓰고 있는지에 대해 약간의 편집증이 생겼습니다. 그래서 스캐너를 직접 만들었고, 제 홈 디렉토리를 대상으로 실행해 보았습니다.
완전히 평범한 업무일에 발견된 결과는 다음과 같습니다:
$ keynv doctor
! zsh history 5 likely secrets across 1 file
...
62,311개입니다. 평문(plaintext) 상태로, 디스크에, 단 한 명의 개발자 머신에 존재하고 있었습니다.
"하지만 저는 시크릿 매니저(secrets manager)를 사용하는데요"
저도 사용합니다. 하지만 이번 사례에서는 아무런 차이가 없었습니다 — 그리고 그것이 바로 핵심입니다.
Vault, Doppler, 1Password, Infisical... 이 도구들은 모두 한 가지 문제를 매우 잘 해결합니다: 저장된 상태(at rest)의 시크릿을 어디에 보관할 것인가 하는 문제입니다. 위에서 언급한 머신은 이 도구들의 어떤 감사(audit)도 깨끗하게 통과할 것입니다.
하지만 시크릿의 수명은 저장 단계에서 끝나지 않습니다. 어느 시점에 시크릿은 실제 값으로 **해결(resolved)**되어 사용되며, 그 시점에 시크릿이 **런타임 텍스트 표면(runtime text surface)**에 닿는 순간, 저장 단계의 도구들은 더 이상 보호를 제공하지 못합니다. 그러한 표면은 다음과 같습니다:
- 쉘 히스토리 (shell history,
~/.zsh_history) - 터미널의 표준 출력 (stdout)
- CI 로그
- 그리고 — 새롭게 떠오른 큰 문제인 — AI 에이전트의 트랜스크립트 (AI agent's transcript)
왜 AI 에이전트가 상황을 훨씬 더 악화시켰는가
Claude Code는 모든 세션을 JSONL로 저장합니다. Cursor는 로그를 유지합니다. 이러한 도구들은 실행하는 모든 명령과 보는 모든 출력 바이트를 충실하게 기록합니다. 이는 컨텍스트(context)를 유지하기 위한 기능입니다.
하지만 이는 에이전트가 다음과 같은 행동을 할 때마다 다음과 같은 의미를 갖습니다:
.env파일을cat명령어로 읽을 때postgres://user:password@hostURI를 포함한 연결 오류가 발생할 때- 스택 트레이스(stack trace)에 API 키를 echo 할 때
...그 시크릿은 존재조차 잊고 있었던 파일 안에 평문(plaintext) 상태로 디스크에 기록됩니다. 제가 발견한 62,311개의 유출 사례 중 62,306개가 정확히 이러한 트랜스크립트 안에 있었습니다.
내가 만든 것
결국 저는 두 가지 아이디어를 바탕으로 keynv라는 오픈 소스 도구를 만들게 되었습니다.
1. 별칭 우선 해결 (Alias-first resolution)
실제 값을 어디에도 직접 넣는 대신, 별칭(alias)을 참조합니다:
# .keynv.env (커밋해도 안전함 — 값은 절대 포함하지 않고 별칭만 보유함)
OPENAI_API_KEY=@demo.dev.openai-key
DATABASE_URL=@demo.prod.db-url
그런 다음 명령어를 다음과 같이 감쌉니다:
keynv exec -- npm run dev
keynv exec는 각 @alias를 AI 에이전트의 프로세스 트리(process tree)가 읽을 수 없는 권한이 부여된 서브프로세스 (privileged subprocess) 내부에서 실제 값으로 해결(resolve)하며, 실제 환경 변수와 함께 명령어를 포크(fork)합니다. 사용자의 셸(shell), 에디터, 그리고 터미널을 구동하는 에이전트는 오직 별칭(alias) 리터럴만을 보게 됩니다:
사용자 코드: keynv exec -- mysql -p@billing.prod.db_password
│
▼
...
2. 텍스트 표면 스크러빙 (Text-surface scrubbing)
전통적인 방식(복사된 에러 메시지, 수동으로 실행한 cat .env 등)으로 유출되는 비밀 정보에 대해, keynv는 해당 표면을 직접 모니터링하고 정화(clean)합니다:
| 명령어 | 기능 |
|---|---|
keynv doctor | 읽기 전용 스캔 — 유출 가능성이 높은 항목을 카운트하며, 원본 값은 절대 출력하지 않음 |
| ... |
이 두 계층은 상호 보완적입니다. 별칭(alias)은 정보가 기록되기 전에 유출을 차단하고, 스크러빙(scrubbing)은 이를 빠져나간 유출 건을 잡아냅니다.
자신의 기기에서 직접 시도해 보세요 (30초 소요, 읽기 전용)
npm install -g @keynv/cli
keynv doctor
doctor는 스캔 전용입니다. 아무것도 기록하지 않고, 네트워크 호출을 하지 않으며, 매칭 미리보기는 3자로 제한되어 원본 값이 출력에 절대 나타나지 않습니다. 여러분이 어떤 숫자를 얻게 될지 정말 궁금합니다.
솔직한 부분
스크러빙 단계에는 **경합 조건(race window)**이 존재합니다. 비밀 정보가 디스크에 기록되는 순간과 감시자(watcher)가 이를 다시 쓰는 순간 사이에는 평문(plaintext) 상태로 존재하게 됩니다. 저는 이것이 완벽하다고 거짓말하지 않겠습니다. 이 점은 위협 모델(threat model)에 문서화되어 있습니다. 바로 그렇기 때문에 별칭(alias) 방식이 주요(primary) 방어 수단(값이 표면에 아예 도달하지 않음)이며, 스크러빙은 최후의 보루(backstop)인 것입니다.
또한 keynv는 Vault/Doppler의 대체제나, .env의 대체제, 또는 비밀 관리자(secrets manager)가 아님을 명시합니다. keynv는 여러분이 이미 사용 중인 도구 옆에 플러그인처럼 연결됩니다. 저장(storage) 문제는 이미 해결되었지만, 런타임 텍스트 표면 보호(runtime text-surface protection)에 공백이 있습니다.
이 프로젝트는 MIT 라이선스이며, 완전히 로컬에서 작동하고(기기 외부로 아무것도 나가지 않음), 셀프 호스팅(self-hostable)이 가능합니다.
만약 keynv doctor를 실행한다면, 여러분이 발견한 숫자를 댓글로 남겨주세요 — 이것이 얼마나 보편적인 현상인지 궁금합니다. 그리고 만약 위협 모델 (threat model)에서 허점을 발견하신다면, 의견을 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기