Inteligencia Artificial

Acceso a contenedores desde Hermes: Proxmox LXC y Docker

El día que le di a un agente de IA acceso a mis servidores, la parte másicipated no fue lo que pudiera hacer, sino lo que hacía con las cosas que ya estaban cerca. Mi homelab tenía un hipervisor con un puñado de máquinas virtuales y contenedores, un host de Docker que ya estaba corriendo un panel de control y un orquestador, y un servidor de producción. La primera versión de todo eso le daba al agente una terminal con acceso completo al host, y no tardó nada en que eso fuera un problema: no por maldad, sino porque el agente hace exactamente lo que le piden y un rm no sabe qué está mirando.

Este post es el resultado de esa historia: cómo darle a un agente acceso a contenedores de forma que el peor caso sea un contenedor roto y no un servidor perdido, las tres formas de darte ese acceso, y los dos bugs que me costaron una tarde cada uno porque parecían otra cosa. Todo con ejemplos genéricos: las IPs y los identificadores de contenedor de un ejemplo no son los de ninguna instalación real.

TL;DR — Lo que vas a tener al final

  • Tiempo de setup: 30 minutos de lectura y dos capturas con la configuración del backend de terminal y el acceso a un contenedor LXC
  • Requisitos: un servidor con Docker o con un hipervisor tipo Proxmox, y una clave SSH para el usuario con el que va a conectar el agente
  • Resultado: tres formas de dar acceso, cada una con su nivel de aislamiento, más la regla de permisos de un LXC no privilegiado que casi nadie conoce
  • Troubleshooting: si el agente dice que un archivo no tiene permiso de escritura en un contenedor, el problema casi nunca es el archivo: es que el uid del contenedor no es el que quedó al escribir

Las tres formas de dar acceso, de la más segura a la más peligrosa

El agente ejecuta sus comandos en el backend de terminal, y ese backend decide qué tan lejos puede llegar. Hay tres opciones y la elección no es de gusto: es de qué quieres que sobreviva el peor caso.

1. Contenedor efímero: el mejor punto de partida

El agente trabaja dentro de un contenedor que se tira al terminar. Todo lo que hace ahí desaparece, incluidos los errores. El backend de Docker de Hermes levanta un contenedor por sesión y admite varias cosas que conviene conocer: memoria y disco limitados, si el contenedor persiste entre sesiones, y los volúmenes que se montan desde el host.

Los volúmenes son el punto delicate, porque son la única puerta entre el contenedor y tu máquina. La regla es montar solo lo que el agente necesita y en la dirección que necesita. Por ejemplo, si el agente debe entregarte un archivo, montas un directorio de salida del host dentro del contenedor y le dices que escriba ahí.

2. Contenedor persistente: cuando hay estado

Si el agente trabaja sobre un proyecto que necesita conservar el estado entre sesiones, conviene un contenedor persistente en lugar de uno efímero. El resto de la configuración es igual, pero ahora los errores sí se acumulan y hay que reiniciar a mano de vez en cuando.

3. Host directo: solo para máquinas desechables

Ejecutar directamente en el host no tiene aislamiento. Es la opción para una máquina de pruebas que no tiene nada que perder, y no para otra cosa. Si el agente tiene que tocar producción, el aislamiento no es opcional: el error más probable no es que el agente decida borrar algo, es que un comando con una ruta mal formada acabe en un sitio que no debía.

Captura de terminal con la configuración del backend de terminal de Hermes y dos errores frecuentes al definir volúmenes y variables de entorno
El backend decide hasta dónde llega el agente. Los volúmenes son la única puerta hacia el host.

La regla de oro: no reenvíes el archivo de secretos entero

El backend de Docker no hereda las credenciales del host por defecto, y esa es una de las decisiones de diseño más correctas del sistema. Si necesitas una credencial específica dentro del contenedor, hay que pedirla de forma explícita, listando la variable.

# EJEMPLO: reenviar solo lo necesario
terminal:
  backend: docker
  docker_forward_env:
    - "TOKEN_ESPECIFICO_DEL_SERVICIO"

El aviso de la documentación es explícito y hay que tomarlo en serio: todo lo que esté en esa lista queda visible para los comandos que se ejecuten dentro del contenedor. Si reenvías el archivo de entorno completo porque es más cómodo, estás dejando el equivalente a todas tus claves de API al alcance de cualquier cosa que el agente ejecute ahí, incluidos los comandos que el agente escriba a partir de contenido que leyó de internet.

Y hay un segundo error clásico en este bloque, que es de YAML puro: si ya tienes una clave docker_volumes: y añades otra más abajo, la segunda pisa la primera sin avisar. Los montajes anteriores desaparecen y el agente empieza a escribir en el sistema de archivos del contenedor en lugar de en tu directorio de salida. El síntoma es desconcertante porque nada falla: simplemente el archivo ya no está donde debería.

Devolverte un archivo: la regla de las rutas

Cuando el agente produce un archivo y quiere mandártelo por el canal de mensajería, hay un detalle de rutas que provoca errores constantes. La regla es: emite la ruta del host, no la ruta dentro del contenedor.

