Los hooks de conexión de un demonio VPN se ejecutan como el demonio, y normalmente eso significa que se ejecutan como root. En el momento en que uno de esos hooks escribe en el directorio personal de alguien o reinicia un asistente de escritorio, has dado a un evento de red la capacidad de actuar como un usuario local arbitrario. El atajo habitual, leer un nombre de usuario de una variable de entorno y pasarlo, no arregla eso. Solo reetiqueta la acción sin cambiar qué cuenta la ejecuta. Lo que funciona es un traspaso en tres partes donde el paso privilegiado dice qué ocurrió y una unidad separada hace la acción con la identidad correcta.

Un nombre de usuario en el entorno no es un conmutador de credenciales

Unix decide a qué puede tocar un proceso por su ID de usuario efectivo, no por las cadenas de su entorno. Establecer USER=alice o exportar LOGNAME cambia lo que imprime un software descuidado en su salida; no cambia nada en las comprobaciones de propiedad de archivos. Un hook que se ejecuta como root con esa variable establecida sigue abriendo los archivos de Alice como root.

Los nombres tampoco son el mismo nombre. El inicio de sesión de acceso remoto, la cuenta Unix local y el nombre común del certificado son tres identificadores que a menudo coinciden y a veces no. Un usuario que se autentica como a.smith puede no tener ninguna cuenta local o puede mapearse a asmith mediante un servicio de directorio. Cualquier diseño que asuma que una cadena sirve para los tres papeles se romperá con el primer contratista.

Así que la primera pregunta no es «qué usuario está conectado» sino «qué cuenta local tiene permiso para realizar esta acción». Responde eso desde tu propia base de datos de cuentas, no desde lo que haya enviado el cliente:

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

El tercer comando es el que la gente se salta. Si el archivo objetivo es propiedad de root porque el hook anterior lo creó, una acción de usuario bien configurada fallará con un error de permisos que parece un problema de systemd y no lo es.

El demonio se queda donde está

Cambiar el User= propio del servicio VPN a Alice es la palanca equivocada. Altera cómo el demonio enlaza interfaces, lee claves, escribe su archivo de estado y gestiona rutas, y traslada todo ese riesgo a una unidad que funcionaba. El demonio necesita sus privilegios; la acción posterior no.

Mira qué hace realmente la unidad antes de tocarla. Dos comandos te dicen la identidad de ejecución y las líneas de comando exactas en vigor, incluido todo lo que haya añadido una anulación:

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

En una instalación de paquete predeterminada el demonio es root y el script --up hereda eso. Confírmalo en lugar de asumirlo, porque una distribución endurecida puede haberlo movido a una cuenta dedicada, y un drop-in escrito a mano puede ejecutar el hook a través de un envoltorio con reglas distintas.

Tres partes con nombre, un disparador estrecho

El arreglo que sobrevive al mantenimiento tiene un evento, un disparador y una acción. El demonio informa de que ha subido una sesión con nombre. Un disparador acepta ese pequeño hecho y pide una acción fija. La acción se ejecuta bajo la cuenta local que posee los archivos que toca. Nada en la cadena acepta una ruta de comando arbitraria.

La acción en sí es una unidad de servicio ordinaria, y la identidad es una sola directiva dentro de ella:

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

Fíjate en que User=alice se aplica a toda la unidad, y systemd documenta una excepción que conviene conocer: un comando con el prefijo + se ejecuta con todos los privilegios independientemente de ello. Ese prefijo es una herramienta legítima para un único paso privilegiado y una forma cómoda de deshacer la separación que acabas de construir, así que trata cualquier + en esa unidad como algo que hay que justificar en la revisión.

Mantén la entrada del disparador mínima y no secreta. El identificador de sesión, la cuenta mapeada y una marca de tiempo suelen ser todo lo necesario. Nunca pases un fragmento de shell, una ruta de ejecutable o un bloque de entorno proporcionado por el cliente a través de la frontera, porque el lado receptor no tiene forma de distinguir un valor legítimo de uno inyectado.

Verifica la identidad que realmente se ejecutó

La revisión de la configuración no es evidencia. Tras una conexión de prueba aprobada, confirma el UID que tocó el archivo y el UID que el demonio sigue 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 primera línea debería mostrar a Alice y una marca de tiempo dentro de tu ventana de prueba. La segunda debería seguir mostrando root o la cuenta de servicio del paquete, lo que demuestra que el demonio no se movió. La tercera es la forma más barata de probar los permisos de la acción sin esperar una reconexión real.

Si el propietario del archivo es Alice pero su grupo o modo cambió entre ejecuciones, busca un UMask= o una diferencia de permisos del directorio en lugar de un error de lógica. Si la marca de tiempo no se movió en absoluto, el disparador nunca se disparó, y el siguiente paso útil es journalctl -u para la unidad de acción en lugar de otra reconexión del cliente.

Qué sigue sin demostrar un evento de conexión

Un túnel establecido es un solo hecho. No significa que la sesión de escritorio esté desbloqueada, que el bus de sesión sea accesible o que el recurso privado que quiere la acción ya esté disponible. Una unidad oneshot bajo el UID de Alice tiene su identidad y nada de su sesión, así que cualquier cosa que requiera un bus gráfico tiene que pasar por el mecanismo de sesión de usuario soportado y definir qué pasa cuando ella cierra la sesión.

Las reconexiones también se repiten. Un portátil que duerme en un tren puede producir una docena de eventos de conexión en una hora, y un hook que añade una línea cada vez convierte un archivo de estado en un registro que nadie puede leer. Haz la acción idempotente, o deja que sustituya un único registro con clave por sesión.

Por último, acota el radio de daño en la propia unidad: una vista del sistema de archivos de solo lectura fuera del directorio de estado, sin privilegios nuevos y con un tiempo de espera explícito. La separación entre demonio y acción de usuario merece la pena por sí sola, pero vale mucho más cuando la acción no puede hacer nada excepto lo único que aprobaste.

Artículos relacionados