OpenVPN funziona in una shell ma fallisce sotto cron: è il PATH
Un comando che si connette da una shell può fallire sotto cron o subprocess prima che il tunnel parta. Correggi il percorso, la directory di lavoro e il terminale.
Lo stesso comando si connette da una shell e muore sotto cron, quindi la conclusione naturale è che il tunnel o il certificato abbiano colpa. Guarda cosa hanno in comune le due esecuzioni prima di crederci: il binario, la configurazione e le credenziali sono identici, ed è cambiato solo l’ambiente del processo. Quando il programma non viene nemmeno trovato, nessuna quantità di debug TLS aiuta. La correzione di solito sta in un percorso, una directory di lavoro e una manciata di variabili d’ambiente, non nella VPN.
command -v openvpn nella shell che funziona stampa la risposta, ed è molto spesso /usr/sbin/openvpn, una directory che cron non mette nel suo percorso di ricerca.
Il tunnel non è mai partito, quindi il tunnel non è il problema
I job avviati da cron ereditano un ambiente minimo, convenzionalmente PATH=/usr/bin:/bin, e nessun terminale di controllo. Tutto ciò che è installato in /usr/sbin o /usr/local/sbin è semplicemente assente da quell’elenco, e la riga di log legge openvpn: not found invece di un errore TLS. Il subprocess di Python fallisce diversamente: eredita l’ambiente del processo dell’interprete, che può essere più ricco di quello di cron ma non ha ancora una shell che espanda una tilde. Entrambi i fallimenti avvengono prima che un solo pacchetto lasci l’host.
Un modo rapido per distinguerli è leggere il primo errore invece dell’ultimo. Un binario mancante produce un’eccezione della shell o di Python, una configurazione sbagliata produce un messaggio d’uso di OpenVPN, e un vero fallimento di stretta di mano produce timestamp e indirizzi dei peer. Solo il terzo è un problema di tunnel.
I percorsi assoluti e le liste di argomenti battono le stringhe di shell
Passa il percorso risolto in modo esplicito e salta del tutto la shell. subprocess.run(["/usr/sbin/openvpn", "--config", config, "--verb", "4"], capture_output=True, text=True, timeout=90) ti dà un codice di ritorno, stdout e stderr senza chiedere a una shell di interpretare un nome di file. Costruisci il percorso della configurazione da os.environ["HOME"] o da pathlib.Path.home(), così una tilde non deve mai essere espansa. Quando ti serve davvero la sintassi della shell, usa shlex.split, perché dividere una stringa con .split() si rompe su qualsiasi percorso che contenga uno spazio. Leggi anche stderr, dato che OpenVPN scrive lì la maggior parte dei suoi fallimenti precoci.
La stessa regola vale per cron: la riga del job non è una shell di login, quindi nulla carica il tuo profilo e nulla definisce i tuoi alias.
Che cosa eredita davvero un crontab
Una voce di crontab riceve l’ambiente che il crontab dichiara più alcuni valori predefiniti ereditati, quindi scrivi il percorso per esteso o avvolgi la chiamata in uno script che lo faccia. Una riga come PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin in cima al file copre le ubicazioni di installazione abituali, e il reindirizzamento con >> cron.log 2>&1 è ciò che rende leggibile il fallimento successivo. Un profilo protetto da passphrase non ha modo di chiedere il proprio segreto sotto cron, dato che non esiste un terminale su cui chiedere. Una connessione del genere appartiene a un’unità systemd che possa leggere un file di credenziali, non a un job che finge di essere interattivo.
I percorsi relativi sono la seconda trappola. Una prova avviene spesso dalla cartella che contiene il profilo, e un job di cron parte altrove, quindi --config office.ovpn si risolve in un file che non c’è e il messaggio si legge come file mancante più che come tunnel mancante.
Dimostralo togliendo l’ambiente
Riproduci l’ambiente minimo direttamente: env -i PATH=/usr/bin:/bin /usr/sbin/openvpn --config office.ovpn --verb 4. Una lamentela su percorso o terminale da quell’esecuzione significa che il problema sta sopra la VPN. Se si connette, i sospetti rimasti sono l’account con cui gira, la directory da cui parte e la credenziale che presenta. Qualunque dei due risultati è meglio di un altro giro di controllo dei certificati.
Poi rendi il wrapper auto-diagnosticante. Esci con uno stato distinto per ogni fase raggiunta, registra entrambi i flussi con un timestamp, e il fallimento successivo ti dirà se è morto nella risoluzione del percorso, nell’autenticazione o dopo che il tunnel era già su.
Un’unità ha bisogno anche dell’ordine giusto: Wants=network-online.target più After=network-online.target impediscono al client di partire prima che esista un indirizzo o una rotta predefinita, cosa che altrimenti si manifesta come errori di certificato o TLS che spariscono al riavvio manuale.