에이전트에게 권한을 주기 전에 울타리부터, NVIDIA OpenShell로 파일·네트워크·키를 가두는 법
Tools17분 읽기

에이전트에게 권한을 주기 전에 울타리부터, NVIDIA OpenShell로 파일·네트워크·키를 가두는 법

OpenShell은 에이전트를 샌드박스에 가두고, 막힌 연결을 거부 기록·초안 제안·위험 점검·사람의 승인을 거쳐 정책으로 바꿉니다. 기본값과 자동 승인의 빈틈, 시작 정책까지 정리했습니다.

에이전트에게 “저장소를 받아서 의존성을 설치하고 테스트를 돌려 줘”라고 맡겼습니다. 에이전트는 OpenShell 샌드박스 안에서 일을 시작했고, 몇 초 뒤 pip install이 멈추면서 403 from proxy after CONNECT라는 한 줄이 돌아옵니다. 에이전트가 고장 난 것이 아닙니다. 아무 규칙도 적지 않은 샌드박스는 바깥 연결을 전부 거부하고, 이 거부가 설계된 첫 반응입니다. 여기까지는 편집부가 공식 문서의 동작을 바탕으로 꾸민 장면입니다.

이 글은 그 한 줄에서 출발해 막힌 연결이 어떻게 기록되고, 초안 제안이 되고, 위험 점검을 거쳐 사람의 승인으로 정책에 들어가는지를 끝까지 따라갑니다. 가는 길에 샌드박스가 무엇을 열어 두고 무엇을 잠그는지, 키는 어디에 있는지, 승인 절차가 어디서 느슨해질 수 있는지를 함께 짚습니다.

NVIDIA가 Apache-2.0 라이선스로 공개한 OpenShell은 AI 에이전트를 격리된 샌드박스에 넣고 파일, 시스템 호출, 네트워크를 정책으로 제한하는 런타임입니다. 2026년 10월 27일(UTC) 기준 GitHub 별은 약 2만 개(20,052개), 포크는 1,981개이고 저장소는 2월 24일에 만들어졌습니다. 최신 안정판은 10월 20일에 나온 v0.1.5이고 그 뒤로 새 안정판은 나오지 않았습니다.

에이전트의 손이 닿는 곳과 닿지 않는 곳

에이전트가 쓸모 있는 이유와 위험한 이유는 같습니다. 파일을 읽고, 패키지를 설치하고, API를 부르고, 자격 증명을 쓰기 때문입니다. 문서가 짚는 위험은 네 가지입니다. 소스 코드가 허가받지 않은 곳으로 올라가는 데이터 유출, SSH 키 같은 로컬 비밀을 읽는 자격 증명 탈취, 프롬프트와 데이터가 승인 안 된 모델 제공자에게 가는 허가받지 않은 API 사용, sudo나 위험한 시스템 호출을 시도하는 권한 상승입니다. OpenShell은 각각을 승인된 목적지만 허용하기, 선언한 경로만 접근하기, 제공자 프로필과 네트워크 정책으로 모델 트래픽 한정하기, 비특권 사용자와 시스템 호출 제한으로 막습니다.

이 통제를 서로 다른 신뢰 구역에 나눠 놓은 것이 구조의 핵심입니다. 아래 그림에서 왼쪽은 에이전트가 사는 곳이고, 가운데는 판단하는 쪽, 오른쪽은 바깥입니다. 요청과 키가 어느 구역을 넘는지에 주목해서 보면 됩니다.

에이전트가 사는 샌드박스, 판단하는 감독자와 게이트웨이, 바깥 세계 세 구역과 요청·키가 오가는 길을 그린 그래프 그림 1. 에이전트의 유일한 출구는 감독자다. 진짜 키는 신뢰 구역에만 있고 샌드박스에는 자리표시자가 들어간다

구성 요소는 넷입니다. 게이트웨이는 사용자 인증, 샌드박스 생명주기, 정책과 제공자 연결을 맡고 감독자에게 정책과 자격 증명을 내려 줍니다. 컴퓨트 드라이버는 샌드박스와 감독자를 띄우고 바깥 울타리를 칩니다. 감독자는 요청을 정책과 대조하고, DNS를 대신 풀고, 허용된 연결만 열고, 자격 증명을 붙입니다. 샌드박스는 에이전트 프로세스를 가두고 호출한 프로그램을 식별해 TCP와 DNS를 감독자에게 넘깁니다. 중요한 점은 샌드박스와 드라이버가 “허용할지 말지”를 결정하지 않는다는 것입니다. 판단은 신뢰 구역의 감독자 한 곳에서만 합니다.

바깥 울타리는 런타임마다 다른 도구로 칩니다. Docker와 Podman은 워크로드 컨테이너의 네트워킹을 꺼 버리고, 쿠버네티스는 감독자 서비스만 허용하는 NetworkPolicy를 쓰고, VM은 게스트에 네트워크 장치를 아예 주지 않습니다. 어느 쪽이든 에이전트가 바깥으로 나가는 길은 감독자와 맺은 보호 연결 하나뿐입니다. DNS 조회도 같은 길을 가서 감독자가 대신 풀고, 보호 연결이 끊기면 샌드박스는 에이전트를 멈춰 세우며 같은 감독자 프로세스만 다시 이어 붙을 수 있습니다. 토큰은 샌드박스 하나와 실행 세대 하나에 묶여 있어서 재시작하면 이전 토큰은 쓸 수 없습니다.

기본 정책이 비워 둔 것

정책을 따로 지정하지 않으면 대체용 기본 정책이 적용됩니다. 무엇이 열려 있고 닫혀 있는지, 그리고 그 값을 언제 바꿀 수 있는지를 한 표로 모으면 “왜 안 되지”를 빨리 풀 수 있습니다.

항목 기본값 바꿀 수 있는 시점
읽기 전용 경로 /bin, /usr, /lib, /proc, /etc, /var/log, /dev/urandom 샌드박스를 만들 때 고정
읽기·쓰기 경로 작업 디렉터리, /tmp, /dev/null 샌드박스를 만들 때 고정
네트워크 규칙 없음, 바깥 연결 전부 거부(연결한 제공자가 규칙을 더하면 넓어질 수 있음) 실행 중에 변경
실행 사용자 비root. Docker·Podman은 이미지의 USER, 없으면 UID·GID 1000 샌드박스를 만들 때 고정
보호 경로 비공개 /.openshell 디렉터리는 어떤 정책으로도 열 수 없음 변경 불가
파일 통제 방식 Landlock. ABI 3 이상(리눅스 6.2+)이어야 하고 기준선을 못 걸면 샌드박스가 시작되지 않음 만들 때 고정

표에서 눈여겨볼 열은 마지막입니다. 파일과 프로세스 설정은 만들 때 고정되므로 풀려면 새 샌드박스가 필요하고, 네트워크 규칙만 실행 중에 바뀝니다. 이 글의 주제인 승인 루프가 네트워크 규칙 하나를 고치는 길에 한정되는 것도 이 때문입니다.

바꿀 수 없는 바닥도 있습니다. seccomp은 ptrace, io_uring_setup, process_vm_readv, memfd_create, mount 계열 같은 호출을 무조건 막고, 소켓은 유닉스 소켓, netlink(네트워크 인터페이스 조회용), 감독자가 중계하는 인터넷 소켓만 만들 수 있습니다. 사설 IP로 가는 연결은 기본으로 막히고(정확한 호스트 이름을 적은 엔드포인트는 예외) 루프백, 링크 로컬, 미지정 주소는 정책으로도 열 수 없습니다. 클라우드 메타데이터 주소 169.254.169.254가 링크 로컬에 속합니다. 이 목록은 사용자가 바꾸지 못하고, 뒤에 나올 승인 절차도 이 선을 넘지 못합니다.

사용자 정책의 Landlock 호환 설정(compatibility) 기본값은 best_effort입니다. 커널이나 이미지에 없는 경로는 건너뛰고 High 심각도의 탐지 기록만 남깁니다. 모든 제한이 반드시 걸려야 한다면 hard_requirement로 바꿔 시작 자체를 거부하게 합니다.

같은 거부라도 두 관문 중 어디서 막혔나

