LDAP

De Justine's wiki
Aller à la navigation Aller à la recherche

LDAP

Sources:

Lightweight Directory Access Protocol. LDAP est un protocole standardisé pour les annuaires; c'est souvent la base de l'IAM (Identity and Authentification Management) au sein d'une organisation. Un annuaire sert à recenser les personnes et les matériels dans un annuaire commun, à un but d'autorisation (à quoi ai-je accès) et d'authentification (suis-je bien qui je prétends être ?).

Il utilise en clair le port 389 et en chiffré le port 636.

Une fois un annuaire LDAP en place, il peut être utilisé à des fins d'authentification sur 99% des applications self-hosted (qu'on parle de LDAP en lui-même ou bien d'Active Directory, qui est sa version Windows). C'est un des piliers centraux d'un système d'informations. Il y aurait beaucoup à dire sur son importance et sur l'importance d'une bonne gestion de l'IAM au sein d'une organisation.

Fonctionnement interne de LDAP

LDAP est une structure en arbre, avec à la base un "naming context" (un domaine, en gros : sq.lan par exemple) contenant des OU (Organizational Units) contenant elles-mêmes des objets. L'arbre est appellé DIT, pour Directory Informational Tree. Les objets sont appellés "entrées", et sont une collection d'attribut clef-valeur. Chaque entrée est distinguée par son DN pour Distinguished Name, sur le même principe qu'un système de fichiers avec ses chemins. Un DN est lui-même composé de RDN (pour Relative Distinguished Name) : dans "uid=justine,ou=people,dc=squi,dc=fr" j'ai 4 RDN différents.

LDAP est typé; chaque objet peut avoir un ou plusieurs "objectClass", qui sont en gros les types de LDAP. Être de tel ou tel objectClass que l'on a certains champs obligatoires, et d'autres optionnels. Les objectClass sont définis dans des schémas. En plus de ceux utilisés pour les données, les serveurs LDAP disposent d'un DIT spécial avec pour naming context "cn=config" qui permet de modifier la configuration à chaud. Il est composé d'entrées de type olc, pour OnLine Configuration.

LDAP dérive de X.500 qui est un système d'annuaires; à l'origine, LDAP était une gateway pour accéder à X.500. Il est désormais intégré directement.

SlapD

Sources:

Slapd est le daemon LDAP par excellence sur Linux. Celui-ci est simple à installer et disponible sur la plupart des distributions; nous sommes ici en Debian (testing, entre la 12 et la 13).

Il se compose d'un daemon (slapd) que l'on va pouvoir configurer et alimenter à l'aide de fichiers LDIF - LDAP Data Interchange Format. Il écoute son port TCP et peux faire du LDAPS avec SSL. Il peut fonctionner en cluster avec slurpd.

Installation et configuration basiques

L'installation se fait normalement:

apt install slapd

Lors de l'installation, celui-ci va nous demander un mot de passe administrateur. Il peut également demander le nom de domaine si celui-ci n'est pas renseigné dans le hostname de la machine. On peut refaire sa configuration de base avec:

dpkg-reconfigure slapd

Voici les réponses que l'on peut donner:

#On veut configurer slapd...
1.Passer la configuration d'OpenLDAP ? non
#Donner notre nom de domaine si il ne le déduit pas
2.Nom de domaine ? example.com
#Pas compris
3.Nom de votre société ? masociété 
#LDAP peut avoir plusieurs backends, hdb est décrit comme 'le backend pour une utilisation normale de slapd'
4.Quelle base de donnée ? hdb 
#Doit-on effacer la base quand la présente config est finie ?
5.Voulez-vous que la base de donnée soit effacée lorsque slapd est purgé ? oui 
#Pareil
6.Supprimer les anciennes bases de données ? oui
7.Mot de passe administrateur ? VotreMotDePasse
8.Confirmer ce mot de passe ? VotreMotDePasse
#LDAPv2 est un vieux vieux protocole
9.Authoriser le protocol LDAPv2 ? non

