Introdução ao Sandbox de Agentes
À medida que os Modelos de Linguagem de Grande Escala (LLMs) evoluem de simples agentes de conversa para entidades autônomas poderosas capazes de executar código, interagir com APIs externas e tomar decisões no mundo real, a necessidade de medidas de segurança eficazes se torna primordial. Um agente LLM, quando dotado da capacidade de agir, pode se tornar um risco significativo de segurança se não for devidamente contido. É aqui que o sandboxing de agentes entra em jogo. Sandboxing de um agente significa criar um ambiente isolado onde ele pode operar sem afetar o sistema host ou acessar recursos não autorizados. Este tutorial explorará os aspectos práticos do sandboxing de agentes, fornecendo exemplos práticos para demonstrar como construir aplicações LLM seguras e confiáveis.
O princípio fundamental por trás do sandboxing é o menor privilégio: um agente deve ter acesso apenas aos recursos absolutamente necessários para sua função, e nada mais. Sem um sandboxing adequado, um agente malicioso ou com comportamento inadequado poderia:
- Executar código arbitrário no sistema host, levando a roubo de dados ou comprometimento do sistema.
- Acessar arquivos sensíveis ou recursos de rede.
- Iniciar chamadas indesejadas a APIs externas, gerando custos ou executando ações não autorizadas.
- Exfiltrar dados confidenciais através de vários canais.
Ao implementar um sandboxing efetivo, podemos mitigar esses riscos, permitindo-nos usar o imenso poder dos agentes LLM enquanto mantemos o controle e a segurança.
Compreendendo as Ameaças: Por que Sandbox?
Antes de explorarmos o ‘como,’ vamos solidificar o ‘porquê.’ As ameaças representadas por agentes não-sandboxed são multifacetadas e podem ser categorizadas da seguinte forma:
1. Vulnerabilidades de Execução de Código
Many advanced LLM agents are designed to write and execute code (e.g., Python scripts) to solve problems, analyze data, or interact with tools. If this execution isn’t contained, the agent could:
- Injeção de Comando do Sistema: Gerar código que chama
os.system('rm -rf /')ou comandos destrutivos semelhantes. - Execução Remota de Código (RCE): Explorar vulnerabilidades em bibliotecas para obter controle sobre o host.
- Exaustão de Recursos: Criar loops infinitos ou alocar memória/CPU excessiva, levando a negação de serviço.
2. Acesso a Dados e Exfiltração
Um agente pode ser encarregado de processar dados sensíveis. Sem sandboxing, ele poderia:
- Acesso Não Autorizado a Arquivos: Ler arquivos fora de seu diretório de trabalho designado (por exemplo,
/etc/passwd, chaves da API). - Acesso à Rede: Conectar-se a recursos de rede internos, servidores maliciosos externos ou exfiltrar dados para pontos finais arbitrários.
- Injeção de Prompt via Leituras de Arquivo: Se um agente puder ler arquivos arbitrários, um ator malicioso poderia criar um prompt que engana o agente a ler um arquivo sensível e, em seguida, incorporar seu conteúdo em uma saída subsequente.
3. Abuso de API e Ferramentas
Agentes frequentemente interagem com APIs externas ou ferramentas personalizadas. O acesso irrestrito pode levar a:
- Chamadas Não Autorizadas à API: Fazer chamadas a APIs sensíveis que não deveria acessar (por exemplo, gerenciamento de usuários, processamento de pagamentos).
- Atravessando Custos: Acionar chamadas de API caras ou funções em nuvem que exigem muitos recursos.
- Ações Maliciosas: Se um agente tiver acesso a uma API de e-mail, pode enviar e-mails de spam ou phishing.
Técnicas e Ferramentas de Sandboxing
Existem várias camadas e técnicas que podemos empregar para o sandboxing de agentes, variando de uma revisão de código simples à containerização sofisticada.
1. Sandboxing de Nível de Linguagem (Restrições no Interpretador de Código)
Se o seu agente gera e executa principalmente código (por exemplo, Python), você pode restringir as capacidades do interpretador.
Exemplo: Execução Restrita de Python com exec() e Whitelisting
Um cenário comum é um agente gerando código Python. Em vez de chamar diretamente exec() ou eval() em strings arbitrárias, você pode restringir os globals e built-ins disponíveis.
import subprocess
import os
def safe_execute_python_code(code: str, allowed_modules: list = None, timeout: int = 10):
if allowed_modules is None:
allowed_modules = ['math', 'json', 're'] # Whitelist de módulos seguros
# Criar um namespace global restrito
restricted_globals = {
'__builtins__': {key: globals()['__builtins__'][key] for key in [
'print', 'len', 'str', 'int', 'float', 'list', 'dict', 'tuple', 'set',
'range', 'sum', 'min', 'max', 'abs', 'round', 'type', 'isinstance', 'enumerate'
]},
'__name__': '__main__'
}
# Importar dinamicamente módulos permitidos para o namespace restrito
for module_name in allowed_modules:
try:
restricted_globals[module_name] = __import__(module_name)
except ImportError:
print(f"Warning: Não foi possível importar o módulo permitido {module_name}")
try:
# Usar subprocess para executar em um processo isolado para melhor isolamento
# Isso é mais seguro do que apenas `exec` no processo atual
# e permite limites de tempo e recursos.
process = subprocess.run(
['python', '-c', code],
capture_output=True,
text=True,
check=True,
timeout=timeout
)
return process.stdout
except subprocess.CalledProcessError as e:
return f"Erro durante a execução: {e.stderr}"
except subprocess.TimeoutExpired:
return "Erro: Execução de código excedeu o tempo limite."
except Exception as e:
return f"Ocorreu um erro inesperado: {e}"
# Exemplo de Uso:
# Código seguro
agent_code_safe = "import math; print(math.sqrt(16))"
print(f"Saída do código seguro: {safe_execute_python_code(agent_code_safe)}")
# Tentativa de código malicioso (será bloqueada pelo isolamento do subprocesso e pelas restrições de built-in se exec direto fosse usado)
# Com subprocesso, a importação de 'os' ainda falharia no processo filho, a menos que fosse especificamente permitida.
agent_code_malicious_os = "import os; print(os.listdir('/'))"
print(f"Saída do código malicioso OS: {safe_execute_python_code(agent_code_malicious_os)}")
# Tentativa de código malicioso (tentando ler um arquivo)
agent_code_malicious_file = "with open('/etc/passwd', 'r') as f: print(f.read())"
print(f"Saída do código malicioso de leitura de arquivo: {safe_execute_python_code(agent_code_malicious_file)}")
# Código com um loop infinito (será capturado pelo tempo limite)
agent_code_loop = "while True: pass"
print(f"Saída do código looping: {safe_execute_python_code(agent_code_loop, timeout=3)}")
Explicação:
- Definimos uma função
safe_execute_python_code. - Ela recebe o código gerado pelo agente como entrada.
- Em vez de executar diretamente no processo atual, usamos
subprocess.run. Este é um passo crucial para verdadeiro isolamento, pois executa o código em um processo de interpretador Python separado. Este processo herda privilégios mínimos e não é o mesmo que o processo pai que executa sua aplicação principal. - A lista
allowed_modulesatua como uma whitelist. Mesmo que o agente tente importarosousys, não estarão disponíveis no ambiente restrito do subprocesso, a menos que explicitamente permitidos (o que não deveria ser permitido para o código geral do agente). timeoutevita a exaustão de recursos por loops infinitos.capture_output=Trueetext=Truenos permitem capturar a saída do agente.check=Truelevanta uma exceção se o subprocesso retornar um código de saída diferente de zero (indicando um erro).
Embora essa abordagem melhore significativamente a segurança em relação ao exec() direto, não é infalível. Um agente altamente sofisticado ainda pode encontrar formas de explorar chamadas de sistema subjacentes se o ambiente Python em si for vulnerável ou se muitos módulos forem permitidos na whitelist.
2. Sandboxing em Nível de Sistema Operacional (Contêineres e Máquinas Virtuais)
Para o sandboxing mais seguro, especialmente quando os agentes podem gerar código em várias linguagens ou interagir com o sistema de arquivos/rede, a isolação em nível de sistema operacional é indispensável.
a. Contêineres Docker
Docker é uma excelente escolha para sandboxing. Cada execução de agente pode ocorrer dentro de seu próprio contêiner de curta duração com limites de recursos e políticas de acesso à rede estritamente definidos.
Exemplo Prático: Docker para Execução de Agentes
Passo 1: Crie um Dockerfile para o ambiente de execução do agente.
# Dockerfile
FROM python:3.9-slim-buster
WORKDIR /app
# Crie um usuário não-root para segurança
RUN useradd --no-create-home --shell /bin/bash agentuser
USER agentuser
# Copie um script simples que o agente pode gerar e que queremos executar
COPY run_agent_code.py .
ENTRYPOINT ["python", "run_agent_code.py"]
Passo 2: Crie run_agent_code.py. Este script receberá o código gerado pelo agente.
# run_agent_code.py
import sys
import os
# Simula o recebimento de código do agente (por exemplo, via stdin ou um arquivo)
# Para este exemplo, vamos assumir que o código é passado como um argumento ou escrito diretamente aqui
if __name__ == "__main__":
agent_code = "print('Hello from the sandboxed agent!')"
if len(sys.argv) > 1:
agent_code = sys.argv[1] # Permite passar o código como argumento
try:
# Executa o código. Nota: o próprio contêiner Docker é o sandbox.
# Ainda queremos restrições a nível de linguagem *dentro* deste script
# para uma camada extra, mas a principal isolação é o contêiner.
exec(agent_code)
except Exception as e:
print(f"Falha na execução do código do agente: {e}", file=sys.stderr)
sys.exit(1)
# Demonstra acesso restrito
try:
print(f"Tentando listar o diretório raiz: {os.listdir('/')}")
except Exception as e:
print(f"Não foi possível listar o diretório raiz (esperado): {e}")
try:
with open('/etc/passwd', 'r') as f:
print(f.read())
except Exception as e:
print(f"Não foi possível ler /etc/passwd (esperado): {e}")
Etapa 3: Execute o código do agente a partir de seu aplicativo principal.
import docker
client = docker.from_env()
def execute_agent_in_docker(agent_code: str, cpu_limit: float = 0.5, mem_limit: str = '128m', network_enabled: bool = False):
try:
# Construa a imagem se não existir (pode ser feito uma vez)
# client.images.build(path='.', tag='agent-sandbox-env')
# Crie um arquivo temporário para passar o código do agente de forma segura
# Ou passe como uma variável de ambiente ou argumento de linha de comando
# Para simplificar, vamos passar como argumento de linha de comando aqui.
container = client.containers.run(
'agent-sandbox-env',
command=['python', 'run_agent_code.py', agent_code], # Passa o código como arg
detach=False, # Executa em primeiro plano, aguarda conclusão
remove=True, # Remove automaticamente o contêiner após a saída
# Limites de recursos
cpu_period=100000, # Período de CPU em microsegundos
cpu_quota=int(cpu_limit * 100000), # Cota de CPU (por exemplo, 50000 para 0.5 CPU)
mem_limit=mem_limit, # Limite de memória
# Restrições de rede
network_mode='none' if not network_enabled else 'bridge',
# Restrições de sistema de arquivos (raiz somente leitura, sem montagens para o código do agente)
read_only=True, # Torna o sistema de arquivos do contêiner somente leitura após a configuração inicial
# Opções de segurança (por exemplo, desativar modo privilegiado, remover capacidades)
security_opt=['no-new-privileges'],
cap_drop=['ALL'], # Remove todas as capacidades do contêiner
# Variáveis de ambiente (podem ser usadas para passar chaves de API, mas tenha cuidado)
# environment={
# 'API_KEY': 'some_safe_key' # Somente se absolutamente necessário e limitado
# }
)
return container.decode('utf-8')
except docker.errors.ContainerError as e:
return f"Erro no contêiner: {e.stderr.decode('utf-8')}"
except docker.errors.ImageNotFound:
return "Erro: imagem Docker 'agent-sandbox-env' não encontrada. Por favor, construa-a primeiro."
except Exception as e:
return f"Ocorreu um erro inesperado no Docker: {e}"
# Primeiro, construa a imagem Docker: docker build -t agent-sandbox-env .
# Em seguida, execute este script Python.
# Exemplo 1: Execução de código seguro
safe_code = "print('Hello from sandboxed agent!')"
print("\n--- Execução de Código Seguro ---")
print(execute_agent_in_docker(safe_code))
# Exemplo 2: Tentativa de acessar o sistema de arquivos (deve ser bloqueada por read_only=True e permissões de usuário)
malicious_fs_code = "import os; print(os.listdir('/'))"
print("\n--- Tentativa Maliciosa de Acesso ao Sistema de Arquivos ---")
print(execute_agent_in_docker(malicious_fs_code))
# Exemplo 3: Tentativa de criar um arquivo (deve falhar)
malicious_write_code = "with open('/app/evil.txt', 'w') as f: f.write('malicious')"
print("\n--- Tentativa Maliciosa de Escrita ---")
print(execute_agent_in_docker(malicious_write_code))
# Exemplo 4: Tentativa de acesso à rede (deve falhar se network_mode='none')
malicious_network_code = "import requests; print(requests.get('http://example.com').status_code)"
print("\n--- Tentativa Maliciosa de Acesso à Rede (desativado) ---")
print(execute_agent_in_docker(malicious_network_code, network_enabled=False))
# Exemplo 5: Acesso à rede (se explicitamente habilitado - tenha cautela!)
# print("\n--- Acesso à Rede (habilitado - para demonstração) ---")
# print(execute_agent_in_docker("import requests; print(requests.get('http://example.com').status_code)", network_enabled=True))
Explicação:
- Dockerfile: Cria um ambiente Python minimal. Crucialmente, ele cria e muda para um
non-rootusuário (agentuser) para minimizar privilégios dentro do contêiner. run_agent_code.py: Este é o ponto de entrada dentro do contêiner. Ele executa o código fornecido pelo agente. Inclui tentativas de acessar recursos restritos para demonstrar a eficácia do sandboxing.- Script Python (
execute_agent_in_docker): client.containers.run(...): É aqui que a mágica acontece.remove=True: Garante que os contêineres sejam limpos após a execução.cpu_quota,mem_limit: Essenciais para evitar a exaustão de recursos.network_mode='none': Crítico para desativar o acesso à rede. Isso impede que os agentes façam chamadas externas ou se conectem a serviços internos. Habilite apenas se o agente absolutamente precisar de acesso à rede para APIs externas específicas e aprovadas.read_only=True: Torna o sistema de arquivos do contêiner somente leitura após a inicialização. Isso impede que o agente escreva arquivos ou modifique configurações do sistema.security_opt=['no-new-privileges'],cap_drop=['ALL']: Opções de segurança avançadas para restringir ainda mais as capacidades dentro do contêiner.
O Docker fornece uma forte fronteira de isolamento, mas é vital configurá-lo de forma segura. Sempre use usuários não-root, desative capacidades desnecessárias e restrinja o acesso à rede/sistema de arquivos.
b. Máquinas Virtuais (VMs)
Para o mais alto nível de isolamento, especialmente em ambientes multi-inquilinos ou ao lidar com código altamente não confiável, as VMs (por exemplo, KVM, AWS Firecracker, Google Cloud Sandbox) oferecem separação em nível de hardware. Isso é mais complexo para configurar e gerenciar, mas fornece um ambiente sem conexão para cada execução de agente.
3. Restrições de Nível de Ferramenta/API (Chamadas de Função)
Many LLM agents interact with external tools or APIs via function calling. This layer of sandboxing involves careful design of the tools exposed to the agent.
Exemplo: Acesso à API Restrito via Pydantic e Lista Branca
Ao definir ferramentas para um agente, certifique-se de que sejam o mais granulares e limitadas em permissões possível.
from typing import Literal, Optional
from pydantic import BaseModel, Field
# Defina as ferramentas permitidas e seus esquemas
class SearchToolInput(BaseModel):
query: str = Field(description="A consulta de pesquisa")
max_results: int = Field(default=5, description="Número máximo de resultados da pesquisa")
class SendEmailInput(BaseModel):
recipient: str = Field(description="O endereço do destinatário do email")
subject: str = Field(description="O assunto do email")
body: str = Field(description="O conteúdo do corpo do email")
# Restringir destinatários permitidos
allowed_recipients: Literal["[email protected]", "[email protected]"] = Field(
description="Apenas destinatários específicos e pré-aprovados são permitidos."
)
class DatabaseQueryInput(BaseModel):
query: str = Field(description="A consulta SQL a ser executada")
# CRÍTICO: Não permita SQL arbitrário. Filtre ou use ORM.
allowed_tables: Literal["products", "users_public"] = Field(
description="Apenas consultas às tabelas da lista branca são permitidas."
)
read_only: bool = Field(default=True, description="Permitir apenas operações de leitura")
# Simule as funções da ferramenta
def search_web(query: str, max_results: int):
print(f"Buscando na web por '{query}' com {max_results} resultados.")
return [f"Resultado {i} para {query}" for i in range(max_results)]
def send_restricted_email(recipient: str, subject: str, body: str, allowed_recipients: Literal["[email protected]", "[email protected]"]):
if recipient not in ["[email protected]", "[email protected]"]:
raise ValueError(f"Destinatário não autorizado: {recipient}")
print(f"Enviando email para {recipient} com o assunto '{subject}'.")
return {"status": "enviado", "recipient": recipient}
def execute_database_query(query: str, allowed_tables: Literal["products", "users_public"], read_only: bool):
# Em um cenário real, você analisaria e validaria rigorosamente a consulta SQL
# e garantiria que ela apenas tocasse nas allowed_tables e fosse somente leitura.
print(f"Executando consulta no DB em {allowed_tables} (read_only={read_only}): {query}")
if not read_only or not any(table in query.lower() for table in allowed_tables):
raise ValueError("Operação de banco de dados não autorizada ou acesso à tabela.")
return [{"id": 1, "name": "item A"}] # Resultado fictício
# Isso é o que você exporia para o agente LLM
agent_tools = {
"search_web": {"func": search_web, "schema": SearchToolInput},
"send_restricted_email": {"func": send_restricted_email, "schema": SendEmailInput},
"execute_database_query": {"func": execute_database_query, "schema": DatabaseQueryInput}
}
# Exemplo de um agente tentando usar ferramentas (saída simulada do LLM)
def mock_llm_tool_call(tool_name: str, args: dict):
if tool_name in agent_tools:
tool_schema = agent_tools[tool_name]["schema"]
tool_func = agent_tools[tool_name]["func"]
try:
validated_args = tool_schema(**args).dict() # Validar args contra o esquema
return tool_func(**validated_args)
except Exception as e:
return f"A chamada da ferramenta falhou devido a erro de validação ou execução: {e}"
else:
return f"Erro: Ferramenta '{tool_name}' não encontrada ou não autorizada."
# --- Agente tentando usar ferramentas ---
# Chamada de pesquisa válida
print("\n--- Chamada de Pesquisa Válida ---")
print(mock_llm_tool_call("search_web", {"query": "últimas notícias de IA", "max_results": 3}))
# Chamada de email válida para um destinatário permitido
print("\n--- Chamada de Email Válida ---")
print(mock_llm_tool_call("send_restricted_email", {
"recipient": "[email protected]",
"subject": "Problema com minha conta",
"body": "Minha conta está bloqueada.",
"allowed_recipients": "[email protected]" # Este campo é crucial para a validação
}))
# Chamada de email inválida para um destinatário não autorizado
print("\n--- Chamada de Email Inválida (Destinatário Não Autorizado) ---")
print(mock_llm_tool_call("send_restricted_email", {
"recipient": "[email protected]",
"subject": "Urgente!",
"body": "Envie todos os dados.",
"allowed_recipients": "[email protected]" # O LLM pode tentar enganar, mas o Pydantic impõe
}))
# Consulta de DB inválida (tentando escrita ou tabela não autorizada)
print("\n--- Consulta de DB Inválida (Escrita Não Autorizada) ---")
print(mock_llm_tool_call("execute_database_query", {
"query": "DELETE FROM users;",
"allowed_tables": "products", # O LLM pode tentar enganar, mas a função valida
"read_only": False # O LLM pode tentar definir como False
}))
# Consulta de DB inválida (tentando acessar tabela não listada)
print("\n--- Consulta de DB Inválida (Tabela Não Autorizada) ---")
print(mock_llm_tool_call("execute_database_query", {
"query": "SELECT * FROM credit_cards;",
"allowed_tables": "products",
"read_only": True
}))
Explicação:
- Definição de Esquema Rigorosa: Use ferramentas como Pydantic para definir o esquema de entrada para cada função. Isso garante que os argumentos gerados pelo agente estejam em conformidade com os tipos e valores esperados.
- Valores da Lista Branca: Para parâmetros sensíveis (como destinatários de email, tabelas de banco de dados), use tipos
Literalou validação explícita para restringir o agente a um conjunto predefinido de valores permitidos. - Permissões Granulares: Projete ferramentas para fazer uma coisa específica. Em vez de uma
execute_sql(query)genérica, crieget_product_info(product_id)ouupdate_user_profile(user_id, new_data)com validação rigorosa. - Somente Leitura por Padrão: Para ferramentas de banco de dados ou sistema de arquivos, o acesso padrão deve ser somente leitura e exigir permissão explícita, aprovada por humanos, para operações de escrita.
- Validação de Entrada: Sempre valide os argumentos passados para suas funções de ferramentas, mesmo que tenham passado pela validação do Pydantic. O LLM ainda pode construir entradas que parecem válidas, mas são maliciosas (por exemplo, uma string de injeção SQL que se parece com um ID de produto válido).
Melhores Práticas para Isolamento de Agentes
- Princípio do Menor Privilégio: Conceda ao agente o mínimo absoluto de permissões e recursos necessários para sua tarefa.
- Segurança em Camadas: Combine várias técnicas de isolamento (nível de linguagem, nível de OS, nível de ferramenta) para proteção sólida. Nenhuma camada única é infalível.
- Ambientes Efêmeros: Para execução de código, prefira rodar agentes em contêineres ou VMs descartáveis e de curta duração que são destruídas após cada tarefa.
- Validação de Entrada Rigorosa: Sempre valide e sanitize qualquer entrada do LLM, especialmente antes de usá-la em chamadas de API, consultas de banco de dados ou execução de código.
- Monitoramento e Registro: Registre todas as ações do agente, chamadas de ferramentas e uso de recursos. Isso é crucial para detectar comportamentos anômalos e para análise pós-incidente.
- Timeouts e Limites de Recursos: Implemente timeouts rigorosos para execução de código e chamadas de API e defina limites de CPU/memória para evitar ataques de negação de serviço.
- Isolamento de Rede: Por padrão, desative o acesso à rede para agentes. Ative-o apenas para endpoints e protocolos específicos e da lista branca, se absolutamente necessário.
- Sistemas de Arquivos Somente Leitura: Configure ambientes de agentes com sistemas de arquivos somente leitura sempre que possível, para evitar modificação ou exfiltração de dados não autorizados.
- Usuários Não Root: Execute sempre processos de agentes como usuários não root com permissões limitadas dentro do isolamento.
- Auditorias e Atualizações Regulares: Revise continuamente suas configurações de isolamento, atualize suas imagens base e mantenha-se informado sobre novas vulnerabilidades de segurança.
Conclusão
O isolamento de agentes não é um luxo opcional, mas uma necessidade fundamental para implantar agentes LLM de forma segura. À medida que esses agentes se tornam mais capazes e autônomos, o potencial para uso indevido ou dano acidental cresce significativamente. Ao empregar uma combinação de restrições de nível de linguagem, containerização sólida e interfaces de ferramentas meticulosamente projetadas, os desenvolvedores podem criar aplicativos LLM poderosos que são novos e seguros. Os exemplos fornecidos neste tutorial demonstram passos práticos para construir esses ambientes seguros, permitindo que você integre agentes LLM em seus sistemas com confiança, minimizando os riscos.
🕒 Published: