OpenAI Codex · 실행 경계

macOS: 보안 정책을 문자열 하나로 짜요

Seatbelt가 먹는 것은 텍스트 한 편이에요. 정적 베이스라인에 그때그때 짜는 읽기·쓰기·네트워크 구간을 더해요. 경로는 이 텍스트에 안 들어가고 옆의 파라미터 표로 가요.

강의 목표읽고 나면 세 가지를 말할 수 있어요. macOS에서 플랫폼 샌드박스는 기본으로 있고 Windows 같은 총스위치는 없어요. sandbox-exec에 넘기는 정책 본문은 자리만 남기고 실제 경로는 -D로 가요. 경로 하나를 파낼 때 이 노드, 그 자손, 그리고 이 노드를 옮기는 일을 동시에 막아야 해요.
먼저 한 번 써 보세요 · 조각을 켜고 메뉴가 어떻게 자라는지 보세요
정책 조립대: 왼쪽에서 조각을 더하고 오른쪽에서 본문, 재료표, 허용 범위를 보세요
조각
조각 스위치를 눌러 정책 본문과 오른쪽 프로브가 어떻게 바뀌는지 보세요.
네트워크
실험
정책 본문 · 코드만 남겨요
재료표와 프로브
논리 궤적 · 애니메이션 각 단계가 소스 어디에 대응하는지
  1. 권한 프로필을 읽기·쓰기·네트워크로 나눠요manager.rs L375
  2. /usr/bin/sandbox-exec를 고정으로 써요seatbelt.rs L56
  3. 정적 sbpl 네 편을 넣어요seatbelt.rs L21
  4. 루트별로 allow와 require-not을 만들어요seatbelt.rs L476
  5. 쓰기 루트에 file-write-unlink를 못 박아요seatbelt.rs L508
  6. 제외 하위 경로에 literal과 subpath를 같이 써요seatbelt.rs L551
  7. 네트워크 구간을 짜고, managed 포트 없음은 제한으로 가요seatbelt.rs L309
  8. sections를 join해 -p 텍스트가 돼요seatbelt.rs L1012
  9. 경로는 -DKEY=value로 써요seatbelt.rs L1024
  10. 짧은 줄표 둘 뒤에 사용자 명령seatbelt.rs L1029
재생을 눌러, 정책 한 편이 조각으로 어떻게 짜이는지 보세요. 조각을 직접 켜고 꺼도 돼요.
본문과 경로는 갈라요메뉴에는 param 코드만 적어요. 실제 디렉터리 이름은 -D에 떨어져요. 따옴표 있는 경로는 SBPL 문법을 오염시키지 못해요.
파내기를 하나 빼면 새요subpath만 남기면 mkdir .codex가 성공해요. require-not 둘에 마지막 unlink를 더해야 노드·자손·옮기기를 같이 못 박아요.
네트워크는 먼저 좁혀요프록시를 쓰려는데 포트가 없으면 전체 네트워크를 허용하지 마세요. 네트워크 베이스라인은 있고, blanket outbound만 없어요.
수업용 스케치: 경로와 프로브는 수업용 설정으로, 조각이 정책 텍스트와 허용 범위를 어떻게 바꾸는지 보여 줘요. 논리 궤적 오른쪽 행 번호는 openai/codex 저장소 commit 4f39251a01에 대응해요.
아이디어 1 · 대문을 먼저 닫고, 조각으로 문을 열어요
어떤 문제를 푸는가

동료가 저장소를 쓰기 루트로 두고 모델이 안에서 코드를 고치게 해요. 모델이 mkdir .codex를 실행해 저장소에 자기 설정을 남기려 해요. 명령은 바로 실패하고 stderr는 Operation not permitted예요. 쓰기 루트는 나무 전체처럼 들리지만 실제로는 아니에요.

더 돌아가는 길을 한 번 더 써요. 모델이 먼저 저장소에 평범한 파일을 쓰고 워크스페이스 전체를 mv해서 안의 .codex를 샌드박스 밖으로 옮기려 해요. 이것도 실패해요. 오류는 여전히 Operation not permitted예요.

아이디어는 무엇인가

macOS에서 get_platform_sandbox는 바로 MacosSeatbelt를 돌려줘요. 그 불리언 스위치는 Windows에만 의미가 있어요. 래퍼는 Codex 자기 exe가 아니라 시스템의 고정 경로 /usr/bin/sandbox-exec예요. PATH에 같은 이름 바이너리를 두어도 소용없어요.

출처: codex-rs/sandboxing/src/manager.rs 62–76행 · codex-rs/sandboxing/src/seatbelt.rs 52–56행

