Restricciones de Red y Sistema de Archivos para Entornos de Build CI/CD

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:

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.