Hello,

La première brique essentielle de ce projet est le serveur OpenLDAP. OpenLDAP est, pour reprendre les termes, “l’implémentation open-source du protocole Lightweight Directory Access Protocol, abrégé LDAP”. Pour tenter de vulgariser le plus simplement possible, LDAP est un protocole informatique de gestion d’annuaire, un annuaire est un logiciel permettant de centraliser les informations utilisateurs et d’y connecter des applications clientes. Un serveur OpenLDAP est donc un logiciel d’annuaire open-source qui va nous permettre de centraliser nos utilisateurs, et de lier Nextcloud avec ce dernier pour les authentifier.

Lorsque vous êtes utilisateurs d’une entreprise, d’une association, où n’importe quelle organisation mettant à disposition plusieurs services, vous pouvez identifier l’utilisation d’un annuaire lorsque vous utilisez le même couple d’utilisateur/mot de passe pour tout ces logiciels. Lorsque vous changez votre mot de passe, celui-ci est également changé de partout.

Il est tout à fait possible d’effectuer la gestion des utilisateurs directement sur Nextcloud, mais j’ai choisi d’utiliser OpenLDAP pour plusieurs raisons. Tout d’abord, nous pourrons lier d’autres logiciels avec le serveur OpenLDAP et donc avoir une authentification unique comme décrit précédemment. Je compte y ajouter tous les outils qui vont me permettre de créer et maintenir ce projet, outils que je décrirai ultérieurement. Cela permet également de constituer un carnet d’adresses utilisables par Nextcloud, de définir une politique de mot de passe commune a tous les logiciels, de définir des groupes et les droits des utilisateurs.

Je dispose et utilise déjà un serveur OpenLDAP depuis plusieures années, mais celui-ci à été construit sans les connaissances adaptées, et est donc relativement basique et ne fait pas usage de toutes les capacitées d’OpenLDAP. C’est l’occasion de reprendre de zéro pour construire quelque chose de beaucoup plus professionnel.

L’avantage final pour les utilisateurs, étant de n’avoir qu’un couple login/password pour tous les services qui seront mis en place.

Technique

Passons à la partie technique qui va être relativement conséquente, et pour laquelle je ne prendrai pas la peine de définir les termes, si vous n’êtes pas déjà un public averti, cela risque de vous perdre rapidement.

J’utilise Ansible pour l’orchestration de l’infrastructure, et Docker pour conteneuriser les applications utilisées. Je reviendrai sur ces deux technologies ultérieurement également. Pour ce qui est du serveur OpenLDAP, j’utilise l’image osixia/openldap qui est l’image la plus populaire du DockerHub, et que je connais déjà. Je regrette cependant le manque de réactivité sur leurs Issues et Pull Requests présentes sur leur dépôt, mais je n’ai pour ma part pas rencontré de véritable “bug” bloquant lors de l’utilisation de cette image. Mes difficultées étaient uniquement dûes à une méconnaissance du protocole LDAP et d’OpenLDAP.

Image Docker

Je ne vais pas préciser ici ma méthode d’orchestration, ni les paramètres que je nommerais “communs” à toutes les images Docker que j’utilise. Ceci sera fait dans un article dédié. Mais voici à quoi ressemble le yaml de mon conteneur OpenLDAP :