네트워크 규칙은 “어떤 실행 파일이 어떤 목적지에 닿을 수 있는가”를 짝지어 적습니다. 공식 첫 튜토리얼에서 만드는 규칙을 문서의 스키마대로 옮기면 이런 모양입니다.

network_policies:
  github_api:
    endpoints:
      - host: api.github.com
        port: 443
        protocol: rest
        enforcement: enforce
        access: read-only
    binaries:
      - path: /usr/bin/curl

검사는 두 관문으로 이뤄집니다. 첫째 관문은 연결 검사로, 실행 파일과 호스트와 포트가 규칙에 있는지 봅니다. 규칙이 없으면 여기서 끝납니다. 둘째 관문은 엔드포인트에 protocol을 지정했을 때만 열리는 요청 검사로, HTTP 메서드와 경로를 봅니다. 위 규칙 하나에 요청 네 개를 넣어 보면 어느 관문에서 끝나는지가 갈립니다.

curl GET은 두 관문을 모두 통과하고, curl POST는 둘째 관문에서, 다른 호스트나 다른 프로그램은 첫째 관문에서 막히는 격자 그림 2. 같은 호스트라도 프로그램이 다르면 첫째 관문, 메서드가 다르면 둘째 관문에서 걸린다

두 번째 줄과 네 번째 줄이 흥미롭습니다. 같은 api.github.com으로 가는 POST는 연결까지는 통과하고 메서드에서 막히지만, 파이썬이 같은 주소로 GET을 보내면 규칙에 나열되지 않은 프로그램이라 연결부터 막힙니다. 호출 프로그램은 에이전트가 내민 경로가 아니라 샌드박스가 /proc 정보로 직접 확인한 실행 파일로 판별합니다. 그래서 규칙에는 심볼릭 링크가 아니라 진짜 경로를 적어야 하고(샌드박스 안에서 readlink -f로 확인), pip install을 허용하려면 pip이 아니라 PyPI에 접속하는 파이썬 인터프리터를 적습니다. 처음 연결에 참여한 실행 파일의 SHA-256 해시를 기록해 두고 같은 경로의 파일이 바뀌면 연결을 거부하는 장치도 있어서, 일부러 바이너리를 교체했다면 샌드박스 런타임을 재시작해 새 해시를 기록하게 해야 합니다.

규칙의 의미에서 조심할 점이 몇 가지 있습니다. 규칙에 나열한 모든 실행 파일이 나열한 모든 엔드포인트에 닿으므로, 어떤 프로그램을 일부 목적지에만 허용하려면 규칙을 나눠 적습니다. 빈 binaries 목록은 아무것도 허용하지 않고, 규칙에 나열한 프로그램이 띄운 하위 프로세스는 같은 규칙을 씁니다. access: read-only는 GET·HEAD·OPTIONS만 허용하는 약식 표기이고, 세밀하게 쓰려면 rules로 메서드와 경로를, deny_rules로 막을 경로를 적으며 허용 규칙과 거부 규칙이 겹치면 거부가 이깁니다. 한 엔드포인트에서 access와 rules를 같이 쓸 수는 없습니다.

요청 검사에는 기본값의 함정이 있습니다. enforcement의 기본값은 audit이어서 규칙을 어긴 요청을 로그에 남기기만 하고 그대로 통과시킵니다. 막으려면 enforce를 적어야 하고, 막힌 요청은 403과 위반 내용을 담은 JSON 응답을 받습니다. 더 근본적으로는 protocol을 적지 않은 엔드포인트가 호스트·포트·실행 파일만 보고 메서드와 경로는 따지지 않으므로, API 엔드포인트에는 protocol: rest를 함께 쓰는 것이 안전합니다. 새 규칙은 audit로 걸고 워크로드를 돌려 로그를 확인한 뒤 enforce로 올리는 순서가 문서가 권하는 방식입니다.

거부 기록 두 줄을 읽는 법

첫 튜토리얼은 위 규칙을 넣기 전과 후를 한 화면에 보여 줍니다. 샌드박스 안에서 curl이 막히고, 호스트에서 로그를 열면 어떤 프로그램이 어디로 가려다 막혔는지가 한 줄로 나옵니다. 규칙을 넣은 뒤에는 GET이 되고 POST만 막히는데, 그 두 번째 거부가 아래 화면의 아랫줄입니다.

