Sécurisation des services avec Open Policy Agent

SCHOUKROUN Leo

By SCHOUKROUN Leo

22 janv. 2020

Open Policy Agent est un un moteur de règles multifonction. L’objectif principal du projet est de centraliser l’application de règles de sécurité à travers la stack cloud native. Le projet a été crée par Styra et il est actuellement incubé au sein de la Cloud Native Computing Foundation. OPA est utilisé chez Netflix, SAP, Cloudflare et d’autres sociétés.

Nous allons maintenant étudier le fonctionnement d’Open Policy Agent et voir comment l’utiliser pour implémenter des règles d’accès à Kafka, Kubernetes et dans une application personnalisée.

Architecture

Open Policy Agent (OPA) est conçu pour tourner avec votre application, en tant que daemon. La principal avantage à ce que OPA tourne localement avec votre service est que cela permet à ce dernier de prendre des décisions rapides sans avoir à contacter de service externe.

Le processus de prise de décision avec OPA est le suivant :

  • L’application reçoit une requête
  • L’application traduit cette requête dans un format JSON
  • L’application envoie ce JSON à OPA
  • OPA évalue la l’objet de la requête par rapport aux règles de sécurité définies
  • OPA envoie la décision (authorisation ou non) dans un objet JSON à l’application
  • L’application répond (ou non) à la requête, la police de sécurité a bien été appliquée.

Dans la plupart des cas, OPA sera executé en tant que daemon et requêté via REST. Pour les applications en Go, OPA peut être utilisé en tant que librairie avec le package github.com/open-policy-agent/opa/rego.

Dissociation des règles de sécurité

Open Policy Agent est conçu autour du concept de policy decoupling, ou dissociation des règles de sécurité. Celui-ci peut être décrit comme le besoin pour un logiciel de pouvoir être capable d’appliquer des règles de sécurités stockées à l’extérieur de l’application et que celles-ci soient facilement modifiables sans avoir à re-builder ou re-déployer toute l’application.

OPA fonctionne de la même manière pour toutes les applications. En effet il peut être utilisé pour à peu près tout tant que la logique est définie dans l’application et que les entrées / sorties de celle-ci peuvent être exprimées à dans le langage Rego. Si vous êtes familier de l’écosystème Big Data, vous pouvez pensez d’OPA qu’il s’agit d’un service comme Apache Ranger implémentable partout.

Il existe des dizaines d’implémentations d’OPA disponibles pour des services variés : Kafka, Docker, SSH, sudo, etc. Elles sont disponibles ici. L’implémentation pour Kubernetes est un peu particulière et fait l’objet d’un projet à part : Gatekeeper.

Le langage Rego

Rego est un langage haut-niveau utilisé par OPA pour définir les polices de sécurité. Il est conçu pour être particulièrement facile à lire et écrire.

Une police OPA se résume à “utilisateur U peut/ne peut pas effectuer l’opération O

Voici un exemple d’une règle très simple qui bloque tous les utilisateurs à part leo :

package example

default allow = false

allow {
    u := input.user
    u == "leo"
}

Rego supporte l’utilisation de variables, des strings, valeurs numériques, bouléens, les comparaisons, etc. Si vous voulez voir rapidement toutes les possibilitées offertes par Rego, la cheat sheet est une bonne solution.

Styra a conçu un outil intéressant : Rego Playground. Celui-ci est très pratique pour tester ses polices.

Voici notre police définie précédemment testée dans l’outil :

Rego Playground

Nous rencontrerons d’autres exemples de polices dans la suite de cet article.

Open Policy Agent avec Apache Kafka

Dans cette partie, nous allons voir comment implémenter des polices de sécurité dans Apache Kafka avec OPA. Par soucis de simplicité, la démo s’effectuera avec une instalation de Kafka sur un noeud unique. Le kafka n’étant pas sécurisé, il n’y a pas de notion d’identité.

En premier lieu, téléchargeons et installons Open Policy Agent :

mkdir /opt/opa && cd /opt/opa
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_darwin_amd64
chmod 755 ./opa

Nous allons également créer une policy qui bloque tout par défaut :

