
Un firewall di egress per carichi di lavoro non attendibili.
I job CI, gli agenti di codifica AI e i container in sandbox possono effettuare richieste outbound arbitrarie. Una dipendenza compromessa, una prompt injection o un passaggio di build malintenzionato possono esfiltrare segreti, fare phone home o aprire una reverse shell. La maggior parte dei team non ha alcuna visibilità su ciò che lascia i propri workload, figurarsi un modo per fermarlo.
iron-proxy è un proxy egress MITM con un server DNS integrato che si pone tra il tuo workload non fidato e internet. Applica il default-deny al confine di rete, così il workload può raggiungere solo i domini che autorizzi esplicitamente. I veri segreti non entrano mai nella sandbox. I workload usano token proxy e iron-proxy li sostituisce con credenziali reali in uscita, il che significa che un workload compromesso può esfiltrare solo un token che fuori dal proxy non vale nulla.
Un singolo binario. Un singolo file YAML di configurazione.
169.254.169.254,
fd00:ec2::254 e fd20:ce::254) e il loopback sono negati per impostazione
predefinita; puoi sovrascrivere tramite proxy.upstream_deny_cidrs o
IRON_PROXY_UPSTREAM_DENY_CIDRS.HTTP_PROXY,
HTTPS_PROXY o impostazioni SOCKS5.SET ROLE sulla sessione
upstream e rifiuta i tentativi del client di modificare il ruolo (SET ROLE,
set_config('role', ...), blocchi DO, ecc.) tramite un'analisi dell'AST SQL.
Si combina con la row-level security di PostgreSQL per garantire l'isolamento
dei dati per tenant quando l'applicazione si connette come utente condiviso
service-account. Richiede che PgBouncer (se usato) sia eseguito in
pool_mode = session — le modalità transaction o statement pool ricollegano
silenziosamente i backend tra le query e vanificherebbero la policy.
Vedi docs.iron.sh per i dettagli.Progettato per pipeline CI, GitHub Actions, agenti AI (Claude Code, Cursor, Codex) e qualsiasi ambiente in cui esegui codice di cui non ti fidi completamente.
Le immagini Docker sono disponibili su Docker Hub e i binari precompilati per Linux/macOS (amd64/arm64) sono su GitHub Releases.
Oppure compila dal sorgente:```bash go build -o iron-proxy ./cmd/iron-proxy
## Avvio rapido```bash
cd examples/docker-compose
docker compose up
Questo avvia iron-proxy e un client demo che invia cinque richieste attraverso il proxy. Controlla i log per vedere le richieste consentite, bloccate e riscritte con segreti:```bash docker compose logs proxy
Ogni richiesta produce una voce di audit JSON strutturata:```json
{
"host": "httpbin.org",
"method": "GET",
"path": "/headers",
"action": "allow",
"status_code": 200,
"duration_ms": 142,
"request_transforms": [
{ "name": "allowlist", "action": "continue" },
{
"name": "secrets",
"action": "continue",
"annotations": { "swapped": [{ "secret": "OPENAI_API_KEY", "locations": ["header:Authorization"] }] }
}
]
}
Le richieste rifiutate includono un campo rejected_by e vengono registrate a livello WARN. Vedi
Formato del log di audit per lo schema completo.
iron-proxy termina TLS generando al volo certificati foglia, firmati da
una CA che fornisci tu. I container client devono considerare attendibile questa CA.```bash
mkdir -p certs
openssl genrsa -out certs/ca.key 4096
openssl req -x509 -new -nodes
-key certs/ca.key
-sha256 -days 3650
-subj "/CN=iron-proxy CA"
-addext "basicConstraints=critical,CA:TRUE"
-addext "keyUsage=critical,keyCertSign"
-out certs/ca.crt
### 2. Crea una rete Docker
iron-proxy necessita di un IP fisso affinché i container possano puntare il loro DNS ad esso:```bash
docker network create --subnet=172.20.0.0/24 iron-proxy
Crea un file env con i tuoi segreti (tienilo fuori dal controllo di versione):```bash echo "OPENAI_API_KEY=sk-real-key" > .env
Il contenuto del blocco da tradurre è vuoto. Non è stato fornito alcun testo di input per questo chunk 15 di 70. Invia il contenuto da tradurre per procedere.```bash
docker run -d --name iron-proxy \
--network iron-proxy --ip 172.20.0.2 \
-v $(pwd)/proxy.yaml:/etc/iron-proxy/proxy.yaml:ro \
-v $(pwd)/certs/ca.crt:/etc/iron-proxy/ca.crt:ro \
-v $(pwd)/certs/ca.key:/etc/iron-proxy/ca.key:ro \
--env-file .env \
ironsh/iron-proxy:latest -config /etc/iron-proxy/proxy.yaml
L'approccio più semplice è il routing basato su DNS: punta il DNS del container su
iron-proxy e tutte le risoluzioni dei nomi host puntano all'IP del proxy, instradando il traffico
attraverso di esso automaticamente:```bash
docker run --rm
--network iron-proxy
--dns 172.20.0.2
-v $(pwd)/certs/ca.crt:/certs/ca.crt:ro
curlimages/curl --cacert /certs/ca.crt https://httpbin.org/get
For stronger enforcement, layer nftables rules to block non-proxy egress, or use
TPROXY for kernel-level interception. Vedi [Instradare il traffico al
proxy](#routing-traffic-to-the-proxy) per i dettagli su ciascun approccio.
## Perché iron-proxy?
| | iron-proxy | Squid | mitmproxy | Envoy |
| ------------------------ | ------------------------------ | --------------------------- | ------------------------- | ---------------------------------- |
| Egress con negazione predefinita | Integrato | Richiede configurazione ACL complessa | Richiede scripting personalizzato | Richiede configurazione RBAC/filtro |
| Iniezione di segreti | Integrato | No | No | No |
| Log di controllo strutturati | Integrato, tracce per trasformazione | Log di accesso di base | Basato su plugin | Log di accesso configurabili |
| Complessità di configurazione | Singolo binario + YAML | Linguaggio di configurazione esteso | Scripting Python | YAML complesso o control plane |
iron-proxy è progettato appositamente per un unico compito: controllare e
verificare l'egress dai workload non affidabili. Squid può fare default-deny ma
richiede una configurazione ACL significativa e non ha alcun concetto di
iniezione di segreti. mitmproxy è un ottimo strumento di debug ma non è
progettato per l'applicazione in produzione. Envoy è un proxy generico che può
essere configurato per fare parte di questo, ma rappresenta molta più
complessità di quanto il problema richieda.```
Container → DNS lookup → iron-proxy IP → TLS termination → transforms → upstream
| Trasformazione | Cosa fa |
|---|---|
allowlist | Consente richieste verso domini/CIDR corrispondenti; rifiuta tutto il resto (403). |
secrets |
iron-proxy accetta un singolo flag: -config path/to/config.yaml. Ecco la
struttura completa (vedi iron-proxy.example.yaml per un
punto di partenza copiabile e incollabile):```yaml
dns:
listen: ":53"
proxy_ip: "10.16.0.1" # IP where iron-proxy is running (required)
passthrough: # Domains forwarded to OS resolver
- "*.internal.corp"
- "metadata.google.internal"
records: # Static DNS records (highest precedence)
- name: "internal.example.com"
type: A
value: "10.0.0.5"
proxy: http_listen: ":80" https_listen: ":443" tunnel_listen: ":8080" # Optional CONNECT/SOCKS5 listener max_request_body_bytes: 1048576 # 1 MiB (default) max_response_body_bytes: 0 # uncapped (default)
tls: ca_cert: "/etc/iron-proxy/ca.crt" # Required ca_key: "/etc/iron-proxy/ca.key" # Required cert_cache_size: 1000 # LRU cache for generated leaf certs leaf_cert_expiry_hours: 72
transforms:
name: allowlist config: domains: - "api.openai.com" - "*.anthropic.com" cidrs: - "10.0.0.0/8"
name: secrets config: secrets: - source: type: env var: OPENAI_API_KEY # Env var holding the real secret proxy_value: "proxy-token-123" # Token the sandbox sends match_headers: ["Authorization"] match_body: false require: true # Reject requests without the proxy token rules: - host: "api.openai.com"
log: level: "info" # debug, info, warn, error
### DNS
Per impostazione predefinita, tutto viene risolto in `proxy_ip`, che è ciò che instrada il traffico attraverso il proxy. Eccezioni:
- **`passthrough`:** modelli glob inoltrati al resolver del sistema operativo (ad es.,
`*.internal.corp`). Il traffico verso questi host bypassa completamente il proxy.
- **`records`:** record A o CNAME statici. Priorità più alta.
### Handler di retry delle risposte
Imposta `IRON_RESPONSE_RETRY_HANDLER_URL`,
`IRON_RESPONSE_RETRY_COMPLETE_URL`, `IRON_RESPONSE_RETRY_HANDLER_TOKEN`,
`IRON_RESPONSE_RETRY_HANDLER_SANDBOX_ID` e un elenco
`IRON_RESPONSE_RETRY_STATUSES` separato da virgole per abilitare i retry delle risposte autorizzati
esternamente.
L'handler di autorizzazione riceve lo schema upstream esatto, l'authority, il metodo,
il path/query, la riproducibilità, lo stato e le intestazioni della risposta, il contesto di trace e
l'identità della sandbox. Può restituire le intestazioni della richiesta più un ID del tentativo per un
singolo replay esatto. L'handler di completamento riceve quindi lo stato del replay e
le intestazioni della risposta selezionate da `IRON_RESPONSE_RETRY_COMPLETION_HEADERS`, che
per impostazione predefinita è `Payment-Receipt`.
I body delle risposte non vengono mai inviati a nessuno dei due handler, le destinazioni non possono cambiare,
e le intestazioni di connessione/framing vengono rifiutate. Le richieste che superano
`proxy.max_request_body_bytes` procedono normalmente ma vengono contrassegnate come non riproducibili;
se contestate, viene restituita la loro risposta originale. Anche i fallimenti degli handler
preservano la risposta originale. Gli URL degli handler devono usare HTTPS a meno che loopback o
`IRON_RESPONSE_RETRY_HANDLER_ALLOW_HTTP=true` sia esplicitamente configurato per una
rete interna affidabile. I reindirizzamenti vengono rifiutati e il token di retry delle risposte
deve essere configurato indipendentemente dal token del piano di controllo. Le richieste WebSocket,
gRPC e streaming a lunghezza sconosciuta bypassano la gestione del retry delle risposte.
Quando un handler attendibile risolve all'interno di `proxy.upstream_deny_cidrs`, imposta
`IRON_RESPONSE_RETRY_HANDLER_ALLOW_CIDRS` su un elenco separato da virgole dei
CIDR privati ristretti che può utilizzare. Questa eccezione si applica solo agli endpoint
authorize e complete esattamente configurati; il traffico proxy ordinario rimane
soggetto all'intera lista deny upstream. Gli intervalli pubblici, loopback, link-local e di metadata cloud
non possono essere aggiunti tramite questa impostazione.
### Allowlist
Negazione predefinita. Le richieste devono corrispondere ad almeno un glob di dominio o CIDR per procedere.
Le richieste non corrispondenti ricevono un `403 Forbidden`.
I pattern di dominio usano il glob matching: `*.example.com` corrisponde a qualsiasi sottodominio e
allo stesso `example.com`.
**Modalità Warn:** imposta `warn: true` per osservare cosa bloccherebbe l'allowlist senza
applicarla effettivamente. Le richieste che verrebbero rifiutate vengono lasciate passare ma
annotate con `"action": "warn"` nel trace di transform. Questo è utile per
distribuire nuove regole allowlist o per svolgere l'audit del traffico esistente prima
di passare all'applicazione.
### Annotate
Cattura le intestazioni delle richieste HTTP nelle annotazioni del log di audit in base a
regole host/metodo/path. È utile per arricchire i log di audit con
contesto specifico della richiesta, come gli ID di richiesta, senza modificare il core del proxy.
Ogni gruppo di annotazioni specifica le regole di corrispondenza e le intestazioni da catturare. Quando una
richiesta corrisponde a qualsiasi regola in un gruppo, i valori delle intestazioni specificati vengono scritti come
voci `header:<Name>` nelle annotazioni del trace di transform. Le richieste che non
corrispondono vengono lasciate passare invariate. Questa trasformazione non rifiuta mai le richieste.
> **Avvertenza:** I valori delle intestazioni vengono emessi in testo semplice nel log di audit.
> Solo registra le intestazioni che sono sicure da esporre, come gli ID di richiesta o le intestazioni contenenti
> token segreti del proxy. Non registrare intestazioni che contengono segreti non elaborati.```yaml
transforms:
- name: annotate
config:
annotations:
- rules:
- host: "api.openai.com"
methods: ["POST"]
paths: ["/v1/*"]
headers: ["x-request-id"]
- rules:
- host: "*.anthropic.com"
headers: ["x-request-id"]
Filtro delle intestazioni di richiesta con negazione implicita (default-deny). Qualsiasi intestazione di richiesta il cui nome canonico non è presente nell'elenco headers configurato viene rimossa prima che la richiesta prosegua verso l'upstream. Utile per bloccare tracciamento, fingerprinting o perdite accidentali di intestazioni (cookie, ID di correlazione interni, X-Forwarded-*, ecc.) che la sandbox potrebbe allegare.
Le voci vengono confrontate senza distinzione tra maiuscole e minuscole rispetto al nome canonico dell'intestazione. I pattern delimitati da /.../ (ad es. /^X-Trace-.*$/) sono espressioni regolari senza distinzione tra maiuscole e minuscole, rispecchiando la sintassi match_headers del transform secrets.
Le rules opzionali limitano l'allowlist a host/metodi/percorsi specifici. Se omesse, l'allowlist si applica a ogni richiesta che raggiunge questo transform.
Quando almeno un'intestazione viene rimossa, la traccia viene annotata con stripped_headers che elenca i nomi rimossi.
Posizionamento: metti
header_allowlistdoposecrets(così le credenziali iniettate non vengono rimosse se non sono nell'allowlist; puoi elencarle) e dopoannotate(così l'annotazione legge le intestazioni originali).```yaml transforms:
### Cattura del corpo
Registra il corpo della richiesta decodificata delle richieste corrispondenti e lo espone nel
record del log di audit in un gruppo `body_capture` contenente `request_body` e
`request_body_truncated`. Utile per verificare i payload che attraversano il
proxy, come i prompt che una sandbox invia a un provider LLM, senza
modificare il traffico a monte.
Host, metodi e percorsi vengono confrontati con la stessa sintassi `rules` di
`allowlist` e `secrets`. `max_request_body_bytes` limita quanto di ogni corpo
viene catturato; i corpi più grandi del limite vengono troncati al prefisso e
`request_body_truncated` viene impostato a `true`. Il limite predefinito è 16 KiB ed è
indipendente dal limite globale `proxy.max_request_body_bytes`. Questa trasformazione
è solo osservativa: non rifiuta mai una richiesta e gli errori di lettura del corpo sono
annotati sulla traccia invece di far fallire la richiesta.
In caso di cattura riuscita, la voce della trasformazione in `request_transforms` viene
annotata con `captured_bytes` e `truncated` così la traccia registra che un
corpo è stato catturato senza duplicare il corpo stesso.
I corpi delle risposte non vengono catturati. Le risposte in streaming (SSE) dovrebbero essere
bufferizzate end-to-end prima dell'inoltro, il che bloccherebbe il client.
> **Avvertenza:** I corpi catturati vengono scritti nel log di audit in testo semplice. Quando
> `secrets` viene eseguito con `match_body: true`, posiziona `body_capture` *prima* di `secrets`
> così il log di audit registra i token proxy della sandbox anziché le credenziali reali
> che `secrets` sostituisce nel corpo.```yaml
transforms:
- name: body_capture
config:
max_request_body_bytes: 16384
rules:
- host: "api.anthropic.com"
methods: ["POST"]
paths: ["/v1/messages"]
- host: "api.openai.com"
methods: ["POST"]
paths: ["/v1/chat/completions"]
La sandbox non conserva mai credenziali reali. Invece:
proxy-openai-abc123).secrets per mappare i proxy token a quelle fonti.iron-proxy analizza le richieste in uscita e sostituisce i proxy token con i valori reali prima di inoltrarle a monte. Puoi controllare dove cerca:
match_headers: elenco dei nomi di header da analizzare. Elenco vuoto = tutti gli header.
I nomi letterali vengono confrontati senza distinzione tra maiuscole e minuscole, ma la capitalizzazione che scrivi viene
preservata quando l'header viene inoltrato a monte. Le voci delimitate da /.../
vengono compilate come espressioni regolari senza distinzione tra maiuscole e minuscole, confrontate con i nomi canonici
degli header (ad es. /^x-.*-key$/).match_body: analizza il corpo della richiesta (bufferizzato fino a max_request_body_bytes).match_query: analizza la query string dell'URL. Il valore predefinito è false; attiva l'opzione per gli
upstream che si aspettano il segreto in un parametro della query. Le query string compaiono spesso
nei log di accesso su entrambi i lati del proxy, quindi questa opzione è disattivata per impostazione predefinita.match_path: analizza il percorso dell'URL. Il valore predefinito è false; attiva l'opzione per upstream
come Telegram che incorporano il segreto nel percorso (ad es.
/bot<TOKEN>/sendMessage). I percorsi URL compaiono spesso nei log di accesso su entrambi
i lati del proxy, quindi questa opzione è disattivata per impostazione predefinita.I parametri della query vengono sempre analizzati.
Fonti del segreto:
env: legge var dall'ambiente del processo proxy. Fissata all'avvio del
processo — usa file se devi ruotare il valore su un proxy in esecuzione.file: legge il segreto da path su disco. Il file viene riletto a ogni
ricaricamento della configurazione (all'avvio e a ogni POST /v1/reload) e, quando ttl è impostato, alla
scadenza della cache — quindi puoi ruotare il segreto di un proxy in esecuzione riscrivendo il
file (atomicamente: scrivi-temp + rinomina) e ricaricando, senza riavviare. Il
valore è il contenuto esatto del file (nessun trimming), quindi chi scrive controlla
gli spazi finali. Sono supportati ttl e failure_ttl opzionali.aws_sm: legge secret_id da AWS Secrets Manager. Sono supportati region,
ttl e opzionali.Ogni fonte accetta anche un json_key opzionale. Quando è impostato, il valore risolto
viene analizzato come oggetto JSON e viene estratto il singolo campo stringa di primo livello a quella chiave.
Usalo per estrarre un campo da un segreto JSON.
ttl controlla per quanto tempo un valore recuperato con successo viene tenuto in cache prima dell'aggiornamento
(se vuoto, lo tiene in cache per sempre). failure_ttl controlla per quanto tempo un errore di recupero viene
tenuto in cache prima di riprovare; il valore predefinito è 1m ed è indipendente da ttl, quindi un
TTL di successo lungo non ritarda il recupero da un'interruzione temporanea del backend.
Nota: un bug in
onepassword-sdk-gointerrompe le build conCGO_ENABLED=0, quindi iron-proxy fissa una fork tramite una direttivareplaceingo.modfinché la correzione non arriva a monte.
La transform giudice chiama un LLM per produrre una decisione di approvazione/rifiuto per
le richieste che corrispondono alle sue regole URL. Ogni voce sotto transforms: è
un'istanza di giudice indipendente con la propria policy in linguaggio naturale, backend LLM,
timeout, semaforo e circuit breaker. Gli operatori possono distribuire zero, una o
molte istanze di giudice con prompt diversi limitati a regole diverse.```yaml
Invarianti:
- Il giudice può solo rifiutare. Non approva mai una richiesta che la allowlist statica
avrebbe negato. La negazione statica vince sempre.
- Le richieste non corrispondenti vengono ignorate: nessuna chiamata LLM, nessuna annotazione di audit.
- In caso di errore LLM, timeout, circuit-breaker aperto o output del modello malformato, si
applica il `fallback` configurato. `deny` blocca la richiesta (il default
consigliato per la produzione). `skip` rimanda al resto della pipeline; poiché
iron-proxy è default-deny, le richieste non corrispondenti vengono comunque bloccate.
Ordinamento della pipeline con il secrets transform:
- **Consigliato:** posizionare il giudice **prima** del secrets transform. Il provider
LLM vede i token del proxy, mai le credenziali reali a cui il workload ha
accesso.
- In alternativa, posizionare il giudice dopo il secrets transform gli consente di valutare l'esatta
forma sul wire che verrà trasmessa in uscita, al costo di inviare credenziali reali al
provider LLM. Scegli questa opzione solo se il tuo modello di minaccia accetta questo compromesso.
Provider supportati:
- **`anthropic`** (API Messages). Usa `api_key_env`, `model`, e opzionalmente
`base_url` e `max_tokens`.
- **`openai`** (API Chat Completions). Stessi campi di cui sopra; imposta
`type: openai`, punta `api_key_env` alla variabile d'ambiente che contiene la tua chiave
OpenAI e scegli un modello come `gpt-5.4-nano`.
Output di audit: ogni richiesta corrispondente aggiunge campi strutturati nella trace del
transform, tra cui `judge.instance`, `judge.decision`, `judge.reason`,
`judge.duration_ms`, `judge.input_tokens`, `judge.output_tokens`,
`judge.fallback_applied` (quando un fallback scatta) e
`judge.circuit_breaker_tripped` (quando il breaker è aperto).
Crediti: grazie a Brex per il loro progetto CrabTrap (con licenza MIT), che
ha informato questo design.
## Policy MCP
iron-proxy può parlare il [trasporto HTTP Streamable di MCP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports). Quando una richiesta corrisponde a un server MCP configurato, il proxy analizza il corpo JSON-RPC, applica una allowlist di strumenti default-deny e filtra le risposte `tools/list` così che gli strumenti negati non raggiungano mai l'agente. Le risposte SSE vengono filtrate per evento, in modo che i flussi MCP di lunga durata restino attivi.
Questa è una capacità proxy di prima classe piuttosto che un transform: le risposte MCP possono essere stream SSE aperti che trasportano messaggi arbitrari avviati dal server, il che non si adatta al contratto request/response dei transform.```yaml
mcp:
# JSON-RPC error envelope returned to the agent on policy denial.
# Defaults: code -32001, message "blocked by iron-proxy policy".
error:
code: -32001
message: "blocked by iron-proxy policy"
servers:
- name: github # appears in audit as mcp.server
rules: # standard host/method/path rules
- host: "mcp.github.com"
paths: ["/mcp", "/mcp/*"]
tools:
- name: "search_repositories" # always allowed
- name: "create_issue"
when: # all clauses must hold; otherwise deny
- path: "owner" # dotted path against arguments
equals: "ironsh"
- path: "repo"
in: ["iron-proxy", "tunis-v2"]
# Anything not listed is denied (default-deny).
Behavior:
tools/call. Le chiamate a strumenti che non sono nell'elenco tools del server, o i cui arguments non superano una qualsiasi clausola when, vengono respinte senza raggiungere l'upstream. Il proxy restituisce una risposta di errore JSON-RPC con codice e messaggio configurati e l'id originale della richiesta, così il client MCP vede un normale errore di protocollo anziché un errore HTTP.tools/list. Nelle risposte a tools/list, qualsiasi strumento non presente nell'allowlist viene rimosso prima di raggiungere l'agente. Funziona sia per le risposte application/json che text/event-stream; il filtraggio SSE opera per singolo evento, quindi gli heartbeat e gli altri messaggi sul flusso passano senza subire modifiche.when ha un path con notazione a punti (es. arguments.repo, ) e uno tra (qualsiasi scalare JSON), (una lista di scalari) o (una regex su valori stringa). Le clausole sono combinate tra loro con AND. Se è omesso, lo strumento è consentito incondizionatamente.Ordine di pipeline: l'intercettore MCP viene eseguito dopo la pipeline di trasformazione, quindi allowlist continua a determinare quali host possono essere raggiunti e secrets ha già sostituito i token del proxy quando l'intercettore valuta il corpo.
mcp_gateway instrada gli host MCP esposti al client verso server upstream concreti dopo che la policy MCP ha accettato la richiesta. Questo consente agli agenti di chiamare host interni stabili mentre iron-proxy inoltra al vero upstream e inietta credenziali che non entrano mai nella sandbox.
Le rotte del gateway si applicano solo alle richieste che corrispondono a un server MCP. La policy MCP applica comunque per prima l'allowlist degli strumenti. Se la policy rifiuta un tools/call, la rotta del gateway non viene applicata e l'upstream non viene raggiunto.```yaml
mcp:
servers:
- name: github
rules:
- host: "github.mcp.local"
paths: ["/mcp", "/mcp/*"]
tools:
- name: "search_repositories"
mcp_gateway: routes: - name: github rules: - host: "github.mcp.local" paths: ["/mcp", "/mcp/*"] upstream: "https://mcp.github.com/v1" credentials: - source: type: env var: GITHUB_MCP_TOKEN inject: header: Authorization formatter: "Bearer {{ .Value }}"
Credentials use the same secret sources as the `secrets` transform. They are required by default. Set `require: false` on a credential to skip it when unavailable. Audit logs record the route, upstream URL, and credential injection locations, but never the injected credential values.
Limitations in v1:
- Only Streamable HTTP transport is supported. The legacy HTTP+SSE transport (separate `/messages` and `/sse` endpoints) is not.
- A JSON-RPC batch with any denied entry is rejected as a whole batch; partial-batch forwarding is not supported.
- Resources and prompts are not enforced. Agents can still call `resources/list`, `resources/read`, etc. without policy filtering.
### Body limits
Transforms that inspect or forward request/response bodies (secrets body
matching, gRPC transforms) operate on buffered bodies. Two global settings
control the maximum buffer sizes:
- **`max_request_body_bytes`** (predefinito: `1048576` / 1 MiB): limita la quantità
del corpo della richiesta bufferizzata per le transform. I dati oltre questo limite vengono
troncati dal punto di vista della transform ma vengono comunque inoltrati all'upstream.
- **`max_response_body_bytes`** (predefinito: `0` / senza limite): limita la quantità
del corpo della risposta bufferizzata. Imposta a `0` per bufferizzare l'intera risposta, che
è l'impostazione predefinita corretta per la maggior parte dei workload (ad es. pacchetti npm, pesi dei modelli).
I corpi vengono bufferizzati incrementalmente man mano che le transform li leggono, e automaticamente
riavvolti tra le fasi della pipeline. Se una transform non legge il corpo, non avviene
alcun buffering e il corpo scorre intatto.
### Tunnel listener (HTTP/CONNECT/SOCKS5)
The tunnel listener accepts absolute-form HTTP proxy requests, HTTP CONNECT,
and SOCKS5 connections on a dedicated port. This is useful for tools that
natively support proxy configuration via `HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY`
environment variables or SOCKS5 settings, rather than relying on DNS-based
routing.
To enable it, set `tunnel_listen` under `proxy`:```yaml
proxy:
tunnel_listen: ":8080"
Quando omesso, il listener del tunnel è disabilitato.
Tutti i protocolli passano attraverso la stessa pipeline di trasformazione delle normali richieste HTTP/HTTPS. Le richieste HTTP in forma assoluta sono gestite dal normale percorso del proxy HTTP. Per CONNECT e SOCKS5, il proxy valuta una richiesta CONNECT sintetica rispetto alla tua allowlist e alle secrets transforms, così le connessioni tunnel sono soggette alla stessa policy di default-deny.
Dopo l'handshake CONNECT o SOCKS5, il proxy osserva il primo byte per rilevare il protocollo interno:
Esempio di HTTP CONNECT:```bash
curl -x http://172.20.0.2:8080
--cacert /certs/ca.crt
https://httpbin.org/get
**Esempio di proxy HTTP semplice:**```bash
curl -x http://172.20.0.2:8080 \
http://httpbin.org/get
Esempio di SOCKS5:```bash
curl --socks5-hostname 172.20.0.2:8080
--cacert /certs/ca.crt
https://httpbin.org/get
Puoi anche impostare le variabili d'ambiente standard così che tutti gli strumenti
vengano instradati automaticamente attraverso il tunnel:```bash
export HTTP_PROXY=http://172.20.0.2:8080
export HTTPS_PROXY=http://172.20.0.2:8080
export ALL_PROXY=socks5h://172.20.0.2:8080
L'implementazione SOCKS5 supporta solo la modalità senza autenticazione e accetta IPv4, IPv6, e tipi di indirizzo a nome di dominio.
iron-proxy genera certificati foglia al volo, firmati dalla CA che fornisci.
Il container client deve considerare attendibile questa CA (aggiungerla al trust store di sistema o passarla
tramite --cacert). I certificati vengono memorizzati in una cache LRU con chiave basata sul nome host SNI.
Esistono tre approcci, con un livello crescente di imposizione.
Punta il DNS del container su iron-proxy. Tutte le risoluzioni puntano all'IP del proxy, quindi il traffico HTTP/HTTPS lo attraversa naturalmente. Questo è ciò che l'esempio Docker Compose utilizza:```yaml services: client: dns: - 172.20.0.2 # iron-proxy IP
Facile da configurare ma facile da bypassare: il workload può impostare IP hardcoded o usare il
proprio resolver DNS per saltare del tutto il proxy.
### DNS + firewall egress nftables (imposto)
Aggiungi un firewall nftables sopra il routing DNS. Il DNS continua a indirizzare il traffico
verso il proxy, ma nftables garantisce che il workload _non_ possa comunicare con nient'altro,
anche con IP hardcoded.
La directory [`examples/nftables`](https://github.com/paradigmxyz/iron-proxy/blob/HEAD/examples/nftables/) contiene una configurazione funzionante.
Il container client carica le regole del firewall all'avvio, prima di eseguire qualsiasi
traffico applicativo:
**nftables.conf** consente il traffico verso il proxy e blocca tutto il resto:```
table ip iron {
chain output {
type filter hook output priority 0; policy drop;
# allow loopback
oif lo accept
# allow traffic to the proxy itself (DNS + HTTP/HTTPS)
ip daddr 172.20.0.2 tcp dport { 80, 443 } accept
ip daddr 172.20.0.2 udp dport 53 accept
# allow established/related (return traffic)
ct state established,related accept
# log and drop everything else
log prefix "iron-proxy-drop: " drop
}
}
docker-compose.yml: l'immagine del client è costruita con nftables
preinstallato. L'entrypoint carica le regole, poi esegue la demo.
CAP_NET_ADMIN è richiesto per caricare le regole:```yaml
services:
proxy:
# ... same as DNS example ...
networks:
demo:
ipv4_address: 172.20.0.2
client: build: context: . dockerfile: Dockerfile.client # alpine + curl + nftables dns: - 172.20.0.2 cap_add: - NET_ADMIN volumes: - ./nftables.conf:/etc/nftables.conf:ro - certs:/certs:ro networks: demo: ipv4_address: 172.20.0.4
In una configurazione di produzione caricherai le regole in un wrapper di entrypoint e poi
`exec` il processo effettivo come utente non root senza `CAP_NET_ADMIN`.
### TPROXY (proxy trasparente)
Per ambienti in cui non puoi controllare affatto il DNS del workload, il TPROXY di nftables
può reindirizzare il traffico a livello di kernel senza alcuna cooperazione da parte del
workload. Questo intercetta i pacchetti nella catena PREROUTING e li consegna
direttamente a iron-proxy:```
table ip iron {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
# redirect HTTP/HTTPS to iron-proxy via TPROXY
tcp dport 80 tproxy to 172.20.0.2:80 meta mark set 1 accept
tcp dport 443 tproxy to 172.20.0.2:443 meta mark set 1 accept
}
chain output {
type route hook output priority mangle; policy accept;
# mark locally-originated packets for policy routing
tcp dport { 80, 443 } meta mark set 1
}
}
Questo richiede la configurazione di ip rule e ip route per instradare i pacchetti marcati verso un socket locale, inoltre iron-proxy deve essere associato con IP_TRANSPARENT. La configurazione è più complessa, ma offre la garanzia più forte che il traffico non possa aggirare il proxy. TPROXY opera al di sotto del DNS, quindi intercetta indirizzi IP hardcoded, resolver personalizzati e qualsiasi altra cosa il carico di lavoro possa tentare.
La directory examples/docker-compose contiene una configurazione funzionante. Gli elementi chiave:
docker-compose.yml: proxy e client su una rete bridge condivisa. I veri segreti sono impostati come variabili d'ambiente solo sul container proxy:```yaml services: proxy: build: context: ../.. dockerfile: examples/docker-compose/Dockerfile environment: - OPENAI_API_KEY=sk-real-openai-key-do-not-share - INTERNAL_TOKEN=real-internal-secret-value volumes: - certs:/certs networks: demo: ipv4_address: 172.20.0.2
client: image: alpine:latest dns: - 172.20.0.2 # Point DNS at the proxy volumes: - certs:/certs:ro networks: demo: ipv4_address: 172.20.0.4
**proxy.yaml** inserisce `httpbin.org` e `icanhazip.com` nell'allowlist, scambia due segreti:```yaml
transforms:
- name: allowlist
config:
domains:
- "httpbin.org"
- "icanhazip.com"
cidrs:
- "172.20.0.0/24"
- name: secrets
config:
secrets:
- source:
type: env
var: OPENAI_API_KEY
replace:
proxy_value: "proxy-openai-abc123"
match_headers: ["Authorization"]
match_query: true # scan the query string
rules:
- host: "httpbin.org"
- source:
type: env
var: INTERNAL_TOKEN
proxy_value: "proxy-internal-tok"
match_headers: [] # scan all headers
rules:
- host: "httpbin.org"
Lo script client invia cinque richieste per dimostrare ciascun comportamento:```bash
curl https://example.com/
curl -H "Authorization: Bearer proxy-openai-abc123" https://httpbin.org/headers
curl -H "X-Internal: proxy-internal-tok" https://httpbin.org/headers
curl "https://httpbin.org/get?token=proxy-openai-abc123&q=hello"
## Formato del log di audit
Ogni richiesta inoltrata tramite proxy produce una voce di log JSON strutturata:```json
{
"host": "httpbin.org",
"method": "GET",
"path": "/headers",
"action": "allow",
"status_code": 200,
"duration_ms": 142,
"request_transforms": [
{
"name": "allowlist",
"action": "continue"
},
{
"name": "secrets",
"action": "continue",
"annotations": {
"swapped": [{ "secret": "OPENAI_API_KEY", "locations": ["header:Authorization"] }]
}
}
],
"response_transforms": []
}
Le richieste rifiutate includono un campo rejected_by e registrano il log a livello WARN.
Gli eventi di audit possono essere esportati come record di log strutturati OpenTelemetry per
l'analisi offline in backend come Axiom, ClickHouse o Logfire. Imposta
OTEL_EXPORTER_OTLP_ENDPOINT per abilitare:```bash
docker run -d --name iron-proxy
-e OTEL_EXPORTER_OTLP_ENDPOINT=https://logfire-us.pydantic.dev
-e OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
-e OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer "
-e OTEL_SERVICE_NAME=iron-proxy
-e OTEL_RESOURCE_ATTRIBUTES="deployment.environment=staging" \
ironsh/iron-proxy:latest -config /etc/iron-proxy/proxy.yaml
Tutta la configurazione usa le variabili d'ambiente OTEL standard:
| Variabile | Descrizione | Predefinito |
| ------------------------------ | ------------------------------------------------------- | ---------------- |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | URL del collector OTLP. L'export OTEL è disattivato se non impostato. | (disattivato) |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` o `grpc`. | `http/protobuf` |
| `OTEL_EXPORTER_OTLP_HEADERS` | Coppie `key=value` separate da virgole per le header di autenticazione. | (nessuno) |
| `OTEL_SERVICE_NAME` | Nome del servizio associato a tutti i record di log. | `iron-proxy` |
| `OTEL_RESOURCE_ATTRIBUTES` | Attributi di risorsa `key=value` separati da virgole. | (nessuno) |
Quando abilitato, ogni evento di audit viene emesso come record di log OTEL insieme ai
log JSON esistenti su stderr. Il record di log trasporta lo stesso schema della voce di
audit JSON: `host`, `method`, `path`, `action`, `status_code`, `duration_ms`,
e gli array completi `request_transforms`/`response_transforms` con annotazioni.
## API di gestione
iron-proxy può opzionalmente esporre un'API HTTP autenticata per attività
operative. Attualmente espone un singolo endpoint, `POST /v1/reload`, che rilegge
la configurazione YAML dal disco e sostituisce atomicamente la pipeline di
trasformazione con una appena costruita. La pipeline in esecuzione viene conservata
se la nuova configurazione non è valida.
Il server di gestione è disabilitato per impostazione predefinita. Per abilitarlo, aggiungi un blocco `management`
alla tua configurazione:```yaml
management:
# Bind on loopback unless you front this with a private network or auth proxy:
# /v1/reload can rebuild the entire transform pipeline.
listen: "127.0.0.1:9092"
# Env var that holds the bearer token. Defaults to IRON_MANAGEMENT_API_KEY.
api_key_env: "IRON_MANAGEMENT_API_KEY"
Solo modalità standalone — incompatibile con la modalità gestita dal control-plane.
Ricarica un proxy in esecuzione:```bash
curl -X POST http://127.0.0.1:9092/v1/reload
-H "Authorization: Bearer $IRON_MANAGEMENT_API_KEY"
## iron.sh
Hai bisogno di backend segreti Vault/KMS, di un operatore Kubernetes o di una gestione centralizzata delle policy? [iron.sh](https://iron.sh) si basa su iron-proxy con funzionalità enterprise per i team che lo eseguono su larga scala.
## Verifica delle firme di rilascio
Gli artefatti di rilascio includono un manifesto checksum firmato:
- `checksums.txt`
- `checksums.txt.asc` (firma distaccata in formato ASCII-armored)
Usa la chiave pubblica inclusa in [`public-key.asc`](https://github.com/paradigmxyz/iron-proxy/blob/HEAD/public-key.asc) per verificare:```bash
# 1) Download release artifacts for a tag
TAG=vX.Y.Z
gh release download "$TAG" --pattern "checksums.txt" --pattern "checksums.txt.asc"
# 2) Import the project signing key
gpg --import public-key.asc
# 3) Verify the signature over checksums.txt
gpg --verify checksums.txt.asc checksums.txt
Se la verifica riesce, GPG riporterà una firma valida da Matthew Slipper <[email protected]>.
Puoi facoltativamente ispezionare l'impronta della chiave importata e confermare che corrisponda alla tua fonte attendibile prima della verifica.
Per verificare un binario specifico rispetto all'elenco di checksum firmati (esempio: iron-proxy-linux-amd64):```bash
shasum -a 256 iron-proxy-linux-amd64 | grep -F "$(grep -F 'iron-proxy-linux-amd64' checksums.txt | awk '{print $1}')"
| Esegue la scansione delle intestazioni (e opzionalmente di query, percorso o body) per individuare token proxy e sostituisce con segreti reali provenienti da variabili d'ambiente. |
body_capture | Registra i body delle richieste decodificati degli host corrispondenti come campi di audit request_body. Solo osservazione; non rifiuta mai. |
require: quando è true, le richieste a un host corrispondente che non contengono
il proxy token vengono respinte con 403. Questo impedisce a un workload compromesso
di aggirare il meccanismo di scambio dei segreti con credenziali alternative. Predefinito: false.hosts: limita lo scambio a domini o CIDR specifici.failure_ttlaws_ssm: legge name da AWS Systems Manager Parameter Store. Sono supportati
region, with_decryption, ttl e failure_ttl opzionali.
with_decryption è impostato su true per impostazione predefinita, che è l'impostazione prevista per
i parametri SecureString.1password: risolve secret_ref (un riferimento op://vault/item/[section/]field)
utilizzando un token dell'account di servizio 1Password. Il token viene letto da
OP_SERVICE_ACCOUNT_TOKEN. Sono supportati ttl e failure_ttl opzionali.1password_connect: risolve lo stesso op://vault/item/[section/]field
secret_ref su un server 1Password Connect self-hosted. L'URL del server
viene letto da OP_CONNECT_HOST e il token API da OP_CONNECT_TOKEN.
Sono supportati ttl e failure_ttl opzionali.labels.0equalsinmatcheswhenmcp nella voce di audit log: nome del server, direzione (request o response), metodo, strumento, decisione (allow, deny o filtered), motivo per i rifiuti e numero di strumenti rimossi negli eventi di filtro.