{"id":699,"date":"2026-03-09T22:57:56","date_gmt":"2026-03-09T21:57:56","guid":{"rendered":"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-enforcing-kubernetes-policies-opa-conftest-ci-cd-2\/"},"modified":"2026-03-25T05:20:18","modified_gmt":"2026-03-25T04:20:18","slug":"lab-enforcing-kubernetes-policies-opa-conftest-ci-cd-2","status":"publish","type":"post","link":"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/lab-enforcing-kubernetes-policies-opa-conftest-ci-cd-2\/","title":{"rendered":"Lab: Aplicar Pol\u00edticas de Despliegue en Kubernetes con OPA Conftest en CI\/CD"},"content":{"rendered":"<h2>Descripci\u00f3n general<\/h2>\n<p>Los manifiestos de Kubernetes mal configurados son una de las principales causas de incidentes de seguridad en producci\u00f3n. Un contenedor ejecut\u00e1ndose como root, una etiqueta de imagen sin fijar, un l\u00edmite 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\u00faster.<\/p>\n<p>El problema es que estas configuraciones err\u00f3neas son invisibles hasta el momento del despliegue \u2014 o peor a\u00fan, hasta que un atacante las explota. La soluci\u00f3n es desplazar la seguridad a la izquierda y detectar violaciones de pol\u00edticas <strong>antes<\/strong> de que los manifiestos lleguen al cl\u00faster.<\/p>\n<p>En este laboratorio pr\u00e1ctico, utilizar\u00e1s <strong>Conftest<\/strong> \u2014 un framework de pruebas construido sobre el motor Open Policy Agent (OPA) \u2014 para escribir pol\u00edticas Rego que validen manifiestos de Kubernetes. Luego integrar\u00e1s esas verificaciones en GitHub Actions y GitLab CI para que cada pull request sea analizado autom\u00e1ticamente en busca de violaciones.<\/p>\n<p>Al finalizar este laboratorio tendr\u00e1s:<\/p>\n<ul>\n<li>Una biblioteca de pol\u00edticas Rego reutilizables que cubren etiquetas de imagen, contextos de seguridad, l\u00edmites de recursos y acceso a nivel de host.<\/li>\n<li>Pruebas unitarias para esas pol\u00edticas usando <code>opa test<\/code>.<\/li>\n<li>Pipelines de CI\/CD funcionales que bloquean manifiestos inseguros y proporcionan mensajes de violaci\u00f3n claros y accionables.<\/li>\n<\/ul>\n<h2>Requisitos previos<\/h2>\n<p>Antes de comenzar, aseg\u00farate de tener las siguientes herramientas y conocimientos:<\/p>\n<ul>\n<li><strong>conftest CLI instalado<\/strong> \u2014 instalar con Homebrew:\n<pre><code>brew install conftest<\/code><\/pre>\n<p>Alternativamente, descarga el binario desde la <a href=\"https:\/\/github.com\/open-policy-agent\/conftest\/releases\" target=\"_blank\" rel=\"noopener\">p\u00e1gina de releases de Conftest<\/a>.<\/li>\n<li><strong>kubectl y un cl\u00faster de prueba (opcional)<\/strong> \u2014 si quieres verificar que tus manifiestos corregidos realmente se despliegan, crea un cl\u00faster local con <code>minikube start<\/code> o <code>kind create cluster<\/code>.<\/li>\n<li><strong>Un repositorio de prueba<\/strong> \u2014 crea un repositorio Git nuevo o usa uno existente. Construiremos todos los archivos desde cero.<\/li>\n<li><strong>Conocimientos b\u00e1sicos de YAML y Kubernetes<\/strong> \u2014 deber\u00edas sentirte c\u00f3modo leyendo manifiestos de Deployment, Service y Pod.<\/li>\n<\/ul>\n<h2>Configuraci\u00f3n del entorno<\/h2>\n<p>Comienza creando la estructura del proyecto y un conjunto de manifiestos de Kubernetes intencionalmente inseguros. Estos servir\u00e1n como nuestros fixtures de prueba a lo largo de todos los ejercicios.<\/p>\n<h3>Estructura del proyecto<\/h3>\n<pre><code>conftest-k8s-lab\/\n\u251c\u2500\u2500 k8s\/\n\u2502   \u251c\u2500\u2500 deployment-latest-tag.yaml\n\u2502   \u251c\u2500\u2500 deployment-run-as-root.yaml\n\u2502   \u251c\u2500\u2500 deployment-no-limits.yaml\n\u2502   \u251c\u2500\u2500 service-loadbalancer.yaml\n\u2502   \u2514\u2500\u2500 pod-host-network.yaml\n\u2514\u2500\u2500 policy\/<\/code><\/pre>\n<p>Crea los directorios:<\/p>\n<pre><code>mkdir -p conftest-k8s-lab\/k8s conftest-k8s-lab\/policy\ncd conftest-k8s-lab<\/code><\/pre>\n<h3>Manifiesto 1 \u2014 Deployment con etiqueta de imagen sin fijar<\/h3>\n<p>Crea <code>k8s\/deployment-latest-tag.yaml<\/code>:<\/p>\n<pre><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: web-latest\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: web-latest\n  template:\n    metadata:\n      labels:\n        app: web-latest\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:latest\n          ports:\n            - containerPort: 80<\/code><\/pre>\n<p>Este manifiesto usa <code>nginx:latest<\/code>, lo que significa que cada pull podr\u00eda introducir silenciosamente un binario diferente en tu cl\u00faster.<\/p>\n<h3>Manifiesto 2 \u2014 Deployment ejecut\u00e1ndose como root<\/h3>\n<p>Crea <code>k8s\/deployment-run-as-root.yaml<\/code>:<\/p>\n<pre><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: web-root\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: web-root\n  template:\n    metadata:\n      labels:\n        app: web-root\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:1.25.4\n          ports:\n            - containerPort: 80<\/code><\/pre>\n<p>No se ha establecido ning\u00fan <code>securityContext<\/code>, por lo que el contenedor se ejecuta como root por defecto \u2014 un vector de escalada de privilegios bien conocido.<\/p>\n<h3>Manifiesto 3 \u2014 Deployment sin l\u00edmites de recursos<\/h3>\n<p>Crea <code>k8s\/deployment-no-limits.yaml<\/code>:<\/p>\n<pre><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: web-no-limits\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: web-no-limits\n  template:\n    metadata:\n      labels:\n        app: web-no-limits\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:1.25.4\n          ports:\n            - containerPort: 80<\/code><\/pre>\n<p>Sin l\u00edmites de CPU y memoria, un solo pod con mal comportamiento puede agotar los recursos de todo el nodo.<\/p>\n<h3>Manifiesto 4 \u2014 Service de tipo LoadBalancer<\/h3>\n<p>Crea <code>k8s\/service-loadbalancer.yaml<\/code>:<\/p>\n<pre><code>apiVersion: v1\nkind: Service\nmetadata:\n  name: web-lb\nspec:\n  type: LoadBalancer\n  selector:\n    app: web\n  ports:\n    - port: 80\n      targetPort: 80<\/code><\/pre>\n<p>Un servicio LoadBalancer sin anotaciones puede exponer cargas de trabajo a la internet p\u00fablica en entornos cloud.<\/p>\n<h3>Manifiesto 5 \u2014 Pod con acceso a la red del host<\/h3>\n<p>Crea <code>k8s\/pod-host-network.yaml<\/code>:<\/p>\n<pre><code>apiVersion: v1\nkind: Pod\nmetadata:\n  name: debug-pod\nspec:\n  hostNetwork: true\n  containers:\n    - name: debug\n      image: busybox:1.36\n      command: [\"sleep\", \"3600\"]<\/code><\/pre>\n<p><code>hostNetwork: true<\/code> otorga al pod acceso completo a la pila de red del nodo, eludiendo por completo las network policies.<\/p>\n<h2>Ejercicio 1: Escribe tu primera pol\u00edtica Rego \u2014 Sin etiquetas latest<\/h2>\n<p>Tu primera pol\u00edtica denegar\u00e1 cualquier imagen de contenedor que use la etiqueta <code>:latest<\/code> u omita una etiqueta por completo (lo cual tambi\u00e9n se resuelve como <code>latest<\/code>).<\/p>\n<h3>Paso 1 \u2014 Crear la pol\u00edtica<\/h3>\n<p>Crea <code>policy\/tags.rego<\/code>:<\/p>\n<pre><code>package main\n\nimport future.keywords.in\n\ndeny[msg] {\n  input.kind == \"Deployment\"\n  container := input.spec.template.spec.containers[_]\n  image := container.image\n  not contains(image, \":\")\n  msg := sprintf(\"Container '%s' uses image '%s' without a tag. Pin to a specific version.\", [container.name, image])\n}\n\ndeny[msg] {\n  input.kind == \"Deployment\"\n  container := input.spec.template.spec.containers[_]\n  image := container.image\n  endswith(image, \":latest\")\n  msg := sprintf(\"Container '%s' uses the ':latest' tag in image '%s'. Pin to a specific version.\", [container.name, image])\n}<\/code><\/pre>\n<h3>Paso 2 \u2014 Ejecutar Conftest contra el manifiesto inseguro<\/h3>\n<pre><code>conftest test k8s\/deployment-latest-tag.yaml<\/code><\/pre>\n<p>Salida esperada:<\/p>\n<pre><code>FAIL - k8s\/deployment-latest-tag.yaml - main - Container 'nginx' uses the ':latest' tag in image 'nginx:latest'. Pin to a specific version.\n\n1 test, 0 passed, 0 warnings, 1 failure<\/code><\/pre>\n<h3>Paso 3 \u2014 Corregir el manifiesto<\/h3>\n<p>Edita <code>k8s\/deployment-latest-tag.yaml<\/code> y cambia la l\u00ednea de imagen:<\/p>\n<pre><code>          image: nginx:1.25.4<\/code><\/pre>\n<p>Ejecuta Conftest de nuevo:<\/p>\n<pre><code>conftest test k8s\/deployment-latest-tag.yaml<\/code><\/pre>\n<p>Salida esperada:<\/p>\n<pre><code>1 test, 1 passed, 0 warnings, 0 failures<\/code><\/pre>\n<h3>Entendiendo la estructura de Rego<\/h3>\n<p>Cada archivo de pol\u00edtica Rego utilizado por Conftest sigue un patr\u00f3n simple:<\/p>\n<ul>\n<li><strong><code>package main<\/code><\/strong> \u2014 Conftest busca el paquete <code>main<\/code> por defecto. Puedes sobrescribirlo con <code>--namespace<\/code>.<\/li>\n<li><strong><code>deny[msg]<\/code><\/strong> \u2014 un conjunto de reglas. Si <em>todas<\/em> las condiciones dentro del cuerpo de la regla se eval\u00faan como verdaderas, la regla se activa y a\u00f1ade <code>msg<\/code> al conjunto de violaciones.<\/li>\n<li><strong><code>input<\/code><\/strong> \u2014 representa el documento YAML que se est\u00e1 probando. Conftest lo convierte autom\u00e1ticamente en un objeto JSON.<\/li>\n<li><strong><code>sprintf<\/code><\/strong> \u2014 formatea un mensaje de error legible que aparece en los logs de CI.<\/li>\n<\/ul>\n<h2>Ejercicio 2: Sin contenedores ejecut\u00e1ndose como root<\/h2>\n<p>Los contenedores que se ejecutan como root pueden modificar el sistema de archivos, instalar paquetes y \u2014 si se combinan con un exploit del kernel \u2014 escapar al host. Esta pol\u00edtica aplica dos controles: <code>runAsNonRoot: true<\/code> y <code>allowPrivilegeEscalation: false<\/code>.<\/p>\n<h3>Paso 1 \u2014 Crear la pol\u00edtica<\/h3>\n<p>Crea <code>policy\/security_context.rego<\/code>:<\/p>\n<pre><code>package main\n\ndeny[msg] {\n  input.kind == \"Deployment\"\n  container := input.spec.template.spec.containers[_]\n  not container.securityContext.runAsNonRoot == true\n  msg := sprintf(\"Container '%s' must set securityContext.runAsNonRoot to true.\", [container.name])\n}\n\ndeny[msg] {\n  input.kind == \"Deployment\"\n  container := input.spec.template.spec.containers[_]\n  not container.securityContext.allowPrivilegeEscalation == false\n  msg := sprintf(\"Container '%s' must set securityContext.allowPrivilegeEscalation to false.\", [container.name])\n}<\/code><\/pre>\n<h3>Paso 2 \u2014 Probar contra el manifiesto inseguro<\/h3>\n<pre><code>conftest test k8s\/deployment-run-as-root.yaml<\/code><\/pre>\n<p>Salida esperada:<\/p>\n<pre><code>FAIL - k8s\/deployment-run-as-root.yaml - main - Container 'nginx' must set securityContext.runAsNonRoot to true.\nFAIL - k8s\/deployment-run-as-root.yaml - main - Container 'nginx' must set securityContext.allowPrivilegeEscalation to false.\n\n1 test, 0 passed, 0 warnings, 2 failures<\/code><\/pre>\n<h3>Paso 3 \u2014 Corregir el manifiesto<\/h3>\n<p>Actualiza <code>k8s\/deployment-run-as-root.yaml<\/code> para incluir un contexto de seguridad en cada contenedor:<\/p>\n<pre><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: web-root\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: web-root\n  template:\n    metadata:\n      labels:\n        app: web-root\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:1.25.4\n          ports:\n            - containerPort: 80\n          securityContext:\n            runAsNonRoot: true\n            allowPrivilegeEscalation: false<\/code><\/pre>\n<p>Ejecuta Conftest de nuevo \u2014 ambas reglas ahora pasan.<\/p>\n<h2>Ejercicio 3: Requerir l\u00edmites de recursos<\/h2>\n<p>Sin l\u00edmites 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\u00edmites expl\u00edcitos.<\/p>\n<h3>Paso 1 \u2014 Crear la pol\u00edtica<\/h3>\n<p>Crea <code>policy\/resources.rego<\/code>:<\/p>\n<pre><code>package main\n\ndeny[msg] {\n  input.kind == \"Deployment\"\n  container := input.spec.template.spec.containers[_]\n  not container.resources.limits.cpu\n  msg := sprintf(\"Container '%s' must define resources.limits.cpu.\", [container.name])\n}\n\ndeny[msg] {\n  input.kind == \"Deployment\"\n  container := input.spec.template.spec.containers[_]\n  not container.resources.limits.memory\n  msg := sprintf(\"Container '%s' must define resources.limits.memory.\", [container.name])\n}<\/code><\/pre>\n<h3>Paso 2 \u2014 Probar contra el manifiesto inseguro<\/h3>\n<pre><code>conftest test k8s\/deployment-no-limits.yaml<\/code><\/pre>\n<p>Salida esperada:<\/p>\n<pre><code>FAIL - k8s\/deployment-no-limits.yaml - main - Container 'nginx' must define resources.limits.cpu.\nFAIL - k8s\/deployment-no-limits.yaml - main - Container 'nginx' must define resources.limits.memory.\n\n1 test, 0 passed, 0 warnings, 2 failures<\/code><\/pre>\n<h3>Paso 3 \u2014 Corregir el manifiesto<\/h3>\n<p>A\u00f1ade l\u00edmites de recursos a <code>k8s\/deployment-no-limits.yaml<\/code>:<\/p>\n<pre><code>apiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: web-no-limits\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: web-no-limits\n  template:\n    metadata:\n      labels:\n        app: web-no-limits\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:1.25.4\n          ports:\n            - containerPort: 80\n          resources:\n            requests:\n              cpu: \"100m\"\n              memory: \"128Mi\"\n            limits:\n              cpu: \"250m\"\n              memory: \"256Mi\"<\/code><\/pre>\n<p>Ejecuta Conftest de nuevo \u2014 las verificaciones de CPU y memoria pasan.<\/p>\n<h2>Ejercicio 4: Denegar acceso privilegiado al host<\/h2>\n<p>Los pods que solicitan acceso a nivel de host \u2014 <code>hostNetwork<\/code>, <code>hostPID<\/code>, <code>hostIPC<\/code> o un contexto de seguridad privilegiado \u2014 efectivamente se ejecutan fuera del sandbox del contenedor. Un pod comprometido con cualquiera de estos flags puede ver todo el tr\u00e1fico del nodo, adjuntarse a otros procesos o escapar completamente al host.<\/p>\n<h3>Paso 1 \u2014 Crear la pol\u00edtica<\/h3>\n<p>Crea <code>policy\/host_access.rego<\/code>:<\/p>\n<pre><code>package main\n\ndeny[msg] {\n  input.kind == \"Pod\"\n  input.spec.hostNetwork == true\n  msg := sprintf(\"Pod '%s' must not use hostNetwork: true.\", [input.metadata.name])\n}\n\ndeny[msg] {\n  input.kind == \"Pod\"\n  input.spec.hostPID == true\n  msg := sprintf(\"Pod '%s' must not use hostPID: true.\", [input.metadata.name])\n}\n\ndeny[msg] {\n  input.kind == \"Pod\"\n  input.spec.hostIPC == true\n  msg := sprintf(\"Pod '%s' must not use hostIPC: true.\", [input.metadata.name])\n}\n\ndeny[msg] {\n  input.kind == \"Pod\"\n  container := input.spec.containers[_]\n  container.securityContext.privileged == true\n  msg := sprintf(\"Container '%s' in Pod '%s' must not run in privileged mode.\", [container.name, input.metadata.name])\n}\n\ndeny[msg] {\n  input.kind == \"Deployment\"\n  input.spec.template.spec.hostNetwork == true\n  msg := sprintf(\"Deployment '%s' must not use hostNetwork: true.\", [input.metadata.name])\n}\n\ndeny[msg] {\n  input.kind == \"Deployment\"\n  container := input.spec.template.spec.containers[_]\n  container.securityContext.privileged == true\n  msg := sprintf(\"Container '%s' in Deployment '%s' must not run in privileged mode.\", [container.name, input.metadata.name])\n}<\/code><\/pre>\n<h3>Paso 2 \u2014 Probar contra el pod inseguro<\/h3>\n<pre><code>conftest test k8s\/pod-host-network.yaml<\/code><\/pre>\n<p>Salida esperada:<\/p>\n<pre><code>FAIL - k8s\/pod-host-network.yaml - main - Pod 'debug-pod' must not use hostNetwork: true.\n\n1 test, 0 passed, 0 warnings, 1 failure<\/code><\/pre>\n<h3>Paso 3 \u2014 Corregir el manifiesto<\/h3>\n<p>Elimina la l\u00ednea <code>hostNetwork: true<\/code> de <code>k8s\/pod-host-network.yaml<\/code>:<\/p>\n<pre><code>apiVersion: v1\nkind: Pod\nmetadata:\n  name: debug-pod\nspec:\n  containers:\n    - name: debug\n      image: busybox:1.36\n      command: [\"sleep\", \"3600\"]<\/code><\/pre>\n<p>Ejecuta Conftest \u2014 el pod ahora pasa todas las verificaciones de acceso al host.<\/p>\n<h2>Ejercicio 5: Probar pol\u00edticas con <code>opa test<\/code><\/h2>\n<p>Las pol\u00edticas son c\u00f3digo, y el c\u00f3digo necesita pruebas. Sin pruebas no puedes estar seguro de que una pol\u00edtica detecta lo que deber\u00eda o de que una futura refactorizaci\u00f3n no introduzca un falso positivo que bloquee despliegues leg\u00edtimos.<\/p>\n<h3>Paso 1 \u2014 Crear casos de prueba<\/h3>\n<p>Crea <code>policy\/tags_test.rego<\/code>:<\/p>\n<pre><code>package main\n\ntest_latest_denied {\n  input := {\n    \"kind\": \"Deployment\",\n    \"spec\": {\n      \"template\": {\n        \"spec\": {\n          \"containers\": [\n            {\n              \"name\": \"app\",\n              \"image\": \"nginx:latest\"\n            }\n          ]\n        }\n      }\n    }\n  }\n  count(deny) > 0\n}\n\ntest_no_tag_denied {\n  input := {\n    \"kind\": \"Deployment\",\n    \"spec\": {\n      \"template\": {\n        \"spec\": {\n          \"containers\": [\n            {\n              \"name\": \"app\",\n              \"image\": \"nginx\"\n            }\n          ]\n        }\n      }\n    }\n  }\n  count(deny) > 0\n}\n\ntest_pinned_allowed {\n  input := {\n    \"kind\": \"Deployment\",\n    \"spec\": {\n      \"template\": {\n        \"spec\": {\n          \"containers\": [\n            {\n              \"name\": \"app\",\n              \"image\": \"nginx:1.25.4\"\n            }\n          ]\n        }\n      }\n    }\n  }\n  count(deny) == 0\n}<\/code><\/pre>\n<h3>Paso 2 \u2014 Ejecutar las pruebas<\/h3>\n<pre><code>opa test policy\/ -v<\/code><\/pre>\n<p>Salida esperada:<\/p>\n<pre><code>policy\/tags_test.rego:\ndata.main.test_latest_denied: PASS (1.234ms)\ndata.main.test_no_tag_denied: PASS (0.567ms)\ndata.main.test_pinned_allowed: PASS (0.432ms)\n--------------------------------------------------------------------------------\nPASS: 3\/3<\/code><\/pre>\n<h3>Por qu\u00e9 importan las pruebas de pol\u00edticas<\/h3>\n<p>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\u00edtimo y erosiona la confianza de los desarrolladores en el pipeline. Al escribir casos de prueba expl\u00edcitos tanto para entradas permitidas como denegadas, obtienes una suite de regresi\u00f3n que se ejecuta en milisegundos y garantiza que tus pol\u00edticas se comportan correctamente a medida que crece la biblioteca de reglas.<\/p>\n<p>Adopta el h\u00e1bito de a\u00f1adir un archivo <code>*_test.rego<\/code> por cada nuevo archivo de pol\u00edtica. Ejecuta <code>opa test policy\/ -v<\/code> como parte de tu pipeline de CI junto con <code>conftest test<\/code>.<\/p>\n<h2>Ejercicio 6: Integrar Conftest en GitHub Actions<\/h2>\n<p>Con las pol\u00edticas escritas y probadas, el siguiente paso es conectarlas a tu pipeline de CI para que cada pull request sea validado autom\u00e1ticamente.<\/p>\n<h3>Paso 1 \u2014 Crear el workflow<\/h3>\n<p>Crea <code>.github\/workflows\/policy-check.yml<\/code>:<\/p>\n<pre><code>name: Kubernetes Policy Check\n\non:\n  pull_request:\n    paths:\n      - \"k8s\/**\"\n      - \"policy\/**\"\n  push:\n    branches: [main]\n    paths:\n      - \"k8s\/**\"\n      - \"policy\/**\"\n\njobs:\n  conftest:\n    name: Validate K8s Manifests\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions\/checkout@v4\n\n      - name: Install Conftest\n        run: |\n          CONFTEST_VERSION=\"0.56.0\"\n          wget -q \"https:\/\/github.com\/open-policy-agent\/conftest\/releases\/download\/v${CONFTEST_VERSION}\/conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz\"\n          tar xzf \"conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz\"\n          sudo mv conftest \/usr\/local\/bin\/\n          conftest --version\n\n      - name: Install OPA\n        run: |\n          OPA_VERSION=\"v0.68.0\"\n          curl -L -o opa \"https:\/\/openpolicyagent.org\/downloads\/${OPA_VERSION}\/opa_linux_amd64_static\"\n          chmod +x opa\n          sudo mv opa \/usr\/local\/bin\/\n          opa version\n\n      - name: Run policy unit tests\n        run: opa test policy\/ -v\n\n      - name: Run Conftest against all manifests\n        run: |\n          echo \"Scanning all Kubernetes manifests in k8s\/ ...\"\n          FAILED=0\n          for file in k8s\/*.yaml; do\n            echo \"\"\n            echo \"--- Testing: $file ---\"\n            if ! conftest test \"$file\" --policy policy\/; then\n              FAILED=1\n            fi\n          done\n          if [ \"$FAILED\" -eq 1 ]; then\n            echo \"\"\n            echo \"\u274c One or more manifests violated policy. Fix the issues above.\"\n            exit 1\n          fi\n          echo \"\"\n          echo \"\u2705 All manifests passed policy checks.\"<\/code><\/pre>\n<h3>Paso 2 \u2014 Observar un PR fallido<\/h3>\n<p>Sube una rama que contenga los manifiestos inseguros originales. La salida del pipeline se ver\u00e1 as\u00ed:<\/p>\n<pre><code>--- Testing: k8s\/deployment-latest-tag.yaml ---\nFAIL - k8s\/deployment-latest-tag.yaml - main - Container 'nginx' uses the ':latest' tag in image 'nginx:latest'. Pin to a specific version.\n\n--- Testing: k8s\/deployment-run-as-root.yaml ---\nFAIL - k8s\/deployment-run-as-root.yaml - main - Container 'nginx' must set securityContext.runAsNonRoot to true.\nFAIL - k8s\/deployment-run-as-root.yaml - main - Container 'nginx' must set securityContext.allowPrivilegeEscalation to false.\n\n--- Testing: k8s\/pod-host-network.yaml ---\nFAIL - k8s\/pod-host-network.yaml - main - Pod 'debug-pod' must not use hostNetwork: true.\n\n\u274c One or more manifests violated policy. Fix the issues above.\nError: Process completed with exit code 1.<\/code><\/pre>\n<p>La verificaci\u00f3n de estado del PR se pone en rojo con mensajes de violaci\u00f3n claros que le indican al desarrollador exactamente qu\u00e9 corregir y d\u00f3nde.<\/p>\n<h3>Paso 3 \u2014 Observar un PR exitoso<\/h3>\n<p>Corrige todos los manifiestos como se muestra en los ejercicios anteriores, sube de nuevo, y el pipeline pasa:<\/p>\n<pre><code>--- Testing: k8s\/deployment-latest-tag.yaml ---\n1 test, 1 passed, 0 warnings, 0 failures\n\n--- Testing: k8s\/deployment-run-as-root.yaml ---\n1 test, 1 passed, 0 warnings, 0 failures\n\n--- Testing: k8s\/deployment-no-limits.yaml ---\n1 test, 1 passed, 0 warnings, 0 failures\n\n\u2705 All manifests passed policy checks.<\/code><\/pre>\n<h2>Ejercicio 7: Integrar Conftest en GitLab CI<\/h2>\n<p>Si tu equipo usa GitLab, la integraci\u00f3n es igual de sencilla. A\u00f1ade el siguiente job a tu <code>.gitlab-ci.yml<\/code>:<\/p>\n<h3>Configuraci\u00f3n completa funcional<\/h3>\n<pre><code>stages:\n  - validate\n\nconftest-policy-check:\n  stage: validate\n  image: alpine:3.19\n  variables:\n    CONFTEST_VERSION: \"0.56.0\"\n    OPA_VERSION: \"v0.68.0\"\n  before_script:\n    - apk add --no-cache curl wget tar\n    - wget -q \"https:\/\/github.com\/open-policy-agent\/conftest\/releases\/download\/v${CONFTEST_VERSION}\/conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz\"\n    - tar xzf \"conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz\"\n    - mv conftest \/usr\/local\/bin\/\n    - curl -L -o \/usr\/local\/bin\/opa \"https:\/\/openpolicyagent.org\/downloads\/${OPA_VERSION}\/opa_linux_amd64_static\"\n    - chmod +x \/usr\/local\/bin\/opa\n  script:\n    - echo \"Running policy unit tests...\"\n    - opa test policy\/ -v\n    - echo \"Running Conftest against all manifests...\"\n    - |\n      FAILED=0\n      for file in k8s\/*.yaml; do\n        echo \"\"\n        echo \"--- Testing: $file ---\"\n        if ! conftest test \"$file\" --policy policy\/; then\n          FAILED=1\n        fi\n      done\n      if [ \"$FAILED\" -eq 1 ]; then\n        echo \"\"\n        echo \"One or more manifests violated policy.\"\n        exit 1\n      fi\n      echo \"\"\n      echo \"All manifests passed policy checks.\"\n  rules:\n    - changes:\n        - k8s\/**\/*\n        - policy\/**\/*\n      when: always<\/code><\/pre>\n<h3>Comportamiento de \u00e9xito\/fallo<\/h3>\n<p>El comportamiento refleja exactamente el workflow de GitHub Actions. Cuando hay manifiestos inseguros presentes, el job falla con mensajes de violaci\u00f3n. Cuando todos los manifiestos cumplen, el job pasa con un resumen limpio. El bloque <code>rules<\/code> asegura que el job solo se ejecute cuando cambian los manifiestos de Kubernetes o los archivos de pol\u00edticas, manteniendo el tiempo de ejecuci\u00f3n del pipeline al m\u00ednimo.<\/p>\n<h2>Avanzado: Advertencias vs. Denegaciones<\/h2>\n<p>No toda violaci\u00f3n de pol\u00edtica deber\u00eda bloquear un despliegue. Algunas son recomendaciones \u2014 mejores pr\u00e1cticas que quieres mostrar sin romper el pipeline. Conftest soporta esta distinci\u00f3n a trav\u00e9s de reglas <code>warn<\/code>.<\/p>\n<h3>C\u00f3mo funciona<\/h3>\n<ul>\n<li><strong><code>deny[msg]<\/code><\/strong> \u2014 una puerta r\u00edgida. Si cualquier regla deny se activa, <code>conftest test<\/code> sale con un c\u00f3digo distinto de cero y el pipeline falla.<\/li>\n<li><strong><code>warn[msg]<\/code><\/strong> \u2014 un aviso. El mensaje se imprime pero el c\u00f3digo de salida permanece en cero, por lo que el pipeline pasa.<\/li>\n<li><strong><code>conftest test --fail-on-warn<\/code><\/strong> \u2014 opcionalmente promueve todas las advertencias a fallos. \u00datil cuando quieres endurecer gradualmente las pol\u00edticas: empieza con <code>warn<\/code>, y una vez que los equipos hayan corregido las violaciones existentes, cambia a <code>deny<\/code> o habilita <code>--fail-on-warn<\/code>.<\/li>\n<\/ul>\n<h3>Crear una pol\u00edtica de asesoramiento<\/h3>\n<p>Crea <code>policy\/recommendations.rego<\/code>:<\/p>\n<pre><code>package main\n\nwarn[msg] {\n  input.kind == \"Service\"\n  input.spec.type == \"LoadBalancer\"\n  not input.metadata.annotations\n  msg := sprintf(\"Service '%s' is of type LoadBalancer with no annotations. Consider adding cloud-provider-specific annotations for internal load balancers.\", [input.metadata.name])\n}\n\nwarn[msg] {\n  input.kind == \"Deployment\"\n  container := input.spec.template.spec.containers[_]\n  not container.readinessProbe\n  msg := sprintf(\"Container '%s' has no readinessProbe. Add one so Kubernetes can route traffic only to healthy pods.\", [container.name])\n}\n\nwarn[msg] {\n  input.kind == \"Deployment\"\n  container := input.spec.template.spec.containers[_]\n  not container.livenessProbe\n  msg := sprintf(\"Container '%s' has no livenessProbe. Add one so Kubernetes can restart unhealthy pods.\", [container.name])\n}<\/code><\/pre>\n<h3>Probar la pol\u00edtica de asesoramiento<\/h3>\n<pre><code>conftest test k8s\/service-loadbalancer.yaml<\/code><\/pre>\n<p>Salida esperada:<\/p>\n<pre><code>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.\n\n1 test, 1 passed, 1 warning, 0 failures<\/code><\/pre>\n<p>Nota: el c\u00f3digo de salida es <code>0<\/code> \u2014 el pipeline sigue pasando. Si quieres forzar las advertencias:<\/p>\n<pre><code>conftest test k8s\/service-loadbalancer.yaml --fail-on-warn<\/code><\/pre>\n<p>Ahora el c\u00f3digo de salida es <code>1<\/code> y el pipeline fallar\u00eda.<\/p>\n<p>Este patr\u00f3n te permite desplegar nuevas pol\u00edticas gradualmente: intr\u00f3ducelas como advertencias, da tiempo a los equipos para remediar, y luego prom\u00f3cionalas a denegaciones.<\/p>\n<h2>Limpieza<\/h2>\n<p>Cuando termines el laboratorio, elimina los recursos de prueba:<\/p>\n<pre><code># Remove the lab directory\nrm -rf conftest-k8s-lab\n\n# If you deployed any fixed manifests to a test cluster\nkubectl delete -f k8s\/ --ignore-not-found\n\n# If you created a kind cluster for this lab\nkind delete cluster --name conftest-lab<\/code><\/pre>\n<h2>Conclusiones clave<\/h2>\n<ul>\n<li><strong>Desplaza la seguridad a la izquierda de forma agresiva.<\/strong> Detectar un manifiesto mal configurado en un pull request es \u00f3rdenes de magnitud m\u00e1s barato que descubrirlo despu\u00e9s de una brecha de seguridad.<\/li>\n<li><strong>Conftest + Rego es un punto de entrada ligero a policy-as-code.<\/strong> No necesitas un servidor OPA completo ni una instalaci\u00f3n de Gatekeeper para empezar a aplicar pol\u00edticas \u2014 un solo binario CLI y unos pocos archivos Rego son suficientes.<\/li>\n<li><strong>Prueba tus pol\u00edticas como c\u00f3digo de aplicaci\u00f3n.<\/strong> Usa <code>opa test<\/code> con casos de prueba expl\u00edcitos positivos y negativos para prevenir regresiones en tu biblioteca de reglas.<\/li>\n<li><strong>Usa advertencias para despliegues graduales.<\/strong> Empieza las nuevas pol\u00edticas como reglas <code>warn<\/code>, social\u00edzalas con el equipo, y prom\u00f3cionalas a <code>deny<\/code> una vez que se resuelvan las violaciones existentes.<\/li>\n<li><strong>Los mensajes de error accionables son cr\u00edticos.<\/strong> Usa <code>sprintf<\/code> en cada regla para indicar al desarrollador qu\u00e9 contenedor, qu\u00e9 campo y qu\u00e9 hacer al respecto. Los mensajes gen\u00e9ricos de \u201cpol\u00edtica violada\u201d erosionan la confianza en las puertas de CI.<\/li>\n<li><strong>Mant\u00e9n las pol\u00edticas en el mismo repositorio que los manifiestos.<\/strong> Co-ubicar <code>policy\/<\/code> con <code>k8s\/<\/code> significa que los cambios de pol\u00edticas pasan por el mismo proceso de revisi\u00f3n que los cambios de infraestructura.<\/li>\n<\/ul>\n<h2>Pr\u00f3ximos pasos<\/h2>\n<p>Ahora que tienes un pipeline de Conftest funcional, contin\u00faa construyendo tu pr\u00e1ctica de policy-as-code:<\/p>\n<ul>\n<li><strong><a href=\"\/es\/ci-cd-security\/policy-as-code-ci-cd-opa-rego-security-gates\/\">Policy as Code para CI\/CD: OPA y Rego<\/a><\/strong> \u2014 profundiza en el lenguaje Rego, aprende sobre importaci\u00f3n de datos, gesti\u00f3n de bundles y registro de decisiones para pistas de auditor\u00eda.<\/li>\n<li><strong><a href=\"\/es\/ci-cd-security\/defensive-patterns-mitigations-ci-cd-pipeline-attacks\/\">Patrones Defensivos y Mitigaciones<\/a><\/strong> \u2014 explora el panorama m\u00e1s amplio del hardening de pipelines de CI\/CD, desde la gesti\u00f3n de secretos hasta la firma de artefactos y la aplicaci\u00f3n en tiempo de ejecuci\u00f3n.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Descripci\u00f3n general Los manifiestos de Kubernetes mal configurados son una de las principales causas de incidentes de seguridad en producci\u00f3n. Un contenedor ejecut\u00e1ndose como root, una etiqueta de imagen sin fijar, un l\u00edmite de recursos faltante o una red de host expuesta pueden abrir la puerta a la escalada de privilegios, el agotamiento de recursos &#8230; <a title=\"Lab: Aplicar Pol\u00edticas de Despliegue en Kubernetes con OPA Conftest en CI\/CD\" class=\"read-more\" href=\"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/lab-enforcing-kubernetes-policies-opa-conftest-ci-cd-2\/\" aria-label=\"Leer m\u00e1s sobre Lab: Aplicar Pol\u00edticas de Despliegue en Kubernetes con OPA Conftest en CI\/CD\">Leer m\u00e1s<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[55,58],"tags":[],"post_folder":[],"class_list":["post-699","post","type-post","status-publish","format-standard","hentry","category-ci-cd-security","category-pipeline-hardening"],"_links":{"self":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/699","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/comments?post=699"}],"version-history":[{"count":0,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/699\/revisions"}],"wp:attachment":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/media?parent=699"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/categories?post=699"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/tags?post=699"},{"taxonomy":"post_folder","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/post_folder?post=699"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}