Ponga un cliente VPN en un contenedor y obtendrá un espacio de nombres con un túnel dentro, no un host que enruta todo su tráfico por una VPN. Son proyectos separados, y tratarlos como uno solo es la forma en que los contenedores acaban privilegiados por accidente. Este manual solo cubre lo primero: una imagen revisada, un perfil protegido y un proceso de cliente confinado a su propio espacio de nombres de red.

Empiece por un artefacto que alguien ya haya aprobado. Los pasos siguientes suponen una imagen que contiene el ejecutable openvpn y un perfil de cliente completo con su certificado y material de clave ya resueltos dentro del montaje. No construyen software, no emiten credenciales ni cubren entornos sin root ni contenedores nativos de Windows, cada uno de los cuales cambia la respuesta.

Qué recibe el contenedor y qué no

Mantenga el perfil y todo lo que referencia en un directorio local con permisos restringidos, montado como solo lectura. Las rutas relativas dentro del perfil deben resolverse dentro de ese montaje; es un detalle que la gente pasa por alto cuando un certificado está un directorio por encima del perfil y el contenedor nunca lo ve.

services:
  vpn:
    image: ${VPN_CLIENT_IMAGE:?set the reviewed image reference}
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    volumes:
      - ./vpn:/vpn:ro
    working_dir: /vpn
    entrypoint: ["openvpn"]
    command: ["--config", "/vpn/client.ovpn"]

Ese archivo concede tres cosas y no pide nada más. La referencia de la imagen debe fijarse por digest si su política de despliegue lo indica, y el perfil debe ser el revisado, no una copia que alguien editó en el host.

Solo lectura aquí significa que el contenedor no puede modificar sus archivos del host; no significa que no pueda leer la clave privada que montó. Ese es todo el sentido del montaje, y también por eso los permisos de archivo, la propiedad y cómo llegó el secreto al host siguen importando. Nunca pase una contraseña por la línea de comandos ni la incruste en una capa de imagen, porque ambas sobreviven en lugares que no podrá limpiar después.

Capacidades, entrypoint y la trampa entre ellas

Hacen falta dos ajustes y hacen trabajos no relacionados. NET_ADMIN permite manipular interfaces y rutas dentro del espacio de nombres del contenedor. La entrada de dispositivo /dev/net/tun permite al proceso abrir el dispositivo de túnel siquiera. Conceder uno sin el otro produce un fallo claro, lo cual es cómodo: un dispositivo TUN ausente falla al abrirlo, mientras que una capacidad ausente falla cuando el cliente intenta configurar la interfaz.

Ninguna de las dos justifica privileged: true. Un contenedor privilegiado recibe todas las capacidades más acceso a dispositivos, y es una concesión mucho mayor de la que necesita esta carga de trabajo. La referencia de privilegios y capacidades del entorno de ejecución explica hasta dónde llega esa concesión. Si un chart o una entrada de blog lo pide, compruebe primero si el requisito era en realidad una ruta de dispositivo ausente o una capacidad ausente.

La sustitución de entrypoint es la parte que muerde. Invocar openvpn directamente es correcto para una imagen corriente e incorrecto para cualquier imagen de producto cuyo script de arranque prepara reglas de cortafuegos, archivos de resolución o credenciales antes de ejecutar el demonio. Sustituir ese script omite la preparación y le deja depurando un túnel que arrancó en un espacio de nombres a medio configurar.

Ejecute el cliente en primer plano y deje que sea el proceso principal del contenedor. Un proceso que se demoniza sale de inmediato, el contenedor se reinicia en bucle, y los registros que necesita quedan repartidos entre reinicios que parecen idénticos.

Verifique desde dentro del espacio de nombres, no desde el host

Levante la pila, confirme en el registro que el cliente alcanzó un estado conectado y luego inspeccione el espacio de nombres que realmente contiene el túnel.

docker compose up -d
docker compose logs --tail 40 vpn
docker compose exec vpn ip -br addr
docker compose exec vpn ip route

La lista de direcciones debería mostrar una interfaz de túnel con una dirección del grupo, y la tabla de rutas debería mostrar los destinos empujados apuntando a ella. Si falta la interfaz, el cliente nunca llegó a crearla. Si existe pero ninguna ruta la usa, el lugar donde mirar son las opciones de push o de ruta.

Después pruebe el destino que le importa, desde el espacio de nombres que se supone que usa el túnel, con la aplicación y no solo con un ping. La resolución de nombres es un fallo aparte que merece probarse explícitamente: una dirección DNS empujada no actualiza por sí sola el resolutor de un contenedor, y un cliente autónomo que ignora el push resolverá nombres a través de lo que haya configurado la imagen.

El estado desconectado forma parte del contrato

El comportamiento de reconexión necesita su propia comprobación, porque una política de reinicio demuestra que el proceso se reinicia y no dice nada sobre qué hace el tráfico mientras tanto. Detenga el túnel, pruebe el destino previsto y observe si la conexión se rechaza, se cuelga o se resuelve en silencio por la ruta ordinaria del contenedor. El último resultado es el que importa, y es invisible en un registro sano.

Si la carga de trabajo no debe usar nunca la ruta ordinaria, diséñelo explícitamente con reglas de cortafuegos y una señal de preparación dentro del espacio de nombres. Un kill switch es una política que usted escribe y prueba, no una propiedad que hereda por ejecutar un cliente en un contenedor.

Por último, recuerde la frontera: estas rutas viven en el espacio de nombres del contenedor y no sustituyen la tabla de enrutamiento del host. Las reglas de reenvío, traducción y cortafuegos de Docker siguen siendo parte del camino exterior, y un puerto publicado no es una interfaz VPN.

Artículos relacionados