네트워크와 자격 증명 프록시: 모델이 보지 못하는 그 열쇠
outbound는 문 몇 개를 지나야 합니다. 진짜 token은 자식 프로세스에 안 들어가요. 모델이 보는 것은 가짜 값과 403 한 줄입니다.
- 명령을 띄우기 전, 진짜 token을 같은 모양 가짜로 바꿉니다credential_broker.rs L92
- 샌드박스는 프록시 포트만 열고, 직결 loopback은 여기까지seatbelt.rs L325
- host를 해석하고, deny를 먼저 판정하며 늘 이깁니다runtime.rs L553
- 로컬 리터럴이 정확히 허용되지 않으면 사설 주소로 막습니다runtime.rs L578
- 공망이 아닌 곳으로 해석되면, 명단에 있어도 막습니다runtime.rs L582
- allowlist가 비었거나 안 맞으면 NotAllowedruntime.rs L598
- NotAllowed만 결정자에게 묻습니다network_policy.rs L349
- Limited에서 GET, HEAD, OPTIONS가 아니면 막습니다config.rs L311
- HTTPS는 안쪽 메서드를 봐야 하고, MITM이 없으면 막습니다http_proxy.rs L312
- 헤더에 가짜가 있어야 짝이 되는 진짜 자격 증명을 넣습니다mitm.rs L301
모델에게 GitHub에 이슈를 올리라 합니다. curl을 쓰고, 환경에는 진짜 token이 있어요. 다음 초에 도메인이 evil.example이 되거나 169.254.169.254를 칩니다. “명단에 없는 도메인”만 막으면 deny 항목과 인트라넷 주소가 샙니다. 빈 명단이 기본 허용이면, 설정을 안 쓴 것은 전부 연 것과 같아요.
판정 순서는 못 박혀 있어요. 먼저 deny, 그다음 로컬 또는 사설, 마지막 allowlist. 빈 명단과 미스는 둘 다 막습니다. 결정자는 allowlist가 빠뜨린 건만 뒤집을 수 있고, denylist나 로컬 망은 못 뒤집어요.
출처:codex-rs/network-proxy/src/runtime.rs 549–552행;codex-rs/network-proxy/src/network_policy.rs 346–378행;codex-rs/network-proxy/README.md 161–162행
*.example.com은 apex를 안 담고, **.example.com이 담습니다. 사설 IP로 해석되는 도메인은 명단에 정확히 써도 막아요. 로컬 리터럴은 localhost나 127.0.0.1을 정확히 써야 하고, 와일드카드 *는 안 칩니다.
출처:codex-rs/network-proxy/src/policy.rs 321–331행;codex-rs/network-proxy/src/runtime.rs 1028–1043행
막히면 명령 프로세스에 403을 돌립니다. 헤더는 x-proxy-error, 본문은 사람 말 한 줄. 밖에서는 not_allowed와 not_allowed_local이 둘 다 blocked-by-allowlist이고, 본문에서야 “명단에 없음”과 “샌드박스가 로컬을 막음”을 가릅니다. 그 403은 exec 출력이 되어 모델에 먹히고, 샘플 루프는 멈추지 않아요.
출처:codex-rs/network-proxy/src/responses.rs 52–83행
같은 pattern이 allow이기도 하고 deny이기도 하면, 유효 값은 큰 쪽입니다. enum 순서는 None < Allow < Deny.
출처:codex-rs/network-proxy/src/config.rs 19–27행
기본 거절, deny는 늘 이김, 인트라넷은 명시적으로 문을 열어야 함. 그게 SSRF 방어의 일반적인 모양이에요. 다른 언어로 다시 써도 물을 것은 같습니다. 명단이 없으면 어쩌나, 충돌하면 누가 이기나, 인트라넷으로 해석된 이름은 허용인가.
token은 명령을 따라 rollout에 들어갑니다. 다음 턴에도 모델이 볼 수 있고, 그다음 턴에는 로그에 들어갈 수 있어요. 자격 증명이 모델 컨텍스트에 들어가면, 뒤의 모든 마스킹은 수습입니다.
명령을 띄우기 전, 자격 증명 브로커가 GH_TOKEN과 OPENAI_API_KEY를 같은 길이·같은 접두의 가짜로 바꿉니다. 모델이 printenv하면 가짜를 봐요. 가짜가 컨텍스트에 들어가면 업스트림에는 쓸모가 없어요.
출처:codex-rs/network-proxy/src/credential_broker.rs 92–119행
outbound에서 MITM은 host로 거릅니다. 요청 헤더에 가짜가 있어야 진짜로 바꿉니다. hook의 헤더 벗기기·넣기는 브로커 뒤에서 일어나, 방금 넣은 Authorization을 벗길 수 있어요. 사용자가 고친 환경 값은 되돌리지 않습니다. 표시는 신뢰할 수 없는 입력. 브로커는 GitHub와 OpenAI만 덮어요.
출처:codex-rs/network-proxy/src/mitm.rs 297–302행
outbound 정책을 잘못 써도, 모델은 403을 보고 마음을 바꿀 수 있어요. 자격 증명이 컨텍스트에 들어가면 철회 비용이 큽니다. 그래서 진짜는 spawn 전에 가져가고, 되돌리기는 프록시 프로세스 메모리에서 일어나요. 가짜가 모양을 지키는 것은, 형식을 검사하는 클라이언트가 그래도 뜨게 하려는 것입니다.
Go의 net/http는 loopback에서 HTTP_PROXY를 건너뜁니다. 트래픽이 프록시에 들어간 줄 알지만, 실제로는 127.0.0.1에 직결이에요. 로컬에는 관리 포트, docker socket이 있습니다. 정책 파일에 명단이 있어도, 요청은 그 층까지 가지 않아요.
샌드박스는 프록시 포트만 엽니다. macOS 제한 Seatbelt는 localhost:{port}만 써요. Linux는 ProxyOnly. allow_local_binding이 거짓이면 NO_PROXY는 빈 문자열이고, loopback 리터럴도 프록시를 타야 합니다. 로컬 바인딩을 켜는 것은 loopback이 더 이상 allowlist를 안 거친다고 인정하는 일이에요.
출처:codex-rs/sandboxing/src/seatbelt.rs 309–336행;codex-rs/network-proxy/src/proxy.rs 450–460행
애플리케이션 층 프록시는 프록시를 안 타는 클라이언트를 못 막아요. 다음 층은 OS나 방화벽이어야 하고, 입구는 하나여야 합니다. 문 셋이 겹쳐야 관리 네트워크가 됩니다. 한 문만으로는 다른 문이 빠뜨린 면을 못 막아요.
Grok: 밸브는 도구 입구, loopback은 기본 허용
Grok의 web_fetch는 도구 안에서 도메인 명단과 해석 후 IP 검사를 합니다. 빈 명단은 모든 URL을 막고, Codex의 allowlist-first와 같은 방향이에요. path는 접두로 써서 문서 한 토막으로 좁힐 수도 있습니다.
SSRF 검사는 RFC1918, link-local, CGNAT를 막습니다. loopback은 명시 허용이고, 주석은 local development. bash의 curl은 이 명단을 안 타요. Codex는 밸브를 모든 자식 프로세스 출구에 두므로 샌드박스와 묶여야 합니다.
web_fetch/domain.rs 110–144행;web_fetch/ssrf.rs 17–19행
DSH: 설정은 변수 이름만 두고, spawn 때 키 이름으로 환경을 지웁니다
DSH는 비밀을 참조로 만듭니다. 설정 파일은 환경 변수 이름만 들고, provider가 작업마다 resolve해요. 설정 표면은 값을 보지 않습니다.
자식에는 지우기가 한 층 더 있어요. KEY, TOKEN처럼 생긴 키와 모든 DSH_*는 버려집니다. 설정 파일에 진짜 값이 나타나는 것은 막아요. 모델이 아직 printenv할 수 있으면, spawn이 이 지우기를 쓰지 않는 한 환경에 진짜 token이 남을 수 있습니다.
packages/credentials/credentials/src/index.ts 1–7행; packages/subprocess/subprocess/src/index.ts 60–66행 · DSH · 자격 증명은 매번 지금 꺼냄
별표는 localhost를 허용할까요
allow_local_binding = false이고 allowlist는 *뿐입니다. 127.0.0.1에 판정을 한 번 부르면 기대는 무엇인가요. 공망 IP 8.8.8.8에 다시 부르면 기대는 또 무엇인가요.
힌트: 로컬 리터럴은 와일드카드를 거절합니다. allowlist 컴파일은 전역 *를 명시 허용하고, denylist 컴파일은 거절해요.