On peut regénérer un mot de passe admin avec la commande suivante:

sudo slappasswd

Insérer des données

Créer un fichier init.ldif :

#Base de notre arbre : c'est le premier objet.
Les attributs (objectClass, dc etc) sont par défaut, mais on peut créer les notres.
dn: dc=sq,dc=lan
objectClass: dcObject
objectClass: organizationalUnit
dc: sq
ou: Sq Dot Lan

#Une OU pour les personnes
dn: ou=people,dc=sq,dc=lan
objectClass: organizationalUnit
ou: people

#Une OU pour les groupes
dn: ou=groups,dc=sq,dc=lan
objectClass: organizationalUnit
ou: groups

#Je créée un compte utilisateur
dn: uid=justine,ou=people,dc=sq,dc=lan
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
#L'UID est une valeur de base pour identifier une personne et doit être unique
uid: justine
sn: Pelletreau
givenName: Justine
cn: Justine Pelletreau
displayName: Justine Pelletreau
uidNumber: 1000
gidNumber: 10000
gecos: Justine Pelletreay
loginShell: /bin/zsh
homeDirectory: /home/justine
shadowExpire: -1
shadowFlag: 0
shadowWarning: 7
shadowMin: 8
shadowMax: 999999
shadowLastChange: 10877
mail: justine.pelletreau@squi.fr
postalCode: 77210
l: Avon
o: SqLan
title: System Administrator
initials: JP

#Je créée un groupe
dn: cn=admins,ou=groups,dc=sq,dc=lan
objectClass: posixGroup
cn: admins
gidNumber: 10000
displayName: Example group

Les attributs peuvent être trouvés sur la doc : https://www.openldap.org/doc/admin22/schema.html ou dans diverses listes sur le net : https://www.zytrax.com/books/ldap/ape/#attributes Ils sont définis dans des schémas par défaut (dans /etc/ldap/schema). On peut créer nos propres schémas.

Structure de notre LDAP après insertion de notre fichier LDIF.

Une fois ce fichier écrit, on va devoir effacer l'installation par défaut et entrer nos données à la place:

