El hook funciona cuando lo ejecutas a mano como root y devuelve Operation not permitted en el momento en que el servicio lo arranca. El archivo no cambió entre esas ejecuciones; el entorno sí. systemd entrega al proceso un mundo más pequeño: menos capacidades, un /devdistinto, rutas del sistema de archivos de solo lectura y un techo de tareas. Lee el fallo como una frontera que rechazó una operación concreta, no como un script que se rompió.

Empieza por la operación que se rechazó

Una línea de registro que dice que la unidad falló no te dice casi nada. La línea útil nombra la llamada que se rechazó: un nodo de dispositivo que no se pudo abrir, un directorio en el que no se pudo escribir, un espacio de nombres que no se pudo crear. Encuentra esa llamada antes de tocar una sola directiva, porque cada una corresponde a una restricción distinta. Luego lee la unidad que realmente se ejecutó, con los drop-in incluidos, en lugar del archivo que envió el paquete.

La consulta de abajo imprime la ruta del fragmento, los directorios de drop-in y las propiedades que importan para un rechazo de dispositivo o de sistema de archivos. Léelas todas antes de decidir qué frontera es relevante:

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'

Nada de ese bloque escribe. Si la llamada rechazada falta en el registro, sube la verbosidad del cliente para una ejecución en lugar de adivinar directivas. Un proceso en marcha demuestra que la unidad arrancó, no que el hook se ejecutó con los privilegios que necesitaba.

Un espacio de nombres necesita derechos de montaje que un dispositivo TUN nunca usa

Dos operaciones que parecen iguales dentro de un mismo hook tienen perfiles de permisos distintos. Abrir /dev/net/tun y crear la interfaz necesita administración de red más acceso a ese nodo. Crear un espacio de nombres de red con nombre también necesita trabajo de montaje, porque la herramienta de espacios de nombres monta una /sys vista nueva y enlaza un directorio de estado en su sitio. unidad de cliente original deja deliberadamente la administración de montaje fuera de su conjunto de límites, así que el paso del espacio de nombres se rechaza mientras el túnel mismo habría funcionado.

Por eso conceder el nodo de dispositivo no rescata el script. Un permiso de dispositivo responde una pregunta, si este proceso puede abrir este nodo, y la pregunta del espacio de nombres es otra. Empareja la llamada que falla con su capacidad antes de ampliar nada.

Una prueba en tu shell hereda todas las capacidades que tiene un root sin restricciones, y la unidad hereda lo que conservó su conjunto de límites. Las dos ejecuciones son experimentos distintos aunque el binario sea idéntico.

Dispositivos, rutas protegidas y techos de tareas

Una vez conocida la operación rechazada, tres grupos de directivas explican la mayoría de los fallos. El acceso a dispositivos lo gobierna PrivateDevices, que sustituye /dev por un conjunto mínimo de pseudo-dispositivos, y DevicePolicy con DeviceAllow, que deciden qué permite el cgroup. /dev/net/tun no forma parte de ese conjunto mínimo, así que una unidad con PrivateDevices=yes no puede abrirlo diga lo que diga la política de dispositivos.

La protección del sistema de archivos responde una segunda pregunta. ProtectHome=yes hace /home, /root y /run/user inaccesibles, y ProtectSystem=full hace /usr, /boot y /etc de solo lectura. Por eso un hook que escribe junto a su propio archivo de configuración funciona en tu shell y falla bajo la unidad, con la misma forma de permiso que una capacidad ausente. Mueve la entrada escribible a una ruta que la política permita, o añade una entrada ReadWritePaths= estrecha.

Los límites de recursos son el tercer grupo, y el menos evidente. TasksMax= limita cuántas tareas puede crear la unidad, y un techo de descriptores de archivo se comporta igual para sockets y manejadores. Un despliegue que ha crecido más allá del techo falla con un error de recursos en lugar de un error de permisos, y por eso el mensaje exacto sigue importando.

Saca el paso del espacio de nombres del hook

La preparación que necesita un privilegio que el cliente no debe tener pertenece a su propia unidad. Un oneshot que se ejecuta antes del cliente, conserva su resultado y se ordena antes de la conexión da al paso del espacio de nombres lo que necesita sin entregar esos derechos al proceso del túnel. Luego el cliente hace referencia a esa unidad en lugar de llamar al script desde su hook up.

La plantilla de abajo es una forma inicial, no algo que se aplique sin leer. Mantiene la preparación donde vive el privilegio y deja intacta la unidad del túnel:

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

Añade el orden con un drop-in en lugar de editar la unidad empaquetada, para que una actualización no pueda descartar tu cambio en silencio. El directorio va ligado al nombre de la instancia, así que un segundo perfil necesita su propio archivo:

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

Repite la misma invocación después del cambio

Una reparación solo se confirma cuando la misma invocación produce un resultado distinto. Reinicia ambas unidades, ejecuta la operación que falló y compara su resultado con la línea de registro que guardaste antes del cambio. Una unidad que arranca mientras el hook no vuelve a ejecutarse no es una reparación.

La secuencia de abajo mantiene la comparación honesta, porque comprueba la llamada rechazada y no el color de la unidad. Guarda su salida junto a la línea de registro que capturaste antes del cambio:

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

Cuenta los rechazos en lugar de leer la última línea, porque un hook puede fallar, reintentar y tener éxito, y solo el recuento muestra si la frontera se movió. Deja un comentario en el drop-in que nombre la directiva que relajaste y el motivo, ya que la siguiente actualización compara tu configuración con la predeterminada del paquete. Una unidad que solo arranca cuando todas las protecciones están desactivadas no está arreglada.

Artículos relacionados