정책 텍스트는 두 층이에요. .sbpl 네 편을 include_str!로 바이너리에 넣어 정적 베이스라인으로 써요. 동적 구간은 이번의 읽기·쓰기 루트로 그때그때 짜요. 이어 붙이는 순서는 고정이에요. 베이스라인, 읽기, 쓰기, 네트워크가 앞이에요. 전체 디스크 읽기일 때만 preferences를 더하고, 제한 읽기일 때만 플랫폼 기본 경로를 더해요. 조상의 file-write-unlink는 마지막에 두어, 앞의 더 넓은 allow가 rename용 unlink를 다시 열지 못하게 해요.

출처: codex-rs/sandboxing/src/seatbelt.rs 21–27행 · codex-rs/sandboxing/src/seatbelt.rs 985–1012행

베이스라인 첫 업무 규칙은 (deny default)예요. 명시하지 않은 일은 모두 거절해요. 자식 프로세스가 이 밑그림을 상속하고, sandbox-exec가 띄운 bash가 다시 fork한 자손도 같은 규칙 안에 있어요.

권한 프로필 읽기 루트 · 쓰기 루트 · 네트워크 정적 sbpl 네 편 동적 읽기·쓰기 규칙 동적 네트워크 구간 join -p 텍스트 -D 파라미터 표 sandbox-exec 그다음 사용자 명령을 exec해요
수업용 구조도: 권한 프로필 하나를 조각으로 나눠 argv로 짠 뒤 시스템의 그 고정 경로 래퍼에 넘겨요.
왜 오래가는가

Seatbelt 커널 인터페이스는 SBPL 텍스트 한 편을 먹어요. 통제할 수 있는 것은 텍스트를 어떻게 만드는지예요. 기본 거절, 그다음 이번 루트로 문을 여는 순서는 다른 언어로 다시 써도 필요해요. 조각 순서는 계약이에요. 더 좁은 deny가 앞의 더 넓은 allow를 눌러야 해요.

아이디어 2 · 경로는 재료표로 가고 메뉴에 안 들어가요
어떤 문제를 푸는가

어떤 사람은 프로젝트를 ~/work/app (copy)처럼 괄호와 공백이 있는 디렉터리에 두거나 경로에 따옴표가 있어요. SBPL에서 괄호, 따옴표, 세미콜론은 문법이에요. 경로가 정책 본문에 들어가면 사용자 디렉터리 이름이 문법이 될 수 있어요. sandbox-exec 파싱이 실패하면 모델이 보는 것은 래퍼 오류이지 샌드박스 거절이 아니에요.

아이디어는 무엇인가

동적 읽기·쓰기 규칙은 본문에 (param "WRITABLE_ROOT_0") 같은 키 이름만 써요. 실제 경로는 -DKEY=value로 독립 argv가 돼요. 제외 하위 경로도 파라미터 표로 가요. 정책 본문과 -D는 같이 나타나야 하고, 테스트가 이 일을 계약으로 잠가요.

출처: codex-rs/sandboxing/src/seatbelt.rs 1022–1028행

경로를 본문에 써 넣어요 subpath "/tmp/app(copy)" 괄호가 문법이 돼요 래퍼 파싱 실패 경로는 파라미터 표로 가요 본문은 param만 써요 WRITABLE_ROOT_0 -D가 argv 한 줄 value에 따옴표가 있어도 돼요 문법이 오염되지 않아요
수업용 대조: 왼쪽은 디렉터리 이름을 파서에 주고, 오른쪽은 코드만 파서에 줘요.
왜 오래가는가

보안 정책이 결국 텍스트 한 편이면, 사용자가 통제하는 문자열은 이 텍스트에 넣지 마세요. 템플릿은 자리만 두고 실제 경로·도메인·포트는 다른 파라미터 표로 가요. Seatbelt 문법과 상관없어요. Python에서도 템플릿 더하기 argv예요.

아이디어 3 · 파내기는 노드·자손·옮기기를 막아야 해요
어떤 문제를 푸는가

subpath만 쓰면 이 디렉터리를 처음 만들 때 새고 mkdir .codex가 성공해요. 메타데이터 보호가 빈 껍질이 돼요. file-write-unlink를 안 넣으면 rename이 읽기 전용 하위 트리를 파낸 구역 밖으로 옮길 수 있어요. 다음에 원래 경로로 권한을 주면 실제 쓰는 곳은 링크 맞은편이에요.

아이디어는 무엇인가

제외 하위 경로에서는 require-not을 두 줄 써야 해요. literal은 디렉터리 자신, subpath는 아래 내용이에요. 둘을 require-all에 넣으면 AND예요. 순서는 둔감하고, literal을 빼는 것이 문제예요.

출처: codex-rs/sandboxing/src/seatbelt.rs 548–555행

쓰기 루트는 기본으로 최상위 이름 셋을 보호해요. .git, .agents, .codex. 쓰기 루트 디렉터리 자체에 deny file-write-unlink를 하나 더 못 박아요. 쓰기 루트 가운데 사용자가 통제하는 심볼릭 링크가 있으면 조립이 바로 오류이고 링크를 따라가지 않아요.

