Come scoprire quale sandbox di systemd ha rifiutato il tuo hook di OpenVPN
L'hook funziona come root e fallisce sotto systemd perché l'unità conserva meno capacità, un /dev più piccolo e percorsi in sola lettura.
L'hook funziona quando lo esegui a mano come root e restituisce Operation not permitted nel momento in cui il servizio lo avvia. Il file non è cambiato tra queste esecuzioni; l'ambiente sì. systemd consegna al processo un mondo più piccolo: meno capacità, un /dev diverso, percorsi del file system in sola lettura e un tetto di attività. Leggi il guasto come un confine che ha rifiutato una singola operazione, non come uno script che si è rotto.
Parti dall'operazione che è stata rifiutata
Una riga di log che dice che l'unità è fallita non ti dice quasi nulla. La riga utile nomina la chiamata che è stata rifiutata: un nodo di dispositivo che non si è potuto aprire, una directory in cui non si è potuto scrivere, uno spazio dei nomi che non si è potuto creare. Trova quella chiamata prima di toccare una sola direttiva, perché ciascuna corrisponde a una restrizione diversa. Poi leggi l'unità che è realmente partita, drop-in compresi, invece del file fornito dal pacchetto.
La query qui sotto stampa il percorso del frammento, le directory di drop-in e le proprietà che contano per un rifiuto di dispositivo o di file system. Leggile tutte prima di decidere quale confine è rilevante:
systemctl show "[email protected]" -p FragmentPath -p DropInPaths -p PrivateDevices -p DevicePolicy -p DeviceAllow -p ProtectHome -p ProtectSystem -p TasksMax
journalctl -u "[email protected]" -b --no-pager | grep -iE 'operation not permitted|permission denied|cannot open|failed'Niente in quel blocco scrive. Se la chiamata rifiutata manca dal journal, alza la verbosità del client per una singola esecuzione invece di indovinare direttive. Un processo in esecuzione dimostra che l'unità è partita, non che l'hook abbia girato con i privilegi che gli servivano.
Uno spazio dei nomi richiede diritti di mount che un dispositivo TUN non usa mai
Due operazioni che sembrano uguali dentro lo stesso hook hanno profili di permessi diversi. Aprire /dev/net/tun e creare l'interfaccia richiede l'amministrazione di rete più l'accesso a quel nodo. Creare uno spazio dei nomi di rete con nome richiede anche lavoro di mount, perché lo strumento degli spazi dei nomi monta una /sys vista nuova e lega una directory di stato al suo posto. L' unità client originale lascia deliberatamente l'amministrazione del mount fuori dal suo insieme di limiti, perciò il passo dello spazio dei nomi viene rifiutato mentre il tunnel stesso avrebbe funzionato.
Ecco perché concedere il nodo di dispositivo non salva lo script. Un permesso di dispositivo risponde a una domanda, se questo processo può aprire questo nodo, e la domanda dello spazio dei nomi è un'altra. Fai corrispondere la chiamata che fallisce alla sua capacità prima di allargare qualcosa.
Un test nella tua shell eredita ogni capacità di un root senza restrizioni, e l'unità eredita ciò che il suo insieme di limiti ha conservato. Le due esecuzioni sono esperimenti diversi anche quando il binario è identico.
Dispositivi, percorsi protetti e tetti delle attività
Una volta nota l'operazione rifiutata, tre gruppi di direttive spiegano la maggior parte dei guasti. L'accesso ai dispositivi è regolato da PrivateDevices, che sostituisce /dev con un insieme minimo di pseudo-dispositivi, e da DevicePolicy con DeviceAllow, che decidono che cosa permette il cgroup. /dev/net/tun non fa parte di quell'insieme minimo, quindi un'unità con PrivateDevices=yes non può aprirlo, qualunque cosa dica la politica dei dispositivi.
La protezione del file system risponde a una seconda domanda. ProtectHome=yes rende /home, /root e /run/user inaccessibili, e ProtectSystem=full rende /usr, /boot e /etc in sola lettura. Perciò un hook che scrive accanto al proprio file di configurazione funziona nella tua shell e fallisce sotto l'unità, con la stessa forma di permesso di una capacità assente. Sposta l'input scrivibile su un percorso che la politica permette, o aggiungi una voce ReadWritePaths= stretta.
I limiti delle risorse sono il terzo gruppo, e il meno evidente. TasksMax= limita quante attività l'unità può creare, e un tetto di descrittori di file si comporta allo stesso modo per socket e handle. Un deployment cresciuto oltre il tetto fallisce con un errore di risorsa invece che con un errore di permesso, ed è per questo che il messaggio esatto conta ancora.
Porta il passo dello spazio dei nomi fuori dall'hook
La preparazione che richiede un privilegio che il client non deve avere appartiene a un'unità propria. Un oneshot che gira prima del client, conserva il suo risultato ed è ordinato prima della connessione dà al passo dello spazio dei nomi ciò che gli serve senza consegnare quei diritti al processo del tunnel. Il client poi fa riferimento a quell'unità invece di chiamare lo script dal proprio hook up.
Il modello qui sotto è una forma di partenza, non qualcosa da applicare senza leggerlo. Tiene la preparazione dove vive il privilegio e lascia intatta l'unità del tunnel:
# /etc/systemd/system/[email protected]
[Unit]
Description=Namespace preparation for %i
Before=openvpn-client@%i.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/netns-up.sh %i
CapabilityBoundingSet=CAP_NET_ADMIN CAP_SYS_ADMIN
[Install]
WantedBy=multi-user.targetAggiungi l'ordinamento con un drop-in invece di modificare l'unità impacchettata, così un aggiornamento non può scartare la tua modifica in silenzio. La directory è legata al nome dell'istanza, quindi un secondo profilo ha bisogno del proprio file:
sudo mkdir -p /etc/systemd/system/[email protected]
sudo tee /etc/systemd/system/[email protected]/10-namespace.conf >/dev/null <<'CONF'
[Unit]
[email protected]
[email protected]
CONF
sudo systemctl daemon-reloadRipeti la stessa invocazione dopo la modifica
Una riparazione è confermata solo quando la stessa invocazione produce un risultato diverso. Riavvia entrambe le unità, esegui l'operazione che è fallita e confronta il suo esito con la riga di log che hai salvato prima della modifica. Un'unità che parte mentre l'hook non gira più non è una riparazione.
La sequenza qui sotto tiene onesto il confronto, perché controlla la chiamata rifiutata e non il colore dell'unità. Salva il suo output accanto alla riga di log che hai catturato prima della modifica:
sudo systemctl restart "[email protected]" "[email protected]"
systemctl status "[email protected]" --no-pager
journalctl -u "[email protected]" -b --no-pager | grep -c 'Operation not permitted'
ip -brief addr show tun0Conta i rifiuti invece di leggere l'ultima riga, perché un hook può fallire, riprovare e riuscire, e solo il conteggio mostra se il confine si è spostato. Lascia un commento nel drop-in che nomina la direttiva che hai allentato e il motivo, dato che l'aggiornamento successivo confronta la tua configurazione con il valore predefinito del pacchetto. Un'unità che parte solo dopo aver disattivato ogni protezione non è riparata.