채널톡이 노트북과 CoS 제품에서 사용자와 AI가 만든 코드를 자사 서버에서 실행하기 위해 만든 Kernel 서비스의 1편이다. 실행할 코드가 요청이 들어온 뒤에야 생기고 사람이 검토할 시간도 없다는 전제에서, 특정 취약점을 하나씩 막는 대신 사용자 코드가 닿을 수 있는 공격 표면 자체를 줄이는 방향을 택했다. Jupyter와 컨테이너·Kubernetes만으로 부족했던 이유를 짚고, 인증·권한·실행 환경 수명을 Kernel이 소유하고 샌드박스는 코드 실행만 맡는 분리 구조를 세운다.
핵심 포인트- 실행할 코드를 미리 알 수 없고 허용할 시스템 콜이나 패키지도 고정할 수 없어, 일반적인 배포 전 리뷰·롤백 모델이 성립하지 않는다.
- 기존 애플리케이션은 인증된 요청과 여러 고객 데이터, 운영 권한을 함께 쓰는 곳이라 여기에 코드 실행을 붙이면 사용자 코드가 그 권한과 데이터에 닿는다.
- 컨테이너와 Kubernetes는 namespace·cgroup·seccomp·capability로 자원과 권한을 나누지만, 시스템 콜은 결국 같은 노드의 호스트 커널이 처리한다.
- 두 가지 조건을 먼저 못박았다. 샌드박스의 시스템 콜을 원래 인자 그대로 호스트 커널이 실행하지 않을 것, 실행 환경에 실제 DB 접속 정보와 스토리지 원본 키를 두지 않을 것.
- 사용자에게 보이는 세션과 내부 실행 프로세스를 분리해, 복구할 상태는 실행 환경 밖에 저장하고 유휴 환경은 내릴 수 있게 했다.
상세 정리- 출발 질문 다섯 가지: 코드가 CPU·메모리를 무한히 쓰면 누가 끊는가, 실행 프로세스가 다른 고객 데이터에 접근할 수 있는가, 요청의 인증 정보가 사용자 코드에서 보이는가, 프로세스가 죽어도 남겨야 할 상태는 무엇인가, 격리가 깨지면 프로세스만 잃는가 호스트 전체를 잃는가.
- 위협 사례 인용: 올해 초 OpenClaw 사고에서 ClawHub 마켓플레이스에 정보 탈취 코드를 숨긴 악성 스킬이 퍼졌고, 외부 노출 인스턴스와 원격 코드 실행 취약점도 연이어 발견됐다. 원인은 하나가 아니라 실행 코드·인증 정보·인프라가 서로 가까이 붙어 있어 한 영역 침해가 다음 자원으로 번진 구조였다.
- 인용한 관점: 에이전트를 "지속되는 인증 정보를 가진 신뢰할 수 없는 코드 실행 환경으로 취급하라"는 정리를 전제로 삼았다.
- Jupyter 검토: 셀을 이어 실행하고 앞선 결과를 다음 분석에 쓰는 경험은 Jupyter나 Colab이 이미 푼 문제지만, Jupyter의 인증·권한 부여는 누가 실행을 요청할 수 있는지만 통제하고 실행 환경 자체를 격리하지 않는다. JupyterHub로 사용자별 서버를 띄워도 실제 격리 수준은 선택한 실행 환경에 달려 있다.
- 컨테이너 검토: 이미 전 워크로드를 컨테이너로 Kubernetes에서 운영 중이라 사용자 코드를 별도 Pod에서 돌리는 안부터 봤다. namespace가 프로세스·네트워크·마운트를 나누고 cgroup이 사용량을 제한하며 seccomp가 시스템 콜을 걸러내고 capability가 root 권한을 쪼개지만, 파일을 열거나 네트워크로 나가는 순간 같은 호스트 커널로 경로가 모인다.
- 범용 실행 환경의 딜레마: 실행할 코드와 패키지를 미리 알 수 없으니 호환성을 위해 비교적 넓은 커널 표면을 허용해야 한다. gVisor 문서는 이 규칙을 집행하는 주체도 같은 호스트 커널이라 애플리케이션과 호스트 침해 사이 거리가 여전히 시스템 콜 하나라고 설명한다.
- Pod Security Admission의 Restricted 기준을 강제해도 허용된 Pod의 시스템 콜은 일반 컨테이너 실행 환경에서 같은 호스트 커널이 처리한다는 점은 변하지 않는다.
- 방향 전환: 제로데이는 앞으로도 나오고 패치와 seccomp 허용 목록은 악용 가능성을 낮출 뿐 허용된 시스템 콜로 도달하는 취약점까지 막는다고 보장할 수 없다는 판단에서, 공격 표면 축소를 1차 목표로 삼았다.
- Kernel의 역할: 사용자 코드를 직접 실행하는 프로세스가 아니라, 요청을 인증하고 권한을 확인한 뒤 어떤 환경에서 실행할지와 그 환경을 언제 만들고 없앨지를 결정하는 제어 계층이다.
- 영역 분리: 제어 영역은 인증·권한 확인·실행 경로와 수명 결정을 맡고, 사용자 코드 실행 영역인 샌드박스는 전달받은 코드를 실행해 결과만 돌려준다.
- DB 접속 정보 분리: 접속 주소·계정·토큰을 실행 환경에 넘기면 사용자 코드가 자기 Python 프로세스의 메모리·환경 변수·요청 객체를 들여다볼 수 있다. 접속 정보는 언제든 노출될 수 있다고 가정하고, 한 번 노출되면 실행 환경 밖에서도 고객 DB에 다시 접근하는 데 쓰인다고 봤다.
- 요청 구조 변경: 기존 요청을 그대로 넘기지 않고 Kernel이 샌드박스용 요청을 새로 만들어 DB 접속 정보를 비롯해 실행에 불필요한 항목을 뺀 뒤 전달한다. 실제 쿼리는 접속 정보를 소유한 데이터 연결 서비스가 대신 실행하고 샌드박스에는 결과만 내려간다.
- 질문 자체가 바뀐 지점: "실행 환경에서 DB에 어떻게 접속할까" 대신 "실행 환경이 실제 접속 정보를 가져야 하는가", "한 프로세스에서 고객을 어떻게 구분할까" 대신 "서로 다른 고객 코드를 같은 실행 환경에 두어도 되는가", "프로세스를 어떻게 오래 살릴까" 대신 "사라지기 전에 어떤 상태를 밖으로 꺼내야 하는가"를 먼저 물었다.
- 세션과 프로세스 분리: 사용자는 하나의 세션으로 작업하지만 Python 실행과 외부 질의는 서로 다른 프로세스와 신뢰 수준의 환경에서 처리할 수 있고, 어느 경로든 결과는 같은 세션 아래 남는다. 덕분에 지원 런타임 확장과 동시 요청 처리가 쉬워졌다.
- Lambda식 모델 기각: 각 요청을 이전 실행 환경과 독립적으로 처리하는 모델은 재사용 여부가 보장되지 않아, 첫 셀에서 만든 변수를 다음 셀에서 쓰는 노트북의 연속성 근거가 될 수 없다. 반대로 세션마다 프로세스를 켜두면 사용자가 떠난 뒤에도 자원을 물고 신뢰할 수 없는 코드가 든 환경이 계속 살아 있다.
- 채택한 절충: 계속 남는 것은 실행 프로세스가 아니라 Kernel이 복구 대상으로 정해 밖에 저장한 세션 상태다. 연달아 실행되는 동안에는 같은 환경을 활용하되 제품이 믿는 상태를 그 프로세스에만 두지 않는다.
- 다음 편 예고: 사용자 코드와 호스트 커널 사이를 실제로 어떻게 격리할지, 상용 에이전트 샌드박스 구조를 비교해 고른 기술 선택을 다룬다. 어떤 상태를 밖에 저장하고 언제 환경을 내리며 어떻게 복구하는지도 이후 편으로 미뤘다.
왜 읽나AI 에이전트나 사용자 제출 코드를 프로덕션에서 실행해야 하는 팀이 컨테이너 격리의 한계와 인증 정보 분리 원칙을 어디까지 밀어야 하는지 가늠할 때 참고가 된다.