curl 거부, 연결 단계 거부 기록, 규칙 추가 후 POST의 policy_denied 응답과 요청 단계 거부 기록을 담은 터미널 화면 그림 3. 공식 튜토리얼의 출력(시각 값은 생략). 두 거부 기록의 engine 값이 어느 관문이 판단했는지를 알려 준다

NET:OPEN은 연결을 여는 단계의 거부이고 /usr/bin/curl(64)는 시도한 실행 파일, engine:opa는 첫째 관문이 판단했다는 뜻입니다. policy:-가 비어 있는 것은 매칭된 규칙이 없다는 신호입니다. 둘째 거부는 HTTP:POST로 시작하고 policy:github_api engine:l7이 붙어 규칙 이름까지 보입니다. 이 차이를 알면 “이 요청은 규칙을 새로 만들어야 하나, 있는 규칙을 좁혔다 넓혀야 하나”를 로그 한 줄에서 가릴 수 있습니다. 정책 이벤트는 INFO 수준으로 기록되므로 --level warn으로 거르면 이 줄들이 보이지 않습니다.

거부가 초안 제안이 되는 순서

사람이 로그를 보고 YAML을 고치는 방식은 에이전트가 막힐 때마다 반복됩니다. OpenShell은 이 반복을 줄이려고 막힌 연결에서 규칙 초안을 스스로 만듭니다. 이 초안은 정책 어드바이저 기능을 켜지 않은 샌드박스에서도 만들어지고, 바이너리 하나가 호스트 하나의 포트 하나에 닿게 해 달라는 좁은 형태입니다. 규칙에 이름이 없는 호스트로의 연결도 대상입니다. 중요한 점은 초안이 생겼다고 막힌 요청이 풀리지 않는다는 것입니다. 요청은 초안이 승인되고 규칙이 적재될 때까지 거부 상태이므로 에이전트가 다시 시도해야 합니다.

막힌 연결에서 초안 제안, 위험 점검, 사람의 승인, 새 정책 리비전, 에이전트의 재시도까지의 주고받는 순서 그림 4. 승인한 규칙은 샌드박스를 재시작하지 않고 적용되고, 적용 때 옛 규칙으로 열린 연결은 닫힌다

초안은 대기 목록에 들어가기 전에 정책 증명기(prover)의 위험 점검을 받습니다. SMT 솔버로 “이 규칙을 더하기 전과 후에 샌드박스가 닿을 수 있는 곳”을 비교하며, 연결된 제공자의 자격 증명과 검사할 수 없는 프로그램까지 계산에 넣습니다. 문서가 정리한 발견은 네 가지입니다.

발견 제안한 규칙이 허용하게 되는 일
link_local_reach 바이너리가 링크 로컬 주소나 클라우드 메타데이터 호스트에 닿음
l7_bypass_credentialed git·ssh·nc처럼 검사할 수 없는 트래픽이 자격 증명이 붙는 호스트에 닿음
credential_reach_expansion 자격 증명을 쓸 수 있는 호스트·포트가 새로 늘어남
capability_expansion 이미 자격 증명을 쓰는 호스트에서 새 HTTP 메서드가 허용됨

두 정책 중 하나를 분석하지 못하면 제안에 validation unavailable이 붙고 검증이 성공할 때까지 승인할 수 없습니다. 또 증명기의 보증은 모델에 담긴 기능(파일, 프로세스 사용자, Landlock, L4 연결, REST 규칙)에 한정되어 GraphQL·MCP·WebSocket 규칙은 unsupported로 돌아오며, 통과가 곧 “최소 권한”을 뜻하지도 않습니다.

사람이 하는 일은 호스트 터미널에서 대기 목록을 읽고 판정하는 것입니다.

  1. 대기 목록 보기. openshell rule get my-agent --status pending. 제안한 규칙, 이유, 위험 점검 결과가 나오고 Chunk 줄에 제안 ID가 있습니다
  2. 규칙을 직접 읽기. 에이전트의 설명이 아니라 목록에 표시된 규칙의 호스트·포트·바이너리·메서드·경로를 봅니다. Security 줄에 표시가 붙은 제안은 한 번 더 따져 봅니다
  3. 승인 또는 거절. openshell rule approve my-agent --chunk-id <ID>, 또는 openshell rule reject my-agent --chunk-id <ID> --reason "docs/ 경로로만 좁혀 주세요". 이유는 에이전트에게 그대로 돌아갑니다
  4. 재시도 확인. 승인하면 규칙이 실행 중인 샌드박스에 적재되고 에이전트가 다시 시도할 수 있습니다. 다른 대기 제안은 갱신된 정책 기준으로 다시 점검되고, 한 번에 최신 32개까지 재검사됩니다

일괄 승인 명령 openshell rule approve-all my-agent도 있지만 보안 표시가 붙은 제안은 건너뛰고(--include-security-flagged를 주면 포함) 건너뛴 개수를 알려 줍니다. 승인할 수 없는 제안도 있습니다. 루프백, 링크 로컬, 메타데이터 주소를 향한 제안은 어떤 경우에도 승인되지 않고, 이름이 사설 주소로 풀리는 호스트는 allowed_ips 항목을 더하거나 엔드포인트를 직접 선언하기 전까지 승인 뒤에도 막힌 채입니다. 전역 정책이 켜져 있는 동안에는 자동 승인을 포함해 어떤 승인도 할 수 없습니다.

에이전트가 직접 규칙을 제안하게 하면

초안을 만드는 쪽이 OpenShell이라면, 한 단계 더 나가 에이전트가 이유를 이해하고 직접 규칙을 제안하게 할 수도 있습니다. 이 기능은 기본으로 꺼져 있고 게이트웨이 전체에는 openshell settings set --global --key agent_policy_proposals_enabled --value true --yes로 켭니다. 켜면 샌드박스 안에 안내문(/etc/openshell/skills/policy_advisor.md)과 http://policy.local API가 생기고, 검사되는 HTTP 엔드포인트의 거부 응답에 요청 방법이 적힙니다. 에이전트는 GET /v1/denials로 최근 거부를 읽고, POST /v1/proposals로 제안하고, /wait로 결과를 기다립니다. 샌드박스를 만들기 전에 켜 두는 편이 좋은데, 에이전트가 /AGENTS.md 같은 파일을 시작할 때만 읽는 경우가 많고 앞서 거부당하고 포기했을 수도 있기 때문입니다.

제안은 네트워크 규칙 추가만 가능합니다. 삭제나 파일·프로세스 설정 변경은 못 하고, protocol: tcp나 tls: skip도 제안할 수 없으며, 자격 증명 설정이나 GraphQL·MCP 규칙 필드는 API에 아예 없습니다. 아래는 문서의 방식을 따라 편집부가 꾸민 장면입니다. 에이전트가 처음 너무 넓게 제안하고, 사람이 이유를 달아 거절하자 좁혀서 다시 올립니다.

pip이 pypi.org:443에서 403으로 막힌다. 에이전트는 거부 응답의 안내를 따라 policy.local을 열고 정책과 최근 거부를 읽는다

python3가 pypi.org와 files.pythonhosted.org의 443 포트에 닿도록 규칙을 제안합니다. 메서드는 제한하지 않았습니다

openshell rule reject --reason "읽기 전용(GET)으로만 열어 주세요"

같은 두 호스트에 access를 read-only로 좁혀 다시 제안합니다

openshell rule approve --chunk-id <ID>

제안 상태가 approved로 바뀌고 policy_reloaded가 true가 된다. 에이전트의 pip install이 다시 시도되어 통과한다

거절 이유와 위험 점검 결과가 에이전트에게 함께 돌아간다는 점이 이 루프의 장점입니다. 다만 사람이 봐야 하는 것은 에이전트의 설명이 아니라 rule get이 보여 주는 실제 규칙입니다. 문서도 같은 이유로 설명 대신 표시된 규칙을 검토하라고 안내합니다.

자동 승인이 통과시키는 것

