Problema
Los runners auto‑hosteados de GitHub Actions son convenientes porque pueden reutilizar infraestructura existente y ejecutar jobs que requieren Docker, Docker‑Compose o herramientas de navegador como Playwright. Sin embargo, cuando el job tiene acceso directo al daemon Docker del host, se rompe el aislamiento esperado: un script malintencionado o un error de configuración puede manipular contenedores del propio host, leer volúmenes sensibles o consumir recursos críticos. Además, la ejecución de código del workflow directamente sobre la máquina física elimina la frontera entre el CI y el entorno de producción, lo que complica la depuración y la rotación de runners.
Este patrón se repite en muchos entornos de CI/CD: se necesita un runner que pueda lanzar contenedores, pero sin que esos contenedores tengan privilegios sobre el host. La solución debe ser reproducible, fácil de operar y compatible con pipelines que ya usan Docker‑Compose.
Causa
-
Montaje del socket
/var/run/docker.sock
La práctica más extendida consiste en montar el socket del daemon dentro del contenedor del runner. Esto otorga a cualquier proceso dentro del job control total sobre el daemon del host. La raíz del problema es la confianza implícita en el código que se ejecuta en el pipeline. -
Ejecutar Docker‑in‑Docker (DinD) en modo privilegiado
DinD funciona creando un daemon Docker dentro de un contenedor. Para que el daemon interno pueda crear sus propios contenedores, el contenedor debe ejecutarse con--privileged. En nodos dedicados eso puede ser aceptable, pero en máquinas compartidas el privilegio abre la puerta a escapes de namespace y a la manipulación del kernel. -
Falta de separación a nivel de máquina
Cuando varios jobs comparten la misma VM o servidor, los artefactos de un job pueden quedar residuales y afectar al siguiente. La ausencia de “clean slate” incrementa la probabilidad de colisiones de puertos, capas de imagen corruptas y consumo inesperado de disco. -
Configuraciones de Docker rootless incompletas
Docker rootless elimina la necesidad de privilegios de root, pero requiere un entorno de usuario configurado y un daemon separado. Si la configuración no es uniforme, los jobs pueden fallar de forma intermitente.
Solución
Una estrategia robusta combina aislamiento a nivel de microVM con un daemon Docker dedicado a cada sandbox. El flujo típico es:
-
Provisionar una microVM por job
Herramientas como Firecracker, QEMU osystemd-nspawnpermiten lanzar microVM ligeras en segundos. Cada microVM contiene su propio kernel, cgroup y espacio de nombres, garantizando que el daemon Docker dentro de ella no vea el host. -
Instalar Docker (o Docker‑rootless) dentro de la microVM
La instalación es idéntica a la de una máquina física. Si se prefiere rootless, basta con crear un usuario no‑root y ejecutardockerd-rootless.sh. La ventaja es que el daemon solo tiene acceso a recursos dentro de la VM. -
Ejecutar Docker‑Compose o cualquier herramienta de contenedorización dentro de la VM
Los scripts del workflow se copian a la VM y se ejecutan allí. Como la VM se destruye al final del job, cualquier contenedor, volumen o red se elimina automáticamente. -
Mantener un pool de microVM “calientes”
Para evitar la latencia de arranque, se pueden mantener N microVM pre‑inicializadas. Cuando llega un job, se asigna una VM del pool, se ejecuta el workflow y, al terminar, la VM se destruye y se reemplaza por una nueva. Este patrón se llama “ephemeral runner”.
Alternativas prácticas
- Kubernetes con ARC: Si ya se dispone de un clúster, ARC permite programar runners como pods. ARC soporta DinD en modo privilegiado o rootless, pero implica la sobrecarga de gestionar un clúster completo.
- Docker Sandboxes (Firecracker + Docker): Algunas distribuciones ofrecen paquetes que combinan Firecracker y Docker en una sola CLI (
docker-sandbox). Son útiles cuando se busca una solución “todo en uno”. - Rootless DinD: Ejecutar DinD sin
--privilegedusando--userns-remapy--security-opt. Funciona en nodos dedicados, pero la compatibilidad con Docker‑Compose es limitada y el rendimiento puede verse afectado.
Cuándo aplicar esta solución
- Entornos con pocos nodos y workloads intensivos en Docker: Si la infraestructura es pequeña (2‑10 servidores) y los jobs requieren Docker‑Compose, la microVM por job es más simple que montar un clúster Kubernetes.
- Necesidad de aislamiento fuerte: Cuando el equipo de seguridad exige que el daemon Docker no pueda tocar el host, o cuando se ejecutan pruebas de seguridad dentro del pipeline.
- Rendimiento aceptable: Si la sobrecarga de una microVM (≈ 200 ms de arranque) no impacta los tiempos de CI críticos.
- No aplicar: En flujos de trabajo que solo construyen imágenes y no ejecutan contenedores, o cuando se dispone de un clúster Kubernetes bien mantenido que ya gestiona aislamiento.
Código
# 1. Crear una microVM con Firecracker (asume que firecracker y jailer están instalados)
sudo jailer --id job-123 --exec /usr/local/bin/firecracker -- \
--api-sock /tmp/firecracker.sock \
--config-file /etc/firecracker/job-123.json
# 2. Dentro de la VM, instalar Docker (ejemplo para Ubuntu)
apt-get update && apt-get install -y docker.io
# 3. Copiar el script del workflow a la VM
scp -i ~/.ssh/id_rsa workflow.sh root@$(cat /tmp/vm_ip):/home/runner/
# 4. Ejecutar el workflow dentro de la VM
ssh -i ~/.ssh/id_rsa root@$(cat /tmp/vm_ip) "cd /home/runner && bash workflow.sh"
# 5. Destruir la VM al terminar
sudo jailer --id job-123 --destroy
Verificación
-
Comprobar que el daemon Docker dentro de la VM no está conectado al socket del host
ssh root@$(cat /tmp/vm_ip) "docker info | grep 'Server Version'"La salida debe mostrar la versión del daemon dentro de la VM, no del host.
-
Validar que los contenedores creados no aparecen en el host
En el host ejecutadocker ps. La lista debe estar vacía o contener solo contenedores del propio host, sin los del job. -
Revisar que la VM se destruye
Después de la fase de limpieza,sudo jailer --listno debe listar la VMjob-123. -
Medir tiempo de ejecución
Registra el tiempo entre el inicio del job y la destrucción de la VM. Si supera los límites de SLA, ajusta el pool de VMs “calientes”.
Notas adicionales
- Persistencia de artefactos: Si el pipeline necesita subir artefactos, usa
actions/upload-artifactantes de destruir la VM. El runner puede montar un volumen compartido (/tmp/artifacts) que se exporta al host. - Límites de recursos: Firecracker permite asignar CPU y memoria por VM. Configura valores que eviten que un job monopolice la máquina física.
- Monitoreo: Integra métricas de creación/destrucción de VMs en Prometheus para detectar cuellos de botella.
- Fallback a DinD: Mantén una ruta de emergencia con DinD privilegiado para jobs que no pueden ejecutarse en microVM (por ejemplo, pruebas de kernel). Marca esos jobs como “high‑risk” y ejecuta en nodos dedicados.
- Actualizaciones: Cuando actualices Docker o el kernel de la VM, recrea el pool completo para evitar inconsistencias. Un script de CI que recrea el pool cada semana suele ser suficiente.