Aller au contenu
Développement medium

OPA en pratique : opa eval, tests unitaires, Conftest et Gatekeeper

21 min de lecture

Écrire des politiques Rego dans un éditeur n'a de valeur que si elles s'exécutent au bon endroit au bon moment. Ce guide couvre la chaîne complète : évaluation locale avec le CLI opa, tests unitaires automatisés, intégration dans un pipeline CI/CD avec Conftest, et déploiement en admission controller Kubernetes avec Gatekeeper.

  • Installer OPA et évaluer une politique avec opa eval
  • Déboguer une règle undefined avec --explain full
  • Écrire des tests unitaires Rego avec fixtures et with input as
  • Intégrer Conftest dans un pipeline GitHub Actions
  • Déployer un ConstraintTemplate + Constraint dans Kubernetes via Gatekeeper

OPA se distribue en binaire statique unique, sans runtime ni dépendance système : c'est ce qui permet de le poser aussi bien sur un poste que dans une image de conteneur scratch. Le projet publie, à côté de chaque binaire, un fichier .sha256 contenant son empreinte. Téléchargez toujours les deux et faites vérifier l'empreinte par sha256sum --check avant de rendre le fichier exécutable : un binaire de politique de sécurité altéré en transit validerait silencieusement tout ce que vous croyez bloquer.

Fenêtre de terminal
# Linux (binaire statique, sans dépendance)
curl -fsSLO https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
curl -fsSLO https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static.sha256
sha256sum --check opa_linux_amd64_static.sha256
chmod +x opa_linux_amd64_static
sudo install -m 0755 opa_linux_amd64_static /usr/local/bin/opa
# macOS avec Homebrew (la formule vérifie déjà l'empreinte)
brew install opa
# Vérification
opa version

La sortie de sha256sum --check doit afficher opa_linux_amd64_static: OK. Toute autre réponse impose de recommencer le téléchargement ; ne passez jamais outre avec --ignore-missing ou en supprimant la vérification.

opa eval est le point d'entrée pour tester n'importe quelle expression Rego sur un input.

Fenêtre de terminal
opa eval \
--input manifest.json \
--data policies/ \
"data.kubernetes.admission.deployment.violations"

Options utiles :

OptionRôle
--input fichier.jsonDocument soumis à évaluation (input)
--data chemin/Fichiers .rego ou .json chargés dans data
--format prettySortie lisible (défaut : JSON brut)
--explain fullTrace complète de l'évaluation (débogage)
--strictActive les vérifications strictes (recommandé)
-b bundle/Charge un bundle OPA

Quand une règle retourne undefined sans raison évidente, --explain full trace chaque étape d'unification :

Fenêtre de terminal
opa eval \
--input manifest.json \
--data policies/ \
--explain full \
--format pretty \
"data.kubernetes.admission.deployment.allow"

La sortie montre quelle expression a échoué en premier dans le corps de chaque règle. C'est le premier réflexe à avoir avant de modifier le code.

Sans --data, opa eval évalue directement l'expression passée en argument, ce qui en fait une calculatrice à fonctions Rego. C'est le moyen le plus rapide de lever un doute sur le comportement exact d'une fonction intégrée avant de l'utiliser dans une politique. Combiné à --input, le même appel sert à explorer la structure d'un document réel : plutôt que de deviner un chemin d'accès, vous le vérifiez.

Fenêtre de terminal
# Tester une fonction built-in
opa eval 'startswith("registry.example.com/api:1.0", "registry.example.com/")'
# Inspecter la structure d'un input
opa eval --input manifest.json 'input.spec.template.spec.containers'

Rego intègre un framework de tests. Un fichier de test est un fichier .rego ordinaire dont les règles commencent par test_.

Trois conventions structurent ce fichier. Le package de test est distinct de celui de la politique (suffixe _test), ce qui évite que les fixtures polluent l'espace de noms évalué en production. Les fixtures sont de simples valeurs Rego dérivées les unes des autres par json.patch, sans framework ni mock : un test Rego ne simule rien, il substitue un input complet. Les règles de test commencent obligatoirement par test_, c'est ce préfixe que opa test cherche. Notez surtout la forme du deuxième test : le modificateur with s'applique à une expression entière et ne peut pas être glissé dans un argument de fonction, il faut donc affecter le résultat à une variable avant de le compter.

