
AI 에이전트에게 비밀을 맡기지 마라. op run만으로는 부족했다
요약
AI 에이전트에게 API 키와 같은 비밀 정보를 노출하지 않기 위한 보안 전략을 다룹니다. 1Password CLI를 통한 환경 변수 관리의 한계를 지적하며, MCP(Model Context Protocol) 브로커 활용과 Claude Code의 권한 설정(deny)을 통한 실질적인 보안 방안을 제시합니다.
핵심 포인트
- op run은 디스크 상의 값만 보호할 뿐, 실행 중인 프로세스나 셸 권한은 막지 못함
- MCP 서버를 브로커로 활용하여 에이전트 컨텍스트에 비밀값이 직접 포함되지 않도록 설계해야 함
- Claude Code의 permissions.deny 설정을 통해 에이전트의 파일 접근을 물리적으로 차단 가능
- 에이전트의 권한 설정 시 bypassPermissions와 같은 예외 상황을 주의해야 함
결론부터 말씀드리면
AI 에이전트에게 비밀(API 키나 토큰)을 다루게 할 때, ".env에 실제 값을 쓰지 않고 1Password를 참조하게 한다"는 것만으로는 다 지켜낼 수 없습니다. 그것은 디스크 상의 실제 값을 지울 뿐, 셸(Shell)을 실행할 수 있는 에이전트는 결국 그 비밀을 스스로 가져올 수 있기 때문입니다.
진정한 해결책은 두 가지가 있습니다. 브로커(Broker)를 통해 사용하여 실제 값을 에이전트의 컨텍스트(Context)에 싣지 않는 것. 그리고 읽게 하고 싶지 않은 파일을 권한(deny)으로 물리적으로 막는 것입니다. 게다가 Claude Code에는 그 deny 설정을 무용지물로 만드는 함정이 있었습니다. 차례대로 적어보겠습니다.
.env와 1Password의 이중 관리를 그만두고 싶다
계기: 1Password와 AI 에이전트의 연동에 관한 기사를 읽은 것이 발단이었습니다. 동기는 단순했습니다. 1Password와 로컬의 .env에서 동일한 비밀을 이중으로 관리하는 것을 그만두고 싶었습니다.
방법 자체는 1Password CLI로 이전부터 가능했습니다. .env에는 실제 값을 쓰지 않고 참조만 작성합니다.
OPENAI_API_KEY=op://Private/openai/api_key
실행 시에 주입합니다.
op run --env-file=.env -- your-app
이렇게 하면 원본은 1Password 하나뿐이게 되어 로컬에 실제 값이 남지 않습니다. 이중 관리는 사라집니다. 여기까지는 좋은 이야기입니다.
하지만 이것은 에이전트 대책이 아니다
문제는 op:// 참조가 지우는 것이 오직 "디스크 상의 실제 값"뿐이라는 점입니다. 에이전트가 무엇을 할 수 있는지에 따라 구분해서 생각하면 효과가 명확히 달라집니다.
- 파일을 읽기만 한다(
cat .env)면 방지할 수 있습니다. 실제 값이 거기에 없기 때문입니다. - 셸을 실행할 수 있고, op의 세션이 unlock 되어 있다면 방지할 수 없습니다. 에이전트도
op read를 실행할 수 있어 동일한 권한으로 비밀을 가져올 수 있습니다. printenv로 실행 중인 프로세스의 환경 변수를 들여다보면,op run이 주입한 실제 값이 거기에 있습니다.
즉, op run은 at-rest, 즉 저장 시의 대책일 뿐 에이전트의 권한 그 자체에 대한 대책이 아닙니다. "op를 도입했으니 안전하다"는 오판이었습니다. 이 지점이 분기점입니다.
본명 1: 브로커(MCP)를 통해 사용하기
비밀을 "에이전트의 컨텍스트에 싣지 않으려면" 브로커를 사이에 두는 것이 가장 정석입니다.
예를 들어 Google Drive나 Google Tasks를 MCP 서버를 경유하여 사용하는 구성에서는, token.json을 읽는 것은 MCP 서버의 프로세스이며, 에이전트(Claude)는 search()와 같은 함수를 호출할 뿐입니다. 토큰의 실제 값은 에이전트의 컨텍스트에 들어가지 않습니다. MCP는 이 점에서 이미 "에이전트가 비밀을 직접 참조하지 않는" 형태를 갖추고 있습니다.
남은 구멍은 하나입니다. 에이전트가 MCP를 사용하지 않고, cat .secrets/token.json으로 파일을 직접 읽으러 가는 경로입니다.
본명 2: 읽게 하고 싶지 않은 파일은 deny로 막기
그 직접 읽기 경로는 Claude Code의 권한 설정으로 막을 수 있습니다. settings.json의 permissions.deny에 다음과 같이 작성합니다.
"Read(**/.secrets/**)",
"Read(**/.env)",
"Read(**/.env.*)",
글로벌 설정에 작성하면 모든 프로젝트에 적용되며, **/로 설정해 두면 작업 디렉토리에 의존하지 않고 잡아낼 수 있습니다.
결정적인 함정: bypassPermissions에서는 ask가 무시된다
여기서 한 번 뼈아픈 착각을 했습니다.
defaultMode를 bypassPermissions로 설정하면, ask(실행 전에 확인하는 설정)는 전부 통과되어 버립니다. 저는 비밀을 Read(./.env)로 설정하여 ask에 넣어두었기에 안전하다고 믿고 있었습니다. 하지만 bypass 상태에서는 ask가 작동하지 않아, 에이전트는 .env를 그대로 읽을 수 있었습니다.
효과가 있는 것은 deny뿐입니다. allow도 ask도 bypass 상태에서는 모두 통과됩니다. 정말로 막고 싶은 비밀은 반드시 deny에 작성해야 합니다. deny는 bypass 상태에서도 작동합니다. 이 부분을 혼동하면, 뚜껑을 덮었다고 생각하면서 실제로는 열어두고 있는 셈이 됩니다.
남은 구멍과 얻은 교훈
솔직히 말씀드리면, 이 방법으로도 완전히 막지 못한 구멍은 있습니다. deny...
는 Read / Write 도구용 덮개이기 때문에, Bash에서 cat을 실행하면 대상에서 제외됩니다. 위험한 명령어를 차단하는 후크(hook) 측에서 cat .secrets나 cat .env를 차단하면, 그 경로도 막을 수 있습니다.
요약하자면, 얻은 교훈은 세 가지입니다.
op run은 at-rest(저장 시) 대책이지, 에이전트 권한에 대한 대책이 아닙니다. "저장 장소를 바꾼 것"과 "에이전트가 가져올 수 없게 된 것"은 별개입니다.- 에이전트 대책의 핵심은 브로커(MCP)를 통해 생값(raw value)을 컨텍스트(context)에 싣지 않는 것과,
deny권한으로 읽지 못하게 하는 것, 이 두 가지를 병행하는 것입니다. bypassPermissions를 사용한다면, 비밀 보호는ask가 아니라deny에 작성해야 효과가 있습니다.
질문은 "어디에 저장할 것인가"가 아니다
AI에게 비밀을 전달할 때 물어야 할 것은 "어디에 저장할 것인가"가 아니라 "에이전트가 생값을 가져올 수 있는 경로가 남아 있는가"입니다. 저장 장소를 깨끗하게 관리하더라도, 가져올 수 있는 길이 남아 있다면 의미가 없습니다.
저희가 AI를 업무 시스템에 통합하는 프로젝트를 진행할 때는, 키(key)의 보관 장소를 논의하기 전에 먼저 이 경로를 차단합니다.
Discussion

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