AI 코딩 도구가 API 키를 유출하는 방법과 제가 이를 막기 위해 만든 것
요약
AI 코딩 도구가 `.env` 파일의 API 키를 읽어 스크립트에 노출시키는 보안 위협을 지적하며, 이 문제를 해결하기 위해 'Phantom'이라는 아키텍처를 개발했습니다. Phantom은 민감한 환경 변수(secrets)를 실제 자격 증명 대신 암호학적으로 생성된 자리 표시자(placeholder token), 즉 'phantom 토큰'으로 대체하여 AI 경계 밖에서 관리합니다.
핵심 포인트
- AI 코딩 도구는 `.env` 파일의 API 키를 읽어 노출시킬 위험이 높습니다.
- API 키가 LLM 컨텍스트 창에 들어가면 세션 로그, 프롬프트 주입 등 다양한 경로로 유출될 수 있습니다.
- Phantom은 민감한 환경 변수를 'phantom 토큰'으로 대체하여 실제 자격 증명을 보호합니다.
- 개발자는 비밀 정보가 필요한 환경에서도 AI의 노출 위험을 줄일 수 있습니다.
지난주 저는 Claude Code가 제 .env 파일을 읽고, OpenAI API 키를 추출하여 자신이 작성해 주는 셸 스크립트의 curl 명령어에 붙여넣는 것을 목격했습니다. 그 키는 작동했고, 스크립트는 작동했습니다. 그리고 제 sk-proj- 키는 이제 Claude의 세션 기록과 Anthropic의 API 로그 등 디스크상의 파일에 자리 잡게 되었습니다.
저는 Claude에게 키를 사용하라고 요청한 적이 없습니다. 단지... 도움이 되는 에이전트가 하는 것처럼 행동했을 뿐입니다. 환경을 보고, 작업을 이해하고, 필요한 것들을 연결했습니다. 이것이 바로 AI 코딩 도구의 핵심 목적입니다. 하지만 동시에 문제가 되기도 합니다.
위협은 가설이 아닙니다
GitGuardian의 State of Secrets Sprawl 2026 보고서에 따르면, 2025년 공용 GitHub 커밋에서 하드코딩된 시크릿은 2865만 개로, 전년 대비 34% 증가했습니다. AI 서비스 시크릿만 따로 따져도 전년 대비 81% 증가했으며, 이 카테고리별 수치가 모든 시크릿에 적용되는 것은 아닙니다.
AI 코딩 도구가 dotenv 접근 권한을 가질 때 흔히 발생하는 고위험 패턴 중 하나는 다음과 같습니다:
.env파일에OPENAI_API_KEY=sk-proj-abc123...가 있습니다.- AI 에이전트가 프로젝트를 이해하기 위해 해당 파일을 읽습니다.
- 실제 API 키가 이제 LLM의 컨텍스트 창 안에 있게 됩니다.
시크릿이 컨텍스트 창에 들어가면, 여러 다운스트림 시스템들이 노출 경로가 될 수 있습니다:
- 세션 로그(Session logs). 대부분의 AI 도구는 대화 기록을 유지합니다. 사용자의 API 키가
~/.claude/또는~/.cursor/에 JSON 파일 형태로, 아마도 암호화되지 않은 상태로 남아있습니다. - 프롬프트 주입(Prompt injection). 악성 README, 의존성(dependency), 또는 코드 주석은 LLM에게 환경 변수(environment variables)를 유출하도록 지시할 수 있습니다. 이는 이론적인 문제가 아닙니다. Check Point Research는 Cursor와 Claude Code 모두에 대한 CVE를 통해 이를 입증했습니다. 즉, 에이전트가 공격자가 통제하는 서버로 비밀 정보를 보내도록 속이는 조작된 프롬프트(crafted prompts)입니다.
- 악성 MCP 서버(Malicious MCP servers). 모델 컨텍스트 프로토콜(Model Context Protocol)은 AI 도구가 외부 서비스를 호출할 수 있게 합니다. 손상된 MCP 서버는 환경 컨텍스트를 요청하고 사용자의 실제 키를 받을 수 있습니다.
- 실수로 커밋하기(Accidental commits). AI가 키를 하드코딩한 코드를 작성하거나, 실제 자격 증명(real credentials)을 포함하는 테스트 더미 파일(test fixture)을 생성합니다. 개발자가 이를 빠르게 검토하고 승인하여 푸시하게 됩니다.
'.env에 비밀 정보를 넣지 마라'라는 일반적인 조언은 현실을 무시합니다. 개발자는 소프트웨어를 구축하기 위해 환경 변수 내에서 비밀 정보가 필요합니다. 문제는 AI가 .env`를 읽을 것인지 여부가 아닙니다. 문제는 AI가 그것을 읽었을 때 무엇을 발견하느냐입니다.
Phantom 토큰: 실제 자격 증명을 AI 경계 밖으로 유지하기
저는 아키텍처 수준에서 이러한 노출(exposure)을 줄이기 위해 Phantom을 구축했습니다. 핵심 아이디어는 다음과 같습니다. 휴리스틱하게 감지된, Phantom이 관리하는 dotenv 비밀 정보를 phantom 토큰으로 대체하는 것입니다. 이 토큰은 암호학적으로 무작위로 생성된 자리 표시자(placeholder)이며, 업스트림 제공업체(upstream provider)로부터 자격 증명으로 받아들여지지 않습니다.
phantom init을 실행하기 전과 후의 .env 파일 모습은 다음과 같습니다:
# Before
OPENAI_API_KEY=sk-proj-abc123def456ghi789...
STRIPE_SECRET_KEY=sk_live_51HG7a9s8d7f6g5h4j3k2l1...
...
해당 phm_ 토큰은 256비트의 CSPRNG 출력을 사용합니다. 이들은 제공업체 자격 증명(provider credentials)이 아니며 OpenAI, Anthropic 또는 Stripe에 직접 사용할 수 없습니다. 그럼에도 불구하고 보호할 가치가 있는 매핑입니다. 클라이언트 헤더와 본문에서는 이를 해결하지 못하지만, 도난당한 라이브 프록시 베어러는 자체 자격 증명을 주입하는 정확한 경로를 승인할 수 있습니다. 노출된 프로젝트 토큰을 순환(Rotate)시키세요. NODE_ENV나 PORT와 같은 비-비밀 설정 값은 일반적으로 탐지 휴리스틱에 의해 건드려지지 않습니다.
실제 비밀 정보는 플랫폼 자격 증명 백엔드(macOS Keychain, Linux 커널 키링 또는 Windows Credential Manager)를 통해 저장되며, 패스프레이즈가 지속적으로 유지되고 검증될 수 있는 경우 암호화된 파일로 대체됩니다. Linux 커널-키링 항목은 재부팅 시 살아남지 않습니다.
리버스 프록시: 경로 소유 인증 (route-owned authentication)
phantom exec -- claude를 실행하면 Phantom이 127.0.0.1에서 로컬 HTTP 리버스 프록시를 시작하고 환경 변수 내의 기본 URL을 재작성합니다:
OPENAI_BASE_URL=http://127.0.0.1:PORT/openai
ANTHROPIC_BASE_URL=http://127.0.0.1:PORT/anthropic
STRIPE_API_BASE=http://127.0.0.1:PORT/stripe
전체 요청 수명 주기(request lifecycle)는 다음과 같습니다:
사용자 코드 / AI 에이전트
|
| POST http://127.0.0.1:PORT/openai/v1/chat/completions
...
이것은 MITM 프록시가 아닙니다. CA 인증서를 주입하거나 TLS를 깨뜨리지 않습니다. 사용자의 코드는 로컬호스트에 평문 HTTP를 전송하고, 프록시는 업스트림 API로 새로운 TLS 연결을 시작합니다. 이것은 표준 리버스 프록시 패턴이며, 애플리케이션 서버 앞에 위치한 nginx와 동일한 아키텍처입니다.
핵심 설계 결정 사항:
- 검토된 라우트(Reviewed routes). Agentic proxy 세션은 Phantom의 내장 서비스 정의를 정확히 수용하고, 저장소에서 작성된 프록시 목적지는 거부합니다. 이는 주입을 제한하지만, 엔드포인트 무결성(endpoint integrity), DNS/TLS, 로컬 계정, 그리고 구성된 프로바이더는 여전히 위협 모델에 남아 있습니다.
- 로컬호스트 전용(Localhost only). 프록시는
127.0.0.1에 바인딩되며, 절대0.0.0.0에는 바인딩되지 않습니다. 네트워크에서 접근할 수 없습니다. - 임시 포트(Ephemeral ports). 각 세션은 무작위의 높은 포트를 할당받아 로컬 프로세스 오용의 창(window)을 줄입니다.
- 세션 범위 프록시 인증(Session-scoped proxy authentication).
phantom exec는 자식 프로세스에 신선한phm_플레이스홀더와 새로운 프록시 베어러를 제공합니다. 클라이언트 플레이스홀더는 절대 해결되지 않으며, 해당 프록시가 종료되면 베어러도 무효화됩니다. dotenv에 저장된 프로젝트 토큰은 다른 값이며phantom rotate전까지 유지됩니다.
Phantom의 구현된 기본 URL 오버라이드를 존중하는 지원 SDK들은 소스 변경 없이 작동할 수 있습니다. 이는 모든 HTTP 클라이언트나 프로토콜에 대한 주장은 아닙니다. 현재 보호되는 데이터베이스 연결 문자열은 phantom exec를 실패 종료(fail closed) 상태로 만들며, Phantom이 이를 자식 환경으로 주입하지 않습니다.
실제 사용 사례: 세 가지 명령어
# 1. 검토된 v0.7.9 GitHub 릴리스 설치 후, 프로젝트 초기화
$ phantom init
-> .env에서 5개의 항목을 찾았습니다
...
AI 네이티브 비밀 관리를 위한 MCP 서버
Phantom은 또한 Claude Code, Cursor, Windsurf, Codex가 실제 값을 보지 않고도 비밀을 네이티브하게 관리할 수 있도록(목록화, 추가, 로테이션) MCP 서버를 제공합니다:
$ phantom setup --client claude
릴리스 스키마로 검증된 카탈로그에는 현재 vault status, init, 안전한 대화형 비밀 입력, 확인(checks), 진단(diagnostics), 클라우드 동기화(cloud sync), 감사(audit), 유효성 검사(validation), 로테이션, 만료(expiry), 규정 준수(compliance), 팀 금고(team vaults)를 포괄하는 54개의 고유 도구가 포함되어 있습니다. 사용 중단된 평문 추가 도구는 MCP를 통해 비밀 값을 거부하므로 실제 자격 증명은 에이전트 컨텍스트 밖에 머무릅니다. 런타임 tools/list가 표준입니다.
아키텍처: 모듈식 Rust 워크스페이스
phantom-core Config, .env 파싱, 토큰 생성 (256비트 CSPRNG, phm_ 접두사)
phantom-vault VaultBackend 트레이트: OS 키체인 + 암호화 파일 폴백
phantom-proxy HTTP 리버스 프록시 (hyper), 토큰 대체, TLS 포워딩 (reqwest)
...
이 볼트는 Argon2id 키 유도 방식을 사용하여 ChaCha20-Poly1305로 암호화합니다 (암호화 파일 폴백용). 주요 볼트 검색, 직렬화 및 복호화된 파일 버퍼는 목표 제로화(targeted zeroization)를 사용하며, 일부 프록시 조회 사본은 일반 문자열로 남아 있습니다. 제로화는 완전한 메모리 삭제 보장이 아닌 깊이 방어(defense in depth)입니다. CI에서는 릴리스 전에 Rust 테스트 스위트와 clippy를 실행합니다.
클라우드 클라이언트는 업로드 전에 볼트 페이로드를 암호화할 수 있어, 볼트 API가 복호화된 비밀 값 대신 암호문자(ciphertext)를 저장하게 합니다. 이 속성이 클라이언트, 계정, 엔드포인트 또는 배포 서비스의 신뢰 경계를 제거하지는 않습니다.
기존 도구들은 왜 안 될까?
이것을 구축하기 전에 저는 시장 상황을 살펴보았습니다.
1Password / HashiCorp Vault — 다양한 런타임 모델을 가진 엔터프라이즈 비밀 관리 시스템입니다. op run과 같은 환경 주입 워크플로우는 여전히 값을 해당 프로세스 환경을 검사할 수 있는 에이전트에게 노출할 수 있습니다. 이들의 현재 제품 기능은 독립적으로 평가되어야 합니다.
Doppler / Infisical — 런타임에 실제 값을 주입할 수 있는 중앙 집중식 비밀 관리 시스템입니다. 만약 에이전트가 대상 환경을 검사할 수 있다면, 그 특정 통합 패턴은 에이전트 컨텍스트 경계를 넘나들 수 있습니다.
dotenvx — 저장된 상태의 .env 파일을 암호화하여 유용합니다. 하지만 런타임에 비밀 값들은 실제 환경 변수로 복호화됩니다. AI가 그것들을 보게 됩니다.
Phantom이 목표로 하는 설계 격차는 더 좁습니다: 읽을 수 있는 dotenv 값이나 실제 프로세스 환경 자격 증명이 모델 컨텍스트, 전사(transcripts), 생성된 파일 또는 도구 호출에 들어갈 수 있는 에이전트 워크플로우입니다.
시작하기
phantom init
이 명령을 실행하기 전에, 현재 플랫폼 지침을 사용하여 체크섬 검증된 v0.7.9 GitHub 릴리스 바이너리를 설치하세요. 세 가지 명령어는 지원되는 API 경로에 대한 로컬 경계를 설정합니다. .env 파일 읽기 거부, 생성된 설정을 검토하고, 나머지 시스템의 복구 및 CI(지속적 통합) 제어를 사용하세요.
Phantom의 로컬 CLI와 프록시는 MIT 라이선스가 적용된 Rust 소프트웨어이며, 로컬 보호를 위해 호스팅 서비스가 필요하지 않습니다. 호스팅 패키징과 향후 가격 책정은 오픈 소스 라이선스와 별개입니다.
- 웹사이트: phm.dev
- GitHub: github.com/ashlrai/phantom-secrets
- 문서: phm.dev/docs/getting-started
만약 AI 코딩 도구에 API 키를 붙여넣은 적이 있고 죄책감을 느꼈다면 — 당신의 느낌이 맞습니다. 하지만 이것을 고치기 위해 AI 사용을 중단할 필요는 없습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기