policies/deployment_test.rego
package kubernetes.admission.deployment_test
import rego.v1
import data.kubernetes.admission.deployment
# --- Fixtures ---
manifest_valide := {
"kind": "Deployment",
"metadata": {
"name": "api",
"namespace": "production",
"labels": {"app": "api", "team": "platform", "env": "prod"}
},
"spec": {"template": {"spec": {
"containers": [{
"name": "api",
"image": "registry.example.com/api:1.4.2",
"securityContext": {"readOnlyRootFilesystem": true}
}]
}}}
}
manifest_image_latest := json.patch(manifest_valide, [{
"op": "replace",
"path": "/spec/template/spec/containers/0/image",
"value": "nginx:latest"
}])
manifest_sans_label := json.patch(manifest_valide, [{
"op": "remove",
"path": "/metadata/labels/app"
}])
# --- Tests ---
test_allow_manifest_valide if {
deployment.allow with input as manifest_valide
}
test_deny_image_latest if {
not deployment.allow with input as manifest_image_latest
violations := deployment.violations with input as manifest_image_latest
count(violations) > 0
}
test_message_image_latest if {
violations := deployment.violations with input as manifest_image_latest
some msg in violations
contains(msg, "nginx:latest")
}
test_deny_label_manquant if {
not deployment.allow with input as manifest_sans_label
}

opa test charge tous les fichiers .rego des chemins fournis, politiques et tests confondus : oublier le répertoire des bibliothèques partagées produit une erreur de compilation, pas un test rouge. Le --verbose affiche une ligne par test avec sa durée, ce qui sert surtout à repérer une règle dont l'évaluation dérape. Le --coverage sort un rapport JSON indiquant les lignes de politique jamais atteintes ; c'est le seul moyen fiable de détecter une règle morte que personne n'exerce.

Fenêtre de terminal
# Tous les tests du répertoire
opa test policies/ lib/ --verbose
# Un fichier spécifique
opa test policies/deployment_test.rego policies/deployment.rego lib/
# Avec couverture de code
opa test policies/ lib/ --coverage

Sortie avec --verbose :

policies/deployment_test.rego:38:
data.kubernetes.admission.deployment_test.test_allow_manifest_valide: PASS (569.683µs)
policies/deployment_test.rego:54:
data.kubernetes.admission.deployment_test.test_deny_label_manquant: PASS (574.448µs)
policies/deployment_test.rego:48:
data.kubernetes.admission.deployment_test.test_message_image_latest: PASS (634.582µs)
policies/deployment_test.rego:42:
data.kubernetes.admission.deployment_test.test_deny_image_latest: PASS (979.831µs)
--------------------------------------------------------------------------------
PASS: 4/4

L'ordre d'affichage n'est pas celui du fichier : opa test évalue les règles en parallèle. Fiez-vous au numéro de ligne rappelé au-dessus de chaque résultat.

Conftest est un outil CLI qui applique des politiques Rego à n'importe quel fichier de configuration : manifests Kubernetes, plans Terraform, Dockerfiles, fichiers GitHub Actions, etc.

Les archives publiées par Conftest portent le numéro de version dans leur nom (conftest_0.68.2_Linux_x86_64.tar.gz), il faut donc cibler un tag précis plutôt que latest/download. Chaque release publie également un fichier checksums.txt couvrant toutes les archives : on en extrait la ligne qui nous concerne et on la passe à sha256sum --check avant d'extraire quoi que ce soit.

Fenêtre de terminal
# Linux
CONFTEST_VERSION=0.68.2
BASE="https://github.com/open-policy-agent/conftest/releases/download/v${CONFTEST_VERSION}"
curl -fsSLO "${BASE}/conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz"
curl -fsSLO "${BASE}/checksums.txt"
grep "conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz" checksums.txt \
| sha256sum --check -
tar xzf "conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz" conftest
sudo install -m 0755 conftest /usr/local/bin/conftest
# macOS
brew install conftest

Vérifiez avec conftest --version : la sortie affiche la version de Conftest et celle d'OPA embarquée. Cette seconde ligne compte, c'est elle qui détermine les fonctions Rego disponibles dans vos politiques.

Conftest attend les politiques dans un dossier policy/ par convention. Le namespace utilisé est main par défaut.

projet/
├── policy/
│ ├── kubernetes.rego
│ ├── terraform.rego
│ └── lib/
│ └── helpers.rego
├── manifests/
│ └── deployment.yaml
└── main.tf

Conftest reconnaît trois règles spéciales : deny, warn et violation. deny bloque avec code de sortie non nul. warn avertit sans bloquer.

policy/kubernetes.rego
package main
import rego.v1
# Bloquant
deny contains msg if {
input.kind == "Deployment"
some c in input.spec.template.spec.containers
endswith(c.image, ":latest")
msg := sprintf("[ERREUR] conteneur '%v' : tag latest interdit", [c.name])
}
# Non bloquant
warn contains msg if {
input.kind == "Deployment"
not input.metadata.labels.team
msg := "[AVERTISSEMENT] label 'team' recommandé mais absent"
}