출처: codex-rs/protocol/src/permissions.rs 24–33행 · codex-rs/sandboxing/src/seatbelt.rs 504–509행 · codex-rs/sandboxing/src/seatbelt.rs 441–450행

네트워크는 다른 동적 텍스트예요. 프록시를 쓰려는데 쓸 포트가 없으면 제한 네트워크 구간 더하기 네트워크 베이스라인을 돌려주고 (allow network-outbound) 같은 전체 네트워크 허용은 안 줘요. 네트워크 베이스라인은 있고 blanket outbound만 없어요.

출처: codex-rs/sandboxing/src/seatbelt.rs 309–335행

literal 이 노드를 막아요 mkdir .codex 디렉터리 자신이 아직 없을 때 subpath가 꼭 먹지 않아요 subpath 그 자손을 막아요 만들어진 뒤의 파일 literal만 남기기 아래 쓰기가 새요 unlink 경계를 옮기는 일을 막아요 rm 다음 ln -s sections 마지막에 둬요 더 넓은 allow가 다시 열지 못하게 해요
수업용 구조도: 규칙 셋을 같이 만들고, 하나 빼면 우회가 하나 새요.
경로는 파라미터 표로 가요. 파내기는 세 줄을 같이.
왜 오래가는가

경로 하나를 파낼 때 노드 자신과 자손을 동시에 막아요. 이 노드의 rename도 막아요. 셋이 같이 만들어지고 테스트가 모양을 잠가요. 플랫폼마다 오류 문구가 달라요. macOS는 Operation not permitted가 흔해 기존 키워드 표가 받을 수 있어요. 다른 두 플랫폼도 이 문장이라고 가정하지 마세요.

출처: codex-rs/sandboxing/src/denial.rs 50–58행

가로 비교 · 경로가 이 텍스트에 들어가는지

Claude Code: 읽기를 먼저 열고, 경로를 stringify한 뒤 텍스트에 넣어요

Claude는 (allow file-read*)에서 deny를 내리고, deny 안에서 다시 허용해요. 뒤에 쓴 규칙이 우선이에요. 경로는 JSON.stringify로 한 겹 싸서 정책 텍스트에 넣어요. 래핑 명령은 PATH에서 sandbox-exec를 찾고 /usr/bin을 고정하지 않아요. 양쪽 모두 rename을 막아야 해서 Claude도 deny file-write-unlink를 만들어요.

Codex는 (deny default)에서 allow를 올리고 경로는 -D에 남겨 둬요. 내는 것은 생성기 복잡도이고, 얻는 것은 사용자 경로가 SBPL 문법에 안 들어가는 거예요. Claude가 내는 것은 뒤에 쓰는 순서를 맞춰야 하는 것이고, 얻는 것은 설정 면이 allowAllExcept에 더 가까운 거예요.

양쪽 소스 확인 · 2026-08-22

DSH와 Grok: 경로가 텍스트에 들어가고, 이스케이프하거나 바로 실패해요

DSH 기본은 (allow default) 다음 (deny file-write*)로 쓰기 권한만 거둬들여요. 경로는 sbplString이 백슬래시와 큰따옴표를 이스케이프한 뒤 -p에 바로 넣어요. -D 파라미터 표는 없어요. sandbox-exec는 PATH에서 가져요. 이 Mac에서는 “쓰기 루트 안에서 .codex를 파낸다”는 이 입도를 못 해요.

Grok은 sandbox-exec를 한 겹 씌우지 않아요. SBPL 조각을 현재 프로세스에 넘겨요. 경로는 그대로 텍스트에 들어가고 제어 문자는 바로 실패해서, 샌드박스가 active라고 하는데 그 경로는 안 막히는 일을 피해요. 커널 apply가 실패하면 계속 달려요. Codex는 Seatbelt 준비가 실패하면 오류를 돌려주고 명령은 맨몸으로 안 달려요.

양쪽 소스 확인 · 2026-08-22
수업 실습
01

require-not을 하나 빼면 어디에 새는지

데모에서 쓰기 루트와 .codex 파내기를 먼저 켜고 mkdir .codex 프로브가 빨간지 보세요. 그다음 “subpath만 남기기”를 켜면 프로브가 초록이 돼요. 자기 말로 설명하세요. subpath는 디렉터리 아래를 맡는데, 왜 이 노드를 처음 만드는 일을 못 막는지. seatbelt.rs 548–555행 주석과 대조하면 되고, 실제 저장소에서 파괴적인 명령을 실행하지 마세요.

Takeaway: macOS 샌드박스는 짜 맞춘 SBPL 한 편이에요. 경로는 -D로 가고 본문은 자리만 남겨요. 파내기는 노드·자손·옮기기를 동시에 막아야 해요. Seatbelt는 정책이 거절한 동작은 막지만, 넓게 쓴 allow는 못 막고, 모델이 쓰기 루트에서 업무 코드를 고치는 것도 못 막아요.