Los pipelines CI/CD se encuentran entre las cargas de trabajo más privilegiadas de cualquier organización. Extraen código fuente, descargan dependencias, acceden a secrets y envían artefactos a registries de producción. Sin embargo, en muchos entornos, los procesos de build detrás de estos pipelines se ejecutan con acceso de red sin restricciones y permisos completos sobre el sistema de archivos — una combinación que representa una de las brechas más explotables en la entrega moderna de software.
Cuando un entorno de build puede alcanzar cualquier dirección IP y escribir en cualquier ruta del disco, una sola dependencia comprometida o un pull request malicioso puede exfiltrar secrets, alterar artefactos o establecer backdoors persistentes. Esta guía cubre técnicas prácticas para restringir el acceso de red y del sistema de archivos en entornos de build CI/CD, desde NetworkPolicies de Kubernetes hasta sistemas de build herméticos.
Por Qué los Entornos de Build Sin Restricciones Son Peligrosos
Antes de adentrarnos en las soluciones, vale la pena comprender las amenazas específicas que crean los entornos de build sin restricciones. Estos riesgos no son teóricos — han sido explotados en ataques reales a la cadena de suministro.
Exfiltración de Datos
Los entornos de build frecuentemente tienen acceso a secrets: claves API, credenciales de registries, claves de firma y tokens de despliegue. Si un proceso de build tiene acceso de red saliente sin restricciones, una dependencia comprometida puede enviar esos secrets a un servidor controlado por el atacante. Esto puede ocurrir a través de un script postinstall malicioso en un paquete npm, una dependencia PyPI comprometida, o incluso un target de Makefile manipulado. Sin restricciones de red, no hay barrera entre el secret y el endpoint del atacante.
Ataques a la Cadena de Suministro
Un atacante que puede ejecutar código arbitrario durante un build puede modificar los artefactos de salida. Si el sistema de archivos es escribible sin restricciones, los binarios compilados, las imágenes de contenedores o los manifiestos de despliegue pueden ser alterados después del paso legítimo de build pero antes de que el artefacto sea enviado. Esta es la esencia de muchos ataques a la cadena de suministro — el código fuente parece limpio, pero el artefacto entregado está envenenado.
Movimiento Lateral
Los entornos de build que comparten red con otra infraestructura (bases de datos, APIs internas, servicios de metadatos en la nube) proporcionan al atacante un punto de pivote. Un job de build comprometido puede escanear redes internas, acceder a endpoints de metadatos de instancias en la nube (como 169.254.169.254) y escalar desde un contexto CI/CD hacia un acceso más amplio a la infraestructura.
Restricciones de Red
El control más impactante que puedes implementar es restringir el acceso de red saliente desde los entornos de build. Los builds necesitan descargar dependencias y enviar artefactos — pero raramente necesitan acceso irrestricto a internet.
Kubernetes NetworkPolicy para Pods de Runners
Si ejecutas runners CI/CD en Kubernetes (por ejemplo, usando Actions Runner Controller o el executor de Kubernetes de GitLab), los recursos NetworkPolicy te ofrecen un control granular sobre el acceso de red a nivel de pod. Una política bien diseñada deniega todo el tráfico de salida por defecto y luego permite solo los endpoints específicos que el build necesita.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ci-runner-netpol
namespace: ci-runners
spec:
podSelector:
matchLabels:
app: ci-runner
policyTypes:
- Egress
egress:
# Allow DNS resolution
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow access to container registry
- to:
- ipBlock:
cidr: 10.0.50.0/24
ports:
- protocol: TCP
port: 443
# Allow access to artifact storage
- to:
- ipBlock:
cidr: 10.0.60.0/24
ports:
- protocol: TCP
port: 443
# Deny everything else by omission
Esta política permite que los pods de runners resuelvan DNS, accedan al registry de contenedores y al almacenamiento de artefactos — nada más. Cualquier otra conexión saliente es descartada. Si utilizas un plugin CNI que soporta NetworkPolicy (Calico, Cilium o Weave Net), esto entra en vigor inmediatamente al aplicarse.
Para un control más granular, la CiliumNetworkPolicy de Cilium soporta reglas basadas en DNS, permitiéndote especificar nombres de dominio en lugar de bloques de IP:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: ci-runner-cilium-policy
namespace: ci-runners
spec:
endpointSelector:
matchLabels:
app: ci-runner
egress:
- toEndpoints:
- matchLabels:
io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: ANY
- toFQDNs:
- matchName: "ghcr.io"
- matchName: "registry.npmjs.org"
- matchName: "pypi.org"
toPorts:
- ports:
- port: "443"
protocol: TCP
Docker –network=none
Para pasos de build basados en Docker que no deberían necesitar acceso de red (compilación, análisis estático, tests unitarios), puedes eliminar el acceso de red por completo ejecutando el contenedor con --network=none:
docker run --network=none \
--rm \
-v "$(pwd)/src:/workspace:ro" \
-v "$(pwd)/output:/output" \
my-build-image:latest \
make build
Con --network=none, el contenedor no tiene interfaces de red en absoluto — ni siquiera loopback en algunas configuraciones. Este es el aislamiento de red más fuerte que puedes lograr para un paso de build. La clave es estructurar tu pipeline de manera que la obtención de dependencias ocurra en una etapa (con acceso de red limitado) y el build real ocurra en una etapa separada sin red.
Reglas de Firewall para Runners Self-Hosted
Si utilizas runners self-hosted en VMs en lugar de contenedores, las reglas de firewall a nivel de host proporcionan una protección equivalente. En Linux, las reglas de iptables o nftables pueden restringir el tráfico saliente de la cuenta de usuario que ejecuta los jobs de CI:
# Allow DNS
iptables -A OUTPUT -m owner --uid-owner ci-runner -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner ci-runner -p tcp --dport 53 -j ACCEPT
# Allow HTTPS to specific registries
iptables -A OUTPUT -m owner --uid-owner ci-runner -p tcp --dport 443 \
-d registry.example.com -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner ci-runner -p tcp --dport 443 \
-d ghcr.io -j ACCEPT
# Deny all other outbound traffic from the CI runner
iptables -A OUTPUT -m owner --uid-owner ci-runner -j DROP
Este enfoque funciona bien cuando ejecutas el agente de CI bajo una cuenta de usuario dedicada y necesitas permitir que el sistema host mantenga una conectividad más amplia para gestión y actualizaciones.
Allowlisting de Registries y APIs
Independientemente del mecanismo de aplicación, el principio es el mismo: denegar por defecto el tráfico saliente y luego permitir solo lo que el build realmente necesita. Una lista de permitidos típica incluye el registry de paquetes (npm, PyPI, Maven Central), el registry de contenedores (Docker Hub, GHCR, ECR), la API de la plataforma CI/CD (para actualizaciones de estado y subida de artefactos), y posiblemente un proxy o mirror que tú controles. Todo lo demás debe ser bloqueado. Usa un proxy interno o mirror para dependencias siempre que sea posible — reduce la lista de permitidos a un solo endpoint y te proporciona caché y registro de auditoría de forma gratuita.
Restricciones del Sistema de Archivos
Las restricciones de red evitan que los datos salgan del entorno de build. Las restricciones del sistema de archivos evitan modificaciones no autorizadas dentro de él. Juntas, forman una sólida postura de defensa en profundidad.
Sistema de Archivos Raíz de Solo Lectura
Ejecutar contenedores de build con un sistema de archivos raíz de solo lectura impide que cualquier proceso modifique la imagen base. Esto bloquea una clase de ataques en los que el código malicioso modifica binarios del sistema, instala backdoors o altera las configuraciones de las herramientas de build a nivel de sistema.
En Docker, usa el flag --read-only:
docker run --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=512m \
--tmpfs /workspace/build:rw,size=2g \
-v "$(pwd)/src:/workspace/src:ro" \
my-build-image:latest \
make build
En Kubernetes, establece el security context en la especificación del pod:
apiVersion: v1
kind: Pod
metadata:
name: ci-build-pod
spec:
containers:
- name: build
image: my-build-image:latest
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
allowPrivilegeEscalation: false
volumeMounts:
- name: build-tmp
mountPath: /tmp
- name: build-output
mountPath: /workspace/build
- name: source
mountPath: /workspace/src
readOnly: true
volumes:
- name: build-tmp
emptyDir:
medium: Memory
sizeLimit: 512Mi
- name: build-output
emptyDir:
sizeLimit: 2Gi
- name: source
configMap:
name: source-code
tmpfs para Artefactos de Build
Cuando el sistema de archivos raíz es de solo lectura, los builds necesitan espacio escribible para archivos temporales, cachés y artefactos de salida. Usa montajes tmpfs (respaldados por RAM) o volúmenes emptyDir (en Kubernetes) para estas rutas. Esto tiene el beneficio adicional de que todos los artefactos de build se limpian automáticamente cuando el contenedor finaliza — no persisten datos obsoletos entre builds.
Monta tmpfs con opciones restrictivas siempre que sea posible: noexec previene la ejecución de binarios escritos en directorios temporales (bloqueando un vector de ataque común), nosuid previene ataques de bit SUID, y size limita para evitar que un build descontrolado agote la memoria del host.
Prevención de Escrituras en Rutas Sensibles
Más allá del sistema de archivos raíz, ciertas rutas específicas merecen protección adicional. Monta el código fuente como solo lectura para evitar que el build modifique sus propias entradas. Asegúrate de que /etc, /usr y /var no sean escribibles. Si el build necesita escribir en un directorio home (para configuración de herramientas), proporciona un montaje escribible dedicado en lugar de hacer todo el directorio home escribible. Bloquea el acceso a sockets de Docker, tokens de service account de Kubernetes y archivos de credenciales en la nube no montándolos en los contenedores de build en absoluto.
Builds Herméticos
El estándar de oro para la seguridad de entornos de build es el build hermético: un build que no tiene acceso de red en absoluto y utiliza solo entradas explícitamente declaradas y pre-obtenidas. Los builds herméticos eliminan clases enteras de ataques a la cadena de suministro porque el proceso de build no puede descargar código que no fue explícitamente especificado y verificado.
El Patrón de Build Hermético
Un pipeline de build hermético típicamente tiene dos fases. En la primera fase (la fase de resolución/obtención), las dependencias se descargan de fuentes aprobadas, sus checksums se verifican contra un lockfile y se almacenan en una caché local o directorio vendorizado. Esta fase requiere acceso de red limitado. En la segunda fase (la fase de build), la compilación o ensamblaje real ocurre con cero acceso de red. Todas las entradas provienen del sistema de archivos local — código fuente y las dependencias pre-obtenidas.
# Phase 1: Fetch dependencies (limited network)
docker run --network=ci-restricted \
-v "$(pwd):/workspace" \
my-build-image:latest \
sh -c "cd /workspace && npm ci --ignore-scripts"
# Phase 2: Build (no network)
docker run --network=none \
--read-only \
--tmpfs /tmp:rw,noexec,size=512m \
-v "$(pwd):/workspace:ro" \
-v "$(pwd)/dist:/dist" \
my-build-image:latest \
sh -c "cd /workspace && npm run build && cp -r build/* /dist/"
Bazel y Builds Herméticos
Bazel está diseñado en torno a la hermeticidad. Con --sandbox_default_allow_network=false, Bazel bloquea el acceso de red durante las acciones de build por defecto. Las dependencias se declaran en archivos WORKSPACE o MODULE.bazel con hashes SHA-256 explícitos, y Bazel las obtiene en una fase separada antes de que el build comience. Si una dependencia no coincide con su hash declarado, el build falla.
# In .bazelrc
build --sandbox_default_allow_network=false
build --incompatible_strict_action_env
fetch --repository_cache=/shared/bazel-cache/repos
Esto hace que los builds de Bazel sean reproducibles y resistentes a ataques de confusión de dependencias. Cada entrada está direccionada por contenido y verificada.
Nix y Builds Reproducibles
Nix adopta un enfoque similar. Cada derivación de build especifica sus entradas por hash de contenido, y el sandbox de build de Nix bloquea el acceso de red por defecto. El comando nix-build obtiene todas las fuentes en el Nix store (verificando hashes), luego ejecuta el build en un entorno aislado sin red y con un sistema de archivos mínimo. Esto garantiza que los builds sean reproducibles — las mismas entradas siempre producen la misma salida.
Implementación Práctica
Veamos cómo implementar estas restricciones en plataformas CI/CD específicas.
GitHub Actions con Actions Runner Controller (ARC) + NetworkPolicy
Si utilizas Actions Runner Controller para ejecutar GitHub Actions en Kubernetes, puedes aplicar NetworkPolicies directamente a los pods de runners. ARC crea pods con labels predecibles, lo que facilita dirigirlos con políticas.
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: secure-runner
namespace: ci-runners
spec:
replicas: 3
template:
metadata:
labels:
app: ci-runner
security-tier: restricted
spec:
containers:
- name: runner
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
volumeMounts:
- name: work
mountPath: /runner/_work
- name: tmp
mountPath: /tmp
volumes:
- name: work
emptyDir:
sizeLimit: 10Gi
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 1Gi
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: secure-runner-netpol
namespace: ci-runners
spec:
podSelector:
matchLabels:
app: ci-runner
policyTypes:
- Egress
- Ingress
ingress: []
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 443
Esta configuración deniega todo el tráfico de entrada (los runners no deberían aceptar conexiones entrantes) y limita el tráfico de salida a DNS y HTTPS. Para uso en producción, reemplaza el CIDR 0.0.0.0/0 con rangos de IP específicos para la API de GitHub, tu registry de contenedores y tu almacenamiento de artefactos.
GitLab CI con Configuración de Runner
El executor de Kubernetes de GitLab soporta la configuración de security context en el config.toml del runner. Puedes establecer el sistema de archivos de solo lectura y otras restricciones directamente:
# config.toml for GitLab Runner (Kubernetes executor)
[[runners]]
name = "secure-k8s-runner"
executor = "kubernetes"
[runners.kubernetes]
namespace = "ci-runners"
image = "alpine:latest"
privileged = false
allow_privilege_escalation = false
[runners.kubernetes.pod_security_context]
run_as_non_root = true
run_as_user = 1000
[runners.kubernetes.build_container_security_context]
read_only_root_filesystem = true
allow_privilege_escalation = false
[runners.kubernetes.build_container_security_context.capabilities]
drop = ["ALL"]
[runners.kubernetes.volumes]
[[runners.kubernetes.volumes.empty_dir]]
name = "build-tmp"
mount_path = "/tmp"
medium = "Memory"
size_limit = "512Mi"
[[runners.kubernetes.volumes.empty_dir]]
name = "build-workspace"
mount_path = "/builds"
size_limit = "5Gi"
Combina esto con una NetworkPolicy aplicada al namespace ci-runners y tendrás tanto las restricciones del sistema de archivos como las de red en su lugar.
Restricciones de Docker-in-Docker
Docker-in-Docker (DinD) se usa comúnmente para construir imágenes de contenedores en CI. También es uno de los patrones más riesgosos porque típicamente requiere modo privilegiado. Si debes usar DinD, aplica estas restricciones:
# Use rootless DinD instead of privileged mode
services:
dind:
image: docker:24-dind-rootless
environment:
- DOCKER_TLS_CERTDIR=/certs
volumes:
- dind-certs:/certs/client
- dind-data:/var/lib/docker
# When running builds inside DinD, pass network and filesystem restrictions
docker --host tcp://dind:2376 --tlsverify \
run --network=none --read-only \
--tmpfs /tmp:rw,noexec,size=256m \
--security-opt=no-new-privileges \
my-build-image:latest make build
Mejor aún, reemplaza DinD con herramientas que no necesiten un daemon de Docker en absoluto. kaniko, buildah y ko pueden construir imágenes de contenedores sin acceso privilegiado, y funcionan bien con sistemas de archivos de solo lectura y redes restringidas.
Monitoreo y Auditoría
Las restricciones solo son útiles si sabes cuándo están siendo probadas o evadidas. El monitoreo completa el panorama de seguridad.
Detección de Conexiones de Red Inesperadas
Usa Hubble de Cilium, los logs de flujo de Calico o Falco para detectar conexiones de red que tu política debería haber bloqueado (o conexiones a destinos inusuales en puertos permitidos). Configura alertas para cualquier consulta DNS a dominios que no estén en tu lista de permitidos, conexiones salientes a puertos no estándar, conexiones a rangos de IP conocidos como maliciosos y cualquier tráfico de salida desde pods que deberían tener --network=none.
# Falco rule: detect unexpected outbound connections from CI runners
- rule: CI Runner Unexpected Outbound Connection
desc: Detect network connections from CI runner pods to non-approved destinations
condition: >
evt.type in (connect, sendto) and
container and
k8s.ns.name = "ci-runners" and
not (fd.sip in (approved_registry_ips) or fd.sport = 53)
output: >
Unexpected outbound connection from CI runner
(command=%proc.cmdline connection=%fd.name container=%container.name
pod=%k8s.pod.name namespace=%k8s.ns.name)
priority: WARNING
tags: [network, ci-cd, supply-chain]
Auditoría de Acceso al Sistema de Archivos
Monitorea las escrituras en el sistema de archivos dentro de los contenedores de build para detectar modificaciones inesperadas. El auditd de Linux puede vigilar rutas específicas, y Falco puede detectar escrituras en ubicaciones sensibles. Las rutas clave a monitorear incluyen /etc y /usr (nunca deberían ser escritas en un build), la ruta del socket de Docker, las rutas de tokens de service account de Kubernetes y cualquier ruta que contenga credenciales o claves de firma.
Si usas sistemas de archivos raíz de solo lectura, cualquier intento de escritura en una ruta protegida genera un error — registra estos errores y genera alertas sobre ellos. Indican ya sea un build mal configurado o un posible ataque.
Compromisos y Experiencia del Desarrollador
Las restricciones estrictas de red y sistema de archivos inevitablemente generan fricción. Comprender y gestionar los compromisos es fundamental para una adopción exitosa.
Velocidad de Build
Los builds herméticos requieren que todas las dependencias sean pre-obtenidas, lo que añade una etapa al pipeline. Sin embargo, esto también significa que las dependencias pueden ser cacheadas agresivamente. En la práctica, muchos equipos encuentran que los builds herméticos son en realidad más rápidos porque la tasa de aciertos de caché es mucho mayor cuando la resolución de dependencias es determinista. Usa una caché compartida (una remote cache de Bazel, una binary cache de Nix o una simple caché HTTP para dependencias vendorizadas) para amortizar el costo entre builds.
Experiencia del Desarrollador
Los desarrolladores encontrarán fallos cuando los builds intenten acceder a endpoints de red bloqueados o escribir en rutas de solo lectura. Los buenos mensajes de error son esenciales. Envuelve tus pasos de build en scripts que capturen errores de permisos y fallos de red, luego muestra mensajes accionables que expliquen por qué el acceso fue bloqueado y cómo solucionar el problema (generalmente agregando una dependencia al lockfile o cambiando la ruta de salida).
Considera implementar un despliegue gradual: comienza con modo de monitoreo (registra violaciones pero no las bloquea), luego pasa a la aplicación. Esto da tiempo a los equipos para actualizar sus configuraciones de build sin romper todos los pipelines de una vez.
Depuración
Depurar fallos de build en un entorno restringido es más difícil cuando no puedes instalar herramientas adicionales ni acceder a servicios externos. Proporciona un «modo de depuración» que relaje las restricciones para una ejecución de pipeline específica, activada manualmente (nunca para ejecuciones automatizadas en la rama principal). Registra que se usó el modo de depuración y quién lo activó. Nunca permitas que el modo de depuración evite las restricciones en builds de artefactos de producción.
Integrando Todo
Aquí hay un resumen del enfoque por capas para asegurar los entornos de build CI/CD:
Capa 1 — Restricciones de red: Denegación por defecto del tráfico de salida con listas de permitidos para registries y APIs. Usa Kubernetes NetworkPolicy, Docker --network=none o reglas de firewall a nivel de host según tu infraestructura de runners.
Capa 2 — Restricciones del sistema de archivos: Sistema de archivos raíz de solo lectura, tmpfs para rutas escribibles con límites de tamaño y noexec, código fuente montado como solo lectura.
Capa 3 — Builds herméticos: Separar la resolución de dependencias del build. Ejecutar la fase de build con cero acceso de red y solo entradas pre-obtenidas y verificadas por hash.
Capa 4 — Monitoreo: Detectar y alertar sobre violaciones de políticas, conexiones inesperadas e intentos de modificación del sistema de archivos.
Ninguna capa es suficiente por sí sola. Las restricciones de red sin controles del sistema de archivos aún permiten la alteración de artefactos. Las restricciones del sistema de archivos sin controles de red aún permiten la exfiltración. Los builds herméticos sin monitoreo te dejan ciego ante intentos de ataque. Las capas se refuerzan mutuamente.
Guías Relacionadas
Para más información sobre cómo asegurar tu pipeline CI/CD, consulta estas guías relacionadas:
- Integridad de Build y Builds Reproducibles en CI/CD — cubre cumplimiento SLSA, verificación de builds reproducibles y procedencia de artefactos.
- Lab: Runners Self-Hosted Efímeros con Actions Runner Controller — guía práctica para desplegar ARC en Kubernetes con pods de runners efímeros y de un solo uso.
Comienza con las restricciones de red — ofrecen el mayor impacto con el menor esfuerzo de implementación. Luego añade restricciones del sistema de archivos y avanza hacia builds herméticos a medida que la madurez de tu pipeline aumente. Cada capa que añadas hace que los ataques a la cadena de suministro sean significativamente más difíciles de ejecutar.