Conftest devine le format d'entrée à partir de l'extension : YAML, JSON, HCL, Dockerfile, INI et quelques autres. Le --parser ne sert que lorsque l'extension ment, typiquement un plan Terraform exporté en JSON. Retenez également que Conftest découpe les fichiers YAML multi-documents et évalue chaque document séparément : un fichier contenant un Deployment et un Service produit deux évaluations, d'où le compte de tests parfois supérieur au nombre de fichiers.

Fenêtre de terminal
# Un manifest
conftest test manifests/deployment.yaml
# Plusieurs manifests
conftest test manifests/
# Un plan Terraform (après terraform show -json)
terraform show -json plan.tfplan > plan.json
conftest test --parser json plan.json --policy policy/terraform.rego
# Un Dockerfile
conftest test Dockerfile --policy policy/docker.rego

Sortie :

WARN - manifests/deployment.yaml - main - [AVERTISSEMENT] label 'team' recommandé mais absent
FAIL - manifests/deployment.yaml - main - [ERREUR] conteneur 'api' : tag latest interdit
2 tests, 0 passed, 1 warning, 1 failure, 0 exceptions

Le code de sortie vaut 1 dès qu'une règle deny se déclenche, 0 si seules des règles warn remontent. C'est ce code, pas le texte, que votre pipeline doit interpréter.

Trois choix de durcissement dans ce workflow méritent d'être copiés tels quels. Les permissions sont vides au niveau du workflow et redonnées au strict minimum sur le job, ce qui empêche le GITHUB_TOKEN d'écrire quoi que ce soit si une étape est compromise. L'action checkout est épinglée par SHA et non par tag, un tag pouvant être redéplacé vers un autre commit par son mainteneur. Le persist-credentials: false empêche le jeton de rester dans .git/config où n'importe quelle étape suivante pourrait le lire.

.github/workflows/policy.yml
name: Policy Check
on: [push, pull_request]
permissions: {}
env:
CONFTEST_VERSION: "0.68.2"
jobs:
conftest:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install Conftest
run: |
BASE="https://github.com/open-policy-agent/conftest/releases/download/v${CONFTEST_VERSION}"
curl -fsSLO "${BASE}/conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz"
curl -fsSLO "${BASE}/checksums.txt"
grep "conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz" checksums.txt \
| sha256sum --check -
tar xzf "conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz" conftest
sudo install -m 0755 conftest /usr/local/bin/conftest
- name: Validate Kubernetes manifests
run: conftest test manifests/ --policy policy/
- name: Validate Terraform plan
run: |
# Suppose que le plan JSON est déjà généré
conftest test --parser json plan.json --policy policy/terraform.rego

Le job échoue automatiquement si une règle deny est déclenchée. Les warn apparaissent dans les logs sans bloquer le pipeline.

OPA Gatekeeper déploie OPA comme webhook d'admission dans Kubernetes. Chaque ressource créée ou modifiée dans le cluster passe par vos politiques avant d'être acceptée.

Le manifeste ci-dessous installe le namespace gatekeeper-system, les CRD, le contrôleur et surtout la configuration de webhook d'admission. C'est cette dernière qui rend l'opération non anodine : à partir de son application, toute création de ressource dans le cluster passe par un appel réseau vers le service Gatekeeper. Si le contrôleur ne démarre pas, l'API server voit un webhook injoignable et le comportement dépend du failurePolicy du manifeste. Épinglez la branche de release et vérifiez les pods avant de déclarer la moindre contrainte.

Fenêtre de terminal
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.23/deploy/gatekeeper.yaml

Vérifiez que les pods sont prêts :

Fenêtre de terminal
kubectl get pods -n gatekeeper-system

Gatekeeper sépare volontairement la règle de son application, et c'est ce découpage qu'il faut comprendre avant d'écrire la moindre ligne de YAML. Un ConstraintTemplate seul ne bloque rien : il ne fait qu'ajouter un type de ressource au cluster. C'est la Constraint qui active la règle, sur un périmètre donné et avec des paramètres donnés. La conséquence pratique est directe : une politique qui « ne s'applique pas » vient presque toujours d'une Constraint manquante ou d'un match qui ne correspond à rien.

Les deux CRD introduits sont donc :

  • ConstraintTemplate : contient la logique Rego et définit un nouveau type de ressource Kubernetes.
  • Constraint : instancie ce type en ciblant des ressources spécifiques avec des paramètres.

Un ConstraintTemplate fait deux choses en même temps, et c'est ce qui le rend déroutant au premier abord. Le bloc crd.spec crée un nouveau type de ressource Kubernetes (ici K8sRequiredRegistry) et décrit, via openAPIV3Schema, les paramètres qu'il acceptera. Le bloc targets.rego contient la logique qui sera évaluée, avec les paramètres exposés sous input.parameters. La règle doit s'appeler violation et produire des objets {"msg": ...} : c'est le contrat attendu par Gatekeeper, un deny comme dans Conftest ne serait jamais lu.

constrainttemplate-image-registry.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredregistry
spec:
crd:
spec:
names:
kind: K8sRequiredRegistry
validation:
openAPIV3Schema:
type: object
properties:
registres_autorises:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredregistry
import rego.v1
violation contains {"msg": msg} if {
some c in input.review.object.spec.template.spec.containers
not registre_autorise(c.image)
msg := sprintf(
"conteneur '%v' : image '%v' hors des registres autorisés",
[c.name, c.image]
)
}
registre_autorise(image) if {
some prefixe in input.parameters.registres_autorises
startswith(image, sprintf("%v/", [prefixe]))
}
Fenêtre de terminal
kubectl apply -f constrainttemplate-image-registry.yaml
# Vérifiez que le CRD est bien créé
kubectl get constrainttemplate k8srequiredregistry

La Constraint est une instance du type créé par le template. Elle répond à deux questions que la logique Rego ne doit pas connaître : sur quoi la règle s'applique (bloc match, par type de ressource et par namespace) et avec quelles valeurs (bloc parameters). Cette séparation est l'intérêt principal de Gatekeeper : un même template sert à imposer des registres différents en production et en préproduction, sans dupliquer une ligne de Rego. Le match est aussi votre filet de sécurité : une contrainte sans restriction de namespace s'appliquerait aux namespaces système et pourrait bloquer les composants du cluster.

constraint-image-registry-prod.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredRegistry
metadata:
name: registry-autorise-production
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet"]
namespaces: ["production", "staging"]
parameters:
registres_autorises:
- "registry.example.com"
- "gcr.io/mon-projet"
Fenêtre de terminal
kubectl apply -f constraint-image-registry-prod.yaml

Testez la contrainte en appliquant un Deployment avec une image non conforme :

Fenêtre de terminal
kubectl create deployment test-nginx \
--image=nginx:latest \
--namespace=production \
--dry-run=server
# Résultat attendu :
# error: failed to create deployment: admission webhook "validation.gatekeeper.sh"
# denied the request: [registry-autorise-production] conteneur 'nginx' :
# image 'nginx:latest' hors des registres autorisés

Deux détails comptent ici. La commande crée un Deployment, pas un Pod : la contrainte cible apps/Deployment, un kubectl run produirait un Pod que le bloc match ignorerait, et vous concluriez à tort que la politique ne fonctionne pas. Le --dry-run=server envoie bien la requête à l'API server, qui appelle le webhook, mais n'écrit rien dans etcd : c'est la façon la plus sûre de valider une contrainte en production.

Gatekeeper peut aussi auditer les ressources déjà déployées et reporter les violations sans les bloquer. C'est indispensable lors d'un déploiement progressif.

spec:
enforcementAction: dryrun # warn | deny | dryrun
Fenêtre de terminal
# Voir les violations détectées en mode audit
kubectl get k8srequiredregistry registry-autorise-production -o json \
| jq '.status.violations'

Ces six outils partagent le même langage mais interviennent à des moments différents du cycle de vie, et c'est le seul critère de choix qui compte. Les deux premières lignes concernent votre poste, la troisième la chaîne d'intégration, la quatrième le cluster en fonctionnement. Un même corpus de règles gagne à couvrir au moins deux de ces moments : ce qui est refusé en CI doit rester refusé à l'admission, sinon un déploiement fait à la main contourne toute la démarche.

BesoinOutil
Tester une expression Rego localementopa eval
Tests unitaires automatisésopa test
Valider des manifests / IaC en CI/CDConftest
Contrôle d'admission KubernetesGatekeeper
Autorisation microservices / APIOPA server (API REST)
Distribution centralisée de politiquesOPA Bundles
  • opa eval --explain full est le premier outil de débogage quand une règle retourne undefined.
  • Les tests unitaires Rego utilisent with input as fixture, pas de mock, pas de stub, juste de la substitution d'input.
  • Conftest utilise deny / warn dans le namespace main et s'intègre directement dans un pipeline sans infrastructure supplémentaire.
  • Gatekeeper sépare la logique (ConstraintTemplate) du ciblage (Constraint) : vous pouvez réutiliser le même template pour différents namespaces ou clusters.
  • Commencez toujours par enforcementAction: dryrun en production, auditez avant de bloquer.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn