Gli hook di connessione di un demone VPN vengono eseguiti come il demone, e di solito questo significa che vengono eseguiti come root. Nel momento in cui uno di questi hook scrive nella home di qualcuno o riavvia un assistente del desktop, hai dato a un evento di rete la capacità di agire come un utente locale arbitrario. La scorciatoia abituale, leggere un nome utente da una variabile d’ambiente e passarlo avanti, non risolve questo. Si limita a rietichettare l’azione senza cambiare quale account la esegue. Ciò che funziona è un passaggio in tre parti in cui il passo privilegiato dice cos’è successo e un’unità separata compie l’azione con l’identità corretta.

Un nome utente nell’ambiente non è un interruttore di identità

Unix decide a cosa può accedere un processo in base al suo ID utente effettivo, non alle stringhe del suo ambiente. Impostare USER=alice o esportare LOGNAME cambia ciò che un software distratto stampa nel proprio output; non cambia nulla nei controlli di proprietà dei file. Un hook eseguito come root con quella variabile impostata apre comunque i file di Alice come root.

Neanche i nomi sono lo stesso nome. L’accesso remoto, l’account Unix locale e il nome comune del certificato sono tre identificatori che spesso coincidono e a volte no. Un utente che si autentica come a.smith può non avere alcun account locale, oppure essere mappato su asmith tramite un servizio di directory. Qualsiasi progetto che presuma che una sola stringa serva tutti e tre i ruoli si romperà al primo collaboratore esterno.

Quindi la prima domanda non è «quale utente è connesso» ma «quale account locale è autorizzato a compiere questa azione». Rispondi da un tuo database di account, non da ciò che ha inviato il client:

id -u alice
getent passwd alice | cut -d: -f1,3,6
stat -c '%U:%G %a %n' /home/alice/.local/state/vpn-status.json

Il terzo comando è quello che la gente salta. Se il file di destinazione appartiene a root perché l’hook precedente l’ha creato, un’azione utente configurata correttamente fallirà con un errore di permessi che sembra un problema di systemd e non lo è.

Il demone resta dove si trova

Cambiare l’User= proprio del servizio VPN in Alice è la leva sbagliata. Modifica il modo in cui il demone associa le interfacce, legge le chiavi, scrive il file di stato e gestisce le rotte, e sposta tutto questo rischio in un’unità che funzionava. Il demone ha bisogno dei suoi privilegi; l’azione successiva no.

Guarda cosa fa realmente l’unità prima di toccarla. Due comandi ti dicono l’identità di esecuzione e le righe di comando esatte in vigore, incluso tutto ciò che un override ha aggiunto:

systemctl show -p User -p Group -p ExecStart openvpn-server@server
systemctl cat openvpn-server@server

In un’installazione da pacchetto predefinita il demone è root e lo script --up eredita questo. Confermalo invece di presumerlo, perché una distribuzione indurita potrebbe averlo spostato su un account dedicato, e un drop-in scritto a mano può eseguire l’hook tramite un wrapper con regole diverse.

Tre parti nominate, un trigger stretto

La disposizione che sopravvive alla manutenzione ha un evento, un trigger e un’azione. Il demone segnala che è salita una sessione con nome. Un trigger accetta quel piccolo fatto e chiede una sola azione fissa. L’azione viene eseguita sotto l’account locale che possiede i file che tocca. Nulla nella catena accetta un percorso di comando arbitrario.

L’azione stessa è un’unità di servizio ordinaria, e l’identità è una sola direttiva al suo interno:

[Service]
Type=oneshot
User=alice
Group=alice
ExecStart=/usr/local/bin/refresh-vpn-status --slot %i

Nota che User=alice vale per l’intera unità, e systemd documenta un’eccezione che vale la pena conoscere: un comando con il prefisso + viene eseguito con tutti i privilegi a prescindere. Quel prefisso è uno strumento legittimo per un singolo passo privilegiato e un modo comodo di annullare la separazione appena costruita, quindi tratta qualsiasi + in quell’unità come qualcosa da giustificare in revisione.

Mantieni minimale e non segreto l’input del trigger. L’identificatore di sessione, l’account mappato e un timestamp sono di solito tutto ciò che serve. Non far mai passare oltre il confine un frammento di shell, un percorso di eseguibile o un blocco di ambiente fornito dal client, perché il lato ricevente non ha modo di distinguere un valore legittimo da uno iniettato.

Verifica l’identità che è realmente stata eseguita

La revisione della configurazione non è una prova. Dopo una connessione di test approvata, conferma l’UID che ha toccato il file e l’UID che il demone sta ancora usando:

stat -c '%U %G %y %n' /home/alice/.local/state/vpn-status.json
ps -o user=,pid=,cmd= -p "$(pgrep -o -f 'openvpn --daemon')"
systemd-run --uid=alice --pipe /usr/local/bin/refresh-vpn-status --dry-run

La prima riga dovrebbe mostrare Alice e un timestamp dentro la tua finestra di test. La seconda dovrebbe mostrare ancora root o l’account di servizio del pacchetto, il che prova che il demone non si è spostato. La terza è il modo più economico di testare i permessi dell’azione senza attendere una vera riconnessione.

Se il proprietario del file è Alice ma il suo gruppo o la sua modalità sono cambiati tra un’esecuzione e l’altra, cerca un UMask= o una differenza di permessi della directory invece di un errore di logica. Se il timestamp non si è mosso affatto, il trigger non è mai scattato, e il passo successivo utile è journalctl -u per l’unità di azione invece di un’altra riconnessione del client.

Cosa non prova ancora un evento di connessione

Un tunnel stabilito è un solo fatto. Non significa che la sessione desktop sia sbloccata, che il bus di sessione sia raggiungibile o che la risorsa privata che l’azione vuole sia già disponibile. Un’unità oneshot sotto l’UID di Alice ha la sua identità e nulla della sua sessione, quindi tutto ciò che richiede un bus grafico deve passare per il meccanismo di sessione utente supportato e definire cosa succede quando lei è disconnessa.

Anche le riconnessioni si ripetono. Un portatile che dorme su un treno può produrre una dozzina di eventi di connessione in un’ora, e un hook che aggiunge una riga ogni volta trasforma un file di stato in un registro che nessuno può leggere. Rendi l’azione idempotente, oppure lascia che sostituisca un singolo record con chiave per sessione.

Infine, limita il raggio del danno nell’unità stessa: una vista del filesystem in sola lettura fuori dalla directory di stato, nessun nuovo privilegio e un timeout esplicito. La separazione tra demone e azione utente vale di per sé, ma vale molto di più quando l’azione non può fare altro che l’unica cosa che hai approvato.

Articoli correlati