Descripción general
Los runners alojados en GitHub son compartidos y efímeros por defecto — cada trabajo obtiene una máquina virtual nueva que se destruye después de que el trabajo se completa. Los runners autoalojados, por otro lado, son persistentes y compartidos entre ejecuciones de flujos de trabajo. Esto crea un riesgo de seguridad significativo: secretos, tokens y artefactos de compilación de un trabajo pueden filtrarse al siguiente. Un flujo de trabajo comprometido puede envenenar el entorno del runner para todos los trabajos futuros.
Actions Runner Controller (ARC) resuelve este problema. ARC es un operador nativo de Kubernetes que te proporciona runners autoalojados efímeros, autoescalables y basados en contenedores. Cada trabajo obtiene un pod nuevo que se destruye cuando el trabajo se completa — igual que los runners alojados en GitHub, pero ejecutándose en tu propia infraestructura con tus propias herramientas y políticas de red.
En este laboratorio práctico, vas a:
- Desplegar ARC en un clúster local de Kubernetes
- Configurar runner scale sets efímeros
- Demostrar el aislamiento entre trabajos (el beneficio de seguridad principal)
- Construir imágenes personalizadas de runners
- Implementar aislamiento de grupos de runners para separación de funciones
- Configurar autoescalado
- Aplicar políticas de red para restringir el acceso de red de los runners
Requisitos previos
Antes de comenzar este laboratorio, asegúrate de tener lo siguiente:
- Clúster de Kubernetes — kind, minikube, o un clúster gestionado en la nube (EKS, GKE, AKS)
- Helm 3 — Instalar desde helm.sh
- kubectl — Configurado para comunicarse con tu clúster
- Cuenta de GitHub — Con acceso de administrador a un repositorio u organización
- GitHub App o Personal Access Token (PAT) — Con los alcances
repoyadmin:org(PAT) o los permisos apropiados de GitHub App - Docker — Para construir imágenes personalizadas de runners (Ejercicio 4)
Configuración del entorno
Usaremos kind (Kubernetes in Docker) para crear un clúster local. Esto mantiene el laboratorio autocontenido y fácil de limpiar.
Crear un clúster kind
kind create cluster --name arc-lab
Verifica que el clúster esté ejecutándose:
kubectl cluster-info --context kind-arc-lab
Crear un repositorio de prueba en GitHub
Crea un nuevo repositorio (por ejemplo, arc-lab-test) en tu cuenta de GitHub. Añade un archivo de flujo de trabajo simple en .github/workflows/test.yml:
name: ARC Test Workflow
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Hello from GitHub-hosted runner
run: echo "This runs on a GitHub-hosted runner"
Haz push de esto a tu repositorio. Lo modificaremos más adelante para apuntar a los runners de ARC.
Ejercicio 1: Instalar ARC con Helm
Actions Runner Controller v2 usa charts de Helm para desplegar dos componentes: un controlador que gestiona el ciclo de vida de los pods de runners, y uno o más runner scale sets que se registran con GitHub y aceptan trabajos.
Paso 1: Añadir el repositorio de Helm
helm repo add actions-runner-controller \
https://actions-runner-controller.github.io/actions-runner-controller
helm repo update
Paso 2: Configurar la autenticación
ARC necesita autenticarse con la API de GitHub. Tienes dos opciones:
Opción A: GitHub App (Recomendado para producción)
Crea una GitHub App en la configuración de tu organización o cuenta:
- Ve a Settings → Developer settings → GitHub Apps → New GitHub App
- Establece los siguientes permisos:
- Repository:
Actions(read),Administration(read/write),Metadata(read) - Organization:
Self-hosted runners(read/write)
- Repository:
- Genera una clave privada y descárgala
- Instala la App en tu organización o repositorio
- Anota el App ID y el Installation ID
Opción B: Personal Access Token (Más simple para laboratorios)
Crea un PAT (clásico) con los alcances repo y admin:org, o un PAT de grano fino con permisos de Actions y Administration. Para este laboratorio, usaremos un PAT por simplicidad.
Paso 3: Instalar el controlador ARC
helm install arc \
actions-runner-controller/gha-runner-scale-set-controller \
--namespace arc-systems \
--create-namespace
Verifica que el controlador esté ejecutándose:
kubectl get pods -n arc-systems
Deberías ver una salida similar a:
NAME READY STATUS RESTARTS AGE
arc-gha-runner-scale-set-controller-xxx 1/1 Running 0 30s
Paso 4: Instalar un Runner Scale Set
Ahora despliega un runner scale set que se registre con tu repositorio de GitHub:
helm install arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
--create-namespace \
--set githubConfigUrl="https://github.com/<org>/<repo>" \
--set githubConfigSecret.github_token="<PAT>"
Reemplaza <org>/<repo> con la ruta de tu repositorio y <PAT> con tu personal access token.
Verifica el runner scale set:
kubectl get pods -n arc-runners
En este punto, puede que no haya pods de runners todavía — ARC utiliza un modelo de escalar a cero. Los pods se crean solo cuando se encolan trabajos.
Paso 5: Verificar en GitHub
Navega a tu repositorio en GitHub: Settings → Actions → Runners. Deberías ver el runner scale set listado con el nombre arc-runner-set. El estado muestra que está listo para aceptar trabajos.
Ejercicio 2: Ejecutar un flujo de trabajo en runners de ARC
Ahora actualiza el flujo de trabajo de prueba para apuntar al runner scale set de ARC en lugar de los runners alojados en GitHub.
Paso 1: Actualizar el flujo de trabajo
Modifica .github/workflows/test.yml para usar la etiqueta del runner de ARC:
name: ARC Test Workflow
on:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: arc-runner-set
steps:
- name: Hello from ARC runner
run: |
echo "This runs on an ephemeral ARC runner!"
echo "Hostname: $(hostname)"
echo "Runner OS: $(uname -a)"
- name: Show environment
run: env | sort
El cambio clave es runs-on: arc-runner-set — esto coincide con el nombre de la release de Helm para el runner scale set.
Paso 2: Activar el flujo de trabajo
Haz push del archivo de flujo de trabajo actualizado o usa el botón «Run workflow» (workflow_dispatch) en la interfaz de GitHub Actions.
Paso 3: Observar el pod del runner
Observa el namespace arc-runners mientras se ejecuta el flujo de trabajo:
kubectl get pods -n arc-runners -w
Verás un pod creado para el trabajo:
NAME READY STATUS RESTARTS AGE
arc-runner-set-xxxxx-runner 1/1 Running 0 5s
Después de que el trabajo se complete, el pod se termina y se elimina:
NAME READY STATUS RESTARTS AGE
arc-runner-set-xxxxx-runner 0/1 Completed 0 45s
Ejecuta kubectl get pods -n arc-runners de nuevo — el pod ya no está. Este es el modelo efímero: cada trabajo obtiene un contenedor nuevo, y el contenedor se destruye cuando el trabajo termina. No hay persistencia de estado entre trabajos.
Ejercicio 3: Demostrar la seguridad efímera
Este ejercicio demuestra el beneficio de seguridad principal de los runners efímeros: no hay contaminación entre trabajos.
Paso 1: Crear un flujo de trabajo que escriba datos sensibles
Crea .github/workflows/ephemeral-test.yml:
name: Ephemeral Security Test
on: workflow_dispatch
jobs:
write-secret:
runs-on: arc-runner-set
steps:
- name: Write sensitive data
run: |
echo "SECRET_API_KEY=sk-prod-abc123xyz" > /tmp/secret-data
echo "DB_PASSWORD=super-secret-password" >> /tmp/secret-data
echo "Written sensitive data to /tmp/secret-data"
cat /tmp/secret-data
read-secret:
runs-on: arc-runner-set
needs: write-secret
steps:
- name: Attempt to read previous job data
run: |
echo "Checking if /tmp/secret-data exists from previous job..."
if [ -f /tmp/secret-data ]; then
echo "SECURITY RISK: Found data from previous job!"
cat /tmp/secret-data
else
echo "SECURE: /tmp/secret-data does not exist."
echo "Each job gets a fresh container — no cross-job contamination."
fi
Paso 2: Ejecutar el flujo de trabajo
Activa el flujo de trabajo mediante workflow_dispatch. El primer trabajo (write-secret) escribe datos sensibles en /tmp/secret-data. El segundo trabajo (read-secret) se ejecuta en un nuevo pod e intenta leer ese archivo.
Paso 3: Verificar los resultados
En los logs de GitHub Actions, verás:
- Trabajo write-secret: Escribe exitosamente el archivo e imprime el contenido
- Trabajo read-secret: El archivo no existe — la salida muestra
SECURE: /tmp/secret-data does not exist.
Cada trabajo se ejecutó en un pod separado, recién creado. Cuando el pod de write-secret fue destruido, todos los datos — incluyendo el archivo sensible — fueron destruidos con él.
Por qué esto importa
En un runner autoalojado persistente, el archivo /tmp/secret-data seguiría en disco cuando se ejecuta el segundo trabajo. Un flujo de trabajo malicioso en un pull request podría leer secretos, tokens o credenciales dejados por trabajos anteriores. Con runners efímeros, este vector de ataque se elimina.
Ejercicio 4: Imágenes personalizadas de runners
Los runners de ARC usan una imagen de contenedor base. Para uso en el mundo real, necesitas personalizar esta imagen para incluir tus herramientas de compilación.
Paso 1: Crear un Dockerfile personalizado
Crea un Dockerfile para tu runner personalizado:
FROM ghcr.io/actions/actions-runner:latest
USER root
# Install build tools
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
jq \
unzip \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install Go
RUN wget -q https://go.dev/dl/go1.22.4.linux-amd64.tar.gz \
&& tar -C /usr/local -xzf go1.22.4.linux-amd64.tar.gz \
&& rm go1.22.4.linux-amd64.tar.gz
ENV PATH="$PATH:/usr/local/go/bin"
# Install cosign
RUN curl -sSL -o /usr/local/bin/cosign \
https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \
&& chmod +x /usr/local/bin/cosign
# Install Docker CLI (for Docker-in-Docker workflows)
RUN curl -fsSL https://get.docker.com | sh
USER runner
Paso 2: Construir y subir la imagen
# Build the image
docker build -t ghcr.io/<org>/custom-runner:latest .
# Authenticate to GitHub Container Registry
echo "<PAT>" | docker login ghcr.io -u <username> --password-stdin
# Push the image
docker push ghcr.io/<org>/custom-runner:latest
Paso 3: Configurar ARC para usar la imagen personalizada
Crea un archivo de valores custom-runner-values.yaml:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
Actualiza el runner scale set con la imagen personalizada:
helm upgrade arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f custom-runner-values.yaml
Paso 4: Verificar las herramientas personalizadas
Crea un flujo de trabajo que use las herramientas personalizadas:
name: Custom Runner Tools Test
on: workflow_dispatch
jobs:
verify-tools:
runs-on: arc-runner-set
steps:
- name: Verify Go
run: go version
- name: Verify cosign
run: cosign version
- name: Verify Docker CLI
run: docker --version
Beneficio de seguridad: Al construir tu propia imagen de runner, controlas exactamente qué herramientas y dependencias están presentes en el entorno de compilación. No hay binarios inesperados, no hay software preinstalado que no hayas aprobado, y puedes fijar cada herramienta a una versión específica. También puedes escanear la imagen en busca de vulnerabilidades antes de desplegarla.
Ejercicio 5: Aislamiento de grupos de runners
Los diferentes flujos de trabajo tienen diferentes niveles de confianza. La validación de pull requests no debería tener acceso a secretos de producción. Los flujos de trabajo de despliegue necesitan secretos pero solo deberían ejecutarse desde la rama main. ARC te permite implementar esta separación creando runner scale sets distintos con diferentes etiquetas y configuraciones.
Paso 1: Crear un Runner Scale Set para validación de PR
Crea pr-runner-values.yaml:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
env:
- name: RUNNER_GROUP
value: "pr-validation"
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "1Gi"
helm install arc-runner-pr \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f pr-runner-values.yaml
Paso 2: Crear un Runner Scale Set para despliegue
Crea deploy-runner-values.yaml:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
template:
spec:
containers:
- name: runner
image: ghcr.io/<org>/custom-runner:latest
command: ["/home/runner/run.sh"]
env:
- name: RUNNER_GROUP
value: "deployment"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
serviceAccountName: deploy-runner-sa
nodeSelector:
runner-type: deployment
helm install arc-runner-deploy \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f deploy-runner-values.yaml
Paso 3: Configurar flujos de trabajo para aislamiento
Usa diferentes etiquetas de runner según el activador del flujo de trabajo:
name: CI/CD Pipeline
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
validate:
if: github.event_name == 'pull_request'
runs-on: arc-runner-pr
steps:
- uses: actions/checkout@v4
- name: Run tests
run: make test
- name: Run linter
run: make lint
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: arc-runner-deploy
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: make deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Esto implementa separación de funciones a nivel de runner. Los trabajos de validación de PR se ejecutan en runners que no tienen acceso a secretos de despliegue ni a segmentos de red privilegiados. Los trabajos de despliegue se ejecutan en un conjunto separado de runners que tienen las credenciales y acceso de red necesarios, pero solo se activan con pushes a main.
Ejercicio 6: Autoescalado
ARC soporta autoescalado de forma nativa. Los pods de runners se crean bajo demanda y se destruyen cuando están inactivos. Puedes configurar réplicas mínimas y máximas para controlar el costo y la capacidad de respuesta.
Paso 1: Configurar parámetros de autoescalado
Actualiza tu archivo de valores del runner scale set para incluir parámetros de escalado:
githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
github_token: "<PAT>"
minRunners: 0
maxRunners: 10
template:
spec:
containers:
- name: runner
image: ghcr.io/actions/actions-runner:latest
command: ["/home/runner/run.sh"]
helm upgrade arc-runner-set \
actions-runner-controller/gha-runner-scale-set \
--namespace arc-runners \
-f autoscale-values.yaml
Paso 2: Generar carga
Crea un flujo de trabajo que active múltiples trabajos en paralelo:
name: Autoscale Test
on: workflow_dispatch
jobs:
parallel-job:
runs-on: arc-runner-set
strategy:
matrix:
id: [1, 2, 3, 4, 5]
steps:
- name: Simulate work
run: |
echo "Job ${{ matrix.id }} running on $(hostname)"
sleep 60
Activa este flujo de trabajo y observa cómo los pods escalan:
kubectl get pods -n arc-runners -w
Verás cinco pods creados — uno para cada trabajo de la matriz:
NAME READY STATUS RESTARTS AGE
arc-runner-set-abcde-runner 1/1 Running 0 5s
arc-runner-set-fghij-runner 1/1 Running 0 5s
arc-runner-set-klmno-runner 1/1 Running 0 5s
arc-runner-set-pqrst-runner 1/1 Running 0 5s
arc-runner-set-uvwxy-runner 1/1 Running 0 5s
Después de que los trabajos se completen (60 segundos), todos los pods se terminan. El namespace vuelve a cero pods.
Paso 3: Configurar el retraso de reducción de escala
Para optimización de costos, puede que quieras que los pods permanezcan activos por un corto período después de que un trabajo se complete. Esto evita la latencia de arranque en frío para cargas de trabajo intermitentes. El comportamiento de escalar a cero de ARC es la opción predeterminada y más segura. Si necesitas runners en caliente, mantén la ventana corta (menos de 5 minutos) y asegúrate de que el modo efímero siga aplicándose.
Ejercicio 7: Políticas de red para runners
Las NetworkPolicies de Kubernetes te permiten restringir el acceso de red de los pods de runners. Esta es una defensa crítica contra la exfiltración de datos desde compilaciones comprometidas.
Paso 1: Crear una NetworkPolicy
Aplica la siguiente NetworkPolicy al namespace arc-runners:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: runner-egress-policy
namespace: arc-runners
spec:
podSelector: {}
policyTypes:
- Egress
egress:
# Allow DNS resolution
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow GitHub API and Actions services
- to:
- ipBlock:
cidr: 140.82.112.0/20
- ipBlock:
cidr: 143.55.64.0/20
- ipBlock:
cidr: 185.199.108.0/22
- ipBlock:
cidr: 4.0.0.0/8
ports:
- protocol: TCP
port: 443
# Allow your container registry (example: ghcr.io)
- to:
- ipBlock:
cidr: 140.82.112.0/20
ports:
- protocol: TCP
port: 443
# Allow your artifact storage (replace with your CIDR)
# - to:
# - ipBlock:
# cidr: 10.0.0.0/8
# ports:
# - protocol: TCP
# port: 443
kubectl apply -f runner-network-policy.yaml
Nota: GitHub publica sus rangos de IP en https://api.github.com/meta. Usa los rangos de actions y api. Los CIDRs anteriores son ejemplos — verifica los rangos actuales y actualízalos en consecuencia.
Paso 2: Probar la NetworkPolicy
Crea un flujo de trabajo que intente alcanzar una URL externa:
name: Network Policy Test
on: workflow_dispatch
jobs:
test-network:
runs-on: arc-runner-set
steps:
- name: Test GitHub API (should work)
run: curl -s -o /dev/null -w "%{http_code}" https://api.github.com
- name: Test external URL (should be blocked)
run: |
if curl -s --connect-timeout 5 https://evil-exfiltration-server.example.com; then
echo "FAIL: External access was allowed"
exit 1
else
echo "PASS: External access was blocked by NetworkPolicy"
fi
Cuando ejecutes este flujo de trabajo:
- La solicitud a la API de GitHub tiene éxito (HTTP 200) porque la NetworkPolicy permite el tráfico a los rangos de IP de GitHub.
- La solicitud a la URL externa agota el tiempo y falla porque no está en la lista de egreso permitido.
Esto previene que una compilación comprometida exfiltre código fuente, secretos o artefactos de compilación a un servidor controlado por un atacante. Incluso si una dependencia maliciosa ejecuta código arbitrario durante la compilación, no puede comunicarse con el exterior.
Limpieza
Elimina todos los recursos creados durante este laboratorio:
# Delete Helm releases
helm uninstall arc-runner-set -n arc-runners
helm uninstall arc-runner-pr -n arc-runners
helm uninstall arc-runner-deploy -n arc-runners
helm uninstall arc -n arc-systems
# Delete namespaces
kubectl delete namespace arc-runners
kubectl delete namespace arc-systems
# Delete the kind cluster
kind delete cluster --name arc-lab
Si creaste una GitHub App para este laboratorio, puedes eliminarla desde Settings → Developer settings → GitHub Apps. Revoca cualquier PAT que hayas creado.
Conclusiones clave
- Los runners efímeros eliminan la contaminación entre trabajos. Cada trabajo obtiene un contenedor nuevo — secretos, tokens y artefactos de compilación se destruyen cuando el trabajo se completa.
- ARC proporciona los beneficios de runners autoalojados sin los riesgos de seguridad. Obtienes herramientas personalizadas, acceso a red privada y control de costos mientras mantienes el modelo de seguridad efímero.
- Las imágenes personalizadas de runners te dan control total sobre el entorno de compilación. Fija versiones de herramientas, escanea vulnerabilidades y elimina el riesgo de cadena de suministro del software preinstalado.
- El aislamiento de grupos de runners implementa la separación de funciones. Los flujos de trabajo de validación de PR y despliegue se ejecutan en conjuntos de runners separados con diferentes privilegios y acceso de red.
- Las políticas de red son una capa crítica de defensa. Restringir el egreso de los runners previene la exfiltración de datos incluso si un paso de compilación está comprometido.
- El autoescalado a cero reduce costos y superficie de ataque. Los pods de runners existen solo durante la duración de un trabajo — no hay infraestructura persistente que mantener o asegurar.
Próximos pasos
Continúa fortaleciendo tu postura de seguridad CI/CD con estas guías relacionadas:
- Securizar los runners de GitHub Actions — Guía detallada sobre las mejores prácticas de seguridad de runners, gestión de tokens y monitoreo tanto para runners alojados en GitHub como autoalojados.
- Separación de funciones y privilegio mínimo en pipelines CI/CD — Guía completa para implementar principios de privilegio mínimo en todo tu pipeline CI/CD, desde el control de código fuente hasta el despliegue en producción.