mkdir -p data/kafka/authz

cat <<EOF >  data/kafka/authz/allow.rego
package kafka.authz

default allow = false

EOF

Démarrons Open Policy Agent en mode serveur :

./opa run --server --watch data/

Téléchargement et installation d’une version récente de Kafka.

curl http://apache.crihan.fr/dist/kafka/2.3.1/kafka_2.12-2.3.1.tgz -o /tmp/kafka_2.12-2.3.1.tgz
tar -xvzf /tmp/kafka_2.12-2.3.1.tgz -C /opt

Il nous faut maintenant récupérer les sources du repository contrib de Open Policy Agent et compiler le plugin Kafka.

git clone https://github.com/open-policy-agent/contrib
cd kafka_authorizer
mvn install

Par défaut, il est compilé pour Kafka 1.0.0, nous allons devoir changer kafka_authorizer/pom.xml pour le rendre compatible avec notre version de Kafka.

Les JARs obtenus doivent être déployés dans le dossier lib de Kafka :

cp target/kafka-authorizer-opa-1.0.jar /opt/kafka_2.12-2.3.1/libs/
cp target/kafka-authorizer-opa-1.0-package/share/java/kafka-authorizer-opa/gson-2.8.2.jar /opt/kafka_2.12-2.3.1/libs/

Toutes les configurations du broker Kafka garderont leurs valeurs par défaut. Nous allons uniquement ajouter les propiétés nécéssaire pour faire de OPA l’autorité de sécurité de Kakfa et lier le service à la police que nous venons de créer.

cd /opt/kafka_2.12-2.3.1

cat <<EOF >> config/server.properties

###################### OPA Properties ######################
authorizer.class.name: com.lbg.kafka.opa.OpaAuthorizer

opa.authorizer.url=http://localhost:8181/v1/data/kafka/authz/allow
opa.authorizer.allow.on.error=false
opa.authorizer.cache.initial.capacity=100
opa.authorizer.cache.maximum.size=100
opa.authorizer.cache.expire.after.ms=600000

EOF

On peut maintenant démarrer Apache ZooKeeper puis le broker Kafka :

bin/zookeeper-server-start.sh config/zookeeper.properties
bin/kafka-server-start.sh config/server.properties

Essayons de créer un topic pour voir ce qui se produit :

bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic MyTopic
Error while executing topic command : org.apache.kafka.common.errors.TopicAuthorizationException: Not authorized to access topics: [Authorization failed.]
[2019-12-07 17:50:56,727] ERROR java.util.concurrent.ExecutionException: org.apache.kafka.common.errors.TopicAuthorizationException: Not authorized to access topics: [Authorization failed.]
	at org.apache.kafka.common.internals.KafkaFutureImpl.wrapAndThrow(KafkaFutureImpl.java:45)
	at org.apache.kafka.common.internals.KafkaFutureImpl.access$000(KafkaFutureImpl.java:32)
	at org.apache.kafka.common.internals.KafkaFutureImpl$SingleWaiter.await(KafkaFutureImpl.java:89)
	at org.apache.kafka.common.internals.KafkaFutureImpl.get(KafkaFutureImpl.java:260)
	at kafka.admin.TopicCommand$AdminClientTopicService.createTopic(TopicCommand.scala:190)
	at kafka.admin.TopicCommand$TopicService.createTopic(TopicCommand.scala:149)
	at kafka.admin.TopicCommand$TopicService.createTopic$(TopicCommand.scala:144)
	at kafka.admin.TopicCommand$AdminClientTopicService.createTopic(TopicCommand.scala:172)
	at kafka.admin.TopicCommand$.main(TopicCommand.scala:60)
	at kafka.admin.TopicCommand.main(TopicCommand.scala)
Caused by: org.apache.kafka.common.errors.TopicAuthorizationException: Not authorized to access topics: [Authorization failed.]
 (kafka.admin.TopicCommand$)

Notre police de sécurité stricte “deny all” a été appliquée : il nous est impossible de créer un topic. Voyons si on peut le faire après avoir modifié la police pour cette fois ci autoriser toutes les actions :

cat <<EOF >  /opt/opa/data/kafka/authz/allow.rego
package kafka.authz

default allow = true

EOF

bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic MyTopic

C’est bon ! Le topic a bien été créé car OPA a appliqué cette nouvelle police.

Cette règle de sécurité est trop simple, nous allons en créer une un peu plus commpliqué qui doit répondre au besoin suivant :

Il est uniquement possible d’écrire dans le topic test_project. Tous les producers qui essayent d’écrire sur un autre topic seront bloqués. TOutes les autres actions sur tous les topics seront authorisées.

package kafka.authz

default allow = false

allow {
	not deny
}

deny {
	is_produce_operation
	not is_topic_test_project
}

is_produce_operation {
    input.operation.name == "Write"
}

is_topic_test_project {
    input.resource.name == "test_project"
}

Avec cette règle, nous pouvons faire toutes les opérations sur tous les topics sauf l’écriture qui est uniquement autorisée dans le topic test_project :

bin/kafka-console-producer.sh --broker-list localhost:9092 --topic project

>This should be blocked
>[2019-12-09 13:59:10,486] ERROR Error when sending message to topic test with key: null, value: 22 bytes with error: (org.apache.kafka.clients.producer.internals.ErrorLoggingCallback)
org.apache.kafka.common.errors.TopicAuthorizationException: Not authorized to access topics: [project]

bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test_project
>This should be allowed
>SUCCESS!

Maintenant vous savez utiliser OPA pour implémenter des règles d’accès aux resources de Kafka.

Quid d’OPA avec mon application ?

Grâce à l’API REST d’Open Policy Agent, il est très aisé d’intégrer ce-dernier à vos applications maison. L’ojbectif est de disposer d’une gestion d’accès simple, portable et centralisée pour votre application sans avoir à réinventer la roue.

Pour cette démo, on dispose d’une WebApp Flask très simple :

from flask import Flask
from flask import request
import requests
app = Flask(__name__)

@app.route('/')
def hello_world():
    allowed = False
    username = request.args.get('username')

    r = requests.post('http://master01.metal.ryba:8181/v1/data/myapp/auth/allow', json={"input": {"user": username}})
    allowed = r.json()['result']

    if allowed == True:
        return 'Hello, World! You are allowed!'
    else:
        return 'Hello, World! You are not allowed!'

Comme nous pouvons le constater dans le code, la partie autorisation est déléguée à Open Policy Agent qui tourne sur le serveur master01.metal.ryba sur le port 8181. L’application peut être démarée avec :

export FLASK_APP=myapp.py
flask run                

La police de sécurité est définie de cette façon :

mkdir -p data/myapp/auth
cat <<EOF >  data/myapp/auth/allow.rego

package myapp.auth
default allow = false
allow {
  user := input.user
  user == "leo"
}

EOF

La règle d’accès est simple. Elle doit permettre à un utilisateur d’afficher la ressource demandée ou non selon le paramètre username qui est envoyé. Essayons :

curl http://127.0.0.1:5000/
Hello, World! You are not allowed!

curl http://127.0.0.1:5000?username=leo
Hello, World! You are allowed!

Denied in custom application

Allowed in custom application

Ça fonctionne !

Quoi d’autre ?

Nous avons vu ce qu’est Open Policy Agent : comment le service fonctionne et comment l’intégrer à Kafka ou à une application personnalisée. Dans un prochain article, nous essayerons l’intégration d’OPA à Kubernetes.

Un homme à côté de règles de sécurité

Canada - Morocco - France

International locations

10 rue de la Kasbah
2393 Rabbat
Canada

Nous sommes une équipe passionnées par l'Open Source, le Big Data et les technologies associées telles que le Cloud, le Data Engineering, la Data Science le DevOps…

Nous fournissons à nos clients un savoir faire reconnu sur la manière d'utiliser les technologies pour convertir leurs cas d'usage en projets exploités en production, sur la façon de réduire les coûts et d'accélérer les livraisons de nouvelles fonctionnalités.

Si vous appréciez la qualité de nos publications, nous vous invitons à nous contacter en vue de coopérer ensemble.