Gestion des droits d'accès à l'annuaire

en chantier !

Cette page a pour objectif d'exposer des bonnes pratiques concernant la mise en œuvre des règles de contrôle d'accès aux requêtes LDAP, via les mécanismes de type “Access Control List” (ACL) offerts par le serveur LDAP ; et plus particulièrement en s'appuyant sur la nomenclature proposée pour Les groupes d'applications.

Ces recommandations s'appliquent à OpenLDAP, et découlent du modèle de sécurité que propose cette implémentation. D'autres implémentations de serveurs LDAP peuvent avoir une logique et une architecture de droits sensiblement différentes.

Ces recommandations s'inscrivent dans un contexte plus large de gestion de la sécurité et de la confidentialité des données, que nous ne détaillerons pas ici mais dont nous rappelons quelques grandes lignes ci-dessous.

N'ouvrir par défaut que les accès qui sont nécessaires (tout ce qui n'est pas explicitement autorisé est interdit). Ce principe est applicable à tous les niveaux : flux réseaux, ACLs LDAP, etc .

Le pare-feu constitue le premier niveau de protection d'un annuaire LDAP. Selon le cas, il pourra être fait appel à un pare-feu central, ou des règles de filtrage IP locales à l'hôte hébergeant le service LDAP. En fonction de l'usage envisagé du serveur LDAP, son accès sera par exemple restreint uniquement aux sous-réseaux hébergeant des serveurs d'applications, ou aux postes de travail internes à l'établissement. Une ouverture du protocole LDAP hors du réseau d'établissement est rarement nécessaire et devra être examinée au cas par cas, en s'assurant que les autres mécanismes de sécurisation sont bien opérationnels, notamment l'authentification, le chiffrement et les ACLs.

L'établissement peut faire le choix de mettre en œuvre plusieurs instances de serveurs LDAP, dédiée chacune à un usage ou une audience particulière : par exemple une instance pour les requêtes provenant de serveurs d'applications, et une autre accessible des postes de travail des utilisateurs, pour les usages de type “carnet d'adresses” des clients de messagerie. Chaque instance pourra avoir sa propre stratégie de sécurité (pare-feu, limites, ACLs, authentification…), et son propre contenu, par le biais de réplications partielles : par exemple, l'instance “postes de travail” ne contiendra pas de données privées.

Le chiffrement garantit la confidentialité et l'intégrité des échanges entre le client et le serveur, et sécurise l'authentification. Lorsque le protocole LDAP est exposé sur un lien non maîtrisé, il est nécessaire de s'assurer que le chiffrement est bien rendu obligatoire, par exemple en n'autorisant que le chiffrement TLS implicite (protocole LDAPS sur le port 636), ou en utilisant des ACLs imposant un “Security Strength Factor” (SSF) minimum : access by ssf=…. Il convient également de s'assurer que les protocoles et algorithmes de chiffrement utilisés sont suffisamment sûrs au regard de l'état de l'art, en désactivant les algorithmes et protocoles obsolètes : utilisation de la directive OpenLDAP TLSCipherSuite, désactivation des anciennes versions de SSL/TLS, suppression des “enctypes” obsolètes dans les keytab Kerberos, etc.

L'authentification constitue le niveau le plus fin du contrôle d'accès, sur lequel s'appuient notamment les limites et les ACLs décrites ci-après. Elle peut être considérée comme nécessaire dès lors que le serveur LDAP héberge des données privées (voir : Visibilité des attributs) et que des distinctions de droits d'accès doivent être faites en fonction du client. Selon le cas, on pourra faire appel à un mécanisme d'authentification interne au serveur LDAP (mot de passe), externe (Kerberos ou certificat TLS par exemple), ou se contenter d'un contrôle d'adresse IP si adapté au contexte. A noter que des mécanismes comme Kerberos ou TLS, en plus d'offrir une couche de chiffrement du flux de données, permettent l'authentification mutuelle : le serveur peut authentifier son client, et le client son serveur.

Dans un certain nombre de cas, des données issues du LDAP peuvent être rendues accessibles à un tiers avec un contrôle d'accès relativement laxiste, charge à ce tiers d'assurer un contrôle plus strict avant de présenter les données au client final. C'est le cas par exemple d'une API se connectant à l'annuaire LDAP avec une identité unique, et présentant des données filtrées à des applications clientes via un autre protocole (web service REST ou SOAP, SAML, etc). Dans ce cas la confiance envers ce tiers fait partie intégrante du modèle de sécurité, et doit être auditée avec rigueur. On parle communément d'identité “de confiance” ou “trusted”.

Imposer des limites d'accès sur les requêtes LDAP permet :

  1. de limiter la collecte massive de données de l'annuaire (malveillance), en complément des autres mesure de protection (pare-feu, authentification) ;
  2. de se protéger contre une rupture ou une dégradation de service par épuisement des ressources du serveur, qu'elle soit malveillante (déni de service) ou accidentelle (requêtes mal formulées, par exemple non indexées ou trop lourdes au regard de leur finalité, qui engendrent une charge exagérée sur le serveur).

Le principe de base est d'appliquer des limites basses aux connexions anonymes (si celles-ci sont autorisées), ainsi qu'aux applications ou utilisateurs n'ayant pas déclaré de besoin particuliers.

Ensuite, proportionner les limites aux usages :

  1. limites plus ou moins élevées correspondant aux besoins pour les applications sous la responsabilité d'un tiers ;
  2. absence de limites pour les comptes les plus privilégiés, parfaitement maîtrisés, s'ils en ont besoin (connecteurs d'alimentation de l'annuaire par exemple).

OpenLDAP propose plusieurs catégories de limites :

  1. limites sur la taille des requête (size);
  2. limites sur la durée des requêtes (time);
  3. limites sur la “complexité” des requêtes (size unchecked) : correspond à la taille de la requête interne qu'OpenLDAP doit exécuter pour répondre à la requête client, quelle que soit la taille de la réponse effective. Ce type de limite offre une protection efficace contre les requêtes mal formulées, notamment les recherches non indexées.

Point important : toutes les requêtes exécutées de façon répétitives et qui attendent une réponse immédiate (temps réel) doivent être indexées : voir Types d'indexation des attributs.

OpenLDAP propose deux variantes de limites: “soft” et “hard”. Les limites “soft” sont des limites par défaut, que le client peut contourner en réclamant une limite plus haute ; tandis que les limites “hard” ne peuvent pas être outrepassées. Seules ces dernières sont utiles pour bloquer des requêtes abusives intentionnelles. Elles ne constituent néanmoins pas une arme absolue contre le moissonnage abusif, car elles peuvent toujours être contournées par morcellement des requêtes ou utilisation de requêtes paginées. En absence de précision, la directive de configuration limits affecte les deux variantes simultanément.

Par défaut OpenLDAP fixe des limites arbitraires : durée (timelimit) de 1h, taille (sizelimit) de 500 entrées.

La première directive limits dont le sujet (<who>) correspond au client l'emporte. Il convient donc de déclarer les limites par ordre décroissant, les plus larges en premier.

L'exemple ci-dessous peut constituer une base de définition des limites d'un annuaire établissement typique, à ajuster en fonction des besoins.

Ce fichier fait appel à la nomenclature préconisée par SupAnn pour les groupes de contrôles d'accès des applications: Les groupes d'applications.

Note: ce fichier est un extrait de configuration au format statique (.conf), qui peut être référencé dans le fichier slapd.conf via une directive include au sein de la section database souhaitée. Si vous utilisez la configuration dynamique (cn=config), il peut être converti au format LDIF en lançant slapd avec les options -f et -F, comme spécifié dans man slapd.

slapd-limits.conf
# -- aucune limite pour les applications de confiance 
# effectuant des requêtes "batch" sur la totalité de l'annuaire
limits group="cn=acl.limits.unlimited,ou=applicationGroups,dc=univ-exemple,dc=fr"
  size=unlimited size.unchecked=unlimited time=unlimited
 
# -- limites élevées, pour des applications amenées à faire de grosses requêtes
limits group="cn=acl.limits.large,ou=applicationGroups,dc=univ-exemple,dc=fr"
  size=10000 size.unchecked=100000 time=60
 
# -- limites moyennes, convenant à une majorité d'applications 
limits group="cn=acl.limits.medium,ou=applicationGroups,dc=univ-exemple,dc=fr"
  size=1000 size.unchecked=50000 time=20
 
# -- limites basses, pour des applications n'effectuant que des requêtes limitées
limits group="cn=acl.limits.small,ou=applicationGroups,dc=univ-exemple,dc=fr"
  size=100 size.unchecked=50000 time=10
 
# -- limites de compatibilité basse, pour les applications ne gérant pas correctement
# l'erreur "administrative limits exceeded" en cas de résultats partiels: 
# pas de "size.unchecked" mais report sur un "time" très réduit.
limits group="cn=acl.limits.noload.small,ou=applicationGroups,dc=univ-exemple,dc=fr"
  size=10 size.unchecked=unlimited time=2
 
# -- limites par défaut pour les connexions authentifiées: 
# autorise les recherches approximatives simples, avec taille plafonnée
limits users 
  size=10 size.unchecked=10000 time=5
 
# -- limites par défaut pour les connexions anonymes: 
# n'autorise que des recherches unitaires indexées
limits anonymous
  size=1 size.unchecked=1000 time=2

Les ACLs permettent de mettre en œuvre le principe de moindre exposition par défaut, en graduant l'étendue des accès en fonction de la confiance accordée au client, et d'assurer la confidentialité des données considérées comme privées ou secrètes, si l'annuaire en contient.

OpenLDAP implémente un contrôle d'accès via des règles statiques, définies au niveau de la configuration du serveur. Ces règles peuvent s'appuyer sur des mécanismes relativement complexes, définis de manière exhaustive dans la page de manuel man slapd.access.

Par défaut, en absence d'ACLs, toute l'arborescence est accessible en lecture seule quel que soit l'identité du client.

Il est recommandé de toujours inclure un minimum de directives de droits d'accès dans sa configuration, ne serait-ce que pour s'assurer que le droit par défaut soit cohérent avec son objectif, et protéger les mots de passe servant à l'authentification des clients.

Bien qu'OpenLDAP permette de gérer des droits d'accès discrétionnaires très fins, SupAnn recommande de se limiter aux 5 niveaux macroscopiques imbriqués qui sont écriture (write), lecture (read), recherche (search), authentification (auth) ou rien (none). Ces niveaux sont ordonnés du plus fort au plus faible, chaque niveau impliquant les privilèges des niveaux inférieurs.

OpenLDAP applique une logique séquentielle de traitement des ACLs : la première rencontrée dont l'objet (champ <what> dans man slapd.access) correspond à la requête en cours s'applique et arrête le traitement pour cet objet, de façon similaire aux ACLs de pare-feu réseau. Les privilèges accordés aux sujets énumérés (champ <who>) sont alors appliqués à cet objet, aucun accès n'étant accordé si aucun sujet ne correspond ; un échappatoire est néanmoins possible avec l'action break, qui permet de continuer la lecture des ACLs suivantes pour le même objet.