systemctl stop slapd
sudo rm -rf /var/lib/ldap/*
sudo slapadd -l ~/init.ldif
#On redonne les droits de lecture sur la base
sudo chown -R openldap:openldap /var/lib/ldap
systemctl start slapd

On va ensuite pouvoir faire une recherche afin de vérifier que nos informations sont bien prises en compte à l'aide de ldapsearch (du paquet ldap-utils).

31 root > ldapsearch -xLLL -b "dc=sq,dc=lan" uid=justine sn givenName cn
#Renvoie
dn: uid=justine,ou=people,dc=sq,dc=lan
sn: Pelletreau
givenName: Justine
cn: Justine Pelletreau

Explication de la commande:

  • -x désactive l'authentification
  • -LLL empêche l'affichage des informations LDIF (c'est juste pour raccourcir la réponse)
  • -b "dc=sq,dc=lan" sert à donner le *base DN*, c'est à dire la base à partir de laquelle on va chercher (ici, la racine)
  • uid=justine : je cherche une valeur dont le champ uid est égal à "justine"
  • sn givenName cn : ce sont les valeurs que je veux récupérer

Sauvegarde / Restauration

https://www.vincentliefooghe.net/content/sauvegarde-et-restauration-openldap

La backup se fait simmplement en sauvegardant les données contenues dans le dossier de données de slapd (par défaut /var/lib/ldap) après arrêt de slapd. On peut les restaurer de la même façon en arrêtant slapd, copiant les fichiers, mettre les droits 0600 et la propriété à l'utilisateur openldap et son groupe openldap.

On peut aussi faire une sauvegarde à chaud au format LDIF avec la commande "slapcat" (quel nom bizarre... Mais il sert juste à faire un "cat" de slapd)

slapcat -b "dc=sq,dc=lan" -l backup.ldif

Ici je sauvegarde mon arbre (donné comme Base DN avec le -b) dans le fichier backup.ldif.

Pour la restauration, le principe est le même que lors de notre installation. Cela peut être long sur un gros annuaire. On peut améliorer les perfs avec l'option -q, pour éviter la génération de fichiers de logs et aller plus vite:

slapadd -c -b dc=sq,dc=lan -l Export-Example-com.ldif -q

Ajouter un mot de passe au compte utilisateur

Maintenant que nous avons un compte utilisateur dans notre annuaire, on va pouvoir lui ajouter un mot de passe afin de pouvoir s'authentifier avec ce dernier. Cela passe par la commande ldappasswd.

Depuis le compte admin

Si le compte en question n'as pas encore de mot de passe, on va pouvoir passer par le compte administrateur pour lui en créer un.

ldappasswd -H ldap://127.0.0.1:389 -x -D "cn=admin,dc=sq,dc=lan" -W -S "uid=justine,ou=people,dc=sq,dc=lan"
  • -H : ldapuri - comment se connecter au ldap en question
  • -x : utiliser une authentification simple au lieu de SASL
  • -D : binddn - quel compte utiliser pour se connecter en admin
  • -W : Demander le mot de passe du binddn avec un prompt au lieu de le donner en argument avec -w
  • -S : Demander le nouveau mdp via un prompt au lieu de le donner en argument avec -s
  • On finit avec le dn du compte à modifier.

Depuis le compte en question

On peut aussi se connecter avec le compte en question pour modifier le mot de passe. Par défaut, un serveur LDAP laisse les utilisateurs modifier leur propre mot de passe.

ldappasswd -H ldap://localhost:389 -x -D "uid=justine,ou=people,dc=sq,dc=lan" -W -A -S
Old password: <entrer l'ancien mdp>
Re-enter old password: <encore>
New password: <entrer le nouveau mdp>
Re-enter new password: <encore>
Enter LDAP Password: <entrer l'ancien mdp>

Le seul argument différent de ceux du mode admin est -A, qui sert à prompt pour l'ancien mot de passe.

Tester une connection

On peut ensuite tester une connection avec le compte.

En local:

ldapsearch -x -LLL -D 'uid=justine,ou=people,dc=sq,dc=lan' -W
  • -x : Connection simple
  • -LLL : Raccourcir la sortie en enlevant toutes les infos LDIF
  • -D : bind dn
  • -W : demander le mot de passe

On peut aussi le faire à distance en ajoutant un hôte avec "-H ldap://mon.hote.tld" par exemple

TLS/SSL

Jusqu'à présent, notre serveur LDAP fonctionne en clair. Mais avec LDAPS, on peut faire du SSL. Pour l'exemple, nous allons utiliser un autosigné, mais pour de la production on peut utiliser un certificat let's encrypt, zeroSSL ou autre. On commence par créer un dossier pour les certifs et donner les droits à l'utilisateur openldap:

mkdir -p /etc/ldap/ssl/certs
mkdir -p /etc/ldap/ssl/priv
chown -R openldap:openldap /etc/ldap/ssl

On créée ensuite un autosigné dans notre cas:

cd /etc/ldap/ssl/priv
openssl genrsa 2048 > ldapserver.key
openssl req -utf8 -new -key ldapserver.key -out ldapserver.csr
cd ../certs
openssl x509 -in ../private/ldapserver.csr -out ldapserver.crt -req -signkey ../private/ldapserver.key -days 3650
#La clef et le certificat doivent ensuite appartenir à openldap:openldap

Un mot sur la config

L'insertion se fait ensuite via un LDIF avec l'aide de la commande ldapmodify. En effet, Slapd stocke une bonne partie de sa configuration directement dans l'annuaire, sur cn=config (on peut d'ailleurs en extraire le contenu avec slapcat, cf. la section sur les backups.

Pourquoi ? Pour citer ma source

Historically OpenLDAP has been statically configured, that is, to make a change to the configuration the slapd.conf file was modified and slapd stopped and started. In the case of larger users this could take a considerable period of time and had become increasingly unacceptable as an operational method. OpenLDAP version 2.3 introduced an optional new feature whereby configuration may be performed at run-time using a DIT entry called cn=config. The feature is known by a varied and confusing number of terms including on-line configuration (OLC) (our favorite), zero down-time configuration, cn=config and slapd.d configuration.

On trouve donc dans cn=config un tas d'attributs en olc, pour OnLine Configuration.

Retour sur notre SSL

Donc, la modif se fait via un LDIF :

dn: cn=config
changetype: modify
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ldap/ssl/certs/ldapserver.crt
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ldap/ssl/priv/ldapserver.key

Il s'agit ici de la version pour notre autosigné. Dans le cas d'un vrai certif, on peut avoir une chaîne intermédiaire:

dn: cn=config
changetype: modify
replace: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ssl/certs/server.pem
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ssl/private/server.key
-
replace: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ssl/certs/intermediate.pem

On peut ensuite effectuer la modification avec:

ldapmodify -Y EXTERNAL -H ldapi:/// -f ssl.ldif
  • -Y EXTERNAL : Choix du mécanisme SASL. Ici, signifie que l'authentification est implicite. Pas tout compris; https://en.wikipedia.org/wiki/Simple_Authentication_and_Security_Layer
  • -H ldapi:/// : donner une "ldapuri", soit une uri pour ldap... ldapi correspond à /var/run/ldapi, soit un socket Unix. Utilisé uniquement pour cn=config.
  • -f ssl.ldif : chemin du fichier.

Activer le port 636

Il nous reste à activer l'écoute sur le port 636, utilisé par LDAPS. Il faut modifier /etc/default/slapd, et modifier la ligne "SLAPD_SERVICES" pour lui ajouter le ldaps:

SLAPD_SERVICES="ldap:/// ldapi:/// ldaps:///"

On peut aussi réserver les connections en clair en local, par exemple:

SLAPD_SERVICES="ldap://127.0.0.1:389/ ldapi:/// ldaps:///"

Puis restart

systemctl restart slapd

On peut vérifier via openldap que le port sert bien un certificat:

openssl s_client -connect ip.du.ldap:636 -showcerts

Tester la connection / Faire une recherche

Autoriser les certificats untrusted et chercher en local

On peut vouloir tester localement avec:

ldapsearch -x -b dc=sq,dc=lan -ZZ

Qui renvoie une erreur:

ldap_start_tls: Connect error (-11)
        additional info: (unknown error code)

Il faut modifier /etc/slapd/ldap.conf pour ajouter:

TLS_REQCERT never

Ensuite, ça fonctionne.

Pareil à distance

Pour tester depuis une autre machine:

  • S'assurer que le fw de notre serveur LDAP laisse passer la connection sur le port 636/TCP.
  • Installer ldap-utils (Debian).

On peut ensuite faire la recherche.

ldapsearch -x -H ldaps://tests.sq.lan -b "dc=sq,dc=lan"

Si le serveur LDAP utiliser un autosigné, on se tape une erreur:

ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)

Il faudrait aller ajouter "TLS_REQCERT" dans /etc/ldap/ldap.conf, mais ça n'existe pas vu qu'on est pas sur un serveur LDAP... Comment faire ? Il suffit d'utiliser une variable d'environnement avec notre commande

LDAPTLS_REQCERT=never ldapsearch -x -H ldaps://tests.sq.lan -b "dc=sq,dc=lan"

Je récupère ainsi le contenu de l'annuaire. Un annuaire étant normalement public, je me récupère tout le contenu.

Insertion / Modification / Suppression : LDIF

La modification des données peut se faire via du LDIF, un peu comme on l'as fait précédemment lors de l'initialisation de notre LDAP; cependant, cela peut se faire à chaud. LDAP peut sembler un peu complexe (et il l'est assez quand on se penche sur les schémas et autres); mais l'utilisation de LDIF est assez simple alors je continue par là.

De manière générale, LDIF comporte des entrées au format clef: valeur. Il faut noter que l'espace après le : est important. On peut faire du multiline en commencant la ligne suivante par un espace. La façon la plus simple de faire un LDIF est d'écrire les choses de la même façon qu'on les récupère avec ldapsearch.

Un bloc LDIF commence toujours par le dn (distinguished name) qui marque l'emplacement de l'objet dans le DIT:

uid=justine,cn=sq,cn=lan

On peut ensuite optionnellement (même si je préfère, perso) ajouter un champ changetype, qui dénote le type de modification que l'on va faire:

  • changetype: add -> ajouter un nouvel objet
  • changetype: modify -> modifier un objet
  • changetype: delete -> supprimer un objet
  • changetype: modrdn -> modifier un des rdn de l'objet
  • changetype: moddn -> modifier le DN de l'objet

Si le champ n'est pas indiqué, c'est de l'ajout par défaut.

On a ensuite la / les objectClass de l'objet (une par ligne), par ex:

objectClass: inetOrgPerson
objectClass: PosixAccount

Enfin, on a les différents attributs, si besoin.

description: John Smith
cn: John Smith
sn: Smith
uid: john

Enfin, on peut activer nos modifications avec ldapadd :

ldapadd -x -D "cn=admin,dc=sq,dc=lan" -W -H ldaps:// -f modifs.ldif

Ajout d'une entrée

Ici, ajout d'un utilisateur "simple":

dn: uid=john,ou=people,dc=sq,dc=lan
changetype: add
objectClass: inetOrgPerson
description: John Smith
cn: John Smith
sn: Smith
uid: john

Un exemple plus complet :

dn: uid=justine,ou=people,dc=sq,dc=lan
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
#L'UID est une valeur de base pour identifier une personne et doit être unique
uid: justine
sn: Pelletreau
givenName: Justine
cn: Justine Pelletreau
displayName: Justine Pelletreau
uidNumber: 1000
gidNumber: 10000
gecos: Justine Pelletreay
loginShell: /bin/zsh
homeDirectory: /home/justine
shadowExpire: -1
shadowFlag: 0
shadowWarning: 7
shadowMin: 8
shadowMax: 999999
shadowLastChange: 10877
mail: justine.pelletreau@squi.fr
postalCode: 77210
l: Avon
o: SqLan
title: System Administrator
initials: JP

Le principe est le même, mais avec plus d'attributs.

Suppression d'un objet

C'est encore plus simple, il suffit d'indiquer le dn et de demander la suppression:

dn: uid=john,ou=people,dc=sq,dc=lan
changetype: delete

Modification d'un objet

Modifier un attribut existant

Il faut indiquer qu'on souhaite effectuer une modification, et préciser quoi modifier, puis la nouvelle valeur:

# justine, people, sq.lan
dn: uid=justine,ou=people,dc=sq,dc=lan
changetype: modify
replace: gecos
gecos: Justine

Il faut recommencer un bloc pour chaque attribut que l'on va modifier.

Ajouter un nouvel attribut

Le principe est un peu le même. Par exemple, si je veux m'ajouter un 2e champ mail:

# justine, people, sq.lan
dn: uid=justine,ou=people,dc=sq,dc=lan
changetype: modify
add: mail
mail: hello@example.org

Schémas, Object Classes : Comment ?

Qu'est-ce qu'un schéma ? Cette définition est assez facile à comprendre:

An LDAP schema is the collection of attribute type definitions, object class definitions, and other information which a server uses to determine whether a filter or attribute value assertion matches against the attributes of an entry, and whether to permit, add, and modify operations.

Le schéma LDAP est donc la liste de tous les attributs, classes d'objets et autres que connaît notre LDAP. Les plus courants sont définis dans diverses RFC, mais je n'ai étonnament pas réussi à en trouver une liste à peu près complète. De mon expérience, il semble normal pour un LDAP d'entreprise de disposer de ses propres classes d'objets et attributs. On peut citer la RFC 4519 pour les applications, la 2798 pour les personnes, mais il y'en a d'autres.

Avec SlapD, elles sont toutes stockées dans /etc/ldap/schema, sous la forme de fichiers LDIF et .schema (qui sont assez lisibles !). Par exemple, le .schema pour inetOrgPerson montre bien tous les attributs avec leur définitions:

#[...] 
attributetype ( 2.16.840.1.113730.3.1.1
        NAME 'carLicense'
        DESC 'RFC2798: vehicle license or registration plate'
        EQUALITY caseIgnoreMatch
        SUBSTR caseIgnoreSubstringsMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )

attributetype ( 2.16.840.1.113730.3.1.2
        NAME 'departmentNumber'
        DESC 'RFC2798: identifies a department within an organization'
        EQUALITY caseIgnoreMatch
        SUBSTR caseIgnoreSubstringsMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )

attributetype ( 2.16.840.1.113730.3.1.241
        NAME 'displayName'
        DESC 'RFC2798: preferred name to be used when displaying entries'
        EQUALITY caseIgnoreMatch
        SUBSTR caseIgnoreSubstringsMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
        SINGLE-VALUE )

attributetype ( 2.16.840.1.113730.3.1.3
        NAME 'employeeNumber'
        DESC 'RFC2798: numerically identifies an employee within an organization'
        EQUALITY caseIgnoreMatch
        SUBSTR caseIgnoreSubstringsMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
        SINGLE-VALUE )

attributetype ( 2.16.840.1.113730.3.1.4
        NAME 'employeeType'
        DESC 'RFC2798: type of employment for a person'
        EQUALITY caseIgnoreMatch
        SUBSTR caseIgnoreSubstringsMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )

attributetype ( 0.9.2342.19200300.100.1.60
        NAME 'jpegPhoto'
        DESC 'RFC2798: a JPEG image'
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )

attributetype ( 2.16.840.1.113730.3.1.39
        NAME 'preferredLanguage'
        DESC 'RFC2798: preferred written or spoken language for a person'
        EQUALITY caseIgnoreMatch
        SUBSTR caseIgnoreSubstringsMatch
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
        SINGLE-VALUE )

## OpenLDAP note: ";binary" transfer should NOT be used as syntax is binary
attributetype ( 2.16.840.1.113730.3.1.40
        NAME 'userSMIMECertificate'
        DESC 'RFC2798: PKCS#7 SignedData used to support S/MIME'
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 )

## OpenLDAP note: ";binary" transfer should NOT be used as syntax is binary
attributetype ( 2.16.840.1.113730.3.1.216
        NAME 'userPKCS12'
        DESC 'RFC2798: personal identity information, a PKCS #12 PFX'
        SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 )

objectclass     ( 2.16.840.1.113730.3.2.2
    NAME 'inetOrgPerson'
        DESC 'RFC2798: Internet Organizational Person'
    SUP organizationalPerson
    STRUCTURAL
        MAY (
                audio $ businessCategory $ carLicense $ departmentNumber $
                displayName $ employeeNumber $ employeeType $ givenName $
                homePhone $ homePostalAddress $ initials $ jpegPhoto $
                labeledURI $ mail $ manager $ mobile $ o $ pager $
                photo $ roomNumber $ secretary $ uid $ userCertificate $
                x500uniqueIdentifier $ preferredLanguage $
                userSMIMECertificate $ userPKCS12 )
        )

On voit donc que le schema est constitué d'attributs (attributeType), lesquels sont au final reliés à une objectClass. Chaque attributeType dispose:

  • d'un nom (NAME) en camelCase
  • d'une description (DESC) en toutes lettres

CONTINUER ICI

Faire une recherche LDAP sur un AD

https://tylersguides.com/guides/search-active-directory-ldapsearch/ <source lang="bash">

  1. Avec TLS

ldapsearch -H ldaps://dc.example.com -x -W -D "user@example.com" -b "dc=example,dc=com" "(sAMAccountName=user)"

  1. Sans TLS

ldapsearch -H ldap://dc.example.com -x -W -D "user@example.com" -b "dc=example,dc=com" "(sAMAccountName=user)" </source>