Aller au contenu
Développement medium

Règles partielles et fonctions Rego : factoriser et réutiliser ses politiques

12 min de lecture

Les deux guides précédents permettent d’écrire des politiques fonctionnelles. Dès qu’un projet dépasse une poignée de fichiers, la duplication devient un problème : la même vérification d’image, les mêmes helpers de securityContext, réécrits dans chaque politique.

Rego dispose de trois mécanismes pour factoriser la logique : les règles partielles (incremental rules), les fonctions paramétrées, et la clause else. Ce guide explique quand utiliser chacun.

  • Écrire des règles partielles pour agréger messages ou valeurs
  • Construire des chaînes de priorité avec else sans imbrication
  • Définir des fonctions paramétrées pour factoriser la logique métier
  • Utiliser les fonctions built-in utiles en DevSecOps (chaînes, regex, JSON)
  • Structurer une bibliothèque de helpers Kubernetes réutilisable

Une règle complète a exactement une valeur pour un nom donné. Si plusieurs corps portent le même nom, OPA lève une erreur — sauf si c’est intentionnel (règle partielle).

# Règle complète — une seule définition possible
niveau_criticite := "haute" if {
input.metadata.labels.env == "production"
}

Une règle partielle peut avoir plusieurs corps portant le même nom. OPA les évalue tous et agrège les résultats :

  • pour un ensemble : union de toutes les valeurs contribuées ;
  • pour un objet : union de toutes les paires clé/valeur ;
  • pour un booléen : vrai si au moins un corps est vrai (OR implicite).

C’est le mécanisme le plus puissant de Rego pour les politiques d’admission.

# Chaque bloc `violations contains msg` contribue des messages au même ensemble
violations contains msg if {
some c in input.spec.template.spec.containers
endswith(c.image, ":latest")
msg := sprintf("conteneur '%v' : tag latest interdit", [c.name])
}
violations contains msg if {
some c in input.spec.template.spec.containers
c.securityContext.privileged == true
msg := sprintf("conteneur '%v' : mode privileged interdit", [c.name])
}
violations contains msg if {
not input.metadata.labels.app
msg := "label 'app' manquant dans metadata.labels"
}

Chaque bloc est indépendant : ajouter une nouvelle vérification ne modifie aucun bloc existant. C’est le pattern ouvert/fermé appliqué aux politiques.

Quand la valeur n’est pas un ensemble mais un booléen, plusieurs corps pour le même nom forment un OU :

# `image_suspecte` est vraie si l'une OU l'autre condition est vraie
image_suspecte if { endswith(input.image, ":latest") }
image_suspecte if { not contains(input.image, ":") }
image_suspecte if { startswith(input.image, "docker.io/") }

C’est équivalent à un || dans un langage impératif, mais chaque cas est isolé dans son propre bloc, ce qui facilite la lecture et les tests unitaires.

else définit une valeur de repli ordonnée pour une règle. Là où default est le filet global (évalué en dernier, sans condition), else permet d’enchaîner des conditions avec priorité explicite.

# Calcule le niveau de risque selon le contexte
niveau_risque := "critique" if {
input.metadata.namespace == "production"
image_suspecte
} else := "eleve" if {
input.metadata.namespace == "production"
} else := "moyen" if {
image_suspecte
} else := "faible"

OPA évalue les clauses dans l’ordre et s’arrête à la première vraie. C’est l’équivalent propre d’un if / else if / else sans imbrication.

Une fonction Rego est une règle qui prend des arguments et retourne une valeur. Elle se distingue d’une règle normale par la présence de paramètres entre parenthèses.

# Syntaxe : nom(param1, param2, ...) := valeur_retour if { corps }
image_conforme(image) if {
startswith(image, "registry.example.com/")
contains(image, ":")
not endswith(image, ":latest")
}

Appel :

deny contains msg if {
some c in input.spec.template.spec.containers
not image_conforme(c.image)
msg := sprintf("conteneur '%v' : image non conforme", [c.name])
}

Une fonction peut retourner n’importe quel type, pas seulement un booléen.

# Retourne le registre d'une image (tout ce qui précède le premier `/`)
registre(image) := reg if {
parties := split(image, "/")
reg := parties[0]
}
# Retourne "inconnu" si l'image ne contient pas de `/`
registre(image) := "inconnu" if {
not contains(image, "/")
}

Rego interdit la récursion directe pour garantir la terminaison de l’évaluation. Si vous avez besoin de traverser une structure arborescente, les compréhensions imbriquées ou les fonctions built-in sont la solution.

OPA embarque une bibliothèque étendue. Voici les plus utiles en contexte DevSecOps.

startswith("registry.example.com/api:1.0", "registry.example.com/") # true
endswith("api:latest", ":latest") # true
contains("apps/v1", "/") # true
split("registry.example.com/api:1.0", "/") # ["registry.example.com", "api:1.0"]
concat(", ", ["a", "b", "c"]) # "a, b, c"
trim_space(" valeur ") # "valeur"
upper("production") # "PRODUCTION"
sprintf("conteneur '%v' sur port %v", ["nginx", 80]) # "conteneur 'nginx' sur port 80"
regex.match(`^[a-z0-9-]+$`, input.metadata.name) # nom DNS valide
regex.match(`^registry\.(example|interne)\.com/`, img) # registre autorisé
# Décoder une annotation stockée comme chaîne JSON
annotation := input.metadata.annotations["config.json"]
config := json.unmarshal(annotation)
config.replicas > 0
# Encoder
payload := json.marshal({"key": "value"})
is_string(input.metadata.name) # true si c'est une string
is_number(input.spec.replicas) # true si c'est un nombre
is_array(input.spec.containers) # true si c'est un tableau

On extrait toute la logique métier dans un fichier lib/kubernetes.rego réutilisable. Les politiques n’y font plus que des appels de fonctions.

lib/kubernetes.rego
package lib.kubernetes
import rego.v1
# Retourne true si le conteneur tourne en mode privileged
est_privilege(conteneur) if {
conteneur.securityContext.privileged == true
}
# Retourne true si l'image ne respecte pas la politique de registre
image_non_conforme(image) if {
not startswith(image, "registry.example.com/")
}
image_non_conforme(image) if {
endswith(image, ":latest")
}
image_non_conforme(image) if {
not contains(image, ":")
}
# Retourne le nom de tous les conteneurs (init + principaux)
tous_les_conteneurs(spec) := conteneurs if {
principaux := spec.containers
inits := object.get(spec, "initContainers", [])
conteneurs := array.concat(principaux, inits)
}
# Retourne true si un label obligatoire est absent
label_manquant(labels, nom) if {
not labels[nom]
}

La politique d’admission importe et utilise cette bibliothèque :

policies/deployment.rego
package kubernetes.admission.deployment
import rego.v1
import data.lib.kubernetes
default allow := false
allow if {
count(violations) == 0
}
violations contains msg if {
some c in kubernetes.tous_les_conteneurs(input.spec.template.spec)
kubernetes.image_non_conforme(c.image)
msg := sprintf("conteneur '%v' : image '%v' non conforme", [c.name, c.image])
}
violations contains msg if {
some c in kubernetes.tous_les_conteneurs(input.spec.template.spec)
kubernetes.est_privilege(c)
msg := sprintf("conteneur '%v' : mode privileged interdit", [c.name])
}
violations contains msg if {
labels_requis := ["app", "team", "env"]
some label in labels_requis
kubernetes.label_manquant(input.metadata.labels, label)
msg := sprintf("label obligatoire manquant : '%v'", [label])
}

L’évaluation charge les deux fichiers :

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

Ajouter une nouvelle vérification se résume à un bloc violations contains msg supplémentaire dans la politique, sans toucher à la bibliothèque. Ajouter une règle réutilisable ne touche pas aux politiques existantes.

Choisir entre règle partielle, fonction et règle complète

Section intitulée « Choisir entre règle partielle, fonction et règle complète »
BesoinForme recommandée
Agréger des messages d’erreurRègle partielle (violations contains msg)
Logique booléenne multi-cas (OU)Règle partielle booléenne
Logique réutilisable avec paramètresFonction
Logique de priorité ordonnéeRègle avec else
Valeur par défaut globaledefault
Valeur calculée uniqueRègle complète
  • Les règles partielles permettent le OU et l’agrégation : plusieurs corps, un résultat fusionné.
  • else exprime une priorité ordonnée ; default est le filet global non conditionnel.
  • Les fonctions paramétrées extraient la logique métier des politiques et la rendent testable isolément.
  • Le pattern bibliothèque (data.lib.*) est la façon standard de partager du code Rego entre plusieurs politiques.
  • Rego interdit la récursion — concevez vos fonctions comme des transformations pures sur des collections.

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn