Descripción general
Los manifiestos de Kubernetes mal configurados son una de las principales causas de incidentes de seguridad en producción. Un contenedor ejecutándose como root, una etiqueta de imagen sin fijar, un límite de recursos faltante o una red de host expuesta pueden abrir la puerta a la escalada de privilegios, el agotamiento de recursos o el movimiento lateral dentro de tu clúster.
El problema es que estas configuraciones erróneas son invisibles hasta el momento del despliegue — o peor aún, hasta que un atacante las explota. La solución es desplazar la seguridad a la izquierda y detectar violaciones de políticas antes de que los manifiestos lleguen al clúster.
En este laboratorio práctico, utilizarás Conftest — un framework de pruebas construido sobre el motor Open Policy Agent (OPA) — para escribir políticas Rego que validen manifiestos de Kubernetes. Luego integrarás esas verificaciones en GitHub Actions y GitLab CI para que cada pull request sea analizado automáticamente en busca de violaciones.
Al finalizar este laboratorio tendrás:
- Una biblioteca de políticas Rego reutilizables que cubren etiquetas de imagen, contextos de seguridad, límites de recursos y acceso a nivel de host.
- Pruebas unitarias para esas políticas usando
opa test. - Pipelines de CI/CD funcionales que bloquean manifiestos inseguros y proporcionan mensajes de violación claros y accionables.
Requisitos previos
Antes de comenzar, asegúrate de tener las siguientes herramientas y conocimientos:
- conftest CLI instalado — instalar con Homebrew:
brew install conftestAlternativamente, descarga el binario desde la página de releases de Conftest.
- kubectl y un clúster de prueba (opcional) — si quieres verificar que tus manifiestos corregidos realmente se despliegan, crea un clúster local con
minikube startokind create cluster. - Un repositorio de prueba — crea un repositorio Git nuevo o usa uno existente. Construiremos todos los archivos desde cero.
- Conocimientos básicos de YAML y Kubernetes — deberías sentirte cómodo leyendo manifiestos de Deployment, Service y Pod.
Configuración del entorno
Comienza creando la estructura del proyecto y un conjunto de manifiestos de Kubernetes intencionalmente inseguros. Estos servirán como nuestros fixtures de prueba a lo largo de todos los ejercicios.
Estructura del proyecto
conftest-k8s-lab/
├── k8s/
│ ├── deployment-latest-tag.yaml
│ ├── deployment-run-as-root.yaml
│ ├── deployment-no-limits.yaml
│ ├── service-loadbalancer.yaml
│ └── pod-host-network.yaml
└── policy/
Crea los directorios:
mkdir -p conftest-k8s-lab/k8s conftest-k8s-lab/policy
cd conftest-k8s-lab
Manifiesto 1 — Deployment con etiqueta de imagen sin fijar
Crea k8s/deployment-latest-tag.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-latest
spec:
replicas: 1
selector:
matchLabels:
app: web-latest
template:
metadata:
labels:
app: web-latest
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
Este manifiesto usa nginx:latest, lo que significa que cada pull podría introducir silenciosamente un binario diferente en tu clúster.
Manifiesto 2 — Deployment ejecutándose como root
Crea k8s/deployment-run-as-root.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-root
spec:
replicas: 1
selector:
matchLabels:
app: web-root
template:
metadata:
labels:
app: web-root
spec:
containers:
- name: nginx
image: nginx:1.25.4
ports:
- containerPort: 80
No se ha establecido ningún securityContext, por lo que el contenedor se ejecuta como root por defecto — un vector de escalada de privilegios bien conocido.
Manifiesto 3 — Deployment sin límites de recursos
Crea k8s/deployment-no-limits.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-no-limits
spec:
replicas: 1
selector:
matchLabels:
app: web-no-limits
template:
metadata:
labels:
app: web-no-limits
spec:
containers:
- name: nginx
image: nginx:1.25.4
ports:
- containerPort: 80
Sin límites de CPU y memoria, un solo pod con mal comportamiento puede agotar los recursos de todo el nodo.
Manifiesto 4 — Service de tipo LoadBalancer
Crea k8s/service-loadbalancer.yaml:
apiVersion: v1
kind: Service
metadata:
name: web-lb
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 80
Un servicio LoadBalancer sin anotaciones puede exponer cargas de trabajo a la internet pública en entornos cloud.
Manifiesto 5 — Pod con acceso a la red del host
Crea k8s/pod-host-network.yaml:
apiVersion: v1
kind: Pod
metadata:
name: debug-pod
spec:
hostNetwork: true
containers:
- name: debug
image: busybox:1.36
command: ["sleep", "3600"]
hostNetwork: true otorga al pod acceso completo a la pila de red del nodo, eludiendo por completo las network policies.
Ejercicio 1: Escribe tu primera política Rego — Sin etiquetas latest
Tu primera política denegará cualquier imagen de contenedor que use la etiqueta :latest u omita una etiqueta por completo (lo cual también se resuelve como latest).
Paso 1 — Crear la política
Crea policy/tags.rego:
package main
import future.keywords.in
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
image := container.image
not contains(image, ":")
msg := sprintf("Container '%s' uses image '%s' without a tag. Pin to a specific version.", [container.name, image])
}
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
image := container.image
endswith(image, ":latest")
msg := sprintf("Container '%s' uses the ':latest' tag in image '%s'. Pin to a specific version.", [container.name, image])
}
Paso 2 — Ejecutar Conftest contra el manifiesto inseguro
conftest test k8s/deployment-latest-tag.yaml
Salida esperada:
FAIL - k8s/deployment-latest-tag.yaml - main - Container 'nginx' uses the ':latest' tag in image 'nginx:latest'. Pin to a specific version.
1 test, 0 passed, 0 warnings, 1 failure
Paso 3 — Corregir el manifiesto
Edita k8s/deployment-latest-tag.yaml y cambia la línea de imagen:
image: nginx:1.25.4
Ejecuta Conftest de nuevo:
conftest test k8s/deployment-latest-tag.yaml
Salida esperada:
1 test, 1 passed, 0 warnings, 0 failures
Entendiendo la estructura de Rego
Cada archivo de política Rego utilizado por Conftest sigue un patrón simple:
package main— Conftest busca el paquetemainpor defecto. Puedes sobrescribirlo con--namespace.deny[msg]— un conjunto de reglas. Si todas las condiciones dentro del cuerpo de la regla se evalúan como verdaderas, la regla se activa y añademsgal conjunto de violaciones.input— representa el documento YAML que se está probando. Conftest lo convierte automáticamente en un objeto JSON.sprintf— formatea un mensaje de error legible que aparece en los logs de CI.
Ejercicio 2: Sin contenedores ejecutándose como root
Los contenedores que se ejecutan como root pueden modificar el sistema de archivos, instalar paquetes y — si se combinan con un exploit del kernel — escapar al host. Esta política aplica dos controles: runAsNonRoot: true y allowPrivilegeEscalation: false.
Paso 1 — Crear la política
Crea policy/security_context.rego:
package main
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.securityContext.runAsNonRoot == true
msg := sprintf("Container '%s' must set securityContext.runAsNonRoot to true.", [container.name])
}
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.securityContext.allowPrivilegeEscalation == false
msg := sprintf("Container '%s' must set securityContext.allowPrivilegeEscalation to false.", [container.name])
}
Paso 2 — Probar contra el manifiesto inseguro
conftest test k8s/deployment-run-as-root.yaml
Salida esperada:
FAIL - k8s/deployment-run-as-root.yaml - main - Container 'nginx' must set securityContext.runAsNonRoot to true.
FAIL - k8s/deployment-run-as-root.yaml - main - Container 'nginx' must set securityContext.allowPrivilegeEscalation to false.
1 test, 0 passed, 0 warnings, 2 failures
Paso 3 — Corregir el manifiesto
Actualiza k8s/deployment-run-as-root.yaml para incluir un contexto de seguridad en cada contenedor:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-root
spec:
replicas: 1
selector:
matchLabels:
app: web-root
template:
metadata:
labels:
app: web-root
spec:
containers:
- name: nginx
image: nginx:1.25.4
ports:
- containerPort: 80
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
Ejecuta Conftest de nuevo — ambas reglas ahora pasan.
Ejercicio 3: Requerir límites de recursos
Sin límites de recursos, un solo contenedor puede consumir toda la CPU y memoria del nodo, causando fallos en cascada en cargas de trabajo no relacionadas. Muchos frameworks de cumplimiento (SOC 2, CIS Benchmarks) requieren límites explícitos.
Paso 1 — Crear la política
Crea policy/resources.rego:
package main
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.resources.limits.cpu
msg := sprintf("Container '%s' must define resources.limits.cpu.", [container.name])
}
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container '%s' must define resources.limits.memory.", [container.name])
}
Paso 2 — Probar contra el manifiesto inseguro
conftest test k8s/deployment-no-limits.yaml
Salida esperada:
FAIL - k8s/deployment-no-limits.yaml - main - Container 'nginx' must define resources.limits.cpu.
FAIL - k8s/deployment-no-limits.yaml - main - Container 'nginx' must define resources.limits.memory.
1 test, 0 passed, 0 warnings, 2 failures
Paso 3 — Corregir el manifiesto
Añade límites de recursos a k8s/deployment-no-limits.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-no-limits
spec:
replicas: 1
selector:
matchLabels:
app: web-no-limits
template:
metadata:
labels:
app: web-no-limits
spec:
containers:
- name: nginx
image: nginx:1.25.4
ports:
- containerPort: 80
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "250m"
memory: "256Mi"
Ejecuta Conftest de nuevo — las verificaciones de CPU y memoria pasan.
Ejercicio 4: Denegar acceso privilegiado al host
Los pods que solicitan acceso a nivel de host — hostNetwork, hostPID, hostIPC o un contexto de seguridad privilegiado — efectivamente se ejecutan fuera del sandbox del contenedor. Un pod comprometido con cualquiera de estos flags puede ver todo el tráfico del nodo, adjuntarse a otros procesos o escapar completamente al host.
Paso 1 — Crear la política
Crea policy/host_access.rego:
package main
deny[msg] {
input.kind == "Pod"
input.spec.hostNetwork == true
msg := sprintf("Pod '%s' must not use hostNetwork: true.", [input.metadata.name])
}
deny[msg] {
input.kind == "Pod"
input.spec.hostPID == true
msg := sprintf("Pod '%s' must not use hostPID: true.", [input.metadata.name])
}
deny[msg] {
input.kind == "Pod"
input.spec.hostIPC == true
msg := sprintf("Pod '%s' must not use hostIPC: true.", [input.metadata.name])
}
deny[msg] {
input.kind == "Pod"
container := input.spec.containers[_]
container.securityContext.privileged == true
msg := sprintf("Container '%s' in Pod '%s' must not run in privileged mode.", [container.name, input.metadata.name])
}
deny[msg] {
input.kind == "Deployment"
input.spec.template.spec.hostNetwork == true
msg := sprintf("Deployment '%s' must not use hostNetwork: true.", [input.metadata.name])
}
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
container.securityContext.privileged == true
msg := sprintf("Container '%s' in Deployment '%s' must not run in privileged mode.", [container.name, input.metadata.name])
}
Paso 2 — Probar contra el pod inseguro
conftest test k8s/pod-host-network.yaml
Salida esperada:
FAIL - k8s/pod-host-network.yaml - main - Pod 'debug-pod' must not use hostNetwork: true.
1 test, 0 passed, 0 warnings, 1 failure
Paso 3 — Corregir el manifiesto
Elimina la línea hostNetwork: true de k8s/pod-host-network.yaml:
apiVersion: v1
kind: Pod
metadata:
name: debug-pod
spec:
containers:
- name: debug
image: busybox:1.36
command: ["sleep", "3600"]
Ejecuta Conftest — el pod ahora pasa todas las verificaciones de acceso al host.
Ejercicio 5: Probar políticas con opa test
Las políticas son código, y el código necesita pruebas. Sin pruebas no puedes estar seguro de que una política detecta lo que debería o de que una futura refactorización no introduzca un falso positivo que bloquee despliegues legítimos.
Paso 1 — Crear casos de prueba
Crea policy/tags_test.rego:
package main
test_latest_denied {
input := {
"kind": "Deployment",
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "app",
"image": "nginx:latest"
}
]
}
}
}
}
count(deny) > 0
}
test_no_tag_denied {
input := {
"kind": "Deployment",
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "app",
"image": "nginx"
}
]
}
}
}
}
count(deny) > 0
}
test_pinned_allowed {
input := {
"kind": "Deployment",
"spec": {
"template": {
"spec": {
"containers": [
{
"name": "app",
"image": "nginx:1.25.4"
}
]
}
}
}
}
count(deny) == 0
}
Paso 2 — Ejecutar las pruebas
opa test policy/ -v
Salida esperada:
policy/tags_test.rego:
data.main.test_latest_denied: PASS (1.234ms)
data.main.test_no_tag_denied: PASS (0.567ms)
data.main.test_pinned_allowed: PASS (0.432ms)
--------------------------------------------------------------------------------
PASS: 3/3
Por qué importan las pruebas de políticas
En un contexto de CI/CD, un falso negativo significa que un manifiesto inseguro se cuela, mientras que un falso positivo bloquea un despliegue legítimo y erosiona la confianza de los desarrolladores en el pipeline. Al escribir casos de prueba explícitos tanto para entradas permitidas como denegadas, obtienes una suite de regresión que se ejecuta en milisegundos y garantiza que tus políticas se comportan correctamente a medida que crece la biblioteca de reglas.
Adopta el hábito de añadir un archivo *_test.rego por cada nuevo archivo de política. Ejecuta opa test policy/ -v como parte de tu pipeline de CI junto con conftest test.
Ejercicio 6: Integrar Conftest en GitHub Actions
Con las políticas escritas y probadas, el siguiente paso es conectarlas a tu pipeline de CI para que cada pull request sea validado automáticamente.
Paso 1 — Crear el workflow
Crea .github/workflows/policy-check.yml:
name: Kubernetes Policy Check
on:
pull_request:
paths:
- "k8s/**"
- "policy/**"
push:
branches: [main]
paths:
- "k8s/**"
- "policy/**"
jobs:
conftest:
name: Validate K8s Manifests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Conftest
run: |
CONFTEST_VERSION="0.56.0"
wget -q "https://github.com/open-policy-agent/conftest/releases/download/v${CONFTEST_VERSION}/conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz"
tar xzf "conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz"
sudo mv conftest /usr/local/bin/
conftest --version
- name: Install OPA
run: |
OPA_VERSION="v0.68.0"
curl -L -o opa "https://openpolicyagent.org/downloads/${OPA_VERSION}/opa_linux_amd64_static"
chmod +x opa
sudo mv opa /usr/local/bin/
opa version
- name: Run policy unit tests
run: opa test policy/ -v
- name: Run Conftest against all manifests
run: |
echo "Scanning all Kubernetes manifests in k8s/ ..."
FAILED=0
for file in k8s/*.yaml; do
echo ""
echo "--- Testing: $file ---"
if ! conftest test "$file" --policy policy/; then
FAILED=1
fi
done
if [ "$FAILED" -eq 1 ]; then
echo ""
echo "❌ One or more manifests violated policy. Fix the issues above."
exit 1
fi
echo ""
echo "✅ All manifests passed policy checks."
Paso 2 — Observar un PR fallido
Sube una rama que contenga los manifiestos inseguros originales. La salida del pipeline se verá así:
--- Testing: k8s/deployment-latest-tag.yaml ---
FAIL - k8s/deployment-latest-tag.yaml - main - Container 'nginx' uses the ':latest' tag in image 'nginx:latest'. Pin to a specific version.
--- Testing: k8s/deployment-run-as-root.yaml ---
FAIL - k8s/deployment-run-as-root.yaml - main - Container 'nginx' must set securityContext.runAsNonRoot to true.
FAIL - k8s/deployment-run-as-root.yaml - main - Container 'nginx' must set securityContext.allowPrivilegeEscalation to false.
--- Testing: k8s/pod-host-network.yaml ---
FAIL - k8s/pod-host-network.yaml - main - Pod 'debug-pod' must not use hostNetwork: true.
❌ One or more manifests violated policy. Fix the issues above.
Error: Process completed with exit code 1.
La verificación de estado del PR se pone en rojo con mensajes de violación claros que le indican al desarrollador exactamente qué corregir y dónde.
Paso 3 — Observar un PR exitoso
Corrige todos los manifiestos como se muestra en los ejercicios anteriores, sube de nuevo, y el pipeline pasa:
--- Testing: k8s/deployment-latest-tag.yaml ---
1 test, 1 passed, 0 warnings, 0 failures
--- Testing: k8s/deployment-run-as-root.yaml ---
1 test, 1 passed, 0 warnings, 0 failures
--- Testing: k8s/deployment-no-limits.yaml ---
1 test, 1 passed, 0 warnings, 0 failures
✅ All manifests passed policy checks.
Ejercicio 7: Integrar Conftest en GitLab CI
Si tu equipo usa GitLab, la integración es igual de sencilla. Añade el siguiente job a tu .gitlab-ci.yml:
Configuración completa funcional
stages:
- validate
conftest-policy-check:
stage: validate
image: alpine:3.19
variables:
CONFTEST_VERSION: "0.56.0"
OPA_VERSION: "v0.68.0"
before_script:
- apk add --no-cache curl wget tar
- wget -q "https://github.com/open-policy-agent/conftest/releases/download/v${CONFTEST_VERSION}/conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz"
- tar xzf "conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz"
- mv conftest /usr/local/bin/
- curl -L -o /usr/local/bin/opa "https://openpolicyagent.org/downloads/${OPA_VERSION}/opa_linux_amd64_static"
- chmod +x /usr/local/bin/opa
script:
- echo "Running policy unit tests..."
- opa test policy/ -v
- echo "Running Conftest against all manifests..."
- |
FAILED=0
for file in k8s/*.yaml; do
echo ""
echo "--- Testing: $file ---"
if ! conftest test "$file" --policy policy/; then
FAILED=1
fi
done
if [ "$FAILED" -eq 1 ]; then
echo ""
echo "One or more manifests violated policy."
exit 1
fi
echo ""
echo "All manifests passed policy checks."
rules:
- changes:
- k8s/**/*
- policy/**/*
when: always
Comportamiento de éxito/fallo
El comportamiento refleja exactamente el workflow de GitHub Actions. Cuando hay manifiestos inseguros presentes, el job falla con mensajes de violación. Cuando todos los manifiestos cumplen, el job pasa con un resumen limpio. El bloque rules asegura que el job solo se ejecute cuando cambian los manifiestos de Kubernetes o los archivos de políticas, manteniendo el tiempo de ejecución del pipeline al mínimo.
Avanzado: Advertencias vs. Denegaciones
No toda violación de política debería bloquear un despliegue. Algunas son recomendaciones — mejores prácticas que quieres mostrar sin romper el pipeline. Conftest soporta esta distinción a través de reglas warn.
Cómo funciona
deny[msg]— una puerta rígida. Si cualquier regla deny se activa,conftest testsale con un código distinto de cero y el pipeline falla.warn[msg]— un aviso. El mensaje se imprime pero el código de salida permanece en cero, por lo que el pipeline pasa.conftest test --fail-on-warn— opcionalmente promueve todas las advertencias a fallos. Útil cuando quieres endurecer gradualmente las políticas: empieza conwarn, y una vez que los equipos hayan corregido las violaciones existentes, cambia adenyo habilita--fail-on-warn.
Crear una política de asesoramiento
Crea policy/recommendations.rego:
package main
warn[msg] {
input.kind == "Service"
input.spec.type == "LoadBalancer"
not input.metadata.annotations
msg := sprintf("Service '%s' is of type LoadBalancer with no annotations. Consider adding cloud-provider-specific annotations for internal load balancers.", [input.metadata.name])
}
warn[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.readinessProbe
msg := sprintf("Container '%s' has no readinessProbe. Add one so Kubernetes can route traffic only to healthy pods.", [container.name])
}
warn[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.livenessProbe
msg := sprintf("Container '%s' has no livenessProbe. Add one so Kubernetes can restart unhealthy pods.", [container.name])
}
Probar la política de asesoramiento
conftest test k8s/service-loadbalancer.yaml
Salida esperada:
WARN - k8s/service-loadbalancer.yaml - main - Service 'web-lb' is of type LoadBalancer with no annotations. Consider adding cloud-provider-specific annotations for internal load balancers.
1 test, 1 passed, 1 warning, 0 failures
Nota: el código de salida es 0 — el pipeline sigue pasando. Si quieres forzar las advertencias:
conftest test k8s/service-loadbalancer.yaml --fail-on-warn
Ahora el código de salida es 1 y el pipeline fallaría.
Este patrón te permite desplegar nuevas políticas gradualmente: intróducelas como advertencias, da tiempo a los equipos para remediar, y luego promócionalas a denegaciones.
Limpieza
Cuando termines el laboratorio, elimina los recursos de prueba:
# Remove the lab directory
rm -rf conftest-k8s-lab
# If you deployed any fixed manifests to a test cluster
kubectl delete -f k8s/ --ignore-not-found
# If you created a kind cluster for this lab
kind delete cluster --name conftest-lab
Conclusiones clave
- Desplaza la seguridad a la izquierda de forma agresiva. Detectar un manifiesto mal configurado en un pull request es órdenes de magnitud más barato que descubrirlo después de una brecha de seguridad.
- Conftest + Rego es un punto de entrada ligero a policy-as-code. No necesitas un servidor OPA completo ni una instalación de Gatekeeper para empezar a aplicar políticas — un solo binario CLI y unos pocos archivos Rego son suficientes.
- Prueba tus políticas como código de aplicación. Usa
opa testcon casos de prueba explícitos positivos y negativos para prevenir regresiones en tu biblioteca de reglas. - Usa advertencias para despliegues graduales. Empieza las nuevas políticas como reglas
warn, socialízalas con el equipo, y promócionalas adenyuna vez que se resuelvan las violaciones existentes. - Los mensajes de error accionables son críticos. Usa
sprintfen cada regla para indicar al desarrollador qué contenedor, qué campo y qué hacer al respecto. Los mensajes genéricos de “política violada” erosionan la confianza en las puertas de CI. - Mantén las políticas en el mismo repositorio que los manifiestos. Co-ubicar
policy/conk8s/significa que los cambios de políticas pasan por el mismo proceso de revisión que los cambios de infraestructura.
Próximos pasos
Ahora que tienes un pipeline de Conftest funcional, continúa construyendo tu práctica de policy-as-code:
- Policy as Code para CI/CD: OPA y Rego — profundiza en el lenguaje Rego, aprende sobre importación de datos, gestión de bundles y registro de decisiones para pistas de auditoría.
- Patrones Defensivos y Mitigaciones — explora el panorama más amplio del hardening de pipelines de CI/CD, desde la gestión de secretos hasta la firma de artefactos y la aplicación en tiempo de ejecución.