
Le contrôle d’accès est un mécanisme essentiel au fonctionnement des systèmes d’exploitation, d’autant plus dans la mesure où ils sont pour la plupart multi-utilisateurs. Dans cet article, nous allons introduire le framework OPA par l’exemple d’une modélisation d’ABAC, un contrôle d’accès offrant une gestion dynamique des droits pour à peu près n’importe quelles entités.
Le contrôle d’accès réglemente les actions permises entre différentes entités. Une entité peut être un sujet ou un objet selon qu’il effectue ou subit l’action. C’est ce qui se produit lorsqu’un processus tente d’accéder à un fichier. Le processus se confronte à une matrice de contrôle d’accès nommée ACL (Access Control List) contenant des ACE (Access Control Entry). La même entité peut être tour à tour sujet et objet. Par exemple, lorsqu’un administrateur change le niveau d’accréditation d’un utilisateur. Le sujet est l’administrateur et l’objet est l’utilisateur. Ensuite, l’utilisateur se comportera comme un sujet lorsqu’il accédera aux différents objets de son système muni de ses nouveaux accès. Nous commencerons avec un état des lieux des contrôles d’accès pour introduire le fonctionnement d’ABAC (Attribute Based Access Control). Ensuite, nous présenterons OPA (Open Policy Agent) qui est le moteur de décision des politiques de sécurité. Nous poursuivrons en exhibant une implémentation d’ABAC se basant sur OPA pour contrôler les interactions sur les structures de données d’un code Python. Enfin, nous proposerons une solution de déploiement de notre architecture sous forme de conteneurs.
1. Les contrôles d’accès en (très) bref
Il existe une quantité phénoménale de contrôle d’accès en circulation dans le monde en informatique. Nous n’allons discuter que de ceux permettant de donner les éléments de compréhension de la philosophie d’ABAC. Le premier contrôle d’accès que tout le monde connaît (et subit) est le modèle IBAC (Identity Based Access Control). Dans ce modèle, pour chaque action qu’un sujet peut réaliser sur un objet, il existe une ACE correspondante. La matrice de contrôle d’accès peut donc devenir assez conséquente. De plus, chaque ajout d’entité dans le système nécessite une mise à jour de cette matrice. C’est par exemple la raison d’être des règles d’héritage de propriétaire et du umask dans le modèle discrétionnaire UGO (User Group Other) utilisé par les systèmes de fichiers Linux. Lorsqu’un nouveau fichier est créé, il nécessite des accès par défaut pour s’insérer dans la matrice. L’utilisateur peut ensuite changer ces accès du fait que UGO est discrétionnaire (c’est-à-dire que les droits sur les objets sont laissés à la discrétion du propriétaire). Nous noterons que la notion de groupe permet, dans une certaine mesure, de limiter la taille de la matrice, mais le problème de 1 nouvelle entité = 1 ajout de N ACE subsiste.
Le RBAC (Role Based Access Control) propose aux sujets d’embrasser un rôle avant de réaliser une interaction. C’est la modélisation de l’expression française « Je mets ma casquette de X ». Dans ce modèle, la matrice de contrôle d’accès est implémentée en considérant des rôles côté sujet. Nous pouvons donc avoir X entités masquées derrière un rôle dans la mesure où ils ont le droit de l’embrasser (ce qui nécessite une autre politique dédiée à la gestion des rôles). Ce modèle rend la matrice plus lisible, car factorisée. Cependant, lorsqu’un utilisateur possède un rôle, il détient l’intégralité des droits associés aux rôles. Ainsi, l’application du principe du moindre privilège peut rapidement multiplier la liste des rôles qu’il faudra étendre pour évaluer les droits réels du sujet.
Le dernier modèle à considérer avant ABAC est DTE (Domain Type Enforcement). Dans ce modèle, les sujets disposent d’un domaine et les objets un type. La matrice de contrôle d’accès définit les droits des domaines sur les types. C’est un contrôle d’accès qui vient du monde du système. C’est pour ça que les domaines et les types sont des singletons. L’évaluation des accès doit être rapide. Le gain de lisibilité est du même ordre que pour RBAC. Chaque sujet est masqué derrière un domaine et chaque objet derrière un type. Cette factorisation de chaque côté de l’ACE améliore grandement la lisibilité de la matrice de contrôle d’accès. Cependant, la même observation que RBAC relative à la gestion du moindre privilège s’applique. SELinux est une implémentation mandataire (c’est-à-dire que même les processus ayant l’identité « root » doivent s’y conformer) de DTE basée sur l’évaluation des appels système invoqués par les sujets en fonction de leurs domaines respectifs.
Le modèle ABAC (Attribute Based Access Control) reprend d’une certaine façon l’idée de DTE en introduisant la notion d’attributs sur les entités. Dans ce modèle, chaque entité possède une liste d’attributs. Dans la plupart des cas, les attributs sont des couples clé / valeur. On pourra également retrouver des singletons, rien n’est réellement précisé dans le guide du NIST sur le sujet [1]. C’est d’ailleurs le côté un peu déroutant d’ABAC. Il n’existe pas d’implémentation référence. C’est à chacun de monter sa trousse à outils dans le cadre présenté par le document référence susmentionné. Le bénéfice de ce contrôle d’accès est que la granularité du contrôle d’accès est plus fine que les modèles DTE et RBAC du fait de l’éclatement des privilèges en attributs en conservant la factorisation de la matrice. En effet, elle n’est mise à jour que si un nouvel attribut est créé. Avec ABAC, la difficulté est plutôt de maintenir la cohérence des accès autorisés par les attributs.
Pour finir, nous notons que pour tous ces modèles, le résultat de l’évaluation de la même ACE peut changer selon son contexte (heure de la journée, emplacement, compte bloqué, etc.). Dans la section suivante, nous allons voir OPA qui est un framework très générique de modélisation de contrôle d’accès.
2. Open Policy Agent (OPA)
OPA est un logiciel libre permettant de modéliser un grand nombre de contrôles d’accès. C’est une pâte à modeler que nous allons façonner pour modéliser notre contrôle accès ABAC sur les structures de données d’un code Python. Cette section est une introduction à OPA pour en saisir le fonctionnement. OPA prend en compte une politique de contrôle d’accès rédigée au format Rego. OPA s’appuie sur Flask pour recevoir les demandes de contrôle d’accès des programmes clients via une API REST (REpresentational State Transfer). Lorsqu'une requête est envoyée à OPA, elle est comparée aux règles de politique pour déterminer si l'action demandée est autorisée ou non. Commençons par installer OPA. Nous prenons la version compilée statiquement, de cette façon nous pouvons poser l’exécutable directement dans notre répertoire utilisateur sans gérer de quelconques dépendances :
Ajoutons les droits d’exécution :
Enfin, exécutons-le sur l’interface de rebouclage en lui passant la politique Rego définie ci-dessous :
La réponse d'OPA est généralement un booléen (true ou false) ou un objet JSON (JavaScript Object Notation) plus complexe selon la politique définie. Voici une politique Rego basique :
Dans cet exemple, nous définissons une politique allow par défaut à false, c’est-à-dire que par défaut l’action demandée est interdite. Nous voyons que dans la section définissant allow il existe une fonction test_niveau. Dans le code de celle-ci, nous voyons que la condition testée est de savoir si le niveau du sujet est supérieur ou égal à celui de l’objet. Éprouvons maintenant cette politique avec curl :
Cette requête contacte le serveur OPA et lui demande d’évaluer les paramètres encodés en JSON passés en brut par le paramètre ‘-d’. Il s’agit des niveaux de confidentialité du sujet et de l’objet. Ces paramètres sont passés à la fonction allow du package policy défini en en-tête du fichier Rego. Nous voyons que le résultat est false, car le niveau du sujet est de 7 et celui de l’objet de 10. Faisons varier le niveau du sujet pour le mettre à 10 :
L’exemple ci-dessus est assez naïf, mais permet de bien comprendre le fonctionnement d’OPA. Nous allons monter en puissance en proposant un code Python s’appuyant sur le package Requests [2]. Notre code comprend deux variables « sujet » et « objet » qui sont deux dict, c’est-à-dire une liste d’attributs clé / valeur. Pour le test, il n’y a qu’un seul attribut, mais évidemment on peut en mettre autant que l’on veut. Nous concaténons ces deux dict dans un un autre dict input que nous encodons en JSON. Enfin, nous nous appuyons ensuite sur la librairie requests pour interroger le serveur Flask d’OPA. Voici le code :
Exécutons-le :
Dans cette section, nous avons vu un exemple basique d’implémentation d’ABAC. Cependant, dans un « vrai » déploiement d’ABAC, il est inconcevable de modifier le code à chaque fois que l’on modifie les attributs associés à une entité. Dans la section suivante, nous donnerons quelques pistes pour rendre les choses plus dynamiques.
3. Persistance et déploiement d’ABAC
Dans cette section, nous allons voir comment rendre les données de notre cas test indépendantes du code et aussi comment le déployer dans des conteneurs Docker. Cette section n’a pas pour but de détailler les étapes, mais plus de donner au lecteur des pistes sur la direction d’un développement dans ce sens.
3.1 Persistance des attributs d’entités
L’enjeu d’ABAC est de déterminer comment stocker les attributs des différentes entités. Pour donner quelques idées au lecteur, nous allons évoquer plusieurs suggestions. La première chose à faire est de bien déterminer les limites de chaque composant de votre infrastructure. Si vous vous appuyez sur un annuaire LDAP pour vos utilisateurs, vous pouvez utiliser un schéma pour stocker les attributs à la façon de Authzforce [3]. SI vous avez besoin de stocker des attributs pour une application, une base de données telle que MongoDB embarquée avec l’application peut être un bon choix. Nous allons faire évoluer notre application pour qu’elle stocke les attributs du sujet et de l’objet dans MongoDB. Pour cela, nous nous appuierons sur le package Python Pymongo qui intègre les fonctions Python permettant de requêter une base MongoDB [4]. Créons la base de données et ajoutons les attributs de nos entités dans une collection dédiée :
Avec le fichier data.json suivant :
Modifions notre code pour qu’il aille chercher ses attributs dans la base MongoDB. Nous voyons que lorsque nous détachons le stockage des attributs de l’application il est nécessaire de pouvoir rattacher le jeu d’attribut à la structure de données dans le code. Nous introduisons donc un attribut « nom » en plus de « niveau ». Nous mettrons cet attribut à la valeur sujet pour le sujet et objet pour l’objet. Voici comment récupérer les attributs dans notre code :
Exécutons le code :
Notre code est bien connecté à la base de données. Pour la suite, il suffit d’interpréter la variable « document » du code d’exemple avec un parser JSON pour récupérer les attributs. À l’issue de cette étape, les données et le code sont séparés. Nous pouvons maintenant encapsuler notre cas test dans des conteneurs pour un déploiement en mode Cloud.
3.2 Déploiement dans des conteneurs
Dans cette dernière section, nous proposons un déploiement de notre infrastructure ABAC à la mode des microservices, c’est-à-dire en utilisant des conteneurs pour séparer les privilèges. Dans notre infrastructure, nous avons trois composants : l’application nécessitant un contrôle d’accès, OPA et le service MongoDB. Les deux derniers composants doivent absolument s’attacher à un volume afin que la politique Rego et la base de données des attributs soient persistantes. Commençons par le Dockerfile de notre application :
Ce Dockerfile est extrêmement simple, il se base sur une image Python et ajoute le code de notre application de test dans le répertoire /app. Le second Dockerfile embarque OPA :
La construction de cette image est un peu plus complexe. Elle se base sur l’image fournie par OPA. Nous y copions la politique de contrôle d’accès Rego dans le répertoire policy. Enfin, nous démarrons le serveur Flask exécutant OPA lors de l’instanciation du conteneur. Pour le dernier composant, rien de particulier. Nous utiliserons l’image MongoDB standard du Docker Hub. Il ne nous reste plus qu’à orchestrer tout ça dans un fichier Docker Compose :
Dans ce fichier Compose, nous instancions nos trois conteneurs (le conteneur app aurait pu rester en dehors si on expose le port de MongoDB). On notera que les conteneurs opa et mongodb utilisent des volumes afin de stocker les fichiers de données définis dans les Dockerfile respectivement la politique de sécurité Rego et la base de données MongoDB.
Conclusion
Dans cet article, nous avons vu comment mettre en œuvre un contrôle d’accès basé sur les attributs au sein d’une application. Au vu des mécaniques mises en œuvre dans l’article, nous pensons avoir montré au lecteur que ce contrôle d’accès modélisé à l’aide d’OPA était particulièrement intéressant dans un contexte d’application dans le Cloud comme le montre son adoption par les géants du Cloud qui proposent des implémentations d’ABAC intégrées à leurs services [5, 6]. En effet, l’utilisation d’API REST sur HTTP et les communications induites par le découpage en microservices nécessaire pour garantir la séparation de privilèges dans un contexte d’application exposée font de ce contrôle d’accès un mauvais candidat pour du bas niveau. C’est carrément l’autre côté de l’échiquier par rapport à des choses comme les LSM (Linux Security Module) intégrées au noyau Linux.
Références
[1] Guide d’implémentation d’ABAC : https://csrc.nist.gov/pubs/sp/800/162/upd2/final
[2] Python Requests : https://fr.python-requests.org/en/latest/
[3] Authzforce : https://authzforce.ow2.org/
[4] Pymongo : https://www.mongodb.com/docs/languages/python/pymongo-driver/current/
[5] Azure ABAC : https://learn.microsoft.com/fr-fr/azure/role-based-access-control/conditions-overview
[6] AWS ABAC : https://docs.aws.amazon.com/fr_fr/IAM/latest/UserGuide/introduction_attribute-based-access-control.html