왜 샌드박스 인증이 어려운가?

LLM 기반 에이전트(예: OpenCode, Claude Code)가 점점 더 많이 사용되면서, 외부 API 호출을 안전하게 처리하는 방법이 중요해졌습니다. 특히 샌드박스 내에서 실행되는 코드는 신뢰할 수 없기 때문에, API 토큰을 직접 주입하는 전통적인 방식은 위험합니다. 토큰이 노출되면 공격자가 이를 악용할 수 있기 때문입니다.

이 글에서는 Cloudflare Sandbox의 outbound Workers라는 기능을 이용해, 에이전트가 외부 서비스를 호출할 때 제로 트러스트(Zero Trust) 원칙을 적용하는 방법을 단계별로 알아보겠습니다. 국내 클라우드 환경(NCP, KT Cloud 등)에서도 Workers 유사 기능이 있다면 동일한 패턴을 적용할 수 있습니다.

기존 인증 방식의 한계

방식장점단점
API 토큰 직접 주입구현 간단토큰 노출 위험, 만료 관리 복잡
OIDC 워크로드 아이덴티티단기 토큰으로 보안 향상서비스 통합 어려움, 자체 토큰 교환 서버 필요
커스텀 프록시완전한 유연성프록시 구축/운영 복잡, 성능 이슈

이상적인 인증 메커니즘은 제로 트러스트, 단순함, 유연성, 성능, 투명성을 모두 만족해야 합니다. outbound Workers가 이 모든 조건을 어떻게 충족하는지 살펴보겠습니다.

Cloudflare Sandbox outbound Workers zero trust credential injection diagram System Abstract Visual

실전: outbound Workers로 제로 트러스트 인증 구현하기

1. 기본: 요청 로깅 및 차단

가장 간단한 예제로, 모든 아웃바운드 HTTP 요청을 가로채서 GET만 허용하고 나머지는 차단하는 코드입니다.

class MySandboxedApp extends Sandbox {
  static outbound = (req, env, ctx) => {
    // GET이 아닌 요청은 모두 차단하고 로그를 남깁니다.
    if (req.method !== 'GET') {
      console.log(`컨테이너가 ${req.method} 요청을 보냄: ${req.url}`);
      return new Response('허용되지 않음', { status: 405 });
    }
    // GET 요청은 정상적으로 전달합니다.
    return fetch(req);
  };
}

이 프록시는 샌드박스 VM과 동일한 머신에서 실행되므로 지연 시간이 거의 없습니다. Workers 대시보드에서 모든 로그를 확인할 수 있어 관측 가능성도 확보됩니다.

2. 제로 트러스트 자격 증명 주입

가장 강력한 활용 사례는 비밀 토큰을 샌드박스에 절대 노출하지 않고 외부 서비스에 안전하게 전달하는 것입니다.

class OpenCodeInABox extends Sandbox {
  static outboundByHost = {
    "my-internal-vcs.dev": (request, env, ctx) => {
      // 요청 헤더에 비밀 토큰을 추가합니다.
      // 샌드박스 내 코드는 이 토큰을 절대 볼 수 없습니다!
      const headersWithAuth = new Headers(request.headers);
      headersWithAuth.set("x-auth-token", env.SECRET);
      return fetch(request, { headers: headersWithAuth });
    }
  };
}

여기서 핵심은 env.SECRET이 샌드박스가 아닌 outbound Worker의 환경 변수로만 존재한다는 점입니다. 또한 ctx.containerId를 활용해 샌드박스 인스턴스별로 다른 토큰을 주입할 수도 있습니다.

static outboundByHost = {
  "my-internal-vcs.dev": async (request, env, ctx) => {
    // KV는 저장/전송 중 암호화됩니다.
    const authKey = await env.KEYS.get(ctx.containerId);
    const requestWithAuth = new Request(request);
    requestWithAuth.headers.set("x-auth-token", authKey);
    return fetch(requestWithAuth);
  }
}

3. 동적 네트워크 제어

초기에는 npm 패키지를 다운로드하기 위해 GitHub, npmjs.org에 접근을 허용하고, 설치가 끝나면 즉시 차단하는 시나리오입니다.

class MySandboxedApp extends Sandbox {
  static outboundHandlers = {
    async allowHosts(req, env, { params }) {
      const url = new URL(request.url);
      if (params.allowedHostnames.includes(url.hostname)) {
        return await fetch(newRequest);
      } else {
        return new Response(null, { status: 403 });
      }
    },
    async noHttp(req) {
      return new Response(null, { status: 403 });
    }
  };
}

async function setUpSandboxes(req, env) {
  const sandbox = await env.SANDBOX.getByName(userId);
  // 초기: GitHub, npm만 허용
  await sandbox.setOutboundHandler("allowHosts", {
    allowedHostnames: ["github.com", "npmjs.org"]
  });
  await sandbox.gitClone(userRepoURL);
  await sandbox.exec("npm install");
  // 설치 완료 후 모든 HTTP 차단
  await sandbox.setOutboundHandler("noHttp");
}

이렇게 하면 네트워크가 열려 있는 시간을 최소화하여 공격 표면을 줄일 수 있습니다.

4. MITM 프록시로 HTTPS 트래픽 제어

HTTPS 요청도 제어하려면 TLS 복호화가 필요합니다. Cloudflare Sandbox는 각 인스턴스마다 **임시 인증 기관(CA)**을 생성하여 샌드박스 내부에 설치합니다.

export class MyContainer extends Container {
  interceptHttps = true;
}

MyContainer.outbound = (req, env, ctx) => {
  // 모든 HTTP(S) 요청이 이 훅을 통해 전달됩니다.
  return fetch(req);
};

이 방식은 완전히 투명하게 동작합니다. 샌드박스 내 애플리케이션은 프록시 존재를 인지하지 못합니다.

5. 고급: WorkerEntrypoint를 활용한 세밀한 제어

IP 범위나 호스트 이름 패턴(glob 매칭)으로 요청을 가로챌 수 있습니다.

export class MyWorker extends WorkerEntrypoint {
  fetch() {
    return new Response(this.ctx.props.message);
  }
}

// 컨테이너 내부
this.ctx.container.start({ enableInternet: false });
const outboundWorker = this.ctx.exports.MyWorker({ props: { message: 'hello' } });
await this.ctx.container.interceptOutboundHttp('15.0.0.1:80', outboundWorker);
// 이후 15.0.0.1:80으로 가는 모든 HTTP 요청은 "hello"를 반환합니다.

// 동적으로 변경 가능! 기존 연결에도 영향 없음
const secondOutboundWorker = this.ctx.exports.MyWorker({ props: { message: 'switcheroo' } });
await this.ctx.container.interceptOutboundHttp('15.0.0.1:80', secondOutboundWorker);

// 호스트 이름, CIDR(IPv4/IPv6) 모두 지원
await this.ctx.container.interceptOutboundHttp('example.com', secondOutboundWorker);
await this.ctx.container.interceptOutboundHttp('*.example.com', secondOutboundWorker);
await this.ctx.container.interceptOutboundHttp('123.123.123.123/23', secondOutboundWorker);

모든 프록시는 샌드박스 VM과 동일한 머신에서 로컬로 실행되므로, 컨테이너와 Worker 간 통신은 "인증 없이"도 안전합니다.

Developer configuring dynamic egress proxy rules in Sandbox dashboard Development Concept Image

주의사항 및 한계

  1. 성능 오버헤드: MITM 프록시를 사용하면 TLS 핸드셰이크가 추가로 발생합니다. 지연 시간이 매우 민감한 애플리케이션은 주의가 필요합니다.
  2. 인증서 신뢰 문제: 샌드박스 내부에 설치된 임시 CA는 해당 샌드박스에서만 유효합니다. 표준 컨테이너에서는 sudo update-ca-certificates 등으로 수동 설정이 필요할 수 있습니다.
  3. 복잡한 정책: outboundHandlers를 여러 개 정의할수록 관리 복잡도가 증가합니다. 정책을 코드로 관리하므로 버전 관리와 테스트가 필수입니다.
  4. 국내 클라우드 호환성: 현재 이 기능은 Cloudflare Workers 생태계 전용입니다. 국내 클라우드에서 유사한 기능을 구현하려면 자체 프록시 서버(예: Envoy, Nginx)를 VM 내에 배포해야 할 수 있습니다.

다음 단계 학습 방향

  • Cloudflare Sandbox 공식 문서에서 더 다양한 예제를 확인하세요.
  • OIDC와 outbound Workers를 결합한 워크로드 아이덴티티 연합 패턴을 학습해보세요.
  • wrangler dev를 이용한 로컬 개발 환경 설정 방법을 익히면 생산성이 크게 향상됩니다.

Sandbox VM architecture with sidecar proxy and Workers runtime Software Concept Art

결론

outbound Workers는 샌드박스 환경에서 제로 트러스트, 동적 제어, 투명성을 모두 만족하는 강력한 인증/네트워크 제어 솔루션입니다. 단 몇 줄의 JavaScript로 API 토큰을 안전하게 주입하고, 동적으로 네트워크 정책을 변경하며, 모든 트래픽을 관찰할 수 있습니다.

특히 LLM 에이전트가 점점 더 많은 권한을 필요로 하는 현재, 이러한 접근 방식은 필수적입니다. "신뢰할 수 없는 코드"에 "최소 권한"만 부여한다는 원칙을 코드 레벨에서 실현할 수 있습니다.

함께 보면 좋은 글

근거자료: Cloudflare 공식 블로그 - Sandbox Auth

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.