Aller au contenu

Définir des policies avec OpenPolicyAgent

L'Open Policy Agent, ou OPA pour les intimes, est un moteur de politique open-source qui te permet de définir et d'appliquer des règles (ou politiques) de manière centralisée. Plutôt que d'avoir des règles éparpillées dans ton code, tu les écris une fois avec OPA, et il s'occupe du reste.

Comment ça marche ?

Imagine que ton application doit prendre une décision, comme vérifier si un utilisateur a le droit d'accéder à une ressource. Au lieu de coder cette logique directement, ton appli envoie une requête à OPA avec les infos nécessaires. OPA analyse ces données en fonction des politiques que tu as définies et renvoie une décision, par exemple "autorisé" ou "refusé".

Le langage Rego

Pour écrire tes politiques, OPA utilise un langage appelé Rego. C'est un langage déclaratif qui te permet d'exprimer des règles complexes de manière simple. Avec Rego, tu peux définir qui a accès à quoi, quelles configurations sont autorisées, et bien plus encore.

Rego v1

Depuis OPA 1.0 (décembre 2024), la syntaxe Rego v1 est obligatoire : mot-clé if avant le corps de chaque règle et contains pour les règles multi-valeurs. Les exemples ci-dessous la respectent (testés avec OPA 1.21).

Pourquoi utiliser OPA ?

  • Centralisation : Toutes tes politiques sont au même endroit, ce qui facilite leur gestion.
  • Flexibilité : OPA s'intègre avec plein de systèmes comme Kubernetes, les microservices, les pipelines CI/CD, etc.
  • Maintenance simplifiée : En séparant les politiques du code, tu peux les modifier sans toucher à ton application.

Exemple de code

Limiter l'installation de dépendances

J'ai lu il y a quelque temps que des devs avaient créé un module NPM (everything ou encore no-one-left-behind), qui lui-même embarquait des dépendances. Rien d'anormal, sauf que ses dépendances faisaient référence à l'ensemble des packages disponibles sur NPM. Cela revient à installer des milliers, voire des millions de packages.

Question

Quelles conséquences, à ton avis ?

Tu risques des coûts exorbitants, que ce soit en réseau ou en espace disque, voire de vrais dommages sur ta plateforme de CI.

Faisons ici un simple exemple de régle permettant d'empêcher sur notre plateforme de CI l'installation de tels modules. On va créer une rule en Rego, et interroger OPA afin de lui demander si notre package.json qui contient l'ensemble des dépendances de notre projet ne contient pas de packages interdits.

J'ai un projet NodeJS avec un package.json défini comme suit :

{
  "name": "open-agent-policy",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

Je définis en Rego la policy suivante :

package ci

# On définit la valeur par défaut à false
default allow := false

# On liste les packages interdits
blacklist := {
    "lodash"
}

# On collecte ceux qui sont interdits (règle multi-valeurs : contains)
violations contains pkg if {
    input.dependencies[pkg]
    blacklist[pkg]
}

# allow vaut true seulement si on a zéro interdit
allow if {
    count(violations) == 0
}

Je peux faire cette évaluation en local directement avec OPA :

opa eval --input package.json --data rules/ci.rego --format pretty 'data.ci'
{
  "allow": false,
  "blacklist": [
    "lodash"
  ],
  "violations": [
    "lodash"
  ]
}

OPA nous empêche donc bien notre action. Quelques explications à propos de cette commande :

  • --input: permet de renseigner notre fichier package.json qui contient les modules NPM que l'on souhaite analyser
  • --data: ici on définit la policy écrite en Rego qui doit être évaluée
  • --format: permet d'afficher en console de façon indentée
  • le dernier argument data.ci permet d'afficher en sortie le résultat de l'évaluation. À tester aussi data.ci.allow ou data.ci.violations selon la verbosité souhaitée

Exemple avec de l'IaC

Si tu fais de l'infra as code avec du Terraform, tu as sûrement un repo GitHub où tu centralises tout ton code. Tu pourrais très bien définir des rules afin d'enforce certaines bonnes pratiques. Pour l'infra de ce blog que tu lis, j'ai défini un bucket public afin que mon site soit disponible sur le net.

Mais quid d'un bucket public que tu définirais au sein même de ton entreprise ? Ce serait une belle faute. On va donc voir comment s'assurer, avant même d'apply ton bucket en prod, que celui-ci reste bien privé.

Commence par exporter ton plan Terraform au format JSON :

terraform plan -out tfplan
terraform show -json tfplan > tfplan.json

Puis définis ta policy en Rego v1 dans rules/s3.rego :

package terraform.s3

# Attributs du public access block qui doivent tous valoir true
public_access_flags := ["block_public_acls", "block_public_policy", "ignore_public_acls", "restrict_public_buckets"]

deny contains msg if {
    some rc in input.resource_changes
    rc.type == "aws_s3_bucket_public_access_block"
    some flag in public_access_flags
    rc.change.after[flag] != true
    msg := sprintf("%s : %s doit valoir true", [rc.address, flag])
}

deny contains msg if {
    some rc in input.resource_changes
    rc.type == "aws_s3_bucket_acl"
    rc.change.after.acl in {"public-read", "public-read-write"}
    msg := sprintf("%s : ACL publique interdite (%s)", [rc.address, rc.change.after.acl])
}

Évalue-la sur ton plan :

opa eval --input tfplan.json --data rules/s3.rego --format pretty 'data.terraform.s3.deny'
[
  "aws_s3_bucket_acl.data : ACL publique interdite (public-read)",
  "aws_s3_bucket_public_access_block.data : block_public_policy doit valoir true"
]

En CI, ajoute --fail-defined et interroge data.terraform.s3.deny[_] : la commande sort en erreur (code 1) dès qu'une violation existe, ce qui bloque ton terraform apply.

Pour aller plus loin