Si montaste el host en /output dentro del contenedor, el archivo existe en el host en el directorio que montaste. Si mandas /output/informe.pdf, el proceso de la plataforma busca esa ruta en su propia máquina y no la encuentra, porque para ella el directorio se llama de otra forma. La ruta que hay que emitir es la del host.

# EJEMPLO: montaje con nombre explicito
terminal:
  backend: docker
  docker_volumes:
    - "/home/usuario/.hermes/cache/documents:/output"
# el agente escribe en /output/informe.pdf dentro del contenedor
# y lo entrega con la ruta DEL HOST:
#   MEDIA:/home/usuario/.hermes/cache/documents/informe.pdf

Acceso a contenedores LXC desde el hipervisor

Si tu homelab usa Proxmox, hay una segunda vía que no pasa por Docker: el agente entra por SSH al hipervisor y ejecuta comandos dentro de un contenedor con la herramienta del hipervisor. La mecánica es directa.

Captura de terminal con comandos para ejecutar dentro de un contenedor LXC desde el host y el procedimiento de corrección de permisos en contenedores no privilegiados
Entrar a un contenedor es un comando. Lo difícil es que el agente no rompa los permisos al arreglarlo.
# EJEMPLO: ejecutar dentro de un contenedor
pct exec 115 -- bash -c "systemctl is-active nginx"
# EJEMPLO: ver la version del sistema operativo del contenedor
pct exec 115 -- bash -c "cat /etc/os-release | head -2"
# EJEMPLO: estado del contenedor desde el host
pct status 115
pct list

El punto fuerte de esta vía es que el agente trabaja con la herramienta nativa del hipervisor, que es lo que quieres cuando el trabajo es de infraestructura y no de aplicación. El punto débil es que el aislamiento lo da el contenedor, no el agente: si al agente le das acceso root al hipervisor, puede tocar todos los contenedores, no solo el que le interesa. Lo correcto es una cuenta sin privilegios en el hipervisor, con sudo limitado a los comandos de pct de los contenedores concretos.

El detalle de los contenedores no privilegiados que nadie te cuenta

Si en algún momento necesitas arreglar un contenedor LXC no privilegiado desde el host en vez de simplemente usarlo, hay una trampa que produce el error más confuso que existe en este mundo: “permiso denegado” en archivos que tú acabas de crear.

La causa es el mapeo de identificadores. En un contenedor no privilegiado, el root del host tiene el identificador 100000 dentro del contenedor. Cuando entras al contenedor mediante un chroot desde el host y instalas o reparas paquetes, los archivos quedan con propietario 0:0, que el contenedor ve como un usuario que no puede escribir. El resultado es que el paquete parece bien instalado y después el sistema se queja de que no puede escribir en su propia caché.

La solución tiene dos pasos y hay que hacerlos siempre, en este orden. Primero, borrar los bloqueos y las cachés de binarios que quedaron corruptos. Segundo, y es el paso que se olvida, devolver la propiedad de los directorios que el contenedor necesita escribir al identificador 100000.

# EJEMPLO: limpiar bloqueos heredados y caches rotos
rm -f /mnt/fix/var/lib/dpkg/lock /mnt/fix/var/lib/dpkg/lock-frontend
rm -f /mnt/fix/var/lib/apt/lists/partial/*
rm -f /mnt/fix/var/cache/apt/srcpkgcache.bin /mnt/fix/var/cache/apt/pkgcache.bin
# EJEMPLO: devolver la propiedad al uid del contenedor no privilegiado
chown -R 100000:100000 /mnt/fix/var/lib/dpkg /mnt/fix/var/cache/apt 
  /mnt/fix/var/log/apt /mnt/fix/root /mnt/fix/usr/share/locale

Dos detalles de esa lista que parecen arbitrarios y no lo son. El directorio /root necesita el cambio porque el shell de inicio del root lo lee y sin el cambio se queja de permisos. Y /usr/share/locale también, porque el instalador de paquetes escribe ahí los archivos de idioma y sin el cambio la instalación falla con un error de permiso que no señala la causa real.

Dos bugs de Docker que parecen de otra cosa

El primero es de los más frustrantes que existen en la historia de los contenedores: un archivo de composición que tiene las dos cosas, la construcción local y la imagen precompilada.

# EJEMPLO: composicion con build Y image a la vez
services:
  web:
    build: .
    image: registry.example.com/proyecto/imagen:v1.0.0

Cuando están las dos, gana la imagen precompilada y tus cambios locales no se aplican nunca. El servicio arranca con el código viejo, tú cambias el archivo, y no pasa nada. El agente puede pasar una hora editando un Dockerfile y reejecutando la composición sin que cambie nada, porque lo que se está levantando no es lo que crees que se levanta. La corrección es una de las dos: quitar la línea image: para construir local, o quitar el build: y subir la imagen nueva con una etiqueta distinta.

El segundo bug es de orden, y tiene una regla que se aprende la primera vez que te pasa. Si el servicio web arranca antes de que la base de datos esté inicializada, entra en un ciclo de reinicio con un error que dice que la relación no existe. La solución no es reiniciar más veces: es inicializar la base de datos antes de levantar el servicio.

# EJEMPLO: inicializar la base de datos ANTES de levantar el servicio
docker compose stop web
docker compose run --rm --entrypoint bash web -c 
  "bundle exec rails db:create db:migrate db:seed"
docker compose up -d

Antes de montar nada: mira lo que ya tienes

El consejo más barato de todos. Si ya tienes un host de Docker en la red, crear un contenedor LXC nuevo para correr lo mismo es trabajo duplicado con dos capas de mantenimiento. Revisa primero qué hay, y después decide si necesitas una máquina virtual, un contenedor del hipervisor o un contenedor de Docker.

La razón no es el ahorro de recursos, que es pequeño. Es que cada capa que añades es una capa donde un cambio de versión de la imagen base rompe algo, y con dos capas encima de la misma aplicación el diagnóstico se vuelve mucho más caro.

La lista de verificación de seguridad

  • ¿El agente tiene la terminal completa sobre el host? Si sí, ¿la máquina tiene algo que perder? Si la respuesta es que sí, el aislamiento no es opcional.
  • ¿El agente entra como root al hipervisor? Con eso controla todos los contenedores. Que sea una cuenta con sudo limitado.
  • ¿Se reenvía el archivo de secretos al contenedor? Solo variables concretas, y solo las necesarias.
  • ¿Los volúmenes están montados en un solo sentido? Escribir es fácil; que el contenedor lea tu directorio personal, no.
  • ¿La clave SSH es una clave, no una contraseña? Y con una frase de paso, si el agente no la necesita para todo.
  • ¿El agente puede ejecutar comandos arbitrarios dentro del contenedor? Eso es inevitable en el modelo actual, y por eso el contenedor tiene que ser desechable.

Preguntas frecuentes

¿Docker o LXC para darle acceso a un contenedor al agente?

Docker si el trabajo es de aplicación y necesitas que el agente pueda crear y tirar contenedores; el aislamiento sale gratis y el peor caso es un contenedor roto. LXC si el trabajo es de infraestructura y necesitas la herramienta nativa del hipervisor, porque ahí lo que se automatiza es el contenedor que ya tienes. Lo que no conviene en ninguno de los dos casos es darle al agente acceso root al hipervisor sin limitarlo.

¿Por qué el agente no encuentra el archivo que acaba de crear?

Casi siempre es el mismo motivo: emitió la ruta de dentro del contenedor en vez de la del host. El proceso que recibe el archivo busca la ruta en su propio sistema de archivos, donde ese directorio tiene otro nombre. La regla es emitir la ruta del host, que es la del punto de montaje que configuraste.

¿Cómo hago para que el agente no toque producción?

Con aislamiento, no con promesas. Un contenedor efímero para el trabajo de aplicación y una cuenta sin privilegios en el hipervisor para el trabajo de infraestructura, con sudo acotado a los contenedores concretos. Si además el agente puede escribir archivos, el sandbox de escritura evita que toque configuración fuera de las rutas permitidas. La producción se protects con el mismo mecanismo: no está en el alcance del agente.

¿Qué hago si un contenedor da permiso denegado en archivos que acabo de crear?

Revisa el propietario de los archivos. Si están en 0:0 y el contenedor es no privilegiado, su root real es 100000, y por eso no puede escribir en ellos. La solución es el chown recursivo a 100000:100000 sobre los directorios que el contenedor necesita escribir, más la limpieza de bloqueos y cachés de binarios corruptos. Hazlo antes de arrancar el contenedor, no con él en pie.

¿Es seguro reenviar las variables de entorno al contenedor?

Reenviar variables concretas, sí. Reenviar el archivo de secretos completo, no: lo que esté en esa lista queda visible para cualquier comando que se ejecute dentro del contenedor, y uno de esos comandos puede haber sido escrito por el agente a partir de contenido que leyó de internet. El aislamiento del contenedor no protege contra eso, porque el contenedor tiene acceso a lo que le reenviaste.

Lee también

Alfonso Moreno | Sysadmin Senior

Soy Alfonso, sysadmin senior con RHCE y más de 10 años administrando sistemas Linux para empresas y proyectos personales. Me especializo en infraestructura, redes, virtualización, monitoring, automation y self-hosting. Sobre SoyLinux.com SoyLinux.com es un blog técnico en español sobre Linux, software libre y self-hosting. Lo mantengo en mi tiempo libre. Áreas de expertise - Linux server administration: RHEL, CentOS, Rocky, Alma, Fedora, Debian, Ubuntu - Network services: Postfix, BIND, Samba, NFS, Squid - Virtualización y containers: KVM, Proxmox, Docker, Podman - Monitoring: Prometheus, Grafana, Zabbix - Automation: Ansible, Bash scripting - Self-hosting: Vaultwarden, Nextcloud, Jellyfin, Immich, Homelab Contacto - Email: [email protected]

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *