본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 27. 10:45

LLM 라우터가 지갑 키를 로그에 남겼습니다. 이미 유출되었습니다.

요약

LLM 라우터와 MCP 프록시를 통과하는 AI 에이전트의 트래픽에서 비밀 정보가 평문으로 유출되는 보안 취약점을 분석합니다. 송신(egress) 단계에서 데이터를 비식별화하여 자격 증명과 지갑 키를 보호해야 함을 강조합니다.

핵심 포인트

  • LLM 라우터 및 프록시는 페이로드에 대한 완전한 평문 접근 권한을 가질 수 있음
  • 에이전트의 비밀 정보는 경계를 넘기 전 송신 단계에서 반드시 비식별화 필요
  • 연구 결과, 일부 라우터가 악성 코드 주입 및 자격 증명 탈취 시도 확인
  • Ethereum 개인 키와 같은 민감한 서명 자료는 중간자를 통해 전송 금지

AI 에이전트의 비밀 정보(secrets)는 요청이 제3자 LLM 라우터(router)나 MCP 프록시(proxy)에 도달할 때 전송 중인 상태가 되며, 해당 라우터의 감사 로그(audit log)는 통제 수단이 될 수 없습니다. 로그에 요청이 기록될 때쯤이면, 자격 증명(credential)은 이미 평문(plaintext) 상태로 여러분의 경계(perimeter)를 넘어갔기 때문입니다. 해결책은 바이트(bytes)가 나가기 전, 송신(egress) 단계에서 비식별화(redact)하는 것입니다.

요약하자면:

  • Authorization 헤더를 통해 자체 모델 제공업체로 전송되는 비밀 정보는 예상된 경로입니다. 하지만 동일한 비밀 정보가 라우터, 게이트웨이(gateway) 또는 MCP 프록시로 전송되는 것은 유출(leak)입니다. 해당 호스트가 여러분의 평문을 읽기 때문입니다.
  • boundary_leak_probe.py는 하나의 JSON 송신 맵(egress map)을 읽고, 목적지 신뢰도(destination trust)에 따라 비밀 정보를 포함하는 모든 필드를 분류합니다. 유출이 발생하는 피스처(fixture)의 경우: 3개의 요청 중 2개가 제3자 중개자(intermediaries)로 향했으며, 5개의 필드가 경계를 넘었고, 6개의 규칙이 적중했으며, 2개의 치명적인 지갑 비밀 정보가 발견되었습니다. Exit 1.
  • 2개의 치명적인 정보는 프록시로 향하는 MCP 도구 호출(tool-call) 인자(arguments)에 포함된 Ethereum 개인 키(private key)와 BIP-39 니모닉(mnemonic)입니다. 서명 자료(Signer material)는 절대로 어떤 중간자(middleman)를 통해서도 전송되어서는 안 됩니다.
  • 표준 라이브러리만 사용합니다 (sys, json, re). 네트워크, 모델, exec를 사용하지 않습니다. 실행 결과는 바이트 단위로 결정론적(deterministic)입니다.
  • 적중(hit)은 신호(SIGNAL)일 뿐, 확인된 실제 비밀 정보는 아닙니다. 코드와 두 가지 피스처는 이 포스트에 포함되어 있습니다.

측정이 필요하게 만든 사건

2026년 4월, Chaofan Shou를 포함한 연구진은 "Your Agent Is Mine: Measuring Malicious Intermediary Attacks on the LLM Supply Chain" (arXiv 2604.08407, 2026년 4월 9일 게시)를 발표했습니다. 이들은 428개의 범용 LLM 라우터를 대상으로 실제 에이전트 트래픽을 투입했습니다. 그중 9개는 응답에 코드를 주입했습니다. 17개는 연구진의 AWS 자격 증명에 접근을 시도했습니다. 이들이 설명한 메커니즘 중 특히 인상 깊었던 부분은 다음과 같습니다: 이 라우터들은 "전송 중인 모든 JSON 페이로드(payload)에 대해 완전한 평문 접근 권한을 가진 애플리케이션 계층 프록시(application-layer proxies)로 작동"하며, 어떤 제공업체도 클라이언트와 업스트림(upstream) 모델 사이의 암호학적 무결성(cryptographic integrity)을 강제하지 않는다는 점입니다.

