OpenAI Codex · Code Mode

네트워크와 자격 증명 프록시: 모델이 보지 못하는 그 열쇠

outbound는 문 몇 개를 지나야 합니다. 진짜 token은 자식 프로세스에 안 들어가요. 모델이 보는 것은 가짜 값과 403 한 줄입니다.

강의 목표이 레슨을 마치면 세 가지를 말할 수 있어요. 도메인은 어떻게 판정되는가. 먼저 deny, 그다음 로컬, 마지막 allowlist. 진짜 token은 spawn 전에 같은 모양 가짜로 바뀌고, outbound에서 프록시가 host로 도로 바꿉니다. 프록시를 안 타는 클라이언트는 OS 샌드박스가 같은 깔때기로 눌러 넣어요.
먼저 해보기 · 자격 증명이 있는 요청이 프록시를 지납니다
도메인과 메서드를 바꿔, 요청이 어느 관문에서 멈추는지, 가짜가 어느 단계에서 진짜로 바뀌는지 보세요
도메인
메서드
모드
가짜가 진짜로 바뀌는 길을 먼저 보세요. 그다음 POST로 바꾸거나, Go가 프록시를 건너뛰게 하세요.
클라이언트
이번 요청
GET https://api.github.com/repos/you/app/issues
Authorization
Bearer github_pat_dmy8f3a2
관문 1 · 샌드박스대기
프록시 포트만 열어 줍니다. 직결 loopback은 이 관문에서 멈춰요.
관문 2 · 도메인대기
먼저 deny, 그다음 로컬 또는 사설, 마지막 allowlist.
관문 3 · 메서드대기
Limited는 GET, HEAD, OPTIONS만 허용합니다.
관문 4 · 자격 증명대기
요청 헤더에 가짜가 있어야, 브로커의 진짜 token으로 바꿉니다.
자식 환경GH_TOKEN = github_pat_dmy8f3a2
브로커 메모리진짜 token은 여기에 잠겨 있어요
모델 컨텍스트진짜 값을 볼 수 없음
재생을 눌러, 이 요청이 관문 넷을 차례로 걷는지, 가짜가 어느 단계에서 진짜로 바뀌는지 보세요.
논리 궤적 · 애니메이션 한 걸음이 소스 어디에 대응하는지
  1. 명령을 띄우기 전, 진짜 token을 같은 모양 가짜로 바꿉니다credential_broker.rs L92
  2. 샌드박스는 프록시 포트만 열고, 직결 loopback은 여기까지seatbelt.rs L325
  3. host를 해석하고, deny를 먼저 판정하며 늘 이깁니다runtime.rs L553
  4. 로컬 리터럴이 정확히 허용되지 않으면 사설 주소로 막습니다runtime.rs L578
  5. 공망이 아닌 곳으로 해석되면, 명단에 있어도 막습니다runtime.rs L582
  6. allowlist가 비었거나 안 맞으면 NotAllowedruntime.rs L598
  7. NotAllowed만 결정자에게 묻습니다network_policy.rs L349
  8. Limited에서 GET, HEAD, OPTIONS가 아니면 막습니다config.rs L311
  9. HTTPS는 안쪽 메서드를 봐야 하고, MITM이 없으면 막습니다http_proxy.rs L312
  10. 헤더에 가짜가 있어야 짝이 되는 진짜 자격 증명을 넣습니다mitm.rs L301
재생을 눌러, 이 요청이 관문 넷을 차례로 걷는지, 가짜가 어느 단계에서 진짜로 바뀌는지 보세요.
도메인을 먼저deny는 늘 이깁니다. 로컬 리터럴은 정확한 허용이 필요해요. 인트라넷으로 해석되는 이름은 명단에 써도 못 지나갑니다.
진짜 열쇠는 브로커를 나가지 않아요자식 환경과 모델 컨텍스트는 늘 가짜입니다. 진짜 token은 outbound 그 한 걸음에서만 바뀌어요.
샌드박스는 깔때기 입구Go가 프록시를 건너뛰면 정책 함수는 안 돌아요. OS는 프록시 포트만 열고, 직결은 첫 관문에서 멈춥니다.
수업용 시연: 명단, 가짜 모양, 해석 결과는 수업용 설정입니다. 논리 궤적 오른쪽 행 번호는 openai/codex 저장소 commit 4f39251a01에 대응해요.
아이디어 1 · 도메인은 어떻게 판정되는가
어떤 문제를 푸는가

모델에게 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로 해석되는 도메인은 명단에 정확히 써도 막아요. 로컬 리터럴은 localhost127.0.0.1을 정확히 써야 하고, 와일드카드 *는 안 칩니다.

