
AI 에이전트에게 GitHub 키를 "만지게 하지 않는" 설계 — s1ngularity에서 배우는 최소 도달(least-reach) 구축법
요약
AI 코딩 에이전트의 자율성이 초래하는 새로운 보안 리스크와 '최소 도달(least-reach)' 설계 원칙을 다룹니다. s1ngularity 공격 사례를 통해 간접 프롬프트 인젝션과 인증 정보 유출 위험을 분석하고 대응 방안을 제시합니다.
핵심 포인트
- AI 에이전트의 자율적 파일 탐색 능력이 새로운 공격면(Attack Surface)으로 작용
- 간접 프롬프트 인젝션(IPI)을 통한 악성 명령 실행 위험성
- 치명적 삼제(Lethal Trifecta): 데이터 접근, 신뢰할 수 없는 콘텐츠, 외부 반출 수단의 결합
- 최소 도달(Least-reach) 원칙을 통한 에이전트 권한 범위 제한 필요성
Claude Code나 Gemini CLI와 같은 AI 코딩 에이전트는 이제 많은 개발 현장의 표준 장비가 되어가고 있습니다. 파일을 읽고, 명령을 실행하며, Issue나 README를 해석하여 코드를 작성하는 것——그 자율성이야말로 가치이지만, 동일한 자율성이 이전과는 질적으로 다른 보안 리스크를 만들어내고 있습니다.
2025년 8월의 nx「s1ngularity」 공격은 그 전형적인 사례였습니다. 악의적인 npm 패키지가 피해자의 머신에 이미 설치되어 있던 AI CLI를 호출하여 로컬 파일을 탐색하게 함으로써, GitHub 토큰, SSH 비밀키, .env의 API 키를 훔쳐냈습니다. 공격자는 "탐색 코드를 작성할" 필요조차 없이, 에이전트에게 "탐색하게" 만들었습니다. 도난당한 인증 정보는 2,349건이었으며, 그중 상당수가 유효한 상태로 유지되어 2차 공격에 재사용되었고, 최종적으로 10,767개의 프라이빗 리포지토리(private repository)가 공개화되었습니다.
이 기사에서는 이러한 "비밀 그 자체를 훔치는" 공격을 구조적으로 봉쇄하기 위한 설계 사상——최소 도달(least-reach)——을 위협 모델부터 구체적인 설정, 그리고 남은 리스크까지 일관되게 해설합니다. 대상은 Mac 상 또는 Docker 컨테이너 상에서 AI 에이전트를 구동하는 개발 팀과, 그 운용을 지원하는 보안 관리자입니다.
왜 AI 에이전트는 "새로운" 리스크인가
기존의 인증 정보 유출은 "인간이 실수로 키를 커밋했다", "멀웨어가 디스크를 스캔했다"가 중심이었습니다. AI 에이전트는 다음 세 가지 점에서 공격면(attack surface)을 질적으로 변화시킵니다.
첫째, 자율적인 파일 탐색 능력입니다. 에이전트는 자연어 지시만으로 ~/.ssh/, ~/.config/gh/, 프로젝트의 .env를 스스로 읽으러 갈 수 있습니다. 공격자는 탐색 도구를 준비하는 대신, 에이전트에게 그렇게 시키기만 하면 되었습니다.
둘째, 신뢰할 수 없는 콘텐츠를 "지시"로 해석해 버린다는 점입니다. 에이전트는 Issue 본문, README, 웹 페이지, 의존성 패키지의 출력을 읽어 들이지만, 그곳에 심어진 문장이 명령으로서 작용합니다. 이것이 간접 프롬프트 인젝션(Indirect Prompt Injection, IPI)입니다.
셋째, Simon Willison이 지적한 **lethal trifecta(치명적인 삼제)**가 에이전트 환경에서는 기본적으로 갖춰져 버린다는 점입니다. ① 프라이빗 데이터에 대한 액세스, ② 신뢰할 수 없는 콘텐츠에 대한 노출, ③ 외부로의 반출 수단——이 세 가지가 동일한 컨텍스트(context)에 공존하면 에이전트는 공격자의 도구가 됩니다. 광범위한 PAT(Personal Access Token)를 가지고, 네트워크에 연결되어 있으며, 외부의 Issue를 읽는 에이전트는 이 세 가지 조건을 그대로 충족합니다.
즉 문제는 "에이전트가 똑똑한가"가 아니라, 에이전트의 손이 닿는 범위에 무엇이 있는가입니다.
실제로 무슨 일이 일어났는가 — 3가지 인시던트
nx「s1ngularity」(2025년 8월): AI CLI를 악용한 자동 탐색
서두에서 언급한 공격입니다. 악성 npm 패키지의 post-install 스크립트가 피해 환경을 스캔하고, 설치된 Claude / Gemini / Amazon Q를 악용하여 파일 탐색을 실행했습니다. --dangerously-skip-permissions와 같은 권한 확인을 우회하는 플래그도 사용되었습니다. 탈취된 데이터는 다중 base64 인코딩되어 피해자 자신의 GitHub 상의 공개 리포지토리에 업로드되었으며, 그 후 유효했던 토큰을 사용하여 10,767개의 프라이빗 리포지토리가 공개화되었습니다.
여기서 효과가 있었다면 피해를 막을 수 있었던 것은 단명·자동 실효 토큰이었습니다. 2차 공격 시점에 토큰이 만료되어 있었다면, 횡적 이동(lateral movement)은 연료가 떨어진 상태가 되었을 것입니다.
GitHub MCP 서버의 간접 프롬프트 인젝션(2025년 5월)
Invariant Labs가 보고한 사례입니다. 공개 리포지토리(Public Repository)의 Issue에 지시 사항을 심어두고, 사용자가 "이 Issue를 봐줘"라고 요청하기만 하면, 에이전트가 프라이빗 리포지토리(Private Repository)의 정보를 끌어와 공개 리포지토리로 PR을 자동 생성하여 유출하게 만들었습니다. 근본 원인은 MCP 서버의 버그가 아니라, 광역 PAT(Personal Access Token) × 신뢰할 수 없는 콘텐츠 × 프라이빗 데이터 × 반출 수단(PR 생성)이 동일한 컨텍스트에 혼재된 것, 즉 lethal trifecta(치명적인 3요소) 그 자체였습니다.
Microsoft의 SAS 토큰 유출(2023년): 과도한 스코프(Scope) × 긴 수명의 교과서적 사례
AI 연구팀이 스토리지 계정 전체에 쓰기 권한이 있으며 약 30년간 유효한 SAS 토큰을 공개 GitHub에 커밋하여, 38TB의 내부 데이터가 노출되었습니다. 최소 스코프와 짧은 유효 기간만 설정되어 있었더라도, 실수로 공개되었을 때 피해 범위는 극소수로 제한되었을 것입니다.
이 사례들의 공통점은 "비밀이 도난당하는 순간, 그 외의 방어 수단이 아무것도 작동하지 않았다"는 점입니다.
핵심 원칙: least-reach — 비밀을 "보유하는 주체"와 "사용하는 주체"를 분리하라
따라서 본 설계의 North Star(북극성)가 되는 것이 **least-reach(최소 도달)**입니다. 한마디로 정의하자면——
에이전트를 "비밀을 보유하는 주체"가 아니라, "비밀을 쥐고 있는 브로커(1Password / 인증 헬퍼 / IdP)를 이용하기만 하는 주체"로 만드는 것입니다.
기존 모델에서는 에이전트 스스로가 id_ed25519도, PAT도, GH_TOKEN도 쥐고 있었습니다. 읽을 수 있다는 것은 곧 빼낼 수 있고, 가져갈 수 있다는 뜻입니다. least-reach 모델에서는 비밀이 브로커 측에만 존재하며, 에이전트는 "서명해줘" 또는 "토큰을 줘"라고 요청할 뿐입니다. 에이전트가 침해되더라도 공격자가 얻는 것은 "브로커에게 조작을 요청할 권리"이지, "비밀 소재 그 자체"가 아닙니다.
이를 통해 유출의 최악의 시나리오가 "키의 영구적인 복제"에서 "세션 지속 중에 한정된 조작의 악용"으로 축소됩니다. 디스크나 환경 변수에 수명이 긴 비밀을 두지 않는다면, 에이전트가 탈취되어도 주워갈 수 있는 것이 없습니다.
한 가지 중요한 보충 설명이 있습니다. "만지게 하지 말아야 할" 대상은 어디까지나 장기적·고가치·추출 가능한 비밀 소재(SSH 비밀키 · 장기 PAT · GitHub App 비밀키)입니다. 단기 토큰도 인증 정보이긴 하지만, 수명이 짧고 · 최소 스코프이며 · 일회용이라면 유출되어도 가치가 거의 없으므로 에이전트에게 넘겨주어도 무방합니다. 원칙은 "일체의 인증 정보에 만지게 하지 않는다"가 아니라, "장기적인 비밀은 보유하지 않는다 / 넘겨준다면 일회용 토큰에 한한다"로 읽어야 합니다.
평가 축: 3요소 모델
방어를 체계적으로 설명하기 위해, 본 설계에서는 인증을 독립된 세 가지 요소로 분해합니다. 공격자가 리소스에 도달하려면 이 세 가지를 모두 돌파해야 하는(AND 조건) 상태를 목표로 합니다.
| 요소 | 의미 | 주요 대응책 |
|---|---|---|
| 크리덴셜(Credential) 본체 | 유효한 비밀 그 자체 (PAT · SSH 비밀키 · 토큰) | 1Password SSH agent, 단기 토큰, 비밀 은닉 |
| 도달 경로 | 조직으로의 도달 경계 (허가된 경로를 통해서만 도달 가능) | SAML SSO 인가, IP 허용 목록 / VPN |
| 본인 확인 · 시간 범위 | 이용 시의 본인 확인과 유효 기간 | Touch ID 수시 승인, 커밋 서명, 토큰 만료 |
이상적인 상태는 "크리덴셜 본체를 도난당하더라도, 도달 경로와 본인 확인 · 시간 범위에서 차단되는" 상태입니다. 앞서 언급한 인시던트의 대부분은 본체를 도난당했을 뿐 나머지 2개 요소가 무방비했기 때문에 피해가 확대되었습니다. 단 하나의 요소만 돌파해서는 연쇄 공격이 성립되지 않는다 —— 이것이 다층 방어(Defense in Depth)의 본질입니다.
구체적 방안: 오늘부터 구축할 수 있는 토대
1. 키를 디스크에 두지 않는다 (1Password SSH agent)
가장 효과가 크고 가장 쉬운 첫걸음입니다. SSH 비밀키를 평문인 id_* 파일로 소유하지 않고, 1Password 내에 보관한 상태로 Git/SSH에서 사용합니다. 에이전트가 ~/.ssh/id_ed25519를 cat 하더라도 그곳에 키는 존재하지 않습니다. 또한, 서명 및 인증을 할 때마다 Touch ID를 통한 명시적 승인이 이루어집니다 (표준 ssh-agent는 한 번 로드하면 임의의 프로세스가 확인 없이 서명할 수 있다는 점에서 결정적으로 다릅니다).
# ~/.ssh/config — 인증에 1Password agent 사용(Mac)
Host *
IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
이것만으로도 s1ngularity가 노린 탈취 대상 그 자체를 디스크에서 제거하고(자격 증명 본체), push를 할 때마다 인간이 개입할 수 있는 관문(본인 확인·시간 범위)을 만들 수 있습니다.
2. 커밋 서명으로 「사칭」을 탐지 가능하게 만들기
Git 2.34 이후부터는 gpg.format=ssh 설정을 통해 SSH 키를 이용한 커밋 서명 (Commit Signing)이 가능합니다. 서명 처리를 1Password의 op-ssh-sign에 위임하면, 서명할 때마다 Touch ID를 요구하며 서명 키 바이트 열은 에이전트(Agent) 측에 존재하지 않습니다.
# ~/.gitconfig
[gpg]
format = ssh
...
조직 측에서 브랜치 보호(Branch Protection)의 Require signed commits를 활성화하면, 서명이 없는(즉, 사칭된) 커밋은 머지(Merge)할 수 없게 됩니다.
3. 장기 수명 PAT를 버리고, 단기 토큰을 기본값으로 설정하기
AI 에이전트에게 전달하는 것은 장기 수명의 classic PAT가 아니라 GitHub App 토큰으로 합니다. 수명의 기준은 다음과 같습니다.
| 토큰 유형 |
|---|---|---|
| App installation token (server-to-server) | 1시간 | 읽기·무인 CI·명시적인 bot 처리 |
| App user access token (user-to-server) | 8시간 (refresh는 6개월) | 본인 귀속이 필요한 쓰기(push / PR) |
| classic PAT (장기 수명) | 무기한 설정 가능 | 원칙적으로 사용 금지 |
핵심은 작업의 성격에 따라 구분하여 사용하는 것입니다. push나 PR 생성처럼 본인 귀속이 필요한 쓰기 작업에는 user-to-server 토큰을 사용합니다 (installation token은 app[bot] 명의가 되어 개인을 식별할 수 없기 때문입니다). 반면, 읽기나 무인 CI에는 1시간 후에 만료되는 installation token을 사용합니다. App 비밀 키(.pem)는 에이전트와 격리된 Token Broker만이 보유하며, 에이전트가 접할 수 있는 것은 「최대 1시간 뒤에 소멸할 토큰」뿐입니다.
4. 「번거로우니까 상주」하는 것을 방지 — 토큰의 자동 주입
단기 토큰은 안전하지만, 매번 op run -- ...을 입력하는 것은 번거롭습니다. 그리고 이러한 번거로움은 ~/.zshrc에 export GH_TOKEN=ghp_...를 상주시키는 최악의 안티 패턴(Anti-pattern)을 유발합니다. 이를 방지하려면 Git credential helper를 통해 토큰을 매번 자동으로 발행하거나, direnv를 사용하여 디렉터리 단위로 주입하거나, 쉘 함수(Shell function)로 op run을 숨기는 등의 방법으로 least-reach를 유지하면서도 수동 입력을 없앨 수 있습니다. 비밀은 어디까지나 브로커(Broker) 측에만 둔다는 원칙을 깨지 않는 것이 중요합니다.
5. 승인 우회 플래그 금지
-dangerously-skip-permissions / -yolo / -trust-all-tools는 인간의 승인(본인 확인·시간 범위)을 무효화하는 작업입니다. s1ngularity의 공격자는 바로 이러한 방식으로 권한 확인을 돌파하려 했습니다. 조직 정책으로 이를 금지하고, push·외부 전송·private에서 public으로의 전환은 인간의 승인을 필수적으로 만듭니다. Touch ID로 키를 보호하더라도, 승인 우회 플래그로 자동 push가 가능해진다면 의미가 없습니다. 두 가지는 세트로 작동해야 합니다.
Docker로 실행할 때의 핵심 포인트
컨테이너화의 이점은 「비밀 정보가 컨테이너 안에 없다면, 컨테이너가 침해되어도 비밀 정보는 유출되지 않는다」는 점에 있습니다. 따라서, ① 이미지 레이어에 비밀 정보를 굽지 말 것 (ARG / ENV / --build-arg는 엄격히 금지하며, 빌드 시에는 BuildKit의 --secret / --ssh를 사용), ② .dockerignore를 통해 .env / .git / .ssh
를 제외하고, ③ 실행 시에는 짧은 수명의 토큰을 -e GH_TOKEN (값을 작성하지 않고 변수명만)으로 호스트에서 주입하여 컨테이너 파기 시 소멸시키는 것이 기본입니다.
컨테이너만이 가진 강력한 무기는 egress 제한입니다. lethal trifecta의 '유출 수단'을 물리적으로 막을 수 있기 때문에, 허가된 도메인만 통과시키는 필터링 프록시를 거치고, --read-only / --cap-drop ALL / 비 root 실행으로 피해를 축소합니다. 다만 공격자가 유출할 목적지로 github.com 자체와 같은 허가된 도메인을 선택하는 수법도 있기 때문에, egress에만 의존하지 않고 다층적으로 중첩해야 합니다.
least-reach만으로는 부족하다 — confused deputy라는 남은 리스크
이 부분이 이 설계에서 가장 성실하게 마주해야 할 지점입니다. 비밀을 비보유(non-possession)로 해도 사라지지 않는 리스크가 **confused deputy(혼란한 대리인)**입니다.
키나 토큰이 빠져도, 정상 세션 내에서 승인된 조작을 악용당하면 브로커나 프록시는 그것을 '정상적인 요청'으로 실행해 버립니다. 1Password agent도 인증 프록시도 정상 토큰도 '진짜 요구'와 '프롬프트 인젝션으로 유도된 요구'를 구별할 수 없습니다. 앞서 언급한 GitHub MCP 사례가 바로 이것이며, 사용자가 'Issue를 봐달라'고 부탁했을 뿐인데도 정상 세션이 공개 PR을 만들며 정보를 유출했습니다.
least-reach는 '최악의 경우를 키의 영구 누수에서 세션 한정 악용으로 축소하는 것'일 뿐이며, 단독으로 완결되는 대책이 아닙니다. 이것에 대처하려면 다음 3층을 독립적으로 중첩해야 합니다.
- PR 거버넌스: 필수 리뷰 + CODEOWNERS + 에이전트의 자동 병합 금지. confused deputy가 만드는 '정당하게 서명된 악성 PR'을 인간 검토라는 마지막 관문에서 막습니다. -
능동적인 탐지 및 자동 만료: 토큰 비정상 사용/대량 clone/private→public화 이벤트를 감사 로그로 감시하고, 탐지하면 즉시 만료시키는 플로우. 짧은 수명화는 '창문을 좁히는 것'일 뿐, '창문 내부의 악용'은 탐지로만 막을 수 있습니다. -
승인 설계 및 egress: 승인 시 대상 리포/목적지/이유를 구조화하여 제시하고, 승리 피로(무조건 yes)를 방지합니다. 1세션 1리포지토리를 철저히 하여 lethal trifecta의 동시 성립을 끊습니다.
플랜 차이: Enterprise Cloud와 Team
지금까지의 대책 중, 클라이언트 측의 시책(1Password agent・커밋 서명・짧은 수명의 토큰・OIDC)은 플랜 비종속적이라 Team 플랜에서도 Enterprise Cloud와 동등하게 기능합니다. 반면, 도달 경로를 담당하는 조직 기능(SAML SSO 인증・org IP 허가 목록・SCIM)은 Enterprise Cloud 한정입니다.
즉, Team 플랜은 자격 증명 본체와 본인성/시간 범위까지는 도달할 수 있지만, 도달 경로가 구조적으로 부족합니다. Team으로 운영한다면, 비밀의 비보유화와 짧은 수명을 최전선에 두면서, 부족한 도달 경로를 네트워크 계층(VPN / NAT / egress 프록시의 고정 IP)에서 유사하게 보완하고, private 리포의 secret scanning은 OSS인 gitleaks / trufflehog으로 대체해야 합니다.
참고로 Enterprise Cloud에서도 빈틈이 하나 있습니다. GitHub App이 사용자 계정에 설치되어 server-to-server 토큰으로 조직에 접근하는 경로는 현재 org IP 허가 목록의 대상이 아닙니다. 에이전트용 App은 반드시 **조직(organization)**에 설치해야 합니다.
어디서 착수할 것인가 — Quick Win 5가지
망설인다면, 플랜 비종속적이고 오늘부터 착수 가능하며 효과가 큰 이 5가지부터 시작하는 것이 비용 대비 효과 측면에서 가장 좋습니다.
| 대책 | 비용 | 효과 | 작동 원리 |
|---|---|---|---|
| 1Password SSH agent (키 비실체화 + 매번 Touch ID 사용) | 낮음 | ★★★ | 평문 id_*를 삭제 = 탈취 대상 자체를 제거 |
| 장기 PAT(Personal Access Token) 전수 조사 → 단기 App token으로 교체 | 낮음 | ★★★ | "유출되어도 1시간이면 무가치"하게 만들어 공격 확산을 차단 |
| 승인 우회 플래그 금지 + 고위험 작업에 대한 인간의 승인 | 낮음 | ★★★ | 공격자가 가장 먼저 노리는 승인 게이트 돌파를 차단 |
GH_TOKEN 자동 주입 | 낮음 | ★★ | "번거로움 → PAT 상주"라는 안티 패턴(Anti-pattern)을 미연에 방지 |
| SSH 커밋 서명 (→ Verified) | 낮음 | ★★ | 사칭 커밋을 검증 가능하도록 고정 |
이러한 조치로 "유출되어도 무가치(단기화)"와 "애초에 두지 않음(비실체화)"을 먼저 공고히 한 뒤, Actions OIDC나 egress 프록시(수주 소요), SSO/SCIM 또는 감사 로그 모니터링 + 자동 무효화(인프라 투자)로 확장해 나갑니다.
요약
AI 에이전트의 보안은 "에이전트를 똑똑하고 예의 바르게 만드는 것"만으로는 해결되지 않습니다. 인젝션(Injection)은 원리적으로 완전히 막을 수 없으며, 에이전트는 정상적인 요청과 유도된 요청을 구분할 수 없기 때문입니다. 그렇기에 발상을 전환하여, 에이전트의 손이 닿는 범위에서 장기적이고 가치가 높은 비밀(Secret) 자체를 제거하는 것 —— 이것이 least-reach입니다.
구체적으로는, 키는 1Password에 맡기고 Touch ID로 매번 승인하며, 토큰은 1시간이면 만료되는 단기적인 것으로 교체하고, 승인 우회를 금지합니다. 그 위에, 키를 훔치지 않고 정상 세션을 악용하는 confused deputy에 대해서는 PR 거버넌스·탐지 및 자동 무효화·승인 설계라는 3개 층을 독립적으로 겹쳐 쌓습니다.
s1ngularity가 제시한 교훈은 단순합니다. 훔칠 수 있는 비밀이 그곳에 없다면, 최악의 연쇄 반응은 시작되지 않습니다. 우선 퀵 윈(Quick win)인 5가지 항목부터 오늘 바로 착수해 보시기 바랍니다.
출처
- nx s1ngularity 공격: Wiz / Wiz(aftermath) / GitGuardian
- GitHub MCP의 간접 프롬프트 인젝션 (Indirect Prompt Injection): Invariant Labs / Simon Willison
- lethal trifecta: Simon Willison
- Microsoft SAS 토큰 유출(38TB): Wiz
- Shai-Hulud npm 웜 (Worm): Wiz
- tj-actions/changed-files(CVE-2025-30066): Wiz / CISA
- 1Password SSH agent: 1Password Developer Docs
- GitHub App의 토큰 무효화: GitHub Docs
- org IP 허용 목록: GitHub Enterprise Cloud Docs
- GitHub Actions OIDC: GitHub Docs
Discussion

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