{"id":535,"date":"2026-03-09T22:57:56","date_gmt":"2026-03-09T21:57:56","guid":{"rendered":"https:\/\/secure-pipelines.com\/?p=535"},"modified":"2026-03-24T12:58:54","modified_gmt":"2026-03-24T11:58:54","slug":"lab-enforcing-kubernetes-policies-opa-conftest-ci-cd-2","status":"publish","type":"post","link":"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/lab-enforcing-kubernetes-policies-opa-conftest-ci-cd-2\/","title":{"rendered":"Lab : Application de Politiques Kubernetes avec OPA Conftest en CI\/CD"},"content":{"rendered":"<h2>Vue d&rsquo;ensemble<\/h2>\n<p>Les manifestes Kubernetes mal configur\u00e9s sont l&rsquo;une des principales causes d&rsquo;incidents de s\u00e9curit\u00e9 en production. Un conteneur s&rsquo;ex\u00e9cutant en tant que root, un tag d&rsquo;image non \u00e9pingl\u00e9, une limite de ressources manquante ou un r\u00e9seau h\u00f4te expos\u00e9 peuvent chacun ouvrir la porte \u00e0 une escalade de privil\u00e8ges, un \u00e9puisement des ressources ou un mouvement lat\u00e9ral \u00e0 l&rsquo;int\u00e9rieur de votre cluster.<\/p>\n<p>Le probl\u00e8me est que ces mauvaises configurations sont invisibles jusqu&rsquo;au moment du d\u00e9ploiement \u2014 ou pire, jusqu&rsquo;\u00e0 ce qu&rsquo;un attaquant les exploite. La solution est de d\u00e9caler la s\u00e9curit\u00e9 vers la gauche (shift left) et de d\u00e9tecter les violations de politiques <strong>avant<\/strong> que les manifestes n&rsquo;atteignent le cluster.<\/p>\n<p>Dans ce lab pratique, vous utiliserez <strong>Conftest<\/strong> \u2014 un framework de test construit sur le moteur Open Policy Agent (OPA) \u2014 pour \u00e9crire des politiques Rego qui valident les manifestes Kubernetes. Vous int\u00e9grerez ensuite ces v\u00e9rifications dans GitHub Actions et GitLab CI afin que chaque pull request soit automatiquement analys\u00e9e pour d\u00e9tecter les violations.<\/p>\n<p>\u00c0 la fin de ce lab, vous disposerez de :<\/p>\n<ul>\n<li>Une biblioth\u00e8que de politiques Rego r\u00e9utilisables couvrant les tags d&rsquo;images, les contextes de s\u00e9curit\u00e9, les limites de ressources et l&rsquo;acc\u00e8s au niveau de l&rsquo;h\u00f4te.<\/li>\n<li>Des tests unitaires pour ces politiques utilisant <code>opa test<\/code>.<\/li>\n<li>Des pipelines CI\/CD fonctionnels qui bloquent les manifestes non s\u00e9curis\u00e9s et fournissent des messages de violation clairs et exploitables.<\/li>\n<\/ul>\n<h2>Pr\u00e9requis<\/h2>\n<p>Avant de commencer, assurez-vous de disposer des outils et connaissances suivants :<\/p>\n<ul>\n<li><strong>CLI conftest install\u00e9<\/strong> \u2014 installez avec Homebrew :\n<pre><code>brew install conftest<\/code><\/pre>\n<p>Vous pouvez \u00e9galement t\u00e9l\u00e9charger le binaire depuis la <a href=\"https:\/\/github.com\/open-policy-agent\/conftest\/releases\" target=\"_blank\" rel=\"noopener\">page des releases de Conftest<\/a>.<\/li>\n<li><strong>kubectl et un cluster de test (optionnel)<\/strong> \u2014 si vous souhaitez v\u00e9rifier que vos manifestes corrig\u00e9s se d\u00e9ploient correctement, d\u00e9marrez un cluster local avec <code>minikube start<\/code> ou <code>kind create cluster<\/code>.<\/li>\n<li><strong>Un d\u00e9p\u00f4t de test<\/strong> \u2014 cr\u00e9ez un nouveau d\u00e9p\u00f4t Git ou utilisez un d\u00e9p\u00f4t existant. Nous construirons tous les fichiers \u00e0 partir de z\u00e9ro.<\/li>\n<li><strong>Connaissances de base en YAML et Kubernetes<\/strong> \u2014 vous devez \u00eatre \u00e0 l&rsquo;aise avec la lecture des manifestes Deployment, Service et Pod.<\/li>\n<\/ul>\n<h2>Configuration de l&rsquo;environnement<\/h2>\n<p>Commencez par cr\u00e9er la structure du projet et un ensemble de manifestes Kubernetes intentionnellement non s\u00e9curis\u00e9s. Ceux-ci serviront de fixtures de test tout au long de chaque exercice.<\/p>\n<h3>Structure du projet<\/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>Cr\u00e9ez les r\u00e9pertoires :<\/p>\n<pre><code>mkdir -p conftest-k8s-lab\/k8s conftest-k8s-lab\/policy\ncd conftest-k8s-lab<\/code><\/pre>\n<h3>Manifeste 1 \u2014 Deployment avec un tag d&rsquo;image non \u00e9pingl\u00e9<\/h3>\n<p>Cr\u00e9ez <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>Ce manifeste utilise <code>nginx:latest<\/code>, ce qui signifie que chaque pull pourrait silencieusement introduire un binaire diff\u00e9rent dans votre cluster.<\/p>\n<h3>Manifeste 2 \u2014 Deployment s&rsquo;ex\u00e9cutant en tant que root<\/h3>\n<p>Cr\u00e9ez <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>Aucun <code>securityContext<\/code> n&rsquo;est d\u00e9fini, donc le conteneur s&rsquo;ex\u00e9cute par d\u00e9faut en tant que root \u2014 un vecteur d&rsquo;escalade de privil\u00e8ges bien connu.<\/p>\n<h3>Manifeste 3 \u2014 Deployment sans limites de ressources<\/h3>\n<p>Cr\u00e9ez <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>Sans limites de CPU et de m\u00e9moire, un seul pod d\u00e9faillant peut affamer l&rsquo;ensemble du n\u0153ud.<\/p>\n<h3>Manifeste 4 \u2014 Service de type LoadBalancer<\/h3>\n<p>Cr\u00e9ez <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 service LoadBalancer sans annotations peut exposer des charges de travail sur l&rsquo;internet public dans les environnements cloud.<\/p>\n<h3>Manifeste 5 \u2014 Pod avec acc\u00e8s au r\u00e9seau h\u00f4te<\/h3>\n<p>Cr\u00e9ez <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> donne au pod un acc\u00e8s complet \u00e0 la pile r\u00e9seau du n\u0153ud, contournant enti\u00e8rement les network policies.<\/p>\n<h2>Exercice 1 : \u00c9crire votre premi\u00e8re politique Rego \u2014 Pas de tags latest<\/h2>\n<p>Votre premi\u00e8re politique refusera toute image de conteneur qui utilise le tag <code>:latest<\/code> ou omet un tag enti\u00e8rement (ce qui r\u00e9sout \u00e9galement vers <code>latest<\/code>).<\/p>\n<h3>\u00c9tape 1 \u2014 Cr\u00e9er la politique<\/h3>\n<p>Cr\u00e9ez <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>\u00c9tape 2 \u2014 Ex\u00e9cuter Conftest sur le manifeste non s\u00e9curis\u00e9<\/h3>\n<pre><code>conftest test k8s\/deployment-latest-tag.yaml<\/code><\/pre>\n<p>Sortie attendue :<\/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>\u00c9tape 3 \u2014 Corriger le manifeste<\/h3>\n<p>\u00c9ditez <code>k8s\/deployment-latest-tag.yaml<\/code> et modifiez la ligne de l&rsquo;image :<\/p>\n<pre><code>          image: nginx:1.25.4<\/code><\/pre>\n<p>Ex\u00e9cutez Conftest \u00e0 nouveau :<\/p>\n<pre><code>conftest test k8s\/deployment-latest-tag.yaml<\/code><\/pre>\n<p>Sortie attendue :<\/p>\n<pre><code>1 test, 1 passed, 0 warnings, 0 failures<\/code><\/pre>\n<h3>Comprendre la structure Rego<\/h3>\n<p>Chaque fichier de politique Rego utilis\u00e9 par Conftest suit un sch\u00e9ma simple :<\/p>\n<ul>\n<li><strong><code>package main<\/code><\/strong> \u2014 Conftest recherche le package <code>main<\/code> par d\u00e9faut. Vous pouvez modifier cela avec <code>--namespace<\/code>.<\/li>\n<li><strong><code>deny[msg]<\/code><\/strong> \u2014 un ensemble de r\u00e8gles. Si <em>toutes<\/em> les conditions dans le corps de la r\u00e8gle s&rsquo;\u00e9valuent \u00e0 vrai, la r\u00e8gle se d\u00e9clenche et ajoute <code>msg<\/code> \u00e0 l&rsquo;ensemble des violations.<\/li>\n<li><strong><code>input<\/code><\/strong> \u2014 repr\u00e9sente le document YAML test\u00e9. Conftest le parse automatiquement en un objet JSON.<\/li>\n<li><strong><code>sprintf<\/code><\/strong> \u2014 formate un message d&rsquo;erreur lisible qui appara\u00eet dans les logs CI.<\/li>\n<\/ul>\n<h2>Exercice 2 : Pas de conteneurs s&rsquo;ex\u00e9cutant en tant que root<\/h2>\n<p>Les conteneurs qui s&rsquo;ex\u00e9cutent en tant que root peuvent modifier le syst\u00e8me de fichiers, installer des paquets et \u2014 combin\u00e9s \u00e0 un exploit du noyau \u2014 s&rsquo;\u00e9chapper vers l&rsquo;h\u00f4te. Cette politique applique deux contr\u00f4les : <code>runAsNonRoot: true<\/code> et <code>allowPrivilegeEscalation: false<\/code>.<\/p>\n<h3>\u00c9tape 1 \u2014 Cr\u00e9er la politique<\/h3>\n<p>Cr\u00e9ez <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>\u00c9tape 2 \u2014 Tester sur le manifeste non s\u00e9curis\u00e9<\/h3>\n<pre><code>conftest test k8s\/deployment-run-as-root.yaml<\/code><\/pre>\n<p>Sortie attendue :<\/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>\u00c9tape 3 \u2014 Corriger le manifeste<\/h3>\n<p>Mettez \u00e0 jour <code>k8s\/deployment-run-as-root.yaml<\/code> pour inclure un contexte de s\u00e9curit\u00e9 sur chaque conteneur :<\/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>Ex\u00e9cutez Conftest \u00e0 nouveau \u2014 les deux r\u00e8gles passent maintenant.<\/p>\n<h2>Exercice 3 : Exiger des limites de ressources<\/h2>\n<p>Sans limites de ressources, un seul conteneur peut consommer tout le CPU et la m\u00e9moire du n\u0153ud, provoquant des d\u00e9faillances en cascade sur des charges de travail non li\u00e9es. De nombreux r\u00e9f\u00e9rentiels de conformit\u00e9 (SOC 2, CIS Benchmarks) exigent des limites explicites.<\/p>\n<h3>\u00c9tape 1 \u2014 Cr\u00e9er la politique<\/h3>\n<p>Cr\u00e9ez <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>\u00c9tape 2 \u2014 Tester sur le manifeste non s\u00e9curis\u00e9<\/h3>\n<pre><code>conftest test k8s\/deployment-no-limits.yaml<\/code><\/pre>\n<p>Sortie attendue :<\/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>\u00c9tape 3 \u2014 Corriger le manifeste<\/h3>\n<p>Ajoutez des limites de ressources \u00e0 <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>Ex\u00e9cutez Conftest \u00e0 nouveau \u2014 les v\u00e9rifications de CPU et de m\u00e9moire passent.<\/p>\n<h2>Exercice 4 : Refuser l&rsquo;acc\u00e8s privil\u00e9gi\u00e9 \u00e0 l&rsquo;h\u00f4te<\/h2>\n<p>Les pods qui demandent un acc\u00e8s au niveau de l&rsquo;h\u00f4te \u2014 <code>hostNetwork<\/code>, <code>hostPID<\/code>, <code>hostIPC<\/code> ou un contexte de s\u00e9curit\u00e9 privil\u00e9gi\u00e9 \u2014 s&rsquo;ex\u00e9cutent effectivement en dehors du sandbox du conteneur. Un pod compromis avec l&rsquo;un de ces drapeaux peut voir tout le trafic sur le n\u0153ud, s&rsquo;attacher \u00e0 d&rsquo;autres processus ou s&rsquo;\u00e9chapper enti\u00e8rement vers l&rsquo;h\u00f4te.<\/p>\n<h3>\u00c9tape 1 \u2014 Cr\u00e9er la politique<\/h3>\n<p>Cr\u00e9ez <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>\u00c9tape 2 \u2014 Tester sur le pod non s\u00e9curis\u00e9<\/h3>\n<pre><code>conftest test k8s\/pod-host-network.yaml<\/code><\/pre>\n<p>Sortie attendue :<\/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>\u00c9tape 3 \u2014 Corriger le manifeste<\/h3>\n<p>Supprimez la ligne <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>Ex\u00e9cutez Conftest \u2014 le pod passe maintenant toutes les v\u00e9rifications d&rsquo;acc\u00e8s \u00e0 l&rsquo;h\u00f4te.<\/p>\n<h2>Exercice 5 : Tester les politiques avec <code>opa test<\/code><\/h2>\n<p>Les politiques sont du code, et le code a besoin de tests. Sans tests, vous ne pouvez pas \u00eatre s\u00fbr qu&rsquo;une politique d\u00e9tecte ce qu&rsquo;elle devrait, ni qu&rsquo;un futur refactoring n&rsquo;introduise un faux positif qui bloque des d\u00e9ploiements l\u00e9gitimes.<\/p>\n<h3>\u00c9tape 1 \u2014 Cr\u00e9er les cas de test<\/h3>\n<p>Cr\u00e9ez <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>\u00c9tape 2 \u2014 Ex\u00e9cuter les tests<\/h3>\n<pre><code>opa test policy\/ -v<\/code><\/pre>\n<p>Sortie attendue :<\/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>Pourquoi tester les politiques est important<\/h3>\n<p>Dans un contexte CI\/CD, un faux n\u00e9gatif signifie qu&rsquo;un manifeste non s\u00e9curis\u00e9 passe \u00e0 travers les mailles du filet, tandis qu&rsquo;un faux positif bloque un d\u00e9ploiement l\u00e9gitime et \u00e9rode la confiance des d\u00e9veloppeurs dans le pipeline. En \u00e9crivant des cas de test explicites pour les entr\u00e9es autoris\u00e9es et refus\u00e9es, vous obtenez une suite de r\u00e9gression qui s&rsquo;ex\u00e9cute en millisecondes et garantit que vos politiques se comportent correctement \u00e0 mesure que la biblioth\u00e8que de r\u00e8gles grandit.<\/p>\n<p>Prenez l&rsquo;habitude d&rsquo;ajouter un fichier <code>*_test.rego<\/code> pour chaque nouveau fichier de politique. Ex\u00e9cutez <code>opa test policy\/ -v<\/code> dans le cadre de votre pipeline CI aux c\u00f4t\u00e9s de <code>conftest test<\/code>.<\/p>\n<h2>Exercice 6 : Int\u00e9grer Conftest dans GitHub Actions<\/h2>\n<p>Avec les politiques \u00e9crites et test\u00e9es, l&rsquo;\u00e9tape suivante est de les connecter \u00e0 votre pipeline CI afin que chaque pull request soit automatiquement valid\u00e9e.<\/p>\n<h3>\u00c9tape 1 \u2014 Cr\u00e9er le workflow<\/h3>\n<p>Cr\u00e9ez <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 \"One or more manifests violated policy. Fix the issues above.\"\n            exit 1\n          fi\n          echo \"\"\n          echo \"All manifests passed policy checks.\"<\/code><\/pre>\n<h3>\u00c9tape 2 \u2014 Observer une PR \u00e9chou\u00e9e<\/h3>\n<p>Poussez une branche contenant les manifestes non s\u00e9curis\u00e9s originaux. La sortie du pipeline ressemblera \u00e0 ceci :<\/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\nOne or more manifests violated policy. Fix the issues above.\nError: Process completed with exit code 1.<\/code><\/pre>\n<p>Le statut de la PR passe au rouge avec des messages de violation clairs qui indiquent au d\u00e9veloppeur exactement quoi corriger et o\u00f9.<\/p>\n<h3>\u00c9tape 3 \u2014 Observer une PR r\u00e9ussie<\/h3>\n<p>Corrigez tous les manifestes comme montr\u00e9 dans les exercices pr\u00e9c\u00e9dents, poussez \u00e0 nouveau, et le pipeline passe :<\/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\nAll manifests passed policy checks.<\/code><\/pre>\n<h2>Exercice 7 : Int\u00e9grer Conftest dans GitLab CI<\/h2>\n<p>Si votre \u00e9quipe utilise GitLab, l&rsquo;int\u00e9gration est tout aussi simple. Ajoutez le job suivant \u00e0 votre <code>.gitlab-ci.yml<\/code> :<\/p>\n<h3>Configuration compl\u00e8te fonctionnelle<\/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>Comportement r\u00e9ussite\/\u00e9chec<\/h3>\n<p>Le comportement est identique au workflow GitHub Actions. Lorsque des manifestes non s\u00e9curis\u00e9s sont pr\u00e9sents, le job \u00e9choue avec des messages de violation. Lorsque tous les manifestes sont conformes, le job passe avec un r\u00e9sum\u00e9 propre. Le bloc <code>rules<\/code> garantit que le job ne s&rsquo;ex\u00e9cute que lorsque les manifestes Kubernetes ou les fichiers de politique changent, minimisant ainsi le temps d&rsquo;ex\u00e9cution du pipeline.<\/p>\n<h2>Avanc\u00e9 : Avertissements vs. refus<\/h2>\n<p>Toutes les violations de politique ne doivent pas bloquer un d\u00e9ploiement. Certaines sont des recommandations \u2014 des bonnes pratiques que vous souhaitez signaler sans casser le pipeline. Conftest prend en charge cette distinction gr\u00e2ce aux r\u00e8gles <code>warn<\/code>.<\/p>\n<h3>Comment \u00e7a fonctionne<\/h3>\n<ul>\n<li><strong><code>deny[msg]<\/code><\/strong> \u2014 une gate stricte. Si une r\u00e8gle deny se d\u00e9clenche, <code>conftest test<\/code> retourne un code de sortie non nul et le pipeline \u00e9choue.<\/li>\n<li><strong><code>warn[msg]<\/code><\/strong> \u2014 un avis consultatif. Le message est affich\u00e9 mais le code de sortie reste z\u00e9ro, donc le pipeline passe.<\/li>\n<li><strong><code>conftest test --fail-on-warn<\/code><\/strong> \u2014 promeut optionnellement tous les avertissements en \u00e9checs. Utile lorsque vous souhaitez resserrer progressivement les politiques : commencez avec <code>warn<\/code>, et une fois que les \u00e9quipes ont corrig\u00e9 les violations existantes, passez \u00e0 <code>deny<\/code> ou activez <code>--fail-on-warn<\/code>.<\/li>\n<\/ul>\n<h3>Cr\u00e9er une politique consultative<\/h3>\n<p>Cr\u00e9ez <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>Tester la politique consultative<\/h3>\n<pre><code>conftest test k8s\/service-loadbalancer.yaml<\/code><\/pre>\n<p>Sortie attendue :<\/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>Remarque : le code de sortie est <code>0<\/code> \u2014 le pipeline passe toujours. Si vous souhaitez appliquer les avertissements :<\/p>\n<pre><code>conftest test k8s\/service-loadbalancer.yaml --fail-on-warn<\/code><\/pre>\n<p>Maintenant le code de sortie est <code>1<\/code> et le pipeline \u00e9chouerait.<\/p>\n<p>Ce mod\u00e8le vous permet de d\u00e9ployer de nouvelles politiques progressivement : introduisez-les comme avertissements, laissez aux \u00e9quipes le temps de rem\u00e9dier, puis promouvez-les en refus.<\/p>\n<h2>Nettoyage<\/h2>\n<p>Lorsque vous avez termin\u00e9 le lab, supprimez les ressources de test :<\/p>\n<pre><code># Supprimer le r\u00e9pertoire du lab\nrm -rf conftest-k8s-lab\n\n# Si vous avez d\u00e9ploy\u00e9 des manifestes corrig\u00e9s sur un cluster de test\nkubectl delete -f k8s\/ --ignore-not-found\n\n# Si vous avez cr\u00e9\u00e9 un cluster kind pour ce lab\nkind delete cluster --name conftest-lab<\/code><\/pre>\n<h2>Points cl\u00e9s \u00e0 retenir<\/h2>\n<ul>\n<li><strong>D\u00e9calez la s\u00e9curit\u00e9 vers la gauche de mani\u00e8re agressive.<\/strong> D\u00e9tecter un manifeste mal configur\u00e9 dans une pull request co\u00fbte des ordres de grandeur moins cher que de le d\u00e9couvrir apr\u00e8s une br\u00e8che.<\/li>\n<li><strong>Conftest + Rego est un point d&rsquo;entr\u00e9e l\u00e9ger vers le policy-as-code.<\/strong> Vous n&rsquo;avez pas besoin d&rsquo;un serveur OPA complet ni d&rsquo;une installation Gatekeeper pour commencer \u00e0 appliquer des politiques \u2014 un simple binaire CLI et quelques fichiers Rego suffisent.<\/li>\n<li><strong>Testez vos politiques comme du code applicatif.<\/strong> Utilisez <code>opa test<\/code> avec des cas de test explicites positifs et n\u00e9gatifs pour pr\u00e9venir les r\u00e9gressions dans votre biblioth\u00e8que de r\u00e8gles.<\/li>\n<li><strong>Utilisez les avertissements pour un d\u00e9ploiement progressif.<\/strong> Commencez les nouvelles politiques comme r\u00e8gles <code>warn<\/code>, partagez-les avec l&rsquo;\u00e9quipe, et promouvez-les en <code>deny<\/code> une fois les violations existantes r\u00e9solues.<\/li>\n<li><strong>Des messages d&rsquo;erreur exploitables sont essentiels.<\/strong> Utilisez <code>sprintf<\/code> dans chaque r\u00e8gle pour indiquer au d\u00e9veloppeur quel conteneur, quel champ et quoi faire. Des messages g\u00e9n\u00e9riques \u00ab\u00a0politique viol\u00e9e\u00a0\u00bb \u00e9rodent la confiance dans les gates CI.<\/li>\n<li><strong>Conservez les politiques dans le m\u00eame d\u00e9p\u00f4t que les manifestes.<\/strong> Co-localiser <code>policy\/<\/code> avec <code>k8s\/<\/code> signifie que les changements de politique passent par le m\u00eame processus de revue que les changements d&rsquo;infrastructure.<\/li>\n<\/ul>\n<h2>Prochaines \u00e9tapes<\/h2>\n<p>Maintenant que vous disposez d&rsquo;un pipeline Conftest fonctionnel, continuez \u00e0 d\u00e9velopper votre pratique de policy-as-code :<\/p>\n<ul>\n<li><strong><a href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/policy-as-code-ci-cd-opa-rego-security-gates-2\/\">Policy as Code pour CI\/CD : OPA et Rego<\/a><\/strong> \u2014 approfondissez le langage Rego, d\u00e9couvrez les imports de donn\u00e9es, la gestion des bundles et la journalisation des d\u00e9cisions pour les pistes d&rsquo;audit.<\/li>\n<li><strong><a href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/defensive-patterns-mitigations-ci-cd-pipeline-attacks\/\">Patterns d\u00e9fensifs et att\u00e9nuations<\/a><\/strong> \u2014 explorez le paysage plus large du durcissement des pipelines CI\/CD, de la gestion des secrets \u00e0 la signature des artefacts et l&rsquo;application en runtime.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Vue d&rsquo;ensemble Les manifestes Kubernetes mal configur\u00e9s sont l&rsquo;une des principales causes d&rsquo;incidents de s\u00e9curit\u00e9 en production. Un conteneur s&rsquo;ex\u00e9cutant en tant que root, un tag d&rsquo;image non \u00e9pingl\u00e9, une limite de ressources manquante ou un r\u00e9seau h\u00f4te expos\u00e9 peuvent chacun ouvrir la porte \u00e0 une escalade de privil\u00e8ges, un \u00e9puisement des ressources ou un &#8230; <a title=\"Lab : Application de Politiques Kubernetes avec OPA Conftest en CI\/CD\" class=\"read-more\" href=\"https:\/\/secure-pipelines.com\/fr\/ci-cd-security\/lab-enforcing-kubernetes-policies-opa-conftest-ci-cd-2\/\" aria-label=\"En savoir plus sur Lab : Application de Politiques Kubernetes avec OPA Conftest en CI\/CD\">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,51],"tags":[],"post_folder":[],"class_list":["post-535","post","type-post","status-publish","format-standard","hentry","category-ci-cd-security","category-pipeline-hardening"],"_links":{"self":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/535","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=535"}],"version-history":[{"count":2,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/535\/revisions"}],"predecessor-version":[{"id":584,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/posts\/535\/revisions\/584"}],"wp:attachment":[{"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/media?parent=535"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/categories?post=535"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/tags?post=535"},{"taxonomy":"post_folder","embeddable":true,"href":"https:\/\/secure-pipelines.com\/fr\/wp-json\/wp\/v2\/post_folder?post=535"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}