CoinDesk도 같은 주에 이 사건을 다루었으며, Shou의 말을 더 직설적으로 전달했습니다: 26개의 라우터가 "비밀리에 악성 도구 호출(tool calls)을 주입하고 자격 증명(creds)을 훔쳤으며", 그중 하나는 "우리 클라이언트의 50만 달러 지갑을 비웠다" (CoinDesk, 2026년 4월 13일). 그 50만 달러와 라우터 수치는 제가 아닌 그들의 측정치에 기반한 그들의 수치입니다. 저는 맥락을 위해 그들을 인용하고 있습니다. 제 도구에 대해 제가 주장하는 모든 것은 제가 전체를 붙여넣을 실행 결과에서 나옵니다.

저는 화요일에 그 논문을 읽고, 라우터를 신뢰한다고 가정했던 제 스택(stack)의 부분을 찾아 나섰습니다. 금방 찾아낼 수 있었습니다. 우리는 장애 조치(failover)와 비용 추적을 위해 게이트웨이(gateway)를 통해 라우팅합니다. 게이트웨이에는 대시보드가 있습니다. 대시보드에는 요청 로그(request log)가 있습니다. 그리고 저는 그 로그를 일종의 안전망으로 조용히 취급해 왔습니다. 무언가 유출된다면, 거기서 확인할 수 있을 것이라고 말이죠.

그 가정은 거꾸로 되어 있으며, 이를 공개적으로 말하는 것이 이 포스트의 핵심입니다.

논쟁의 여지가 있을 만큼 날카로운 주장

반증 가능한 버전은 다음과 같습니다: 라우터의 감사 로그(audit log)는 영수증이지, 브레이크가 아닙니다. 자격 증명이 라우터의 로그에 나타날 때쯤이면, 라우터 프로세스는 이미 이를 평문(plaintext)으로 읽은 상태입니다. 로깅(logging)은 경계(boundary)의 훨씬 뒷단에서 발생합니다. 비밀은 이미 사라진 상태입니다. 먹지를 읽는다고 해서 이미 보낸 편지를 회수할 수 없는 것과 마찬가지로, 로그 항목을 검토한다고 해서 이미 전송된 것을 되돌릴 수는 없습니다.

만약 그 주장이 틀렸다면, 비식별화(redaction)는 중요하지 않을 것이며 저와 같은 조사도 무의미할 것입니다. 그냥 로그를 지켜보다가 사후에 교체(rotate)하면 될 테니까요. 하지만 "사후에 교체한다"는 것은 전송과 탐지 사이의 시간 간격이 무해하다는 것을 전제로 합니다. 지갑 개인 키(private key)의 경우, 그 시간 간격은 단 한 번의 트랜잭션에 서명하기에 충분히 긴 시간입니다. 서명 비밀값(signer secret)은 월요일에 교체하면 되는 API 키와는 다릅니다. 제삼자가 이를 손에 넣는 순간, 자금은 sign_tx 호출 한 번이면 빠져나갑니다.

따라서 제어권은 전송(send) 단계보다 상류(upstream)로 이동해야 합니다. 먼저 목적지를 분류하십시오. 넘어가서는 안 될 모든 것을 가리십시오(Redact). 그런 다음 바이트(bytes)를 방출하십시오. 로그를 남긴다면, 그것은 피해가 발생한 후 읽는 함정(tripwire)이 아니라, 당신이 무엇을 외부로 허용했는지에 대한 기록이 되어야 합니다.

프로브(probe)가 실제로 수행하는 작업

입력값은 제가 '이그레스 맵(egress map)'이라 부르는 하나의 JSON 파일입니다. 이는 에이전트가 방출하는 아웃바운드 요청(outbound requests)으로, 목적지 호스트(destination host), 종류(kind), 헤더(headers), 그리고 바디(body)를 포함합니다. 이는 요청 인터셉터(request interceptor), 테스트 하네스(test harness), 또는 수동으로 추출할 수 있습니다. 프로브는 절대 요청을 생성하지 않습니다. 대신 맵을 정적으로 읽습니다.

두 가지 아이디어가 작업을 수행합니다.