- name: openldap
  image: osixia/openldap
  restart_policy: unless-stopped
  security_opts:
    - no-new-privileges=true
  hostname: "ldap.mydomain.com"
  purge_networks: true
  networks_cli_compatible: yes
  networks:
    - name: net-openldap
      aliases:
        - ldap.mydomain.com
  volumes:
    - openldap-data:/var/lib/ldap
    - openldap-conf:/etc/ldap/slapd.d
    - openldap-tls:/container/service/slapd/assets/certs
    - /var/lib/docker/volumes/openldap-custom/_data/ppolicy/pqparams.dat:/etc/ldap/pqchecker/pqparams.dat
    - /var/lib/docker/volumes/openldap-custom/_data/lib/pqchecker.la:/usr/lib/ldap/pqchecker.la:ro
    - /var/lib/docker/volumes/openldap-custom/_data/lib/pqchecker.so:/usr/lib/ldap/pqchecker.so:ro
    - openldap-custom:/etc/ldap-custom:ro
    - /etc/localtime:/etc/localtime:ro
  ports:
    - 389:389
  command: --loglevel warning
  labels:
    com.centurylinklabs.watchtower.enable: 'false'
  env:
    LDAP_ORGANISATION: "MyOrg"
    LDAP_DOMAIN: "MyDomain"
    LDAP_ADMIN_PASSWORD: "adminPwd"
    LDAP_READONLY_USER: "true"
    LDAP_READONLY_USER_USERNAME: "readonly"
    LDAP_READONLY_USER_PASSWORD: "readonlyPwd"
    LDAP_CONFIG_PASSWORD: 'configPwd'
    LDAP_RFC2307BIS_SCHEMA: 'true'
    LDAP_TLS: "true"
    LDAP_TLS_ENFORCE: "false"
    LDAP_TLS_VERIFY_CLIENT: "allow"
    LDAP_TLS_CRT_FILENAME: "custom-cert.crt"
    LDAP_TLS_KEY_FILENAME: "custom-cert.key"
    LDAP_TLS_CA_CRT_FILENAME: "custom-ca.crt"
    LDAP_REPLICATION: "true"
    LDAP_REPLICATION_HOSTS: "#PYTHON2BASH:['ldap://ldap.mydomain.com','ldap://ldap2.mydomain.com']"

Je vais donc décrire ici uniquement les paramètres spécifiques à cette image : Nous avons le nom du conteneur, le nom de son image, l’usage de l’option hostname qui se traduit ainsi :

docker exec openldap cat /etc/hosts
0.0.0.0  ldap.mydomain.com

et va lui permet de résoudre son FQDN sur toutes ses interfaces, ce qu’il a besoin pour s’identifier lui-même.

On va ensuite décrire nos volumes :

volumes:
  - openldap-data:/var/lib/ldap
  - openldap-conf:/etc/ldap/slapd.d
  - openldap-tls:/container/service/slapd/assets/certs
  - /var/lib/docker/volumes/openldap-custom/_data/ppolicy/pqparams.dat:/etc/ldap/pqchecker/pqparams.dat
  - /var/lib/docker/volumes/openldap-custom/_data/lib/pqchecker.la:/usr/lib/ldap/pqchecker.la:ro
  - /var/lib/docker/volumes/openldap-custom/_data/lib/pqchecker.so:/usr/lib/ldap/pqchecker.so:ro
  - openldap-custom:/etc/ldap-custom:ro
  - /etc/localtime:/etc/localtime:ro

Les deux premiers sont les indispensables pour faire persister la conf et les données, le troisième est le dossier qui contient les certificats TLS que l’on souhaite utiliser, les trois suivants sont dédiés à pqChecker (on va y venir), l’avant-dernier est un dossier dans lequel je placerais les fichiers LDIF qui me sont utile, et le dernier permet d’avoir la bonne TimeZone dans le conteneur. On laissera le volume openldap-tls en rw car le conteneur change les permissions au démarrage et bloque si il n’y arrive pas.

On a ensuite le port LDAP, on ne laissera pas 636 (LDAPS) car on ne veut utiliser que startTLS, l’option command qui est ici utilisée pour n’avoir que les logs de niveau warning en production, un label permettant de désactiver l’auto-update du conteneur par Watchtower. A noter que je déconseille fortement l’auto-update du conteneur OpenLDAP, cela m’a déjà causé problème par le passé, et si l’on souhaite tenter une mise à jour du conteneur, je recommande de le faire manuellement, j’en parlerais également plus tard.

On a ensuite les variables d’environnement, les plus intérressantes étant les suivantes : LDAP_TLS_VERIFY_CLIENT: "allow" qui permet de configurer le paramètre TLSVerifyClient.

