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'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 :
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 :
OpenLDAP propose plusieurs catégories de limites :
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.
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
.
# -- 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 :
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
.
# ====== 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