Come tenere i secret fuori dagli agenti AI
Una API key nell'ambiente può essere esfiltrata in una sola chiamata. Usa token con ambito limitato e breve durata tramite un credential gateway, mai la chiave reale.
Cosa significa tenere i secret fuori dagli agenti AI
Un agente AI è un normale processo Linux che esegue comandi. Il codice eseguito dal processo può leggere tutte le variabili d'ambiente disponibili. Di conseguenza, una API key presente nell'ambiente dell'agente può essere inviata dall'agente a qualsiasi host raggiungibile. Tenere i secret fuori dall'agente significa fornirgli un riferimento invece della chiave: un token con ambito limitato e validità breve, oppure un segnaposto che un altro componente sostituisce con il valore reale al confine della rete.
Non si tratta di un modello che diventa ostile. Il meccanismo è più semplice. Un agente legge una pagina Web, un README o un commento a una issue che contiene istruzioni e le segue, perché per un modello linguistico non c'è differenza tra il testo scritto dall'utente e quello recuperato dal modello. Questo è il prompt injection. Dopo l'iniezione, l'entità del danno dipende da un solo elemento: ciò che il processo può leggere. Se non hai ancora definito un confine, eseguire in sicurezza un agente di coding su un server descrive i livelli di isolamento su cui si basa questa guida.
Il modello di minaccia in termini semplici
Esegui questo comando con lo stesso utente utilizzato dall'agent.
tr '\0' '\n' < /proc/self/environ | grep -iE 'key|token|secret|password'Ogni riga che stampa equivale a una richiesta HTTP verso il server di uno sconosciuto. Ora verifica cosa si trova sul disco nelle vicinanze dell'agent.
grep -rIl --exclude-dir=.git -e 'API_KEY' -e 'SECRET' ~/projects
find ~ -maxdepth 3 -name '.env' -o -name 'credentials' -o -name '*.pem'Un agent con accesso a una shell non ha bisogno di un exploit sofisticato per trasferire quei dati all'esterno. Quattro percorsi comuni sono sufficienti e, nei log, tutti e quattro sembrano normali attività:
- Un
curlofetchin uscita verso qualsiasi host, con il valore nella stringa di query. - Un
git commitegit pushverso un repository su cui l'agent può scrivere. - Uno script di installazione di un pacchetto, che esegue codice arbitrario con l'utente dell'agent.
- Una ricerca DNS di un hostname che contiene il valore; la richiesta esce anche quando il traffico HTTP in uscita è bloccato.
Non puoi risolvere il problema limitandoti a esaminare il codice. La soluzione consiste nell'assicurarsi che l'agent non possa raggiungere dati di valore.
Un segreto nell’albero di lavoro è un segreto nella finestra di contesto
Un agente legge i file. Un file .env presente nel repository su cui sta lavorando verrà letto e, una volta letto, entrerà nella finestra di contesto. Questo significa che sarà presente nella trascrizione, in qualsiasi log conservato e in tutto ciò che l’agente scriverà in seguito.
Prima, con la chiave presente nell’albero su cui lavora l’agente:
cd ~/projects/billing
cat .env
# STRIPE_SECRET_KEY=sk_live_...
# DATABASE_URL=postgres://app:hunter2@db.internal:5432/billingDopo, con il file spostato fuori dalla sua portata:
sudo install -d -m 750 -o root -g agent-review /etc/agent-review
sudo install -m 640 -o root -g agent-review ~/projects/billing/.env /etc/agent-review/billing.env
rm ~/projects/billing/.envL’utente dell’agente non può più aprire il file, perché l’albero di lavoro non lo contiene più. Le regole di deny nella configurazione dell’agente costituiscono un secondo livello, non il primo. Claude Code legge le regole di autorizzazione da .claude/settings.json nel progetto:
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
}
}Questo impedisce all’agente di aprire per errore un file durante l’esplorazione e si applica anche oltre gli strumenti di gestione dei file: Claude Code verifica queste regole anche rispetto ai comandi sui file che riconosce in Bash, come cat e head, e rispetto alle destinazioni dei reindirizzamenti come < .env. Non impedisce a un’istruzione iniettata di eseguire grep -r KEY . nella directory del progetto, né a un breve script Python di aprire direttamente .env, perché la documentazione sui permessi specifica che le regole di deny non si applicano ai comandi che leggono file senza nominarli o ai subprocess che aprono autonomamente i file. Per questi casi, la documentazione indica il sandbox di Claude Code, che applica il blocco a livello del sistema operativo. La richiesta di conferma prima dell’esecuzione di un comando di questo tipo dipende dalla modalità di autorizzazione della sessione e la modalità automatica è ora la modalità iniziale integrata per le sessioni interattive di Claude Code, quindi un server che non state monitorando eseguirà più comandi di questo tipo senza chiedere conferma. Lo stesso limite vale per tutto ciò che influenza le abitudini dell’agente invece dei suoi permessi: una skill che impone all’agente di applicare la modifica minima necessaria impedisce a un’esecuzione di esplorare file che non avrebbe dovuto aprire, ma resta comunque un’indicazione che il modello può essere indotto a ignorare. Considerate la configurazione una barriera di sicurezza e i permessi del filesystem la barriera fisica. La stessa separazione si applica all’interno dei container: i file env e i secret in Docker Compose trattano la versione di questo problema che si verifica a un livello inferiore.
Assegna a ogni agent un proprio utente non privilegiato
Se l’agent viene eseguito con il tuo account, eredita le tue chiavi SSH, le tue credenziali cloud e la cronologia della shell. Un utente separato richiede un solo comando e impedisce tutto questo.
sudo adduser --disabled-password --gecos "" agent-review
sudo chmod 700 /home/agent-review
sudo -u agent-review cat ~/.ssh/id_ed25519L’ultima riga deve fallire con cat: /home/you/.ssh/id_ed25519: Permission denied. Se invece stampa una chiave, l’utente dell’agent può raggiungere il file. Per aprire un file specificandone il nome serve il permesso di ricerca (x) su ogni directory del percorso, non il permesso di lettura. Di conseguenza, una home directory con modalità 711, che gli altri utenti non possono elencare, consente comunque di raggiungere la chiave se il file della chiave è leggibile. chmod 700 ~ chiude il percorso e chmod 600 ~/.ssh/id_ed25519 corregge i permessi della chiave. Non aggiungere l’utente dell’agent a sudo e non assegnargli una regola NOPASSWD più ampia dell’unico comando realmente necessario. Utenti con privilegi minimi su un VPS descrive nel dettaglio i gruppi e sudoers. Mantieni questa separazione quando esegui più di una sessione sul server, perché una sessione di Claude Code può inviare testo direttamente a un’altra e qualunque dato disponibile nella prima sessione può attraversare quel canale in un solo messaggio.
Su un cloud VPS è opportuno aggiungere un ulteriore limite. Il servizio di metadati dell’istanza risponde su un indirizzo link-local fisso e spesso fornisce credenziali del ruolo a chiunque le richieda. L’endpoint non verifica quale utente locale effettua la chiamata: su EC2, AWS documenta che le chiamate IMDSv1 non richiedono un token e indica una regola del firewall locale basata sull’ID dell’utente come metodo per impedire a un singolo utente di accedere al servizio.
sudo iptables -I OUTPUT 1 -p tcp -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECTInserisci la regola all’inizio con -I invece di aggiungerla in coda con -A. Su un host che esegue ufw con la policy predefinita di autorizzazione delle connessioni in uscita, l’ultima regola della catena OUTPUT di ufw accetta ogni nuova connessione TCP. Una regola aggiunta in coda non viene quindi mai raggiunta. Le regole iptables scompaiono inoltre al riavvio se non vengono salvate in modo persistente. Se hai abilitato l’endpoint IPv6 del servizio di metadati, [fd00:ec2::254], aggiungi la stessa regola con ip6tables.
Verifica il risultato dal lato dell’agent. sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ non dovrebbe stampare nulla e dovrebbe terminare con un codice diverso da zero, perché il pacchetto viene rifiutato prima di uscire dal server. Se stampa un elenco, la regola non viene raggiunta.
Iniettare la credenziale al confine
Il modello che risolve effettivamente questo problema è l'iniezione delle credenziali. L'agent non conserva mai una chiave reale. Invia la richiesta tramite un gateway locale, che sostituisce un segnaposto con il secret reale durante l'inoltro. Il secret risiede nello storage del gateway, in un processo separato, eseguito da un utente diverso.
OneCLI è un'implementazione open source di questo modello. È distribuito con licenza Apache-2.0, ad eccezione delle directory ee/, soggette a una licenza enterprise separata che richiede un abbonamento per l'uso in produzione. Viene eseguito in container affiancati all'agent. A ottobre 2026 il progetto documenta questa configurazione Docker Compose, che legge tre secret generati da un file .env accanto al file Compose:
git clone https://github.com/onecli/onecli.git && cd onecli/docker
cat > .env <<EOF
SECRET_ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
GATEWAY_INTERNAL_SECRET=$(head -c 32 /dev/urandom | base64)
BETTER_AUTH_SECRET=$(head -c 32 /dev/urandom | base64)
COMPOSE_PROFILES=runner
EOF
chmod 600 .env
docker compose up -d --waitEsegui il backup del file .env, perché la perdita di SECRET_ENCRYPTION_KEY rende inaccessibili tutti i secret memorizzati al suo interno. La dashboard è in ascolto sulla porta 10254 e il gateway sulla porta 10255; per impostazione predefinita, entrambi sono associati a 127.0.0.1. Memorizzi la credenziale reale una sola volta, quindi assegni a ogni agent un valore segnaposto al posto della chiave e un proprio token di accesso con ambito limitato, che l'agent invia in un header Proxy-Authorization. Il gateway identifica la richiesta in uscita tramite host e percorso, decritta la credenziale corrispondente e la sostituisce. Nell'ambiente dell'agent non è presente nulla che valga la pena sottrarre.
Il valore di questo approccio non risiede nella crittografia. Risiede nel fatto che la domanda «quale credenziale ha usato questo agent e quando?» diventa una query nei log. È sufficiente consultare un unico audit trail invece di cercare di capire in quale delle sei configurazioni fosse presente una copia della chiave.
Passare il secret al processo, non all’ambiente
Se esegui l’agent tramite systemd, non hai bisogno di variabili d’ambiente. LoadCredential= inserisce il secret in una directory privata leggibile soltanto dall’utente del servizio, esposta come %d nel file dell’unità e come $CREDENTIALS_DIRECTORY all’interno del processo. Questa restrizione si applica all’utente, non al servizio. Assegna quindi a ogni servizio agent un utente dedicato: qualsiasi altro processo eseguito dallo stesso utente può aprire la directory, a meno che l’unità non utilizzi anche il namespace del filesystem, ad esempio PrivateMounts=yes, che la nasconde agli altri servizi. Il valore non compare mai in /proc/<pid>/environ, quindi ps eww non può mostrarlo, e la directory scompare quando il servizio si arresta.
Cifra prima la credenziale per la macchina. Questi comandi provengono dalla documentazione di systemd e funzionano con systemd 250 o versioni successive, comprese Ubuntu 24.04 e Debian 13:
echo -n 'sk-example-value' > /tmp/plain.txt
sudo systemd-creds encrypt --name=api_key /tmp/plain.txt /etc/credstore/api_key.cred
shred -u /tmp/plain.txt
sudo systemd-run -P --wait -p LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred \
systemd-creds cat api_keyL’ultimo comando stampa sk-example-value. Questo dimostra che il file cifrato viene decifrato correttamente su questo host. Fai quindi riferimento al file dall’unità:
[Service]
User=agent-review
LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred
Environment=AGENT_KEY_FILE=%d/api_key
ExecStart=/usr/local/bin/agent-workerIl codice dell’agent apre il file in $AGENT_KEY_FILE quando deve leggere il valore. La lettura di un file è momentanea. Una variabile d’ambiente resta disponibile per tutta la durata del processo e per ogni processo figlio che questo avvia.
Preferisci i token a breve durata alle chiavi a lunga durata
Una chiave che non scade resta valida ogni volta che riemerge, mesi dopo, in un log o in una trascrizione. Se il servizio offre un token di sessione, usa il token di sessione e imposta la durata più breve compatibile con l'attività.
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/agent-readonly \
--role-session-name agent-review \
--duration-seconds 900Quindici minuti è il valore minimo accettato da AWS STS (security token service) e di solito è sufficiente per un'attività dell'agente. Per GitHub, assegna all'utente dell'agente un proprio accesso gh con un token a granularità fine limitato al singolo repository su cui opera, in modo che gh auth token durante quella sessione restituisca qualcosa che non possa accedere ad altro. Limita prima per risorsa, poi per durata.
Verificare, quindi continuare a verificare
Dopo qualsiasi modifica alla configurazione di un agent conviene eseguire tre controlli. Eseguili come utente dell'agent, non come il tuo utente.
sudo -u agent-review env | grep -iE 'key|token|secret'
sudo -u agent-review ls -la /home/you/ 2>&1 | head -3
sudo -u agent-review curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 https://api.github.com/userIl primo non deve produrre alcun output. Il secondo deve stampare ls: cannot open directory '/home/you/': Permission denied, ma questo dimostra soltanto che la directory non può essere elencata; è il test cat eseguito in precedenza a dimostrare che non è possibile leggere un file al suo interno. Il terzo indica quale identità presenta il percorso di rete dell'agent. È proprio la domanda a cui risponde il pattern del gateway: 401 significa che la richiesta ha raggiunto GitHub senza una credenziale accettata da GitHub, perché non ne è stata fornita alcuna oppure perché quella fornita non è valida o è scaduta; 200 significa invece che la richiesta contiene una credenziale valida. Devi quindi sapere quale token viene utilizzato. Se esegui agent senza supervisione, controllare i costi degli agent AI su un VPS tratta i limiti di budget da associare a questi limiti di accesso.
FAQ
Posso fidarmi del modello e basta, senza che divulghi le mie chiavi?
No, perché in questo modello di minaccia il modello non è l'attaccante. L'agent legge testo da pagine web, repository e sistemi di gestione delle issue, e quel testo può contenere istruzioni. Il modello non ha un modo affidabile per distinguere le tue istruzioni dal testo che ha recuperato. Qualsiasi controllo che dipenda dalla scelta corretta del modello fallisce non appena un'istruzione iniettata risulta convincente. Il controllo deve quindi risiedere nel sistema operativo o nella rete.
Le variabili d'ambiente sono davvero così rischiose per i secret dell'agent?
Sono rischiose per un motivo specifico: vengono ereditate. Ogni processo figlio avviato dall'agent ne riceve una copia, inclusi uno script di build, un test runner e qualsiasi hook di installazione di un package. Le variabili sono inoltre leggibili tramite /proc/<pid>/environ dallo stesso utente. Di conseguenza, qualsiasi processo avviato dall'agent può leggerle senza che sia l'agent a passarle esplicitamente. Un file letto al momento dell'uso, tramite LoadCredential= o un gateway, limita l'esposizione a quel momento.
Inserire i secret in un vault risolve il problema da solo?
Solo in parte. Un vault risolve il problema dello storage. Se gestisci autonomamente il vault, devi applicare un proprio processo di hardening, perché un server Vaultwarden viene solitamente violato tramite il token amministrativo o il file di backup, non tramite gli elementi cifrati che contiene. Il vault non risolve l'ultimo passaggio: qualcosa estrae il secret dal vault e lo passa all'agent come variabile d'ambiente, riportandoti al punto di partenza. Conta chi esegue la sostituzione. Se recupera il secret l'agent, allora il secret è nelle mani dell'agent. Se la sostituzione viene eseguita da un gateway o dal sistema init al di fuori del processo dell'agent, l'agent non entra mai in possesso del secret.
Come posso sapere se un agent ha già divulgato qualcosa?
Di solito non puoi determinarlo a posteriori. È proprio questo il motivo per usare un gateway. Senza gateway, le evidenze sono distribuite tra la cronologia della shell, il transcript dell'agent e i log delle connessioni in uscita, che probabilmente non stai conservando. Con un credential gateway, ogni utilizzo di una credenziale è registrato su una riga con l'identità dell'agent e un timestamp. Se sospetti una divulgazione, ruota prima la chiave e indaga dopo. La rotazione è economica; la certezza non lo è.
Qual è il minimo che dovrei fare oggi?
Sposta ogni file .env fuori dalle directory in cui lavorano gli agent e crea un utente non privilegiato per ogni agent. Queste due modifiche richiedono circa dieci minuti e chiudono il percorso più comune: un agent legge un file di credenziali che non aveva motivo di trovarsi accanto al codice. Il gateway e i token a breve durata sono il passo successivo, non il primo. Lo stesso punto di partenza vale per qualsiasi runtime di agent, incluso eseguire un agent autonomo in sicurezza su un VPS.