보안 코드 실행 (Secure code execution)
보안 코드 실행 (Secure code execution)
[!TIP] 에이전트 만들기가 처음이라면 에이전트 소개와 smolagents 가이드 투어를 먼저 읽어보는 걸 추천해요.
코드 에이전트
여러 연구 논문이 LLM이 자신의 액션(툴 콜)을 코드로 쓰는 게 업계에서 흔한 현재 표준 형식, 즉 "도구 이름과 사용할 인자의 JSON으로 액션을 쓰는" 방식보다 훨씬 낫다는 걸 보여줬어요.
코드가 왜 더 좋을까요? 우리는 코드 언어를 컴퓨터가 수행하는 액션을 표현하는 데 뛰어나도록 직접 설계했기 때문이에요. JSON 스니펫이 더 나았다면 이 패키지도 JSON 스니펫으로 작성됐을 테고, 그랬다면 그건 잘못된 판단이었을 거예요.
코드는 컴퓨터에서 액션을 표현하는 더 좋은 방법이에요. 장점이 이렇죠.
- 합성성 — 파이썬 함수를 정의하듯 JSON 액션을 서로 중첩하거나 재사용할 JSON 액션 세트를 정의할 수 있을까요?
- 객체 관리 —
generate_image같은 액션의 출력을 JSON으로 어떻게 저장하나요? - 일반성 — 코드는 컴퓨터가 할 수 있는 무엇이든 간단히 표현하도록 만들어졌어요.
- LLM 훈련 말뭉치에서의 표현 — 이미 많은 고품질 액션이 LLM 훈련 말뭉치에 포함되어 있어서 이 하늘의 축복을 활용하지 않을 이유가 없어요.
이 내용은 Executable Code Actions Elicit Better LLM Agents에서 가져온 그림으로 설명돼요. 그래서 우리가 코드 에이전트, 여기서는 파이썬 에이전트를 제안하는 데 힘을 쏟았고, 이는 곧 안전한 파이썬 인터프리터를 만드는 데 더 큰 노력을 기울였다는 뜻이에요.
로컬 코드 실행?
기본적으로 CodeAgent는 LLM이 생성한 코드를 내 환경에서 실행해요.
이건 본질적으로 위험해요. LLM이 생성한 코드가 내 환경에 해를 끼칠 수 있으니까요. 악성 코드 실행은 여러 경로로 일어날 수 있어요.
- 단순한 LLM 오류 — LLM은 아직 완벽하지 않아서 도움이 되려다 의도치 않게 해로운 명령을 생성할 수 있어요. 위험은 낮지만, LLM이 잠재적으로 위험한 코드를 실행하려 시도한 사례가 관찰된 적은 있어요.
- 공급망 공격(Supply chain attack) — 신뢰할 수 없는 또는 손상된 LLM을 실행하면 해로운 코드 생성에 노출될 수 있어요. 안전한 추론 인프라에서 잘 알려진 모델을 쓸 때 이 위험은 극히 낮지만, 이론적으로는 가능한 일이에요.
- 프롬프트 인젝션 — 웹을 브라우징하는 에이전트가 해로운 지침을 담은 악의적인 웹사이트에 도달하면, 공격이 에이전트 메모리에 주입될 수 있어요.
- 공개 접근 에이전트의 악용 — 공개에 노출된 에이전트는 악의적인 행위자가 해로운 코드를 실행하도록 남용할 수 있어요. 공격자가 적대적 입력을 만들어 에이전트의 실행 능력을 악용해 의도치 않은 결과를 초래하게 할 수 있죠.
악성 코드가 실행되면(우연이든 의도든) 파일 시스템을 손상시키고, 로컬·클라우드 리소스를 악용하며, API 서비스를 남용하고, 심지어 네트워크 보안까지 손상시킬 수 있어요.
agency 스펙트럼 관점에서 보면 코드 에이전트는 덜 에이전틱한 구조보다 LLM에게 훨씬 높은 agency를 주는데, 이는 더 높은 위험과 맞물려요. 그래서 보안에 매우 신중해야 해요.
안전을 높이기 위해 우리는 더 높은 보안 수준을 제공하되 설정 비용이 더 드는 일련의 조치를 제안해요. 어떤 해결책도 100% 안전하지 않다는 점은 명심하세요.
우리의 로컬 파이썬 실행기
보안의 첫 번째 계층을 더하기 위해, smolagents의 코드 실행은 일반 파이썬 인터프리터로 수행되지 않아요. 우리는 더 안전한 LocalPythonExecutor를 처음부터 다시 만들었어요.
정확히 말하면 이 인터프리터는 코드의 AST(Abstract Syntax Tree)를 로드해서 연산 단위로 하나씩 실행하며, 항상 다음 규칙들을 지켜요.
- 기본적으로 사용자가 승인 목록에 명시적으로 추가하지 않는 한 import는 금지돼요.
- 게다가 하위 모듈(submodule) 접근도 기본적으로 비활성화되고, 각각 import 목록에 명시적으로 승인되거나
numpy.*처럼 넘겨서numpy와numpy.random,numpy.a.b같은 모든 하위 패키지를 허용할 수 있어요.random같은 겉보기에 무해한 패키지도random._os처럼 잠재적으로 해로운 하위 모듈에 접근을 줄 수 있다는 점에 주의하세요.
- 처리되는 기본 연산의 총 개수에 상한을 두어 무한 루프와 리소스 부풀림을 막아요.
- 우리의 커스텀 인터프리터에 명시적으로 정의되지 않은 연산은 오류를 발생시켜요.
이 안전장치를 직접 시험해볼 수 있어요.
from smolagents.local_python_executor import LocalPythonExecutor
# 커스텀 실행기 설정, 패키지 "numpy" 승인
custom_executor = LocalPythonExecutor(["numpy"])
# 오류를 보기 좋게 출력하는 유틸리티
def run_capture_exception(command: str):
try:
custom_executor(harmful_command)
except Exception as e:
print("ERROR:\n", e)
# 정의되지 않은 명령은 동작하지 않음
harmful_command="!echo Bad command"
run_capture_exception(harmful_command)
# >>> ERROR: invalid syntax (<unknown>, line 1)
# os 같은 import는 명시적으로 additional_authorized_imports에 추가하지 않으면 수행되지 않음
harmful_command="import os; exit_code = os.system('echo Bad command')"
run_capture_exception(harmful_command)
# >>> ERROR: Code execution failed at line 'import os' due to: InterpreterError: Import of os is not allowed. Authorized imports are: ['statistics', 'numpy', 'itertools', 'time', 'queue', 'collections', 'math', 'random', 're', 'datetime', 'stat', 'unicodedata']
# 승인된 import에서도 잠재적으로 해로운 패키지는 import되지 않음
harmful_command="import random; random._os.system('echo Bad command')"
run_capture_exception(harmful_command)
# >>> ERROR: Code execution failed at line 'random._os.system('echo Bad command')' due to: InterpreterError: Forbidden access to module: os
# 무한 루프는 N회 연산 후 중단됨
harmful_command="""
while True:
pass
"""
run_capture_exception(harmful_command)
# >>> ERROR: Code execution failed at line 'while True: pass' due to: InterpreterError: Maximum number of 1000000 iterations in While loop exceeded
이 안전장치 덕분에 우리 인터프리터는 더 안전해요. 다양한 사용 사례에서 환경에 어떤 손상도 관찰되지 않은 채 사용해왔어요.
[!WARNING] 어떤 로컬 파이썬 샌드박스도 완전히 안전할 수 없다는 걸 이해하는 게 중요해요. 우리 인터프리터는 표준 파이썬 인터프리터보다 훨씬 나은 안전성 향상을 제공하지만, 결심한 공격자나 파인튜닝된 악성 LLM이 취약점을 찾아 환경에 해를 끼칠 여지는 여전히 있어요.
예를 들어
Pillow같은 패키지가 이미지를 처리하도록 허용했다면, LLM이 하드 드라이브를 채우는 수천 개의 대형 이미지 파일을 만드는 코드를 생성할 수 있어요. 다른 고급 탈출 기법은 승인된 패키지의 더 깊은 취약점을 노릴 수도 있어요.로컬 환경에서 LLM 생성 코드를 실행하는 일은 항상 어느 정도 내재된 위험을 수반해요. 진정으로 견고한 보안 격리로 LLM 생성 코드를 실행하는 유일한 방법은 아래에 설명된 E2B나 Docker 같은 원격 실행 옵션뿐이에요.
신뢰할 수 있는 추론 프로바이더의 잘 알려진 LLM을 사용할 때 악의적인 공격 위험은 낮지만, 0은 아니에요. 고보안 애플리케이션이거나 덜 신뢰되는 모델을 쓰는 경우에는 원격 실행 샌드박스를 고려해야 해요.
보안 코드 실행을 위한 샌드박스 접근법
코드를 실행하는 AI 에이전트를 다룰 때 보안은 가장 중요해요. smolagents에서 코드 실행을 샌드박싱하는 주요 접근법은 두 가지가 있고, 각각 다른 보안 특성과 역량을 가져요.
- 개별 코드 스니펫을 샌드박스에서 실행 — 이 접근법(다이어그램 왼쪽)은 에이전트가 생성한 파이썬 코드 스니펫만 샌드박스에서 실행하고 나머지 에이전트 시스템은 내 로컬 환경에 두는 방식이에요.
executor_type="blaxel",executor_type="e2b",executor_type="modal",executor_type="docker"처럼 설정하기 간단하지만, 멀티에이전트를 지원하지 않고 여전히 내 환경과 샌드박스 사이에 상태 데이터를 전달해야 해요. - 전체 에이전트 시스템을 샌드박스에서 실행 — 이 접근법(다이어그램 오른쪽)은 에이전트, 모델, 도구를 포함한 전체 에이전트 시스템을 샌드박스 환경 안에서 실행해요. 더 나은 격리를 제공하지만 수동 설정이 더 필요하고, 민감한 자격 증명(API 키 같은)을 샌드박스 환경에 전달해야 할 수도 있어요.
이 가이드는 두 유형의 샌드박스 접근법을 에이전트 애플리케이션에 어떻게 설정하고 사용하는지 설명해요.
Blaxel 설정
설치
- blaxel.ai에서 Blaxel 계정을 만드세요.
- 필요한 패키지를 설치하세요.
pip install 'smolagents[blaxel]'
Blaxel로 에이전트 실행: 퀵스타트
Blaxel Sandbox를 쓰는 간단한 방법은 에이전트 초기화에 executor_type="blaxel"을 추가하는 거예요.
from smolagents import InferenceClientModel, CodeAgent
with CodeAgent(model=InferenceClientModel(), tools=[], executor_type="blaxel") as agent:
agent.run("Can you give me the 100th Fibonacci number?")
[!TIP] 에이전트를 context manager(즉
with문)로 사용하면 에이전트가 작업을 끝낸 직후 Blaxel 샌드박스가 정리돼요. 또는 에이전트의cleanup()메서드를 직접 호출해도 돼요.
이 방식은 각 agent.run() 시작 시 에이전트 상태를 서버에 보내요. 그러면 모델은 로컬 환경에서 호출되지만, 생성된 코드는 샌드박스로 보내져 실행되고 출력만 돌아와요. Blaxel은 25ms 미만의 하이버네이션에서 시작하는 빠르게 뜨는 가상 머신을 제공하고, 비활성 후 0으로 스케일 다운하면서 메모리 상태는 유지해요. 빠르고 안전한 코드 실행이 필요한 에이전트 애플리케이션에 좋은 선택이에요.
[!TIP] 더 강한 보안 격리를 위해 에이전트 전체를 Blaxel에서 원격으로 호스팅할 수도 있어요. 이는 에이전트, 모델, 도구를 완전히 샌드박싱해요. Blaxel 에이전트 호스팅 문서를 참조하세요.
E2B 설정
설치
- e2b.dev에서 E2B 계정을 만드세요.
- 필요한 패키지를 설치하세요.
pip install 'smolagents[e2b]'
E2B에서 에이전트 실행: 퀵스타트
E2B Sandbox를 쓰는 간단한 방법은 에이전트 초기화에 executor_type="e2b"을 추가하는 거예요.
from smolagents import InferenceClientModel, CodeAgent
with CodeAgent(model=InferenceClientModel(), tools=[], executor_type="e2b") as agent:
agent.run("Can you give me the 100th Fibonacci number?")
[!TIP]
with문으로 에이전트를 쓰면 E2B 샌드박스가 작업 직후 정리돼요. 아니면cleanup()메서드를 직접 호출해도 돼요.
이 방식은 각 agent.run() 시작 시 에이전트 상태를 서버로 보내요. 그러면 모델은 로컬에서 호출되고, 생성된 코드는 샌드박스로 보내져 실행되며 출력만 돌아와요.
다만 관리 에이전트에 대한 어떤 호출도 모델 호출을 필요로 하므로, 우리는 원격 샌드박스에 시크릿을 전달하지 않기 때문에 모델 호출에 자격 증명이 없을 거예요. 그래서 이 방식은 더 복잡한 멀티에이전트 설정에서는 (아직) 동작하지 않아요.
E2B에서 에이전트 실행: 멀티에이전트
E2B 샌드박스에서 멀티에이전트를 쓰려면 에이전트를 E2B 안에서 완전히 실행해야 해요. 이렇게 하면 돼요.
from e2b_code_interpreter import Sandbox
import os
# 샌드박스 생성
sandbox = Sandbox()
# 필요한 패키지 설치
sandbox.commands.run("pip install smolagents")
def run_code_raise_errors(sandbox, code: str, verbose: bool = False) -> str:
execution = sandbox.run_code(
code,
envs={'HF_TOKEN': os.getenv('HF_TOKEN')}
)
if execution.error:
execution_logs = "\n".join([str(log) for log in execution.logs.stdout])
logs = execution_logs
logs += execution.error.traceback
raise ValueError(logs)
return "\n".join([str(log) for log in execution.logs.stdout])
# 에이전트 애플리케이션 정의
agent_code = """
import os
from smolagents import CodeAgent, InferenceClientModel
# 에이전트 초기화
agent = CodeAgent(
model=InferenceClientModel(token=os.getenv("HF_TOKEN"), provider="together"),
tools=[],
name="coder_agent",
description="This agent takes care of your difficult algorithmic problems using code."
)
manager_agent = CodeAgent(
model=InferenceClientModel(token=os.getenv("HF_TOKEN"), provider="together"),
tools=[],
managed_agents=[agent],
)
# 에이전트 실행
response = manager_agent.run("What's the 20th Fibonacci number?")
print(response)
"""
# 샌드박스에서 에이전트 코드 실행
execution_logs = run_code_raise_errors(sandbox, agent_code)
print(execution_logs)
Modal 설정
설치
- modal.com에서 Modal 계정을 만드세요.
- 필요한 패키지를 설치하세요.
pip install 'smolagents[modal]'
Modal에서 에이전트 실행: 퀵스타트
Modal Sandbox를 쓰는 간단한 방법은 에이전트 초기화에 executor_type="modal"을 추가하는 거예요.
from smolagents import InferenceClientModel, CodeAgent
with CodeAgent(model=InferenceClientModel(), tools=[], executor_type="modal") as agent:
agent.run("What is the 42th Fibonacci number?")
[!TIP]
with문으로 에이전트를 쓰면 Modal 샌드박스가 작업 직후 정리돼요. 아니면cleanup()메서드를 직접 호출해도 돼요.
InferenceClientModel의 에이전트 상태와 생성된 코드는 Modal 샌드박스로 보내져 내부에서 안전하게 코드를 실행할 수 있어요.
Docker 설정
설치
- 시스템에 Docker를 설치하세요.
- 필요한 패키지를 설치하세요.
pip install 'smolagents[docker]'
Docker에서 에이전트 실행: 퀵스타트
위 E2B 샌드박스와 비슷하게, Docker로 빠르게 시작하려면 에이전트 초기화에 executor_type="docker"를 추가하면 돼요.
from smolagents import InferenceClientModel, CodeAgent
with CodeAgent(model=InferenceClientModel(), tools=[], executor_type="docker") as agent:
agent.run("Can you give me the 100th Fibonacci number?")
[!TIP]
with문으로 에이전트를 쓰면 Docker 컨테이너가 작업 직후 정리돼요. 아니면cleanup()메서드를 직접 호출해도 돼요.
Docker 고급 사용법
Docker에서 멀티에이전트 시스템을 실행하려면 샌드박스에 커스텀 인터프리터를 설정해야 해요. Dockerfile은 이렇게 설정해요.
FROM python:3.10-bullseye
# 빌드 의존성 설치
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
python3-dev && \
pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir smolagents && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 작업 디렉터리 설정
WORKDIR /app
# 제한된 권한으로 실행
USER nobody
# 기본 명령
CMD ["python", "-c", "print('Container ready')"]
코드를 실행할 샌드박스 매니저를 만들 수 있어요.
import docker
import os
from typing import Optional
class DockerSandbox:
def __init__(self):
self.client = docker.from_env()
self.container = None
def create_container(self):
try:
image, build_logs = self.client.images.build(
path=".",
tag="agent-sandbox",
rm=True,
forcerm=True,
buildargs={},
# decode=True
)
except docker.errors.BuildError as e:
print("Build error logs:")
for log in e.build_log:
if 'stream' in log:
print(log['stream'].strip())
raise
# 보안 제약과 적절한 로깅으로 컨테이너 생성
self.container = self.client.containers.run(
"agent-sandbox",
command="tail -f /dev/null", # 컨테이너 계속 실행
detach=True,
tty=True,
mem_limit="512m",
cpu_quota=50000,
pids_limit=100,
security_opt=["no-new-privileges"],
cap_drop=["ALL"],
environment={
"HF_TOKEN": os.getenv("HF_TOKEN")
},
)
def run_code(self, code: str) -> Optional[str]:
if not self.container:
self.create_container()
# 컨테이너에서 코드 실행
exec_result = self.container.exec_run(
cmd=["python", "-c", code],
user="nobody"
)
# 모든 출력 수집
return exec_result.output.decode() if exec_result.output else None
def cleanup(self):
if self.container:
try:
self.container.stop()
except docker.errors.NotFound:
# 컨테이너가 이미 제거됨. 예상된 상황
pass
except Exception as e:
print(f"Error during cleanup: {e}")
finally:
self.container = None # 참조 해제
# 사용 예시:
sandbox = DockerSandbox()
try:
# 에이전트 코드 정의
agent_code = """
import os
from smolagents import CodeAgent, InferenceClientModel
# 에이전트 초기화
agent = CodeAgent(
model=InferenceClientModel(token=os.getenv("HF_TOKEN"), provider="together"),
tools=[]
)
# 에이전트 실행
response = agent.run("What's the 20th Fibonacci number?")
print(response)
"""
# 샌드박스에서 코드 실행
output = sandbox.run_code(agent_code)
print(output)
finally:
sandbox.cleanup()
샌드박스 모범 사례
이 핵심 관행들은 Blaxel, E2B, Docker 샌드박스에 공통으로 적용돼요.
- 리소스 관리
- 메모리와 CPU 한도를 설정하세요.
- 실행 타임아웃을 구현하세요.
- 리소스 사용량을 모니터링하세요.
- 보안
- 최소 권한으로 실행하세요.
- 불필요한 네트워크 접근을 비활성화하세요.
- 시크릿에는 환경변수를 사용하세요.
- 환경
- 의존성을 최소한으로 유지하세요.
- 고정된 패키지 버전을 사용하세요.
- 베이스 이미지를 쓴다면 주기적으로 업데이트하세요.
- 정리
- 특히 Docker 컨테이너는 리소스를 잡아먹는 방치된 컨테이너가 없도록 항상 적절한 정리를 보장하세요.
✨ 이 관행들을 따르고 적절한 정리 절차를 구현하면 에이전트가 샌드박스 환경에서 안전하고 효율적으로 실행된다는 걸 보장할 수 있어요.
보안 접근법 비교
앞서 다이어그램에서 봤듯, 두 샌드박싱 접근법은 서로 다른 보안 함의를 가져요.
접근법 1: 코드 스니펫만 샌드박스에서 실행
- 장점:
- 간단한 파라미터(
executor_type="blaxel",executor_type="e2b",executor_type="docker")로 쉽게 설정. - API 키를 샌드박스로 전달할 필요 없음.
- 내 로컬 환경을 더 잘 보호.
- Blaxel의 하이버네이션 기술로 빠른 실행(25ms 미만 시작).
- 간단한 파라미터(
- 단점:
- 멀티에이전트(관리 에이전트)를 지원하지 않음.
- 여전히 내 환경과 샌드박스 사이에 상태를 전달해야 함.
- 특정 코드 실행에 제한됨.
접근법 2: 전체 에이전트 시스템을 샌드박스에서 실행
- 장점:
- 멀티에이전트를 지원.
- 전체 에이전트 시스템의 완전한 격리.
- 복잡한 에이전트 아키텍처에 더 유연.
- 단점:
- 수동 설정이 더 필요.
- 민감한 API 키를 샌드박스로 전달해야 할 수도 있음.
- 더 복잡한 연산으로 인해 잠재적으로 더 높은 지연.
보안 요구와 애플리케이션 요구의 균형을 맞추는 접근법을 선택하세요. 대부분의 더 단순한 에이전트 아키텍처 애플리케이션에는 접근법 1이 보안과 사용 편의성의 좋은 균형을 제공해요. 완전한 격리가 필요한 더 복잡한 멀티에이전트 시스템에는 접근법 2가 설정은 더 번거롭지만 더 나은 보안 보장을 제공해요.
출처 인용
- 원문: smolagents - Secure code execution (Hugging Face Docs)
- 원본 파일: huggingface/smolagents - docs/source/en/tutorials/secure_code_execution.md