Introduzione al Sandboxing degli Agenti
Man mano che i Modelli Linguistici di Grandi Dimensioni (LLM) si evolvono da semplici agenti conversazionali a potenti entità autonome capaci di eseguire codice, interagire con API esterne e prendere decisioni nel mondo reale, diventa fondamentale implementare misure di sicurezza solide. Un agente LLM, quando dotato della possibilità di agire, può rappresentare un significativo rischio per la sicurezza se non viene opportunamente vincolato. Qui entra in gioco il sandboxing degli agenti. Sandboxing un agente significa creare un ambiente isolato in cui può operare senza influenzare il sistema host o accedere a risorse non autorizzate. Questo tutorial esplorerà gli aspetti pratici del sandboxing degli agenti, fornendo esempi pratici per dimostrare come costruire applicazioni LLM sicure e affidabili.
Il principio fondamentale alla base del sandboxing è il minimo privilegio: un agente dovrebbe avere accesso solo alle risorse assolutamente necessarie per la sua funzione e nulla di più. Senza un adeguato sandboxing, un agente malevolo o errante potrebbe:
- Eseguire codice arbitrario sul sistema host, portando a furto di dati o compromissione del sistema.
- Accedere a file sensibili o risorse di rete.
- Iniziare chiamate API esterne indesiderate, comportando costi o eseguendo azioni non autorizzate.
- Esfiltrare dati riservati attraverso vari canali.
Implementando un sandboxing efficace, possiamo mitigare questi rischi, permettendoci di utilizzare l’immenso potere degli agenti LLM mantenendo il controllo e la sicurezza.
Comprendere le Minacce: Perché Sandbox?
Prima di esplorare il ‘come,’ solidifichiamo il ‘perché.’ Le minacce poste dagli agenti non sandboxed sono multifaccettate e possono essere categorizzate come segue:
1. Vulnerabilità di Esecuzione del Codice
Molti agenti LLM avanzati sono progettati per scrivere ed eseguire codice (ad esempio, script Python) per risolvere problemi, analizzare dati o interagire con strumenti. Se questa esecuzione non è contenuta, l’agente potrebbe:
- Injection di Comandi di Sistema: Generare codice che chiama
os.system('rm -rf /')o comandi distruttivi simili. - Esecuzione Remota di Codice (RCE): Sfruttare vulnerabilità nelle librerie per ottenere il controllo sul sistema host.
- Esaurimento delle Risorse: Creare loop infiniti o allocare memoria/CPU eccessiva, portando a un diniego di servizio.
2. Accesso ai Dati e Esfiltrazione
Un agente potrebbe essere incaricato di elaborare dati sensibili. Senza sandboxing, potrebbe:
- Accesso Non Autorizzato ai File: Leggere file al di fuori della sua directory di lavoro designata (ad esempio,
/etc/passwd, chiavi API). - Accesso alla Rete: Connettersi a risorse di rete interne, server malevoli esterni, o esfiltrare dati a endpoint arbitrari.
- Injection di Prompts tramite Letture di File: Se un agente può leggere file arbitrari, un attore malevolo potrebbe creare un prompt che inganna l’agente facendogli leggere un file sensibile e incorporando poi il suo contenuto in un output successivo.
3. Abuso di API e Strumenti
Gli agenti spesso interagiscono con API esterne o strumenti personalizzati. Un accesso illimitato può portare a:
- Chiamate API Non Autorizzate: Effettuare chiamate a API sensibili a cui non dovrebbe accedere (ad esempio, gestione utenti, elaborazione pagamenti).
- Superamento dei Costi: Attivare chiamate API costose o funzioni cloud intensive in termini di risorse.
- Azioni Malevole: Se un agente ha accesso a un’API email, potrebbe inviare email di spam o phishing.
Tecniche e Strumenti di Sandboxing
Esistono diversi livelli e tecniche che possiamo impiegare per il sandboxing degli agenti, che spaziano da semplici revisioni del codice a sofisticate tecniche di containerizzazione.
1. Sandboxing a Livello di Linguaggio (Restrizioni all’Interprete di Codice)
Se il tuo agente genera ed esegue principalmente codice (ad esempio, Python), puoi limitare le capacità dell’interprete.
Esempio: Esecuzione Python Limitata con exec() e Whitelisting
Uno scenario comune è un agente che genera codice Python. Invece di chiamare direttamente exec() o eval() su stringhe arbitrari, puoi limitare i globali e i built-in disponibili.
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 moduli sicuri
# Crea uno spazio dei nomi globale ristretto
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__'
}
# Importa dinamicamente i moduli consentiti nello spazio dei nomi ristretto
for module_name in allowed_modules:
try:
restricted_globals[module_name] = __import__(module_name)
except ImportError:
print(f"Attenzione: Impossibile importare il modulo consentito {module_name}")
try:
# Usa subprocess per eseguire in un processo isolato per una migliore isolazione
# Questo è più solido rispetto a semplicemente `exec` nel processo corrente
# e consente timeout e limiti di risorse.
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"Errore durante l'esecuzione: {e.stderr}"
except subprocess.TimeoutExpired:
return "Errore: Esecuzione del codice scaduta."
except Exception as e:
return f"Si è verificato un errore imprevisto: {e}"
# Esempio di Utilizzo:
# Codice sicuro
agent_code_safe = "import math; print(math.sqrt(16))"
print(f"Output codice sicuro: {safe_execute_python_code(agent_code_safe)}")
# Tentativo di codice malevolo (sarà bloccato dall'isolamento del subprocess e dalle restrizioni sui built-in se fosse stato usato exec diretto)
# Con subprocess, l'import di 'os' fallirebbe ancora nel processo secondario a meno che non sia esplicitamente consentito.
agent_code_malicious_os = "import os; print(os.listdir('/'))"
print(f"Output codice malevolo OS: {safe_execute_python_code(agent_code_malicious_os)}")
# Tentativo di codice malevolo (cercando di leggere un file)
agent_code_malicious_file = "with open('/etc/passwd', 'r') as f: print(f.read())"
print(f"Output codice di lettura file malevolo: {safe_execute_python_code(agent_code_malicious_file)}")
# Codice con un loop infinito (sarà catturato dal timeout)
agent_code_loop = "while True: pass"
print(f"Output codice in loop: {safe_execute_python_code(agent_code_loop, timeout=3)}")
Spiegazione:
- Definiamo una funzione
safe_execute_python_code. - Prende in input il codice generato dall’agente.
- Invece di eseguire direttamente nel processo corrente, usiamo
subprocess.run. Questo è un passo cruciale per una vera isolazione, in quanto esegue il codice in un processo interprete Python separato. Questo processo eredita privilegi minimi e non è lo stesso del processo padre che esegue l’applicazione principale. - La lista
allowed_modulesfunge da whitelist. Anche se l’agente cerca di importareososys, non saranno disponibili nell’ambiente ristretto del subprocess a meno che non siano esplicitamente consentiti (cosa che non dovrebbe essere per il codice generale dell’agente). timeoutpreviene l’esaurimento delle risorse da loop infiniti.capture_output=Trueetext=Trueci permettono di catturare l’output dell’agente.check=Truegenera un’eccezione se il subprocess restituisce un codice di uscita diverso da zero (indicando un errore).
Sebbene questo approccio migliori significativamente la sicurezza rispetto a exec() diretto, non è infallibile. Un agente altamente sofisticato potrebbe comunque trovare modi per sfruttare le chiamate di sistema sottostanti se l’ambiente Python stesso è vulnerabile o se troppi moduli sono stati autorizzati nella whitelist.
2. Sandboxing a Livello di Sistema Operativo (Container e Macchine Virtuali)
Per il sandboxing più solido, specialmente quando gli agenti potrebbero generare codice in più lingue o interagire con il filesystem/rete, l’isolamento a livello di OS è indispensabile.
a. Container Docker
Docker è un’ottima scelta per il sandboxing. Ogni esecuzione dell’agente può avvenire all’interno del proprio container, di breve durata, con limiti di risorse e politiche di accesso alla rete rigorosamente definiti.
Esempio Pratico: Docker per l’Esecuzione dell’Agente
Passo 1: Crea un Dockerfile per l’ambiente di esecuzione dell’agente.
# Dockerfile
FROM python:3.9-slim-buster
WORKDIR /app
# Crea un utente non root per maggiore sicurezza
RUN useradd --no-create-home --shell /bin/bash agentuser
USER agentuser
# Copia uno script semplice che l'agente potrebbe generare e vogliamo eseguire
COPY run_agent_code.py .
ENTRYPOINT ["python", "run_agent_code.py"]
Passo 2: Crea run_agent_code.py. Questo script riceverà il codice generato dall’agente.
# run_agent_code.py
import sys
import os
# Simula la ricezione di codice dall'agente (ad esempio, tramite stdin o un file)
# Per questo esempio, supporremo che il codice venga passato come argomento o scritto direttamente qui
if __name__ == "__main__":
agent_code = "print('Hello from the sandboxed agent!')"
if len(sys.argv) > 1:
agent_code = sys.argv[1] # Permette di passare il codice come argomento
try:
# Esegue il codice. Nota: il container Docker stesso è la sandbox.
# Potremmo comunque voler applicare restrizioni a livello di linguaggio *all'interno* di questo script
# per un ulteriore livello di sicurezza, ma l'isolamento principale è il container.
exec(agent_code)
except Exception as e:
print(f"Agent code execution failed: {e}", file=sys.stderr)
sys.exit(1)
# Dimostra accessi limitati
try:
print(f"Attempting to list root: {os.listdir('/')}")
except Exception as e:
print(f"Could not list root directory (expected): {e}")
try:
with open('/etc/passwd', 'r') as f:
print(f.read())
except Exception as e:
print(f"Could not read /etc/passwd (expected): {e}")
Passaggio 3: Esegui il codice dell’agente dalla tua applicazione principale.
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:
# Crea l'immagine se non esiste (può essere fatto una volta)
# client.images.build(path='.', tag='agent-sandbox-env')
# Crea un file temporaneo per passare il codice dell'agente in modo sicuro
# Oppure passalo come variabile di ambiente o argomento da riga di comando
# Per semplicità, lo passeremo come argomento da riga di comando qui.
container = client.containers.run(
'agent-sandbox-env',
command=['python', 'run_agent_code.py', agent_code], # Passa il codice come argomento
detach=False, # Esegui in primo piano, attendi il completamento
remove=True, # Rimuovi automaticamente il container dopo l'uscita
# Limiti delle risorse
cpu_period=100000, # Periodo della CPU in microsecondi
cpu_quota=int(cpu_limit * 100000), # Quota della CPU (ad esempio, 50000 per 0.5 CPU)
mem_limit=mem_limit, # Limite di memoria
# Restrizioni di rete
network_mode='none' if not network_enabled else 'bridge',
# Restrizioni del filesystem (root in sola lettura, nessun montaggio per il codice dell'agente)
read_only=True, # Rende il filesystem del container in sola lettura dopo la configurazione iniziale
# Opzioni di sicurezza (ad esempio, disabilita la modalità privilegiata, elimina capacità)
security_opt=['no-new-privileges'],
cap_drop=['ALL'], # Rimuove tutte le capacità per il container
# Variabili di ambiente (possono essere utilizzate per passare chiavi API, ma fai attenzione)
# environment={
# 'API_KEY': 'some_safe_key' # Solo se assolutamente necessario e a scopo limitato
# }
)
return container.decode('utf-8')
except docker.errors.ContainerError as e:
return f"Container error: {e.stderr.decode('utf-8')}"
except docker.errors.ImageNotFound:
return "Error: Docker image 'agent-sandbox-env' not found. Please build it first."
except Exception as e:
return f"An unexpected Docker error occurred: {e}"
# Prima costruisci l'immagine Docker: docker build -t agent-sandbox-env .
# Poi esegui questo script Python.
# Esempio 1: Esecuzione di codice sicuro
safe_code = "print('Hello from sandboxed agent!')"
print("\n--- Esecuzione di Codice Sicuro ---")
print(execute_agent_in_docker(safe_code))
# Esempio 2: Tentativo di accesso al filesystem (dovrebbe essere bloccato da read_only=True e permessi dell'utente)
malicious_fs_code = "import os; print(os.listdir('/'))"
print("\n--- Tentativo di Accesso Maligno al Filesystem ---")
print(execute_agent_in_docker(malicious_fs_code))
# Esempio 3: Tentativo di creare un file (dovrebbe fallire)
malicious_write_code = "with open('/app/evil.txt', 'w') as f: f.write('malicious')"
print("\n--- Tentativo di Scrittura Maligna ---")
print(execute_agent_in_docker(malicious_write_code))
# Esempio 4: Tentativo di accesso alla rete (dovrebbe fallire se network_mode='none')
malicious_network_code = "import requests; print(requests.get('http://example.com').status_code)"
print("\n--- Tentativo di Accesso Maligno alla Rete (disabilitato) ---")
print(execute_agent_in_docker(malicious_network_code, network_enabled=False))
# Esempio 5: Accesso alla rete (se esplicitamente abilitato - fai attenzione!)
# print("\n--- Accesso alla Rete (abilitato - per dimostrazione) ---")
# print(execute_agent_in_docker("import requests; print(requests.get('http://example.com').status_code)", network_enabled=True))
Spiegazione:
- Dockerfile: Crea un ambiente Python minimale. Fondamentalmente, crea e passa a un
non-rootuser (agentuser) per ridurre al minimo i privilegi all’interno del container. run_agent_code.py: Questo è il punto di ingresso all’interno del container. Esegue il codice fornito dall’agente. Include tentativi di accesso a risorse riservate per dimostrare l’efficacia del sandboxing.- Script Python (
execute_agent_in_docker): client.containers.run(...): Qui avviene la magia.remove=True: Garantisce che i container vengano puliti dopo l’esecuzione.cpu_quota,mem_limit: Essenziali per prevenire l’esaurimento delle risorse.network_mode='none': Critico per disabilitare l’accesso alla rete. Questo impedisce agli agenti di effettuare chiamate esterne o connettersi a servizi interni. Abilitare solo se l’agente ha assolutamente bisogno di accesso alla rete per API esterne specifiche e approvate.read_only=True: Rende il filesystem del container in sola lettura dopo l’inizializzazione. Questo impedisce all’agente di scrivere file o modificare configurazioni di sistema.security_opt=['no-new-privileges'],cap_drop=['ALL']: Opzioni di sicurezza avanzate per ulteriormente limitare le capacità all’interno del container.
Docker fornisce un forte confine di isolamento, ma è fondamentale configurarlo in modo sicuro. Usa sempre utenti non root, disabilita capacità non necessarie e limita l’accesso alla rete/filesystem.
b. Macchine Virtuali (VMs)
Per il livello più alto di isolamento, specialmente in ambienti multi-tenant o quando si tratta di codice altamente non affidabile, le VM (ad esempio, KVM, AWS Firecracker, Google Cloud Sandbox) offrono una separazione a livello hardware. Questo è più complesso da configurare e gestire ma fornisce un ambiente isolato per ogni esecuzione dell’agente.
3. Restrizioni a Livello di Strumento/API (Chiamata di Funzione)
Molti agenti LLM interagiscono con strumenti esterni o API tramite chiamate di funzione. Questo strato di sandboxing richiede una progettazione attenta degli strumenti esposti all’agente.
Esempio: Accesso API Riservato tramite Pydantic e Whitelisting
Quando si definiscono strumenti per un agente, assicurati che siano il più granulari e limitati possibile in termini di permessi.
from typing import Literal, Optional
from pydantic import BaseModel, Field
# Definire gli strumenti consentiti e i loro schemi
class SearchToolInput(BaseModel):
query: str = Field(description="La query di ricerca")
max_results: int = Field(default=5, description="Numero massimo di risultati della ricerca")
class SendEmailInput(BaseModel):
recipient: str = Field(description="L'indirizzo email del destinatario")
subject: str = Field(description="L'oggetto dell'email")
body: str = Field(description="Il contenuto del corpo dell'email")
# Limitare i destinatari consentiti
allowed_recipients: Literal["[email protected]", "[email protected]"] = Field(
description="Sono consentiti solo destinatari specifici e pre-approvati."
)
class DatabaseQueryInput(BaseModel):
query: str = Field(description="La query SQL da eseguire")
# CRITICO: Non consentire SQL arbitrario. Filtrare o utilizzare ORM.
allowed_tables: Literal["products", "users_public"] = Field(
description="Sono consentite solo query su tabelle autorizzate."
)
read_only: bool = Field(default=True, description="Consentire solo operazioni di lettura")
# Simulare le funzioni degli strumenti
def search_web(query: str, max_results: int):
print(f"Cercando sul web '{query}' con {max_results} risultati.")
return [f"Risultato {i} per {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"Destinatario non autorizzato: {recipient}")
print(f"Inviando email a {recipient} con oggetto '{subject}'.")
return {"status": "inviato", "recipient": recipient}
def execute_database_query(query: str, allowed_tables: Literal["products", "users_public"], read_only: bool):
# In uno scenario reale, dovresti analizzare e convalidare rigorosamente la query SQL
# e garantire che tocchi solo le tabelle consentite e sia di sola lettura.
print(f"Eseguendo la query DB su {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("Operazione o accesso alla tabella non autorizzati.")
return [{"id": 1, "name": "elemento A"}] # Risultato fittizio
# Questo è ciò che esporresti all'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}
}
# Esempio di un agente che cerca di utilizzare strumenti (output LLM simulato)
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() # Convalida gli argomenti rispetto allo schema
return tool_func(**validated_args)
except Exception as e:
return f"Chiamata allo strumento fallita a causa di un errore di convalida o esecuzione: {e}"
else:
return f"Errore: Strumento '{tool_name}' non trovato o non autorizzato."
# --- Agente che cerca di utilizzare strumenti ---
# Chiamata di ricerca valida
print("\n--- Chiamata di Ricerca Valida ---")
print(mock_llm_tool_call("search_web", {"query": "ultime notizie sull'AI", "max_results": 3}))
# Chiamata email valida a un destinatario autorizzato
print("\n--- Chiamata Email Valida ---")
print(mock_llm_tool_call("send_restricted_email", {
"recipient": "[email protected]",
"subject": "Problema con il mio account",
"body": "Il mio account è bloccato.",
"allowed_recipients": "[email protected]" # Questo campo è cruciale per la convalida
}))
# Chiamata email non valida a un destinatario non autorizzato
print("\n--- Chiamata Email Non Valida (Destinatario Non Autorizzato) ---")
print(mock_llm_tool_call("send_restricted_email", {
"recipient": "[email protected]",
"subject": "Urgente!",
"body": "Inviami tutti i dati.",
"allowed_recipients": "[email protected]" # LLM potrebbe tentare di ingannare, ma Pydantic applica
}))
# Query DB non valida (tentativo di scrittura o tabella non autorizzata)
print("\n--- Query DB Non Valida (Scrittura Non Autorizzata) ---")
print(mock_llm_tool_call("execute_database_query", {
"query": "DELETE FROM users;",
"allowed_tables": "products", # LLM potrebbe tentare di ingannare, ma la funzione convalida
"read_only": False # LLM potrebbe cercare di impostare su False
}))
# Query DB non valida (tentativo di accedere a una tabella non elencata)
print("\n--- Query DB Non Valida (Tabella Non Autorizzata) ---")
print(mock_llm_tool_call("execute_database_query", {
"query": "SELECT * FROM credit_cards;",
"allowed_tables": "products",
"read_only": True
}))
Spiegazione:
- Definizione Schemi Rigorosa: Usa strumenti come Pydantic per definire lo schema di input per ogni funzione. Questo garantisce che gli argomenti generati dall’agente siano conformi ai tipi e valori attesi.
- Valori in Lista Bianca: Per parametri sensibili (come i destinatari email, le tabelle del database), usa tipi
Literalo convalida esplicita per limitare l’agente a un insieme predefinito di valori consentiti. - Permessi Granulari: Progetta strumenti per fare una cosa specifica. Invece di un generico
execute_sql(query), creaget_product_info(product_id)oupdate_user_profile(user_id, new_data)con convalida rigorosa. - Sola Lettura per Default: Per strumenti di database o filesystem, imposta l’accesso di sola lettura come predefinito e richiedi permesso esplicito, approvato da un umano, per le operazioni di scrittura.
- Convalida degli Input: Valida sempre gli argomenti passati alle funzioni del tuo strumento, anche se hanno superato la convalida di Pydantic. L’LLM potrebbe ancora generare input che sembrano validi ma sono malevoli (ad es. una stringa di iniezione SQL che somiglia a un ID prodotto valido).
Best Practices per il Sandboxing degli Agenti
- Principio del Minimo Privilegio: Concedi all’agente il minimo assoluto di permessi e risorse necessarie per il suo compito.
- Sicurezza a Strati: Combina più tecniche di sandboxing (livello linguistico, livello OS, livello strumento) per una protezione solida. Nessun singolo strato è infallibile.
- Ambienti Effimeri: Per l’esecuzione del codice, preferisci eseguire agenti in contenitori o VM di breve durata e usa e getta che vengono distrutti dopo ogni compito.
- Convalida Stratta degli Input: Valida sempre e sanifica qualsiasi input dall’LLM, soprattutto prima di usarlo in chiamate API, query di database o esecuzione di codice.
- Monitoraggio e Logging: Registra tutte le azioni dell’agente, le chiamate agli strumenti e l’uso delle risorse. Questo è cruciale per rilevare comportamenti anomali e per l’analisi post-incidente.
- Timeout e Limiti delle Risorse: Implementa timeout rigorosi per l’esecuzione del codice e le chiamate API, e imposta limiti di CPU/memoria per prevenire attacchi di denial-of-service.
- Isolamento della Rete: Per impostazione predefinita, disabilita l’accesso alla rete per gli agenti. Abilitalo solo per endpoint e protocolli specifici in lista bianca se assolutamente necessario.
- Filesystem di Sola Lettura: Configura gli ambienti degli agenti con filesystem di sola lettura ove possibile per prevenire modifiche ai dati non autorizzate o esfiltrazione.
- Utenti Non Root: Esegui sempre i processi degli agenti come utenti non root con permessi limitati all’interno del sandbox.
- Audit e Aggiornamenti Regolari: Rivedi continuamente le tue configurazioni di sandboxing, aggiorna le tue immagini di base e rimani informato su nuove vulnerabilità di sicurezza.
Conclusione
Il sandboxing degli agenti non è un lusso opzionale, ma un requisito fondamentale per distribuire gli agenti LLM in modo sicuro. Man mano che questi agenti diventano più capaci e autonomi, il potenziale di abuso o danni accidentali cresce in modo significativo. Combinando restrizioni a livello linguistico, una solida containerizzazione e interfacce di strumenti progettate meticulosamente, gli sviluppatori possono creare applicazioni LLM potenti che siano sia nuove che sicure. Gli esempi forniti in questo tutorial dimostrano passi pratici verso la costruzione di questi ambienti sicuri, permettendoti di integrare con fiducia gli agenti LLM nei tuoi sistemi riducendo al minimo i rischi.
🕒 Published: