ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 130개의 보안 경고를 가진 에이전트를 집에서 안전하게 돌리는 법 — OpenShell로 케이지에 넣기
    IT 2026. 8. 7. 22:00
    130개의 보안 경고를 가진 에이전트를 집에서 안전하게 돌리는 법 — OpenShell로 케이지에 넣기

    지난 글에서 "에이전트를 샌드박싱한다"는 세 가지 경계 배치가 있고, 그중 에이전트 전체를 통째로 담에 넣는 배치가 공급망 문제의 실질적 답이라고 정리했다. 남이 만든 MCP 서버가 악성이든, 에이전트 본체가 오염됐든, 담이 호스트를 지켜준다. 그래서 말로만 끝내지 않고 실제로 씌워봤다. 집에 둔 DGX Spark에서 상시로 돌리는 개인 AI 에이전트에, NVIDIA가 2026년 공개한 오픈소스 런타임 OpenShell을 얹었다.

    대상 에이전트를 OpenClaw로 고른 이유가 오히려 이 실험의 명분이었다. 이 커뮤니티 에이전트는 프롬프트 인젝션·데이터 삭제·크레덴셜 절도를 포함해 보안 경고가 130건 넘게 쌓인 이력이 있다. 위험한 물건일수록 "이번 버전은 믿어도 될까"를 매번 판단하는 건 지는 게임이다. 그럴 바엔 판단을 포기하고 "틀렸어도 여기까지"라는 담을 긋는 게 낫다. 이 글은 그 담이 실제로 무엇을, 어떻게 잠그는지를 계층별로 뜯어본다.

    두 줄이면 케이지가 선다

    먼저 체감부터 보자. OpenShell은 설치와 실행이 두 명령이다.

    # 1단: CLI 설치 (uv = 파이썬 패키지/툴 설치기)
    uv tool install -U openshell
    
    # 2단: 기본 보안 정책이 적용된 샌드박스를 만들고 그 안에서 에이전트를 띄운다
    #   -- 뒤의 명령만 바꾸면 claude·codex·openclaw 어느 에이전트든 같은 이미지로 실행
    openshell sandbox create -- openclaw
    

    코드 설명. 두 번째 명령 하나가 로컬에 도커 기반 게이트웨이를 부트스트랩하고, 기본 보안 정책을 입힌 샌드박스를 만들고, 환경변수에서 API 키를 감지해 provider로 등록한 뒤, 그 격리된 샌드박스 안에서 에이전트를 실행한다. Claude Code·Codex·OpenClaw가 같은 베이스 이미지를 쓰기 때문에, 바꾸고 싶으면 -- 뒤 명령만 갈아끼우면 된다. DGX Spark에서는 이 게이트웨이를 기기에 올리고 미리 빌드된 OpenClaw 커뮤니티 샌드박스 이미지로 띄우는 경로가 준비돼 있다. "샌드박싱"이라는 단어의 무게에 비해 진입 비용이 낮다는 게 핵심이다 — 안 켤 이유를 하나 줄여준다.

    케이지 안에서 실제로 조이는 네 겹

    겉보기엔 "도커 컨테이너 하나"지만, 그 안에서 강제되는 통제는 네 계층으로 갈린다. 하나씩 보자.

    diagram

    다이어그램 설명. 호스트 위에 OpenShell 자체의 Gateway가 앉아 샌드박스의 생성과 인증만 관리하고, 실제 에이전트는 자물쇠가 걸린 Sandbox 컨테이너 안에서 돈다. 여기서 말하는 Gateway는 OpenShell 런타임의 구성요소다 — 에이전트 제품이 자체적으로 "gateway"라는 이름의 데몬을 갖고 있더라도(OpenClaw가 그렇다) 그것과는 무관하며, 그런 데몬은 통째로 Sandbox 컨테이너 안에 들어가는 대상일 뿐이다. 컨테이너 안을 보면 통제가 세 축으로 갈라진다 — "파일시스템: 생성 순간 잠금"은 컨테이너가 태어나는 순간 접근 가능한 경로를 못 박고(리눅스 커널의 Landlock 기능), "프로세스: 비특권 + seccomp 필터"는 권한 상승과 위험한 시스템 호출을 막고, "네트워크: 기본 전면 차단"은 밖으로 나가는 모든 연결을 기본으로 끊는다. 그 네트워크에서 유일하게 뚫린 구멍이 "egress 지점"이고, 여기서 허용목록에 적힌 목적지만 통과한다. 마지막으로 "Privacy Router"는 모델 호출을 에이전트 대신 수행해서, 공급자 API 키가 샌드박스 안으로 들어오는 일 자체를 없앤다. 이 네 겹이 "에이전트 전체를 담에 넣는다"는 말의 실체다.

    파일시스템 — 생성 순간 잠긴다

    Landlock은 리눅스 커널이 프로세스별로 "이 경로만 읽고 쓸 수 있다"를 강제하는 기능이다. OpenShell은 샌드박스를 만드는 그 순간 이 규칙을 확정한다(정책의 파일시스템·프로세스 섹션은 "static" — 실행 중엔 못 바꾼다). 그래서 안에서 도는 에이전트가 아무리 오염됐어도 ~/.ssh나 클라우드 크레덴셜 파일에 애초에 손이 닿지 않는다. 지난 글에서 스펙이 직접 예로 든 curl -d @~/.ssh/id_rsa 같은 유출 시나리오가 이 계층에서 원천 차단된다.

    네트워크 — 나가는 길은 하나, 그마저 화이트리스트

    여기가 이 글의 집중 꼭지다. 각 샌드박스는 자기만의 네트워크 네임스페이스(리눅스가 프로세스 그룹에 독립된 네트워크 환경을 주는 격리 단위)를 갖고, 나가는 트래픽은 통제된 egress 지점 하나로만 흐른다. 허용목록에 없는 목적지는 패킷이 호스트를 떠나기도 전에 connection refused로 잘린다. 정책은 이렇게 생겼다.

    # policies/network.yaml — 나가는 목적지를 명시적으로 열어준다
    networkPolicy:
      egress:
        allow:
          - domain: "api.example-mcp.com"   # 이 MCP 서버 API만
            methods: [GET, POST]            # 허용 HTTP 메서드까지 제한
            headers:
              required: ["Authorization"]   # 특정 헤더가 있어야 통과
    

    코드 설명. 도메인 하나, 메서드 두 개, 필수 헤더 하나 — 이 세 줄이 "이 샌드박스가 바깥과 나눌 수 있는 대화 전부"를 정의한다. 목적지 도메인뿐 아니라 어떤 메서드어떤 헤더를 달고 나가는지까지 조인다는 점이 흥미롭다. 다만 반드시 짚어야 할 한계가 있다 — 이 정책은 "어디로, 어떻게" 나가는지는 보지만 "무엇이 실려" 나가는지는 보지 않는다. 요청 본문(payload) 안에 무엇이 담겼는지는 검사 대상이 아니다. 이 틈이 다음 글의 주제인 tool shadowing이 빠져나가는 바로 그 구멍인데, 여기서는 씨앗만 심어둔다. 네트워크 정책은 static이 아니라 "dynamic" 섹션이라 openshell policy set으로 실행 중에도 갈아끼울 수 있다는 점도 실무에서 유용하다 — 새 MCP 서버를 붙일 때 컨테이너를 재시작하지 않아도 된다.

    크레덴셜 — 훔칠 키가 샌드박스 안에 없다

    공급망 사고의 단골이 "설정 파일에 평문으로 박힌 API 키를 악성 코드가 읽어가는" 것이다. OpenShell이 여기에 대는 답은 두 갈래인데, 둘을 뭉뚱그리면 오히려 이해가 어긋난다. 하나는 크레덴셜 프로바이더 — GitHub 토큰처럼 에이전트가 실제로 써야 하는 키를 샌드박스 생성 시점에 환경변수로만 주입하고 파일시스템에는 쓰지 않는 방식이다. 컨테이너를 만들 때 프로세스의 환경 블록에 값을 넣어주는 것이라, 키는 커널이 프로세스에 넘겨주는 메모리상의 데이터로만 존재한다. .env 파일도, ~/.config/gh/hosts.yml 같은 설정 파일도 컨테이너 안 디스크에는 생기지 않는다.

    여기서 솔직해질 필요가 있다 — 이건 키를 숨기는 장치가 아니다. 샌드박스 안에서 echo $GITHUB_TOKEN 한 줄이면 값이 그대로 보이고, 악성 코드가 환경변수를 덤프하기로 마음먹으면 값 자체는 얼마든지 손에 넣는다. 그런데도 의미가 있는 건 막는 대상이 다르기 때문이다. 실제 공급망 사고에서 크레덴셜 절도의 대다수는 ~/.aws/credentials, .env, .npmrc, ssh 키 같은 잘 알려진 경로를 무차별로 읽어 올리는 파일시스템 스캐너다. 디스크에 키가 없으면 이 부류가 통째로 헛손질한다. 파일에 없으니 컨테이너 이미지 커밋·볼륨 스냅샷·백업·git 혼입 같은 부수 유출 경로도 함께 사라진다. 그리고 설령 값을 읽어내는 데 성공해도, 그걸 밖으로 보내는 단계에서 뒤에 볼 네트워크 egress 화이트리스트에 다시 걸린다 — "값을 읽는다"와 "값을 유출한다" 사이에 담이 하나 더 있는 셈이다.

    "그럼 아예 에이전트가 못 보게 하면 되지 않나" 싶지만, GitHub 토큰은 원리적으로 그게 안 된다. 에이전트가 GitHub API를 직접 호출할 때 자기가 제시해야 하는 키라서, 못 보게 하려면 GitHub 호출까지 대리해 주는 중계자가 따로 있어야 한다. 크레덴셜 프로바이더는 그런 중계 없이 "에이전트가 직접 쓰는 키"를 다루는 장치이므로 은닉이 아니라 노출면 축소까지가 보장의 전부다. 그 은닉을 실제로 해내는 것이 모델 호출에 한정된 다음 장치다.

    다른 하나가 Privacy Router이고, 이쪽은 보장 수준이 한 칸 더 높다. 모델 호출에 한해서는 에이전트가 키를 보지도 못한다. 이름 때문에 오해하기 쉬운데 라우터는 백엔드가 아니다 — 실제 백엔드(Anthropic, OpenAI 호환 엔드포인트, 로컬에 띄운 추론 서버 등)는 따로 설정하고, 라우터는 그 앞에 서서 호출을 대신 수행하는 중계 계층이다.

    diagram

    다이어그램 설명. 에이전트는 평소처럼 API 키를 담아 요청을 보내지만, 그 값은 무엇이든 상관없다 — 어차피 버려지기 때문이다. 요청이 향하는 "inference.local"은 실재하는 도메인이 아니라 OpenShell 게이트웨이가 HTTPS 연결을 가로채기로 약속해둔 주소다. 가로챈 요청을 라우터가 받아 판정하고, 통과하면 받은 키와 모델명을 폐기한 뒤 게이트웨이에 설정된 진짜 크레덴셜을 넣어 백엔드로 넘긴다. 에이전트에게 돌아오는 건 응답뿐이다. 진짜 키는 컨테이너 경계를 한 번도 넘어오지 않는다 — 디스크에 없는 정도가 아니라 프로세스 메모리에도 없다. 안에서 도는 코드가 완전히 장악당해도 유출할 대상 자체가 존재하지 않는다는 뜻이다.

    판정 갈래에서 갈라져 나온 "거부" 쪽도 그냥 곁가지가 아니다. 설정된 공급자의 지원 패턴에 맞지 않는 요청은 거기서 끊긴다. 이 설정이 게이트웨이 단위라서, 그 게이트웨이를 쓰는 모든 샌드박스가 동일한 백엔드만 바라보게 된다. 다시 말해 "이 에이전트가 다루는 코드는 외부 모델 엔드포인트로 절대 나가지 않는다"를 문서상의 약속이 아니라 인프라 수준에서 못 박을 수 있다. 추론 트래픽을 일반 네트워크 트래픽과 다른 채널로 취급해 별도 정책을 매긴다는 게 이 설계의 요점이고, 로컬에 모델을 띄워 쓰는 환경에서는 특히 값이 크다.

    이 케이지가 확실히 막는 것 — 그리고 못 막는 것

    정리하면 배치 ②(에이전트 전체 감싸기)는 서버의 본래 기능과 무관한 모든 접근을 자른다. 악성 MCP 패키지가 홈 디렉터리를 읽을 이유가 없고, 개인 비서 에이전트가 임의의 서버로 리버스셸을 열 이유가 없다. 이 "이유 없음"을 커널 수준에서 강제하는 게 이 케이지이고, OpenClaw처럼 위험 이력이 두꺼운 물건일수록 효과가 크다. 신뢰 판단을 아예 안 해도 되게 만들어 주기 때문이다.

    diagram

    다이어그램 설명. 케이지가 나가는 요청을 어떻게 판정하는지를 보여준다. 목적지가 허용목록에 없으면 리버스셸이든 임의 사이트 유출이든 "connection refused"로 잘려 호스트를 떠나지도 못한다 — 이게 케이지가 확실히 이기는 경우다. 그런데 목적지가 등록된 MCP API처럼 허용목록 안이면 통과하고, 이때 요청 본문에 무엇이 실렸는지는 검사하지 않는다. 판정 기준이 "어디로 가느냐"에만 걸려 있다는 게 이 그림의 핵심이자, 케이지의 사정거리가 끝나는 지점이다. 정상 통로로 위장해 정문으로 걸어 나가는 유출은 이 판정을 그대로 통과한다.

    그래서 이 글은 케이지 예찬으로 끝내되 한 문장을 남긴다 — OpenShell은 공급망 문제의 답이지 프롬프트 인젝션의 답이 아니다. 검증되지 않은 코드를 내 권한으로 실행하는 문제는 이 두 명령이 상당 부분 해결한다. 하지만 오염된 도구 설명문이 내 에이전트의 판단을 조종해 정상 통로로 데이터를 흘려보내는 문제는, 케이지를 아무리 촘촘히 조여도 그대로 남는다. 그 남은 문제를 다음 글에서 따로 떼어 다룬다.

    정리 — 안 켤 이유를 지운다

    OpenShell을 실제로 씌워보고 남은 결론은 단순하다. 공급망 위험이 큰 에이전트일수록, 신뢰 판단을 붙들고 씨름하기보다 케이지에 넣는 편이 실용적이다. 두 명령이면 서고, 커널 수준에서 파일·네트워크·프로세스·크레덴셜을 동시에 조인다. 진입 비용이 낮다는 건 "이번엔 그냥 호스트에서 돌리자"는 유혹을 지운다는 뜻이기도 하다.

    내 경우 개인 에이전트를 DGX Spark에서 상시로 돌리는 만큼, "이 MCP 서버를 믿어도 되나"를 매번 고민하는 대신 담부터 세우기로 했다. 다만 담을 세운 뒤에 남아 있는 위협 — 정문으로 걸어 나가는 유출 — 을 계속 세고 있는 것이, 케이지가 주는 안심에 잡아먹히지 않는 유일한 방법이다.


    참고한 공개 자료:

    • OpenShell: The safe, private runtime for autonomous AI agents — NVIDIA (https://github.com/NVIDIA/OpenShell)
    • Get Started with AI Coding Agents in Secure Containers — NVIDIA (https://perspectives.nvidia.com/nvidia-openshell/get-started-ai-coding-agents-secure-containers/)
    • Secure Long Running AI Agents with OpenShell on DGX Spark — NVIDIA (https://build.nvidia.com/spark/openshell)
    • Practical Security Guidance for Sandboxing Agentic Workflows and Managing Execution Risk — NVIDIA Technical Blog (https://developer.nvidia.com/blog/practical-security-guidance-for-sandboxing-agentic-workflows-and-managing-execution-risk/)

    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.