LDAP_TLS_CRT_FILENAME: "custom-cert.crt"
LDAP_TLS_KEY_FILENAME: "custom-cert.key"
LDAP_TLS_CA_CRT_FILENAME: "custom-ca.crt"

Les variables qui permettent de spécifier le couple certificat/clef TLS à utiliser, ainsi que le root CA qu’on veut accepter. J’ai utilisée cette nomenclature car initalement je nomme mes certificats avec le FQDN (du style subdomain.domain.com.crt), sauf que lorsqu’on réalise une réplication LDAP, on va aussi répliquer les noms de fichiers des certificats. Sauf que les certificats ne vont pas être les mêmes suivant le réplica (à part le root CA). J’opte alors pour quelque chose de neutre, forcément, qui ne m’induira pas en erreur ultérieurement.

On a ensuite les variables de réplication :

LDAP_REPLICATION: "true"
LDAP_REPLICATION_HOSTS: "#PYTHON2BASH:['ldap://ldap.mydomain.com','ldap://ldap2.mydomain.com']"

Attention je recommande de finir la mise en place du cluster avant tout import de données.

Et enfin, la variable LDAP_RFC2307BIS_SCHEMA: 'true' qui nous interresse pour la suite de cet article.

Overlays

memberOf

La variable LDAP_RFC2307BIS_SCHEMA: 'true' permet d’utiliser le schema LDAP RFC2307 BIS, qui à la différence du schema par défaut (NIS), va nous permettre d’utiliser l’overlay memberOf, plutôt que les posixGroup qui ne nous intérressent pas. Pour décrire l’overlay memberOf, la documentation est très claire :

In some scenarios, it may be desirable for a client to be able to determine which groups an entry is a member of, without performing an additional search. Examples of this are applications using the DIT for access control based on group authorization.

The memberof overlay updates an attribute (by default memberOf) whenever changes occur to the membership attribute (by default member) of entries of the objectclass (by default groupOfNames) configured to trigger updates.

Thus, it provides maintenance of the list of groups an entry is a member of, when usual maintenance of groups is done by modifying the members on the group entry.

C’est une fonctionnalitée disponible probablement sur toutes les applications clientes LDAP qui se respectent, et cela nous sera utile pour une meilleure gestion des groupes. L’image Docker qu’on utilise nous permet donc d’utiliser l’overlay memberOf nativement, parfait.

Pour en comprendre un peu plus sur la différence entre les types de groupes d’utilisateurs, je vous invite à lire le lien suivant : https://stackoverflow.com/questions/15818382/what-type-of-group-to-choose-in-openldap-for-grouping-users qui m’a beaucoup éclairé.

Password Policy

L’étape suivante est l’implémentation d’une politique de mot de passe avec l’overlay Password Policy/ppolicy), qui par défaut n’est pas fonctionnel dans notre image Docker.

J’ai eu beaucoup de mal à comprendre comment le faire fonctionner, je me suis aidé de ces deux tutoriels, qui n’ont cependant pas fonctionné indépendamment:

Au final ma procédure aura été la suivante :

ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/ppolicy.ldif
ldapadd -x -w adminPassword -D "cn=admin,dc=MyDomain" -f oupolicy.ldif
ldapadd -Y EXTERNAL -H ldapi:/// -f ppmodule.ldif
ldapadd -Y EXTERNAL -H ldapi:/// -f ppolicyoverlay.ldif
ldapadd -x -w adminPassword -D "cn=admin,dc=MyDomain" -f passwordpolicy.ldif

les fichiers en question :

# oupolicy.ldif
dn: ou=ppolicies,dc=MyDomain
objectClass: top
objectClass: organizationalUnit
ou: ppolicies
description: Password policy

# ppmodule.ldif
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: ppolicy

# ppolicyoverlay.ldif
dn: olcOverlay=ppolicy,olcDatabase={1}mdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcPPolicyConfig
olcOverlay: ppolicy
olcPPolicyDefault: cn=default,ou=ppolicies,dc=MyDomain

# passwordpolicy.ldif
# Exemples d'attributs : https://www.zytrax.com/books/ldap/ch6/ppolicy.html
dn: cn=default,ou=ppolicies,dc=MyDomain
cn: default
objectClass: pwdPolicy
objectClass: pwdPolicyChecker
objectClass: device
objectClass: top
pwdAllowUserChange: TRUE
pwdAttribute: userPassword
pwdCheckQuality: 2
pwdCheckModule: pqchecker.so
pwdExpireWarning: 0
pwdFailureCountInterval: 0
pwdGraceAuthNLimit: 0
pwdInHistory: 0
pwdLockout: TRUE
pwdLockoutDuration: 7200
pwdMaxAge: 0
pwdMaxFailure: 5
pwdMinAge: 0
pwdMinLength: 8
pwdMustChange: FALSE
pwdSafeModify: FALSE

Remarque : L’import schema/modules/overlay est à faire sur tous les noeuds.

L’objectif suivant est d’utiliser un module permettant de vérifier la qualité des mots de passe. Nativement on ne vérifie ici que la longueur du mot de passe (pwdMinLength), mais on aimerait avoir quelque chose de plus costaud.

Lors de mes recherches, j’ai identifié plusieurs modules, pqchecker, check_password.c (complément) et ppm.c, deux modules de LDAP Tool Box Project que j’aurais l’occasion de mentionner à nouveau.

J’ai eu l’agréable surprise de m’apercevoir que pqchecker était inclu dans l’image Docker osixia/docker-openldap comme on peut le voir dans le Dockerfile :

# ls -la /usr/lib/ldap/pq*
lrwxrwxrwx 1 root root     18 Nov 15 22:58 /usr/lib/ldap/pqchecker.so -> pqchecker.so.2.0.0
-rw-r--r-- 1 root root 140840 Aug  7  2017 /usr/lib/ldap/pqchecker.so.2.0.0

Puis que pqchecker n’est disponible que pour amd64… Tristesse.

Tristesse donc. En effet, j’utilise beaucoup l’architecture ARM (RaspberryPi pour ma part) et j’ai pu utiliser pendant un temps l’architecture ARM64 chez Scaleway, être restreint à l’architecture amd64 me pose donc problème. Le module ppm.c me semble être le plus récent, mais même en lisant la documentation, je n’ai aucune idée claire de comment l’implémenter sans avoir à y passer des mois. A contrario, la documentation de pqchecker semble relativement simple et concise, la dernière release (2.0.0) date de 2017, mais cela reste amplement acceptable.

pqChecker

J’ai donc décidé de creuser un peu plus profondément sur pqChecker afin de vérifier si il était possible de le faire fonctionner sur Raspberry Pi, en bref, oui aucun problème. Deux choix s’offrent alors à moi, soit de compiler manuellement et de faire une installation à base de volume Docker pour faire persister la librairie, soit de forker l’image Docker pour y inclure moi-même pqChecker pour l’architecture manquante. J’ai pris la première solution, mais après quelques détours.

Objectivement la première solution est très simple à mettre en oeuvre et ne devrait pas poser de problème ultérieurement. Eh. La deuxième solution à le désavantage (majeur à mes yeux) de ne plus pouvoir bénéficier des mises à jour upstream de l’image Docker osixia, à moins de manuellement faire le nécessaire à chaque mise à jour. Ce qui est hors de question, je n’ai pas que ça à faire.

Je me suis donc lancé sur la compilation de pqChecker pour toutes les architectures supportée par l’image Docker Debian officielle (que j’utilise comme base image), et en faire profiter tout le monde. Bon euh, la documentation Debian m’a un peu perdu honnêtement, donc j’ai cherché comment faire plus simple, tant pis pour les dépôts Debian. De toutes façons, c’est surtout pour l’image osixia/docker-openldap que je fais ça et celle-ci ne supporte que amd64/arm64 et arm/v7, mais autant tout faire tant qu’on y est.

Après avoir passé trois soirées et trois jours complets dessus (et heureusement que j’avais re-installé mes gitlab-runners correctement récemment), je suis finalement parvenu à ce que je souhaitais. Au final, j’ai un dépôt Git (un projet à part entière clairement), à chaque push sur la branche Master, un cycle de CI se lance, et via Docker buildx, compile pqChecker pour toutes les différentes architectures, construit un package .deb avec l’utilitaire checkinstall. Toujours depuis le Docker buildx, un script est lancé afin d’envoyer avec cURL le .deb ainsi qu’un sha1sum directement sur un dépôt Github.

Bien sûr il y a eu énormément de complications, entre openjdk-11-jdk incapable de s’installer sur certaines architectures, l’utilitaire github-release en Go que j’utilisais avant cURL dont l’installation plante complètement sur certaines architectures également… Et qemu qui en v5.0 plante au milieu de build pour l’archi arm64, obligé de downgrade en v3.5 sans que je n’ai vraiment l’explication. Egalement, je n’arrive pas à accéder à l’architecture arm/v5, je n’en ai pas besoin pour ce projet, mais je n’ai pas trouvé de moyen de compiler pour cette architecture, un jour peut-être.

Le dépôt du projet (les .deb sont dans les releases) : https://github.com/NoxInmortus/docker-build-pqchecker

Voilà, une fois mis en place correctement, on test sur le RaspberryPi que cela fonctionne :

# Module bien chargé :
ldapsearch -Y external -H ldapi:/// -b cn=default,ou=ppolicies,dc=MyDomain pwdCheckModule pwdCheckQuality -LLL
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
dn: cn=default,ou=ppolicies,dc=MyDomain
pwdCheckQuality: 2
pwdCheckModule: pqchecker.so

# On test que le changement de mot de passe est OK avec le pqparams.dat par défaut :
ldappasswd -Q -Y EXTERNAL -H ldapi:/// -s azerty12345@ "cn=myuser,ou=users,dc=MyDomain"
Result: Constraint violation (19)
ldappasswd -Q -Y EXTERNAL -H ldapi:/// -s Azerty12345@ "cn=myuser,ou=users,dc=MyDomain"
# Si il ne nous dit rien c'est tout bon (on validera pas un test d'identification tout de même) !

J’ai par la suite fait une Issue sur le dépôt Github d’osixia/docker-openldap en espérant qu’ils en fassent quelque chose.

Pour continuer le projet je vais faire un volume docker, et je croise les doigts que mes paquets soient utilisés, je nettoierais à ce moment là.

Force TLS connections

Comme indiqué précédemment on ne va pas laisser le port 636 dédié à LDAPS, car on va utiliser uniquement startTLS, qui est une extension du protocle TLS, et permet de sécuriser les connections sur le port 389, initialement non chiffrées.

On va rajouter un petit bout de configuration pour forcer l’utilisation du TLS :

# ldapadd -Y EXTERNAL -H ldapi:/// -f forcetls.ldif
dn: olcDatabase={1}hdb,cn=config
changetype: modify
add: olcSecurity
olcSecurity: tls=1

Par la suite on sera donc obligé d’initier une connexion TLS, par exemple :

ldapsearch -x -LLL -h localhost -D cn=readonly,dc=MyDomain -w readonlyPwd -b "dc=MyDomain"
ldap_bind: Confidentiality required (13)
        additional info: TLS confidentiality required

Il nous suffit alors de rajouter de rajouter l’option -Z :

-Z[Z] Issue StartTLS (Transport Layer Security) extended operation. If you use -ZZ, the command will require the operation to be successful.`

Petite astuce pour vérifier qu’on authentifie bien un certificat avec openssl :

openssl s_client -connect ldap.mydomain.com:389 -starttls ldap -CAfile root.ca.cert

On va s’arrêter là pour la partie purement mise en place d’OpenLDAP. Prochain article sur son administration.

Merci à ma chérie pour la relecture.

A+