승인 방식을 정하는 설정은 proposal_approval_mode이고 값은 manual(기본)과 auto입니다. 게이트웨이 전체에 정한 값은 샌드박스별 값을 이기므로 관리자가 모든 샌드박스를 manual로 묶어 둘 수 있습니다. auto에서도 모든 제안이 자동으로 통과하는 것은 아닙니다. 위험 발견이 하나도 없고, OpenShell이 목적지에 표시를 붙이지 않았을 때만 사람 없이 승인됩니다.

제안의 성격 auto 모드의 결과
자격 증명이 걸려 있지 않은 새 공개 호스트 자동 승인
사설 IP 호스트, 와일드카드 호스트, 49152번 위 포트, 5432·6379 같은 데이터베이스·캐시 포트 사람의 검토
자격 증명이 붙는 호스트의 새 포트·새 메서드, 검사할 수 없는 트래픽 위험 발견이 붙어 사람의 검토
루프백·링크 로컬·메타데이터 주소 승인 불가

첫 줄이 가장 주의할 대목입니다. 점검은 제공자 자격 증명이 닿지 않는 새 공개 호스트를 위험으로 보지 않으므로, 자동 모드에서는 OpenShell의 초안까지 포함해 샌드박스 안의 프로그램이 공개 호스트로 나가는 길이 사람 검토 없이 넓어질 수 있습니다. 데이터 유출은 자격 증명 없이도 일어나는 위험이므로, 처음에는 manual로 시작해 어떤 제안이 올라오는지 눈으로 본 뒤 자동 승인 범위를 판단하는 편이 안전합니다. 자동 승인은 기록에도 남습니다. CONFIG:APPROVED 항목에 auto:true, 제안의 출처(agent_authored 또는 OpenShell이 만든 초안인 mechanistic), 승인 모드를 어디서 받았는지가 붙습니다.

승인해도 키는 샌드박스 밖에 남는다

규칙이 늘어나도 에이전트가 손에 쥐는 키는 늘지 않습니다. 에이전트는 실제 자격 증명을 보지 못하고, 샌드박스가 시작될 때 환경 변수에 불투명한 자리표시자만 들어갑니다. 요청이 프록시를 지나갈 때 두 조건이 모두 맞아야 자리표시자가 진짜 값으로 바뀝니다. 첫째, 네트워크 정책이 그 바이너리와 목적지를 허용할 것. 둘째, 자격 증명의 바인딩이 그 호스트·포트·경로를 포함할 것. 에이전트가 프롬프트 주입에 넘어가 키를 다른 곳으로 보내려 해도 샌드박스 안에는 보낼 실물이 없고, 다른 호스트로 보내면 불일치로 403(credential_endpoint_mismatch)이 돌아옵니다. 알 수 없거나 만료된 자리표시자는 그대로 전달되지 않습니다.

이 구조 덕분에 위험 점검의 네 발견 중 셋이 “자격 증명이 닿는 범위가 넓어지는가”를 묻습니다. 승인으로 규칙이 늘어나는 순간 키가 쓰일 수 있는 목적지도 함께 늘 수 있기 때문입니다. 키를 쓰는 절차는 세 단계입니다. 게이트웨이에는 내장 프로필이 없으므로 먼저 저장소의 providers/ 폴더에 있는 프로필(claude-code.yaml, github.yaml, openrouter.yaml 등)을 가져오고, 프로필에 적힌 바이너리 경로가 내 이미지와 맞는지 확인합니다. 그다음 제공자를 만들고 샌드박스에 붙입니다.

openshell profile import --url https://raw.githubusercontent.com/NVIDIA/OpenShell/main/providers/claude-code.yaml --global
ANTHROPIC_API_KEY=<키> openshell provider create --name my-claude --type claude-code --from-existing
openshell sandbox create --name agent --from <에이전트가 든 이미지> --provider my-claude -- claude

--from-existing은 환경 변수의 키를 읽어 게이트웨이에 저장하고 샌드박스 쪽에는 노출하지 않습니다. 키를 바꿀 때는 openshell provider update my-claude --from-existing으로 교체하되, 이미 떠 있는 프로세스는 옛 참조를 들고 있으니 새 프로세스를 띄워야 합니다. 실행 중인 샌드박스에서 제공자를 떼면(sandbox provider detach) 접근이 바로 회수되고, 샌드박스를 삭제하면 주입된 자격 증명도 함께 폐기됩니다. 기본 이미지는 에이전트가 설치되지 않은 최소 우분투(nvcr.io/nvidia/base/ubuntu:24.04)라서 쓸 에이전트가 든 OCI 이미지를 --from으로 지정해야 하며, 문서는 Claude Code, OpenCode, Codex, GitHub Copilot CLI를 이 런타임에서 돌릴 수 있는 에이전트로 듭니다.

공식 튜토리얼 하나가 이 둘의 관계를 잘 보여 줍니다. Claude Code가 든 샌드박스에서 파일을 만들고 커밋해 저장소에 푸시하라고 시키면 푸시 단계만 실패합니다. GitHub 프로필은 clone과 fetch만 허용하고 푸시에 쓰이는 git-receive-pack 요청은 거부하기 때문입니다. 토큰을 바꿔도 소용이 없습니다. 모자란 것은 키가 아니라 네트워크 권한이고, 해법은 푸시할 저장소 하나로 좁힌 규칙을 추가하는 것입니다.

승인이 리비전으로 남은 뒤

승인된 규칙은 새 정책 리비전이 됩니다. openshell policy list my-sandbox로 리비전 목록과 샌드박스가 적재했는지를 볼 수 있고, 성공한 변경은 Loaded로 나와야 합니다. 현재 기본 정책은 openshell policy get my-sandbox --base, 제공자 규칙까지 합친 실효 정책은 --full로 봅니다. 규칙 하나를 빼려면 openshell policy update my-sandbox --remove-rule <이름> --wait이고, 되돌릴 때는 이전 리비전을 --rev N --base로 받아 policy set으로 다시 올립니다. 이 되돌리기는 제공자 프로필과 연결, 자격 증명, 전역 정책은 복원하지 않습니다.

네트워크 변경은 실행 중인 샌드박스에 적용되면서 옛 규칙으로 열린 연결을 닫습니다. 적용에 실패하면 fail_closed(기본)는 트래픽을 막고, retain_last_valid는 마지막으로 유효했던 정책을 유지합니다. 적용 속도는 v0.1.5에서 달라졌습니다. 설정 전달 방식을 push로 바꾸면 정책과 설정 변경이 약 100밀리초 안에 감독자에게 가고, 기본인 폴링 방식은 최대 10초가 걸립니다. 승인한 직후 에이전트가 재시도할 때 체감 차이가 나는 부분입니다.

승인이 눈에 띄지 않게 쌓인다는 점은 따로 챙길 만합니다. TUI(openshell term)에서 승인한 엔드포인트는 같은 샌드박스를 재시작해도 남고, 샌드박스를 지우고 다시 만들어야 초기화됩니다. 자주 쓰는 목적지는 승인에 맡겨 두지 말고 바이너리와 경로를 좁힌 YAML로 옮겨 저장소에 두는 편이 낫습니다. 정책이 YAML이라 변경을 코드 리뷰로 볼 수 있고, openshell-prover는 게이트웨이 없이도 후보 정책이 경계 정책을 넘는지 검사해 CI에서 종료 코드로 알려 줍니다.

openshell-prover check candidate.yaml --boundary boundary.yaml
# 종료 코드 0 = within_boundary, 1 = exceeds_boundary, 2 = error, 3 = unsupported 또는 inconclusive

열어 둔 곳은 곧 나가는 길

승인 루프가 매끄러울수록 열린 길도 조용히 늘어납니다. 문서가 직접 경고하거나 기본값에서 드러나는 남은 위험을 정리하면 다음과 같습니다.

  • 허용한 목적지는 데이터가 나갈 수 있는 길입니다. 문서도 허용한 엔드포인트마다 유출 경로가 될 수 있다고 밝힙니다. 최소 정책으로 시작해 거부 로그를 보며 하나씩 여는 방식이 기본 태도입니다
  • 와일드카드 호스트(*.example)는 일치하는 모든 이름의 DNS 조회를 허용하므로 이름을 통한 유출 경로가 될 수 있습니다. 가능하면 정확한 호스트 이름을 적습니다
  • tls: skip은 검사를 끕니다. 인증서가 필요한 mTLS나 서버가 먼저 말을 거는 프로토콜(SMTP 등)에만 쓰고, 자격 증명이 붙는 엔드포인트에서는 별도 허용 설정이 있어야만 쓸 수 있습니다
  • **Landlock 호환 기본값 best_effort**는 적용하지 못한 파일 제한을 건너뛰고 기록만 남깁니다. 반드시 걸려야 한다면 hard_requirement를 지정합니다
  • Docker의 감독자 네트워킹은 기본이 호스트 방식입니다. v0.1.5부터 브리지 네트워킹을 고를 수 있으니 격리 요구가 높다면 설정을 확인합니다
  • 텔레메트리가 켜져 있습니다. 작업 범주와 횟수만 익명으로 수집하며 샌드박스 이름, 파일 경로, 프롬프트, 자격 증명은 수집하지 않는다고 밝힙니다. 끄려면 게이트웨이에 OPENSHELL_TELEMETRY_ENABLED=false를 지정합니다

변화 속도도 위험 요인입니다. 안정판 날짜는 v0.1.0이 9월 25일, v0.1.1이 9월 26일, v0.1.2가 9월 28일, v0.1.3이 10월 9일, v0.1.4가 10월 13일, v0.1.5가 10월 20일(모두 UTC)입니다. 보안과 치명적 안정성 수정은 최신 마이너와 직전 마이너에만 제공되고 문서도 버전별로 갈라져 있으므로 쓰는 버전의 문서를 보아야 합니다. v0.1.5로 올릴 때는 감독자의 OCSF 로그 파일 확장자가 .log에서 .jsonl로 바뀌어(옛 로그는 옮겨 주지 않음) 수집기 패턴과 옛 기록을 챙겨야 하고, 쿠버네티스 샌드박스 템플릿의 레이블·주석은 허용 목록 없이는 거부됩니다. 10월 22일에는 쿠버네티스 샌드박스를 공유 감독자에 올리는 변경이 main에 들어갔지만 v0.1.5에는 포함되지 않았습니다. 열린 이슈와 풀 리퀘스트는 합쳐서 581건이므로 쓸 기능의 알려진 문제를 먼저 검색해 보는 편이 좋습니다.

처음 쓸 때의 정책 뼈대

맨 앞의 pip install 장면으로 돌아가 보면, 처음 필요한 것은 넓은 허용이 아니라 설치용 규칙 하나와 사람이 직접 보는 승인입니다. 아래는 그 출발점입니다. 파일·Landlock·프로세스 설정은 실행 중인 샌드박스에서 바꿀 수 없으므로 openshell policy get work --base로 받은 파일에서 network_policies만 이렇게 고쳐 씁니다.

version: 1
# filesystem_policy, landlock, process 는 policy get --base 결과를 그대로 둔다
network_policies:
  python_install:
    endpoints:
      - host: pypi.org
        port: 443
        protocol: rest
        enforcement: enforce      # audit 로 먼저 돌려 보고 올려도 된다
        access: read-only
      - host: files.pythonhosted.org
        port: 443
        protocol: rest
        enforcement: enforce
        access: read-only
    binaries:
      - path: /usr/bin/python3.12   # 샌드박스 안에서 readlink -f $(which python3) 로 확인한 실제 경로
# 어드바이저는 켜고, 승인은 사람이 한다(전역 값이 샌드박스별 값을 이긴다)
openshell settings set --global --key agent_policy_proposals_enabled --value true --yes
openshell settings set --global --key proposal_approval_mode --value manual --yes

# 샌드박스를 만들고 규칙을 적용한 뒤 적재 여부를 확인한다
openshell sandbox create --name work --from <에이전트가 든 이미지> --provider <제공자>
openshell policy set work --policy starter.yaml --wait
openshell policy list work

# 에이전트가 막힐 때마다: 기록 → 대기 목록 → 규칙을 읽고 승인 또는 이유와 함께 거절
openshell logs work --since 5m --source sandbox
openshell rule get work --status pending
openshell rule approve work --chunk-id <제안 ID>
#OpenShell#NVIDIA#AI 에이전트#샌드박스#에이전트 보안#오픈소스