
13 dicembre 2025 - Infrastructure
Un anno fa abbiamo scritto di Patela v1, il nostro sistema di orchestrazione per relay Tor diskless, progettato per nodi di uscita che utilizzano stboot di System Transparency. L’idea di base era semplice: i nodi avviano da immagini in sola lettura, generano autonomamente le proprie chiavi, cifrano le chiavi localmente e ne effettuano il backup sul server. In questo post presentiamo un’architettura aggiornata che si basa sull’attestazione del TPM dei nodi ed elimina la necessità di effettuare backup.
Nella v1, l’identità dei nodi era basata sui digest dei certificati mTLS. I certificati client venivano incorporati a compile time tramite uno script, e l’hash SHA-256 di ciascun certificato diventava l’identità del nodo nel database.
// V1: Identity = hash(certificate)
let node_id = sha256(client_cert);
Il flusso di autenticazione era il seguente: il client presentava un certificato, il server lo validava rispetto alla propria CA, e successivamente utilizzava il digest del certificato come chiave nel database. Tuttavia, questa soluzione era sia piu’ complessa del necessario sia distante dall’obiettivo finale; l’abbiamo usata come scorciatoia per avviare le prime fasi di test. L’obiettivo reale, invece, era avere le chiavi direttamente all’interno del TPM, in modo che fossero archiviate nel chip e non venissero mai sottoposte a backup, rimanendo vincolate all’hardware, senza essere vincolate o meno alla presenza di dischi.
La v2 sostituisce questo approccio rendendo il TPM la fonte di verità:
// V2: Identity = (EK_public, AK_public, AK_name)
let node_identity = (ek_public, ak_public, ak_name);
Il funzionamento è il seguente:
Il server memorizza questa terna nel database:
CREATE UNIQUE INDEX idx_nodes_tpm_identity
ON nodes(ek_public, ak_public, ak_name);
A questo punto, l’autenticazione verso il server di configurazione richiede il possesso fisico di quello specifico chip TPM.
Al momento non validiamo i certificati EK rispetto alle CA dei produttori di TPM: quando un nodo si connette per la prima volta, il server non dispone di una prova crittografica che l’EK provenga da hardware reale piuttosto che da un emulatore software. È nel nostro elenco di cose da fare, ma per il nostro caso d’uso non aggiunge di per sé molte garanzie.
Utilizziamo quindi un approccio TOFU: i nuovi nodi vengono creati con enabled=0. Un amministratore dovrebbe sapere quando un nuovo nodo è effettivamente previsto, e può semplicemente eseguire patela node enable <node_id> prima che il nodo possa autenticarsi. Questo impedisce a dispositivi arbitrari di unirsi automaticamente alla rete, mantenendo comunque in gran parte automatizzato il flusso operativo.
Il flusso era il seguente:
Questo approccio consentiva ai relay Tor diskless di mantenere un’identità persistente. I relay Tor costruiscono la propria reputazione e vengono considerati affidabili dalla rete sulla base di alcuni parametri, ma soprattutto in funzione della loro stabilità e del tempo di attività. Conservare chiavi a lungo termine è quindi fondamentale affinché i nostri relay siano effettivamente utili. La differenza, ora, è che le chiavi del relay risiedono esclusivamente nello storage persistente del TPM, eliminando la necessità di effettuare backup. In definitiva, eventuali problemi hardware dovrebbero essere sufficientemente rari da rendere sensato questo tipo di scelta.
Poiché non abbiamo ancora integrato completamente Tor con il TPM, la chiave è attualmente memorizzata come stringa di byte nello storage non volatile, il che significa che è ancora possibile esportarla. Dal momento che lo standard TPM non supporta operazioni su Ed25519, è improbabile che questa situazione cambi nel breve termine, anche se riconosciamo che sia una soluzione subottimale.
Nella V1, ogni relay riceveva lo stesso template torrc, costruito tramite interpolazione di stringhe:
// V1: One template to rule them all
let torrc = format!(r#"
Nickname {name}
ORPort {ip}:{or_port}
DirPort {dir_port}
ContactInfo your@email.com
ExitPolicy reject *:*
...
"#, name = relay.name, ip = relay.ip_v4, ...);
Questo approccio ha funzionato fino a un certo punto, ma la personalizzazione per singolo relay risultava difficile da gestire: alcuni nodi necessitavano regole ExitPolicy personalizzate, altri limiti di banda diversi in base all’upstream, e in casi più rari porte personalizzate. Ogni modifica alla configurazione comportava cambiamenti al codice, ricompilazione e redeployment.
Con la V2 introduciamo invece una configurazione a cascata:
│┌────────────────────────┐ O ││ Default Config │ v │└───────────┬────────────┘ e │ │ r │┌───────────▼────────────┐ r ││ Per-machine Config │ i │└───────────┬────────────┘ d │ │ e │┌───────────▼────────────┐ ││ Per-instance Config │ ▼└────────────────────────┘
Questo modello è rappresentanto direttamente nello schema del database:
-- Default globali per tutti i relay
CREATE TABLE global_conf (
id INTEGER PRIMARY KEY,
tor_conf TEXT, -- formato torrc
node_conf TEXT -- JSON per le impostazioni di rete
);
-- Override per singolo nodo
ALTER TABLE nodes ADD COLUMN tor_conf TEXT;
ALTER TABLE nodes ADD COLUMN node_conf TEXT;
-- Override per singolo relay
ALTER TABLE relays ADD COLUMN tor_conf TEXT;
Quando un client si avvia, il server risolve la gerarchia di configurazione:
// Pseudo-codice per la risoluzione della configurazione
let config = global_conf
.merge(node_conf) // Il nodo sovrascrive il globale
.merge(relay_conf); // Il relay sovrascrive tutto
Abbiamo scritto un parser per files torrc (server/src/tor_config.rs) per gestire il formato di configurazione di Tor. Il parser valida le opzioni rispetto a quelle note di Tor e unisce le configurazioni in modo intelligente, facendo sì che i valori successivi sovrascrivano quelli precedenti.
Il flusso di lavoro ora è il seguente:
# Impostare i default globali (una tantum)
patela torrc import misc/default.torrc default
# Sovrascrivere ContactInfo per i nodi in cantina
patela torrc import basement.torrc node --id 3
echo "ContactInfo basement@example.com" >> basement.torrc
# Assegnare più banda a un relay specifico
patela torrc import high-bandwidth.torrc relay --id murazzano
echo "RelayBandwidthRate 100 MB" >> high-bandwidth.torrc
Le modifiche alla configurazione diventano quindi semplici aggiornamenti del database, poiché i nodi scaricano dal server, all’avvio, la configurazione più recente a loro associata.
Il protocollo utilizza il meccanismo di challenge–response make_credential / activate_credential di TPM2:
EK_public, AK_public, AK_nameencrypted_session_token = make_credential(EK_public, AK_name, session_token)session_token = activate_credential(encrypted_session_token)Il session_token è un bearer token Biscuit. Solo il TPM con l’EK corrispondente può decifrarlo tramite activate_credential. Se il client restituisce correttamente il token decifrato, il server ottiene una prova crittografica che il client possiede quello specifico hardware TPM.
Abbiamo due obiettivi finali, che richiederanno ancora un po’ di tempo per lo sviluppo e una comprensione più approfondita di diverse tecnologie. Il primo consiste nel fare il seal dei segreti e dello storage del TPM tramite measured boot, completando l’integrazione con System Transparency e coreboot. È un obiettivo ambizioso, ma idealmente il sistema finale dovrebbe funzionare nel modo seguente:
Questo renderebbe il sistema resistente a compromissioni fisiche: anche nel caso in cui il server venisse sequestrato fisicamente, o venisse avviato con un firmware o un bootloader differenti, il TPM non effettuerebbe l’unsealing, rendendo le chiavi irrecuperabili in qualsiasi configurazione alternativa.
Nella nostra lista di attività future ci sono anche miglioramenti più semplici dal punto di vista dell’usabilità: ad esempio, mostrare correttamente agli operatori le differenze di configurazione tra i livelli globale / nodo / istanza prima di applicarle.
Il codice è disponibile su GitHub: osservatorionessuno/patela
Per avviare un ambiente di sviluppo v2:
# Setup del server
export DATABASE_URL="sqlite:$PWD/patela.db"
cargo sqlx database reset --source server/migrations -y
# Impostare la configurazione richiesta
cargo run -p patela-server -- torrc import misc/default.torrc default
cargo run -p patela-server -- node set ipv4_gateway 10.10.10.1 default
cargo run -p patela-server -- node set ipv6_gateway fd00::1 default
# Avviare il server
cargo run -p patela-server -- run
# Setup del client (richiede TPM2 o un emulatore swtpm)
export TPM2TOOLS_TCTI="swtpm:host=localhost,port=2321"
cargo run -p patela-client -- run --server https://localhost:8020
# Approvare il nodo (dal terminale del server)
cargo run -p patela-server -- list node
cargo run -p patela-server -- node enable 1
Il codice è ancora relativamente piccolo, circa 6000 righe di Rust complessive tra client e server; è leggibile (grazie all’uso obbligatorio di cargo fmt e clippy) e parzialmente documentato. Il lavoro è tuttora in corso: come anticipato, continueremo a esplorare e iterare finché la nostra configurazione non sarà solida quanto desideriamo.
Questo progetto non sarebbe stato possibile senza:
Bug, domande, patch: github.com/osservatorionessuno/patela/issues
Hai letto un articolo della sezione Infrastruttura, dove raccontiamo il nostro impegno, sia materiale che digitale, per un’infrastruttura costruita come atto politico di riappropriazione delle risorse digitali.
Siamo un’organizzazione no-profit gestita interamente da volontari. Se apprezzi il nostro lavoro, puoi aiutarci con una donazione: accettiamo contributi economici, ma anche materiale e banda per sostenere le nostre attività. Per sapere come supportarci, visita la pagina delle donazioni.





