Le hook fonctionne quand vous l’exécutez à la main en root et renvoie Operation not permitted à l’instant où le service le démarre. Le fichier n’a pas changé entre ces exécutions ; l’environnement, si. systemd remet au processus un monde plus petit : moins de capacités, un /devdifférent, des chemins de système de fichiers en lecture seule et un plafond de tâches. Lisez l’échec comme une frontière qui a refusé une opération précise, pas comme un script qui a cassé.

Partez de l’opération qui a été refusée

Une ligne de journal qui dit que l’unité a échoué ne vous apprend presque rien. La ligne utile nomme l’appel qui a été refusé : un nœud de périphérique qui n’a pas pu être ouvert, un répertoire dans lequel on n’a pas pu écrire, un espace de noms qui n’a pas pu être créé. Trouvez cet appel avant de toucher à une seule directive, car chacune correspond à une restriction différente. Lisez ensuite l’unité qui a réellement tourné, drop-ins compris, plutôt que le fichier livré par le paquet.

La requête ci-dessous affiche le chemin du fragment, les répertoires de drop-in et les propriétés qui comptent pour un refus de périphérique ou de système de fichiers. Lisez-les toutes avant de décider quelle frontière est pertinente :

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'

Rien dans ce bloc n’écrit. Si l’appel refusé manque au journal, augmentez la verbosité du client pour une exécution au lieu de deviner des directives. Un processus qui tourne prouve que l’unité a démarré, pas que le hook a tourné avec les privilèges dont il avait besoin.

Un espace de noms a besoin de droits de montage qu’un périphérique TUN n’utilise jamais

Deux opérations qui se ressemblent dans un même hook ont des profils de permissions différents. Ouvrir /dev/net/tun et créer l’interface demande l’administration réseau plus l’accès à ce nœud. Créer un espace de noms réseau nommé demande aussi du travail de montage, parce que l’outil d’espaces de noms monte une /sys vue neuve et lie un répertoire d’état à sa place. L’ unité client d’origine laisse délibérément l’administration du montage hors de son ensemble de limites, donc l’étape d’espace de noms est refusée alors que le tunnel lui-même aurait fonctionné.

C’est pourquoi accorder le nœud de périphérique ne sauve pas le script. Une autorisation de périphérique répond à une question, si ce processus peut ouvrir ce nœud, et la question de l’espace de noms en est une autre. Faites correspondre l’appel qui échoue à sa capacité avant d’élargir quoi que ce soit.

Un test dans votre shell hérite de toutes les capacités d’un root sans restriction, et l’unité hérite de ce que son ensemble de limites a conservé. Les deux exécutions sont des expériences différentes même quand le binaire est identique.

Périphériques, chemins protégés et plafonds de tâches

Une fois l’opération refusée connue, trois groupes de directives expliquent la plupart des échecs. L’accès aux périphériques est régi par PrivateDevices, qui remplace /dev par un ensemble minimal de pseudo-périphériques, et par DevicePolicy avec DeviceAllow, qui décident de ce que le cgroup permet. /dev/net/tun ne fait pas partie de cet ensemble minimal, donc une unité avec PrivateDevices=yes ne peut pas l’ouvrir, quoi que dise la politique de périphériques.

La protection du système de fichiers répond à une deuxième question. ProtectHome=yes rend /home, /root et /run/user inaccessibles, et ProtectSystem=full rend /usr, /boot et /etc en lecture seule. Un hook qui écrit à côté de son propre fichier de configuration fonctionne donc dans votre shell et échoue sous l’unité, avec la même forme de permission qu’une capacité absente. Déplacez l’entrée inscriptible vers un chemin que la politique autorise, ou ajoutez une entrée ReadWritePaths= étroite.

Les limites de ressources sont le troisième groupe, et le moins évident. TasksMax= plafonne le nombre de tâches que l’unité peut créer, et un plafond de descripteurs de fichiers se comporte de la même façon pour les sockets et les handles. Un déploiement qui a dépassé le plafond échoue avec une erreur de ressource plutôt qu’une erreur de permission, et c’est pourquoi le message exact compte encore.

Sortez l’étape d’espace de noms du hook

La préparation qui demande un privilège que le client ne doit pas détenir appartient à sa propre unité. Un oneshot qui tourne avant le client, conserve son résultat et est ordonné avant la connexion donne à l’étape d’espace de noms ce dont elle a besoin sans remettre ces droits au processus du tunnel. Le client référence ensuite cette unité au lieu d’appeler le script depuis son hook up.

Le modèle ci-dessous est une forme de départ, pas quelque chose à appliquer sans le lire. Il garde la préparation là où vit le privilège et laisse l’unité du tunnel intacte :

# /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.target

Ajoutez l’ordre avec un drop-in plutôt qu’en modifiant l’unité empaquetée, afin qu’une mise à jour ne puisse pas écarter votre changement en silence. Le répertoire est indexé sur le nom de l’instance, donc un second profil a besoin de son propre fichier :

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-reload

Répétez le même appel après le changement

Une réparation n’est confirmée que lorsque le même appel produit un résultat différent. Redémarrez les deux unités, exécutez l’opération qui a échoué et comparez son résultat à la ligne de journal que vous avez conservée avant le changement. Une unité qui démarre alors que le hook ne tourne plus jamais n’est pas une réparation.

La séquence ci-dessous garde la comparaison honnête, parce qu’elle vérifie l’appel refusé et non la couleur de l’unité. Enregistrez sa sortie à côté de la ligne de journal que vous avez capturée avant le changement :

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 tun0

Comptez les refus au lieu de lire la dernière ligne, car un hook peut échouer, réessayer et réussir, et seul le compte montre si la frontière a bougé. Laissez un commentaire dans le drop-in nommant la directive que vous avez assouplie et la raison, puisque la mise à jour suivante compare votre configuration à la valeur par défaut du paquet. Une unité qui ne démarre qu’une fois toutes les protections désactivées n’est pas réparée.

Articles similaires