첫째, **목적지 신뢰(destination trust)**입니다. 당신은 first_party_hosts를 선언합니다. 이는 당신이 직접 계약한 호스트, 즉 자신의 백엔드나 모델 제공업체 자체를 의미합니다. 이러한 호스트 중 하나로 향하는 Authorization 헤더는 예상되는 자격 증명 경로이므로, 프로브는 이에 대해 경고를 울리지 않습니다. 그 외의 모든 호스트, 즉 중간에 있는 라우터(routers), 게이트웨이(gateways), MCP 프록시(proxies)는 기본적으로 제삼자(third-party)입니다. 그곳으로 향하는 바디(body)나 도구 호출(tool-call) 인자(arguments)에 담긴 비밀 정보는 당신이 소유하지 않은 경계를 넘어간 것입니다.

둘째, **서명 자료(signer material)는 항상 유출 위험이 있다(always-leak)**는 점입니다. Ethereum 개인 키(private key)나 BIP-39 니모닉(mnemonic)은 제삼자든 일차 당사자(first-party)든 어떠한 중간 매개체도 거쳐서는 안 됩니다. 에이전트가 프록시를 통해 시드 구절(seed phrase)을 보내는 정당한 경로는 존재하지 않습니다. 프로브가 이를 발견한다면, 목적지와 관계없이 이는 '치명적(CRITICAL)' 상황입니다.

다음은 규칙과 값 스캐너(value scanner)입니다:

import sys, json, re

# 비밀 정보의 형태. critical = 절대로 전송되어서는 안 되는 서명 자료.
...

SAFE_REF 규칙은 보이는 것보다 더 중요합니다. ${OPENAI_KEY} 또는 $VAULT_REF:openai와 같은 값은 핸들(handle)이지 비밀 정보가 아닙니다. 실제 값은 신뢰할 수 있는 에지(edge)에서 치환되며, 에이전트의 페이로드(payload)에 실려 전달되지 않습니다. 만약 당신이 이미 라우터에 핸들 참조를 전달하고 자체 이그레스 프록시(egress proxy)가 이를 교체하도록 설정했다면, 이미 안전한 단계에 거의 도달한 것입니다. 프로브는 조용히 유지됨으로써 이를 보상합니다.

분류기(classifier)는 각 요청의 모든 문자열 리프(string leaf)를 순회하며 스캔하고, 유출 규칙(leak rule)을 적용합니다:

def classify(spec):
    first_party = set(spec.get("first_party_hosts", []))
    rows = []
...

walk 헬퍼 함수는 딕셔너리(dict)와 리스트(list)를 순회하며 모든 리프(leaf)에 대해 JSON 경로(path)와 문자열을 생성하는 명백한 재귀적 하강(recursive descent) 방식입니다. 아래에 보여드릴 --redact 모드를 포함한 전체 파일은 약 95줄 정도입니다. 여기서는 상용구(boilerplate) 코드를 생략하는 것이지, 숨기는 것이 아닙니다.

실행 결과 전문

두 가지 피스처(fixture)가 있습니다. 'clean' 피스처는 여전히 두 개의 제3자 중개자(third-party intermediaries)와 통신하지만, 실제 비밀 정보는 제1자 호스트(first-party hosts)로만 라우팅하며, 중개자들에게는 대신 핸들 참조(handle references)를 보내 정보가 유출되지 않도록 합니다. 반면 'leaky' 피스처는 게으른 실제 에이전트와 유사한 형태를 띱니다. 중간에 있는 API 게이트웨이는 Authorization 헤더에 GitHub PAT(Personal Access Token)를 담고 있고, 시스템 메시지(system message)에는 AWS 키가 숨겨져 있으며, 메타데이터에는 OpenAI 키가 있고, MCP 프록시(proxy)는 도구 호출(tool-call) 인자에 지갑 개인 키(wallet private key)와 니모닉(mnemonic)을 전달받고 있습니다.

$ python3 boundary_leak_probe.py fixtures/egress_clean.json
requests=3  third_party_intermediaries=2
secret_bearing_fields_crossing_boundary=0  rule_hits=0  critical_signer_material=0
...

이제 수치에 대해 솔직하게 말씀드리겠습니다. 게으른 헤드라인이라면 여기서 거짓말을 할 것이기 때문입니다. 프로브(probe)는 제3자 중개자로 넘어가는 **5개의 서로 다른 필드(distinct fields)**를 발견했지만, **6개의 규칙 적중(rule-hits)**이 발생했습니다. 왜 불일치가 발생할까요? 하나의 필드인 게이트웨이의 Authorization 헤더가 두 개의 규칙을 동시에 건드렸기 때문입니다. 즉, 일반적인 베어러 토큰(bearer token)처럼 보이면서 동시에 그 안에 GitHub PAT가 포함되어 있었습니다. 이는 하나의 유출이지만 두 개의 신호(signal)인 셈입니다. 이것은 여섯 개의 서로 다른 비밀 정보가 아니며, 저는 이를 여섯 개라고 부르지 않을 것입니다. 가장 중요한 숫자는 작은 쪽인 **2개의 치명적인 필드(critical fields)**입니다. 지갑 개인 키와 니모닉이 모두, 둘 중 어느 것도 볼 권한이 없는 MCP 프록시로 향하고 있었습니다.

놓치기 쉬운 세부 사항이 하나 더 있습니다. 유출이 발생하는 피스처(fixture)의 요청 r1은 제1자 호스트(first-party host)인 api.openai.com으로 실제와 유사한 Bearer sk-...를 전송합니다. 프로브(probe)는 이를 플래그(flag)로 표시하지 않습니다. 이것이 바로 목적지 신뢰(destination trust)의 핵심입니다. 제공자(provider)로 향하는 인증 헤더(auth header)는 자신의 역할을 수행 중인 자격 증명(credential)입니다. 라우터(router)에게는 동일한 형태가 탈취되고 있는 자격 증명일 뿐입니다. 단순한 시크릿 스캐너(secret scanner)는 이 둘을 구분할 수 없습니다. 하지만 이 도구는 이를 구분하도록 설계되었습니다. 한 가지 솔직한 주의 사항은, 그 신뢰가 헤더 수준이 아닌 호스트 수준이라는 점입니다. 프로브는 목적지를 신뢰하므로, 본문(body)을 포함하여 제1자 요청의 어디에든 위치한 비임계적 시크릿(non-critical secret)도 통과됩니다. 오직 서명자 자료(signer material)만이 신뢰를 무시하고 유출됩니다. 따라서 귀하의 first_party_hosts 목록이 승부의 전부입니다. 도구가 귀하가 보내는 무엇이든 해당 호스트를 신뢰하므로, 목록을 엄격하게 유지하십시오.

잘못된 입력(Bad input)은 세 번째 종료 코드(exit code)이므로, CI 단계에서 이를 기준으로 분기할 수 있습니다:

$ python3 boundary_leak_probe.py            # 인자 없음
usage: boundary_leak_probe.py [--redact] <egress_map.json>
exit=2

그리고 실행 결과는 결정론적(deterministic)입니다. 유출된 STDOUT을 두 번 해싱해 보았습니다:

$ python3 boundary_leak_probe.py fixtures/egress_leaky.json | shasum -a 256
28c5eb9ff8e7ad0abc6b1ad67a617cdd5fdaa09bfce26d3f9f00022217e0a6c5  -
28c5eb9ff8e7ad0abc6b1ad67a617cdd5fdaa09bfce26d3f9f00022217e0a6c5  -

두 번 모두 동일한 바이트가 나왔습니다. 이는 게이트(gate)로서 매우 중요합니다. 결과가 흔들리는(flicker) 체크는 사람들이 비활성화하기 마련입니다.

경계에서 마스킹(Redact)하고, 게이트가 닫혔음을 증명하라

유출을 보고하는 것은 쉬운 절반에 불과합니다. 본 논지의 핵심은 제어가 송출(egress) 지점에서 이루어져야 한다는 것이었습니다. 따라서 프로브에는 경계 게이트(boundary gate)가 실제로 방출할 마스킹된 맵(masked map)을 출력하는 --redact 모드가 있습니다. 이 모드는 제1자 Authorization은 그대로 두고, 경계를 넘을 수 있는 모든 것을 마스킹합니다:

$ python3 boundary_leak_probe.py --redact fixtures/egress_leaky.json
...
    "to": "router.3rdparty.ai",
...

그다음 제가 좋아하는 부분입니다. 이 마스킹된 맵을 다시 프로브에 입력합니다:

$ python3 boundary_leak_probe.py --redact fixtures/egress_leaky.json | python3 boundary_leak_probe.py /dev/stdin
requests=3  third_party_intermediaries=2
secret_bearing_fields_crossing_boundary=0  rule_hits=0  critical_signer_material=0
...

Exit 0, 그리고 여전히 두 개의 제3자 중개자(third-party intermediaries)가 나열되어 있음에 주목하십시오. 홉(hops)은 여전히 존재하지만, 이제는 어떤 비밀 정보도 그곳으로 넘어가지 않습니다. 마스킹된 토큰은 SAFE_REF와 일치하므로, 두 번째 패스(pass)에서는 플래그를 지정할 대상이 아무것도 발견되지 않습니다. 또한 --redact는 헤더와 본문뿐만 아니라 감사가 스캔하는 모든 필드를 마스킹하므로, 이 하나의 피스처(fixture) 형상에 국한되지 않고 왕복(round trip) 과정이 유지됩니다. 이 왕복 과정이 로그를 지켜보는 것과 브레이크를 잡는 것의 차이입니다. 로그는 비밀 정보가 유출되었다고 알려줍니다. 하지만 마스킹 패스(redact pass)는 그것이 결코 유출되지 않았음을 의미합니다. 다만, 이것은 여전히 정적 정규 표현식 휴리스틱(static regex heuristic)일 뿐이며, 당신의 바이트(bytes)가 깨끗하다는 증명은 아닙니다.

이것이 위치한 지점, 그리고 제가 이미 작성한 내용들

이것은 시리즈의 다섯 번째 도구이며, 저는 축(axes)들이 겹치지 않고 쌓일 수 있도록 의도적으로 분리해 두었습니다. 이전 도구들은 빌드 아티팩트에 포함되어 배포되는 비밀 정보 (npm pack이 실제로 게시하는 것), 키가 유출될 경우의 폭발 반경 (blast radius) (범위에 따라 얼마나 많은 것이 망가지는지), MCP 매니페스트의 정체 및 버전, 그리고 평가 하네스 (eval harness) 내의 오염을 다루었습니다. 이 중 그 어떤 것도 이번 도구가 던지는 질문을 던지지 않았습니다: 내 에이전트가 보내려는 요청들 중, 어떤 목적지가 신뢰할 수 있는 곳이며, 어떤 비밀 정보가 포함된 필드가 내가 제어하지 않는 호스트로 넘어가려 하는가? 여기서의 대상은 디스크 상의 파일이나 매니페스트, 혹은 범위 점수(scope score)가 아니라, 아웃바운드 트레이스(outbound trace)와 그 목적지입니다. 새로운 축, 새로운 도구입니다.

이것이 아닌 것

저는 성과를 과장하기보다 차라리 한계를 신뢰받기를 바랍니다.

이것은 실시간 비밀 정보 스캐너 (live secret scanner)가 아닙니다. 모든 탐지(hit)는 하나의 신호(SIGNAL)이며, 특정 형태에 대한 정규 표현식 (regex) 매칭일 뿐입니다. 0x...는 누군가 붙여넣은 트랜잭션 해시 (transaction hash)일 수 있으며, 개인 키 (private key)가 아닐 수도 있습니다. 중요한 사항은 반드시 귀하의 자체 금고 (vault)를 통해 확인하십시오. 이 프로브 (probe)는 키가 실제인지 또는 폐기되었는지 여부를 알려주지 않습니다.

이것은 런타임 인터셉터 (runtime interceptor)가 아닙니다. 이는 정적 송신 맵 (static egress map)을 읽습니다. 요청 경로 (request path)에 위치하지 않으며, TLS를 스니핑 (sniff)하지도 않고, 스스로 전송을 중단할 수도 없습니다. 이것을 실제 게이트 (a real gate)로 만들려면, 바이트 (bytes)를 방출하는 지점에 이 프로브의 종료 코드 (exit code)를 연결하거나, 그곳에서 --redact 변환 (transform)을 실행해야 합니다. 프로브는 정책 (policy)이며, 배관 (plumbing)은 귀하의 몫입니다.

이것은 mTLS나 게이트웨이 자체 제어 기능의 대체제가 아닙니다. 만약 귀하의 게이트웨이가 진정한 퍼스트 파티 (first-party)이며 운영자를 신뢰한다면, 이 도구는 귀하를 겨냥한 것이 아닙니다. 이 도구는 편의를 위해 채택했지만 위협 모델링 (threat-modeling)을 한 번도 수행하지 않은 중간 호스트 (middle hosts)들을 겨냥하고 있습니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0