출처:codex-rs/network-proxy/src/policy.rs 321–331행;codex-rs/network-proxy/src/runtime.rs 1028–1043행

host 해석 실패해도 막음 1 deny 맞으면 바로 멈춤 403 denylist 2 로컬 또는 사설 리터럴은 정확해야 함 403 allowlist 3 allowlist 빈 명단도 막음
수업용 구조도: 세 단계 순서는 고정입니다. Denied와 NotAllowedLocal은 결정자에게 묻지 않아요.

막히면 명령 프로세스에 403을 돌립니다. 헤더는 x-proxy-error, 본문은 사람 말 한 줄. 밖에서는 not_allowednot_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 방어의 일반적인 모양이에요. 다른 언어로 다시 써도 물을 것은 같습니다. 명단이 없으면 어쩌나, 충돌하면 누가 이기나, 인트라넷으로 해석된 이름은 허용인가.

아이디어 2 · 진짜 열쇠는 프록시 안에서만 나타납니다
어떤 문제를 푸는가

token은 명령을 따라 rollout에 들어갑니다. 다음 턴에도 모델이 볼 수 있고, 그다음 턴에는 로그에 들어갈 수 있어요. 자격 증명이 모델 컨텍스트에 들어가면, 뒤의 모든 마스킹은 수습입니다.

아이디어는 무엇인가

명령을 띄우기 전, 자격 증명 브로커가 GH_TOKENOPENAI_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행

모델 진짜 token을 볼 수 없음 자식 환경 같은 모양 가짜 프록시 MITM host로 진짜를 바꿈 업스트림 진짜 token 진짜 값은 프록시 프로세스 메모리에만 나타나고, rollout에도, 모델의 다음 턴 컨텍스트에도 안 들어가요
수업용 타임라인: 가짜는 보이는 면 전체를 걷고, 진짜는 outbound 그 한 점프에만 나타납니다.
자식 주머니에는 가짜 반쪽표, 카운터 뒤에는 진짜 표가 잠겨 있어요.
왜 오래가는가

outbound 정책을 잘못 써도, 모델은 403을 보고 마음을 바꿀 수 있어요. 자격 증명이 컨텍스트에 들어가면 철회 비용이 큽니다. 그래서 진짜는 spawn 전에 가져가고, 되돌리기는 프록시 프로세스 메모리에서 일어나요. 가짜가 모양을 지키는 것은, 형식을 검사하는 클라이언트가 그래도 뜨게 하려는 것입니다.

아이디어 3 · 샌드박스가 트래픽을 프록시로 누릅니다
어떤 문제를 푸는가

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는 밸브를 모든 자식 프로세스 출구에 두므로 샌드박스와 묶여야 합니다.

양쪽 모두 소스 대조 완료 · 2026-08-22 · 출처:web_fetch/domain.rs 110–144행;web_fetch/ssrf.rs 17–19행

DSH: 설정은 변수 이름만 두고, spawn 때 키 이름으로 환경을 지웁니다

DSH는 비밀을 참조로 만듭니다. 설정 파일은 환경 변수 이름만 들고, provider가 작업마다 resolve해요. 설정 표면은 값을 보지 않습니다.

자식에는 지우기가 한 층 더 있어요. KEY, TOKEN처럼 생긴 키와 모든 DSH_*는 버려집니다. 설정 파일에 진짜 값이 나타나는 것은 막아요. 모델이 아직 printenv할 수 있으면, spawn이 이 지우기를 쓰지 않는 한 환경에 진짜 token이 남을 수 있습니다.

양쪽 모두 소스 대조 완료 · 2026-08-22 · 출처: packages/credentials/credentials/src/index.ts 1–7행; packages/subprocess/subprocess/src/index.ts 60–66행 · DSH · 자격 증명은 매번 지금 꺼냄
수업 실습
01

별표는 localhost를 허용할까요

allow_local_binding = false이고 allowlist는 *뿐입니다. 127.0.0.1에 판정을 한 번 부르면 기대는 무엇인가요. 공망 IP 8.8.8.8에 다시 부르면 기대는 또 무엇인가요.

힌트: 로컬 리터럴은 와일드카드를 거절합니다. allowlist 컴파일은 전역 *를 명시 허용하고, denylist 컴파일은 거절해요.

Takeaway:outbound는 기본 거절, deny는 늘 이기고, 인트라넷은 정확히 문을 열어야 합니다. 진짜 비밀은 모델이 보는 환경에 넣지 말고, spawn 전에 가짜로 바꾸고 outbound에서 다시 바꾸세요. 프록시는 건너뛰는 클라이언트를 못 막고, 다음 층은 프록시 포트만 여는 샌드박스여야 해요.