Pour chaque niveau de privilège (écriture, lecture), les règles définissant des exceptions sur un périmètre plus restreint que la règle générale (en terme d'objets ciblés ou de sujets autorisés) doivent donc être placées avant celle-ci.

Dans ce contexte SupAnn préconise l'ordonnancement suivant des ACLs :

  • droit d'administration global
  • droits d'administration discrétionnaires sur des branches
  • masquage sélectif d'entrées sur la base d'attributs matérialisant leur état (ex: supannRessourceEtat), avec gestion d'exceptions
  • droits d'écriture discrétionnaires sur attributs ou groupes d'attributs
  • droit d'accès anonyme nécessaire pour l'authentification
  • droit de lecture des branches publiques
  • droit de parcours des branches à visibilité restreinte
  • droits de lecture discrétionnaires sur des branches
  • droits de lecture discrétionnaires sur des attributs ou groupes d'attributs
  • droits par défaut de recherche ou de lecture sur les attributs
  • rejet de toutes les requêtes restantes

Le fichier de configuration suivant reprend l'ensemble de ces concepts, et donne un modèle pouvant être adapté et enrichi en fonction des besoins. Il fait appel à la syntaxe recommandée par SupAnn pour attribuer les privilèges via des groupes : voir Les groupes d'applications.

Note: ce fichier est un extrait de configuration au format statique (.conf), qui peut être référencé dans le fichier slapd.conf via une directive include au sein de la section database souhaitée. Si vous utilisez la configuration dynamique (cn=config), il peut être converti au format LDIF en lançant slapd avec les options -f et -F, comme spécifié dans man slapd.

slapd-access.conf
# ====== AUTORISATIONS D'ECRITURE GLOBALES ======
 
# --- administrateurs LDAP: accès total
access to *
    by group=cn=acl.dit.admin,ou=applicationGroups,dc=univ-exemple,dc=fr write
    by * break
 
# --- Accès global à ou=groups
# cet exemple permet de déléguer la gestion des groupes à une application spécifique
access to 
    dn.sub="ou=groups,dc=univ-exemple,dc=fr"
    by group=cn=acl.dit.groups.admin,ou=applicationGroups,dc=univ-exemple,dc=fr write
    by * break
 
# ====== INTERDICTIONS ======
 
# --- masquage des comptes inactifs
# cet exemple masque les entrées inactives (et en bloque l'authentification), 
# sauf pour les applications dûment autorisées.
access to
    filter="(supannRessourceEtat={COMPTE}I*)"
    by group=cn=acl.filter.inactive.access,ou=applicationGroups,dc=univ-exemple,dc=fr break
    by * none
 
# ====== AUTORISATIONS D'ECRITURE DISCRETIONNAIRES SUR LES ATTRIBUTS ======
 
# --- écriture supannFCPerson
# cet exemple délègue à une application la gestion de l'identité France Connect
access to
    attrs=@supannFCPerson,objectClass
    by group=cn=acl.class.supannFCPerson.write,ou=applicationGroups,dc=univ-exemple,dc=fr write
    by * break
 
# ====== AUTHENTIFICATION ======
 
# cet exemple autorise l'authentification par mot de passe, sauf pour les entrées à l'état "suspendu"
access to
    filter="(!(supannRessourceEtat={COMPTE}S*))"
    attrs=userPassword
    by anonymous auth
    by self write
    by * none
 
# ====== AUTORISATIONS PAR DEFAUT SUR LES BRANCHES =====
 
# --- branches publiques ---
 
# schéma
access to dn.sub=cn=Subschema
    by * read
 
# -- base
access to dn.base=dc=univ-exemple,dc=fr
    by * read
 
# -- premier niveau d'arborescence
access to dn.one=dc=univ-exemple,dc=fr
    by * read
 
# -- entités SupAnn
access to dn.children=ou=structures,dc=univ-exemple,dc=fr
    by * read
 
# --- Branches énumérables publiquement (avec attributs filtrés) ---
 
# -- personnes
access to 
    dn.children=ou=people,dc=univ-exemple,dc=fr 
    attrs=entry
    by * read
 
# -- groupes 
access to 
    dn.children=ou=groups,dc=univ-exemple,dc=fr 
    attrs=entry
    by * read
 
# ====== AUTORISATIONS DE LECTURE DISCRETIONNAIRES SUR LES BRANCHES ======
 
# -- lecture de ou=people
# cet exemple donne "carte blanche" sur la branche ou=people,
# sauf pour les mots de passe bloqués à la section "AUTHENTIFICATION".
access to
    dn.children=ou=people,dc=univ-exemple,dc=fr
    by group=cn=acl.dit.people.read,ou=applicationGroups,dc=univ-exemple,dc=fr read
    by * break
 
# -- lecture de ou=groups
access to
    dn.children=ou=groups,dc=univ-exemple,dc=fr
    by group=cn=acl.dit.groups.read,ou=applicationGroups,dc=univ-exemple,dc=fr read
    by * break
 
# ====== AUTORISATIONS DE LECTURE DISCRETIONNAIRES SUR LES ATTRIBUTS ======
 
# -- lecture du scope "identifiants"
access to
    attrs=eduPersonPrincipalName,eduPersonPrincipalNamePrior,eduPersonTargetedID,eduPersonUniqueId,supannAliasLogin,supannEmpId,supannEtuId,supannFCSub,supannRefId,uid
    by group=cn=acl.scope.id.read,ou=applicationGroups,dc=univ-exemple,dc=fr read
    by * break
 
# -- lecture du scope "identité numérique"
access to
    attrs=cn,displayName,eduPersonNickname,sn,givenName,supannCivilite,supannCodeINSEEPaysDeNaissance,supannCodeINSEEVilleDeNaissance
    by group=cn=acl.scope.identity.read,ou=applicationGroups,dc=univ-exemple,dc=fr read
    by * break
access to
    attrs=,supannNomDeNaissance,supannOIDCDateDeNaissance,supannOIDCGenre,supannPrenomsEtatCivil
    by group=cn=acl.scope.identity.read,ou=applicationGroups,dc=univ-exemple,dc=fr read
    by * break
 
# -- lecture du scope "gestionnaires d'un compte"
access to
    attrs=supannMailContact,owner,manager,supannGroupeAdminDN,supannParrainDN
    by group=cn=acl.scope.manager.read,ou=applicationGroups,dc=univ-exemple,dc=fr read
    by * break
 
# -- lecture du scope "données personnelles"
access to
    attrs=homePostalAddress,homePhone,mailForwardingAddress,preferredLanguage,supannAdressePostalePrivee
    by group=cn=acl.scope.manager.read,ou=applicationGroups,dc=univ-exemple,dc=fr read
    by * break
access to
    attrs=supannConsentement,supannListeRouge,supannMailPerso,supannMailPrive,supannTelephonePrive
    by group=cn=acl.scope.private.read,ou=applicationGroups,dc=univ-exemple,dc=fr read
    by * break
 
# -- lecture de la carte multi-services
access to 
    attrs=@supannCMS
    by group=cn=acl.class.supannCMS.read,ou=applicationGroups,dc=univ-exemple,dc=fr read
    by * break
 
# -- lecture des timestamps
# cet exemple permet à des applications de s'alimenter sur l'annuaire en mode "delta" (différentiel)
access to
    attrs=contextCSN,entryCSN,modifyTimestamp,createTimestamp
    by groupe=cn=acl.scope.timestamps.read,ou=applicationGroups,dc=univ-exemple,dc=fr read
    by * break
 
# -- lecture des membres des groupes
# cet exemple autorise des applications à lire les membres de tous les groupes
# et par symétrie, donne accès aux memberOf des membres.
access to
    attrs=member,memberOf
    by groupe=cn=acl.attr.member.read,ou=applicationGroups,dc=univ-exemple,dc=fr read
    by * break
 
 
# ====== AUTORISATIONS PAR DEFAUT SUR LES ATTRIBUTS =====
 
# -- visibilité sélective des propriétés des groupes
# cet exemple ne montre les membres et autres propriétés des groupes
# qu'en fonction des l'appartenance ou des privilèges du sujet
access to
    filter="(objectClass=groupOfNames)"
    attrs=member
    by dnattr=member read
    by dnattr=supannGroupeLecteurDN read
    by * break
access to
    filter="(objectClass=groupOfNames)"
    attrs=member,@supannGroupe
    by dnattr=owner read
    by dnattr=supannGroupeAdminDN read
    by * break
 
# -- attributs limités en recherche
access to attrs=eduPersonPrincipalNamePrior,eduPersonUniqueId
    by self read
    by * search
access to attrs=supannAutreMail,supannRessourceEtat,supannRessourceEtatDate,supannAliasLogin,supannListeRouge,supannFCSub
    by self read
    by * search
 
# -- attributs publics
access to
    attrs=entryDN,objectClass,structuralObjectClass,subschemaSubentry,hasSubordinates,aliasedObjectName,entryUUID
    by * read
access to
    attrs=uid,cn,mail,o,ou,dc,l,displayName,supannCivilite,title,sn,givenName,description,labeledURI,preferredLanguage
    by * read
access to
    attrs=eduPersonNickname,eduPersonPrincipalName,eduPersonOrgDN,eduPersonPrimaryOrgUnitDN,eduPersonOrgUnitDN
    by * read
access to
    attrs=supannEtablissement,supannEntiteAffectation,supannEntiteAffectationPrincipale,supannTypeEntite,supannAffectation,supannActivite
    by * read
access to
    attrs=eduPersonAffiliation,eduPersonPrimaryAffiliation,supannRole,supannRoleGenerique,supannRoleEntite
    by * read
access to
    attrs=supannEtuAnneeInscription,supannEtuCursusAnnee,supannEtuDiplome,supannEtuElementPedagogique,supannEtuEtape,supannEtuInscription
    by * read
access to
    attrs=supannEtuRegimeInscription,supannEtuSecteurDisciplinaire,supannEtuTypeDiplome
    by * read
access to 
    attrs=telephoneNumber,supannAutreTelephone,facsimileTelephoneNumber,mobile,postalAddress
    by * read
access to 
    attrs=supannCodePopulation,supannExtProfil,supannEmpProfil,supannEmpDateFin,supannExtDateFin,supannEtuDateFin
    by * read
 
# -- tout le reste est privé
access to *
    by self read
    by * none
  • documentation/supann/courant/recommandations/pratique/acls.txt
  • Dernière modification : 2022/04/20 14:26
  • de 127.0.0.1