Dernière mise à jour : juin 2026
État : opérationnel et validé (login système en ligne ET hors ligne fonctionnels via sssd côté client — voir doc client).
Remplace Kanidm (abandonné : authentification hors ligne cassée). LLDAP = annuaire LDAP léger avec UI web. Le login système et le cache offline sont gérés côté client par sssd (doc séparée).
Internet
|
v
[ Routeur OpenWrt ] 192.168.1.90 (auth.beafrancois.fr pointe ici)
| redirections par port :
| 443 -> 192.168.1.25:443 (proxy Caddy) [web autres services, zones wan/lan/invite]
| 6360 -> 192.168.1.9:6360 (serveur LLDAP) [LDAPS, zones lan/invite]
|
└──(6360)─> [ Serveur LLDAP ] 192.168.1.9 (LDAPS direct, sssd)
[ Proxy Caddy ] 192.168.1.25
reverse_proxy HTTP :
ldap.beafrancois.fr -> 192.168.1.9:17170 (UI web LLDAP, tls internal)
[ Serveur LLDAP ] 192.168.1.9 (cahute)
:3890 LDAP clair (NON exposé : interne serveur uniquement)
:6360 LDAPS chiffré (cert auto-signé, pour sssd — atteint via routeur:6360)
:17170 UI web (HTTP) (atteint via Caddy / ldap.beafrancois.fr)
Séparation des deux noms :
auth.beafrancois.fr = le LDAP lui-même (LDAPS 6360, pour sssd)
-> routeur:6360 -> serveur .9:6360
ldap.beafrancois.fr = l'UI web d'administration
-> proxy .25 -> serveur .9:17170
[ Portables ] LAN filaire 192.168.1.0/26 + WiFi invité 192.168.115.0/24
sssd -> ldaps://auth.beafrancois.fr:6360 (= routeur:6360 -> serveur)
Réseaux :
auth.beafrancois.fr = le LDAP (LDAPS). Résout vers le routeur (.90), qui relaie le 6360 vers le serveur. Ne sert PAS au web.ldap.beafrancois.fr = l'UI web d'administration. Résout vers le proxy (.25, DNS local), qui reverse-proxy vers LLDAP:17170 (tls internal)./mnt/stockage/docker/
├── compose/lldap/docker-compose.yml
└── data/lldap/
├── lldap_config.toml (généré au 1er démarrage)
├── users.db (base SQLite)
├── ca-cert.pem / ca-key.pem (CA auto-signé)
├── cert.pem / key.pem (cert serveur LDAPS, CN=auth.beafrancois.fr)
├── san.ext
└── secrets/
├── jwt_secret
└── ldap_user_pass (mot de passe admin LLDAP)
mkdir -p /mnt/stockage/docker/data/lldap/secrets tr -cd '[:alnum:]' < /dev/urandom | fold -w 64 | head -n 1 > .../secrets/jwt_secret tr -cd '[:alnum:]' < /dev/urandom | fold -w 24 | head -n 1 > .../secrets/ldap_user_pass chmod 600 .../secrets/*
cd /mnt/stockage/docker/data/lldap # CA sudo openssl req -x509 -newkey rsa:4096 -keyout ca-key.pem -out ca-cert.pem \ -days 3650 -nodes -subj "/CN=LLDAP-CA-beafrancois" # Clé serveur + CSR sudo openssl req -newkey rsa:4096 -keyout key.pem -out server.csr \ -nodes -subj "/CN=auth.beafrancois.fr" # SAN sudo tee san.ext > /dev/null <<'EOF' subjectAltName = DNS:auth.beafrancois.fr EOF # Signature sudo openssl x509 -req -in server.csr -CA ca-cert.pem -CAkey ca-key.pem \ -CAcreateserial -out cert.pem -days 3650 -extfile san.ext
Vérification :
openssl s_client -connect 192.168.1.9:6360 -CAfile .../ca-cert.pem < /dev/null 2>/dev/null \ | grep -E "subject=|Verify return code" # Attendu : subject=CN=auth.beafrancois.fr / Verify return code: 0 (ok)
services: lldap: image: lldap/lldap:stable container_name: lldap restart: unless-stopped ports: - "192.168.1.9:3890:3890" # LDAP clair (interne) - "192.168.1.9:6360:6360" # LDAPS chiffré - "192.168.1.9:17170:17170" # Web UI volumes: - /mnt/stockage/docker/data/lldap:/data - /mnt/stockage/docker/data/lldap/secrets:/secrets:ro environment: - LLDAP_LDAP_BASE_DN=dc=beafrancois,dc=fr - LLDAP_JWT_SECRET_FILE=/secrets/jwt_secret - LLDAP_LDAP_USER_PASS_FILE=/secrets/ldap_user_pass - LLDAP_LDAPS_OPTIONS__ENABLED=true - LLDAP_LDAPS_OPTIONS__PORT=6360 - LLDAP_LDAPS_OPTIONS__CERT_FILE=/data/cert.pem - LLDAP_LDAPS_OPTIONS__KEY_FILE=/data/key.pem - UID=0 - GID=0 networks: default: name: lldap_net
Lancement : cd /mnt/stockage/docker/compose/lldap && docker compose up -d
Version déployée : LLDAP 0.6.3.
# LDAPS : /24 (PAS /26 !) + wifi invité /24 # ⚠️ /24 nécessaire : le LDAPS arrive via le ROUTEUR (.90), qui est HORS du /26 # (le /26 s'arrête à .63). Une règle /26 bloque le trafic relayé par le routeur. sudo ufw allow from 192.168.1.0/24 to any port 6360 proto tcp sudo ufw allow from 192.168.115.0/24 to any port 6360 proto tcp # Web admin : via proxy Caddy uniquement sudo ufw allow from 192.168.1.25 to any port 17170 proto tcp # Port 3890 (LDAP clair) : NON ouvert (interne au serveur)
⚠️ Piège vécu : le LAN filaire est en /26 (.1 à .62), mais le routeur .90 est hors de cette plage. Comme le LDAPS d'auth.beafrancois.fr est relayé par le routeur, la source vue par le serveur peut être .90. Une règle UFW en /26 bloque alors le LDAPS. Utiliser /24 pour le port 6360.
Caddy ne gère QUE l'UI web (HTTP). Le LDAPS d'auth.beafrancois.fr ne passe pas par Caddy (voir architecture).
Sur le proxy (/opt/proxy/Caddyfile) :
# UI web LLDAP (cert interne, pas de Let's Encrypt car pas de DNS public)
ldap.beafrancois.fr {
tls internal
reverse_proxy 192.168.1.9:17170
}
Recharge : docker compose exec caddy caddy reload –config /etc/caddy/Caddyfile
Note : tls internal génère un certificat auto-signé Caddy (avertissement navigateur à accepter). Utilisé car ldap.beafrancois.fr n'a pas d'entrée DNS publique (Let's Encrypt échouerait avec NXDOMAIN).
auth.beafrancois.fr résout vers le routeur. Le routeur redirige par port vers la bonne machine. Les services web internes sont résolus par DNS local ciblé (pas de redirection 443 globale qui casserait l'accès internet).
Règles DNAT nécessaires, par zone source (wan + lan + invite) :
⚠️ Piège rencontré : une règle 443 avec uniquement src=wan casse l'accès web interne. Il faut des règles 443 aussi pour lan et invite (ou NAT loopback).
⚠️ Piège majeur : l'interface admin OpenWrt écoutait sur 443, donc la redirection 443 invité → proxy coupait l'accès à l'admin du routeur. Solution : déplacer l'admin sur 8443.
uci del_list uhttpd.main.listen_https='0.0.0.0:443' uci del_list uhttpd.main.listen_https='[::]:443' uci add_list uhttpd.main.listen_https='0.0.0.0:8443' uci add_list uhttpd.main.listen_https='[::]:8443' uci commit uhttpd /etc/init.d/uhttpd restart
Admin routeur désormais : https://routeur:8443
⚠️ Ne PAS faire de wildcard /beafrancois.fr/… : ça casserait Gitea SSH (un nom ne peut pas résoudre vers 2 IP selon le protocole). Déclarer les services un par un :
uci add_list dhcp.@dnsmasq[0].address='/nuage.beafrancois.fr/192.168.1.25' uci add_list dhcp.@dnsmasq[0].address='/wiki.beafrancois.fr/192.168.1.25' uci add_list dhcp.@dnsmasq[0].address='/blog.beafrancois.fr/192.168.1.25' uci add_list dhcp.@dnsmasq[0].address='/musique.beafrancois.fr/192.168.1.25' uci add_list dhcp.@dnsmasq[0].address='/ldap.beafrancois.fr/192.168.1.25' uci commit dhcp /etc/init.d/dnsmasq restart
auth.beafrancois.fr reste vers le routeur (.90) pour la redirection par port.
Base DN : dc=beafrancois,dc=fr. Users sous ou=people, groupes sous ou=groups.
⚠️ LLDAP n'a PAS d'attributs POSIX par défaut. L'UUID affiché dans l'UI n'est PAS le uidNumber. Créer manuellement dans le schéma (UI : section Schema) :
uidNumber, gidNumber, homeDirectory (+ unixShell optionnel)gidNumberPuis remplir ces attributs pour chaque user et groupe.
Groupe primaire partagé (modèle simple, validé). Tous les users ont famille comme groupe primaire. PAS de groupes privés par utilisateur, PAS de auto_private_groups (voir piège MPG dans la doc client).
| Groupe | gidNumber | Rôle |
|---|---|---|
| famille | 302 | groupe primaire de tous les users + accès partages communs |
| partage | (GID serveur) | partage /export/partage |
| multimedia | (GID serveur) | partage /export/multimedia |
| Utilisateur | uidNumber | gidNumber (primaire) |
|---|---|---|
| francois | 10000 | 302 (famille) |
| beatrice | 10001 | 302 |
| corentin | 10002 | 302 |
| louis | 10003 | 302 |
| testldap | 15000 | 302 |
Plage UID haute (10000+) choisie pour éviter toute collision avec les comptes locaux (1000-9999) sur les portables. Implique un chown des homes existants lors de la bascule (voir doc client).
Note : les objectClass réels dans LLDAP — user = inetOrgPerson/posixAccount/mailAccount/person ; groupe = groupOfNames/groupOfUniqueNames (PAS posixGroup). Important pour le mapping sssd.
User binduser (DN : uid=binduser,ou=people,dc=beafrancois,dc=fr), membre du groupe lldap_strict_readonly, SANS attributs POSIX (pas un compte loginnable). Sert à sssd pour interroger l'annuaire. Mot de passe stocké dans sssd.conf côté client.
Tout se fait via l'interface web : https://ldap.beafrancois.fr (UI d'administration), compte admin (mot de passe = secrets/ldap_user_pass). Rappel : auth.beafrancois.fr est le LDAP lui-même (sssd), pas l'UI.
paul), email (obligatoire, même interne), display name.uidNumber : prochaine valeur libre en plage haute (10004, 10005…)gidNumber : 302 (famille) — groupe primaire partagé, modèle retenuhomeDirectory : /home/paulfamille (et partage/multimedia selon les accès voulus).⚠️ Rappels :
gidNumber primaire DOIT être 302 (un groupe existant). Ne PAS créer de groupe privé du même nom que le user (conflit MPG → casse le offline côté client).ldapsearch … “(uidNumber=10004)”.Deux méthodes selon qui fait le changement.
Interface web : ouvrir la fiche du user → changer le mot de passe. Pas de contrainte particulière. Utile pour réinitialiser un mot de passe oublié.
Sur son portable, l'utilisateur lance simplement :
passwd
Demande le mot de passe actuel, puis le nouveau (deux fois). Le changement est écrit directement dans LLDAP. Validé et fonctionnel (grâce à chpass_provider = ldap côté sssd).
⚠️ Contraintes :
passwd écrit dans l'annuaire — impossible hors réseau. Hors ligne, la commande échoue.passwd suffit, rien de spécial à installer.
Après tout changement (web ou passwd), côté chaque autre portable : le cache offline conserve l'ancien mot de passe jusqu'au prochain login en ligne réussi de cet utilisateur sur ce portable. Tant qu'il ne s'est pas reconnecté en ligne sur une machine donnée, l'ancien mot de passe y reste valable hors ligne.
offline_credentials_expiration = 0. Pour révoquer réellement un accès, il faut que le portable se reconnecte au moins une fois (sssd constate la suppression), ou intervenir sur le portable (sss_cache -u paul / vider le cache). D'où l'intérêt de LUKS sur les portables.projets).gidNumber (valeur cohérente avec les permissions serveur si lié à un partage).groupadd -g <gid> projets).
Fiche du groupe → ajouter/retirer des membres. Les groupes secondaires (partage, multimedia…) sont gérés par appartenance ; le groupe primaire reste famille (302) via le gidNumber du user.
Les changements LLDAP sont pris en compte par sssd selon ses délais de cache. Pour forcer un rafraîchissement immédiat sur un portable :
sudo sss_cache -E # invalide tout le cache (resync au prochain accès, en ligne) # ou ciblé : sudo sss_cache -u paul # un user sudo sss_cache -g famille # un groupe
(N'efface PAS les credentials offline, contrairement à rm du cache.)
# Test bind + lecture d'un user LDAPTLS_CACERT=/mnt/stockage/docker/data/lldap/ca-cert.pem LDAPTLS_REQCERT=allow \ ldapsearch -x -H ldaps://192.168.1.9:6360 \ -D "uid=binduser,ou=people,dc=beafrancois,dc=fr" -W \ -b "dc=beafrancois,dc=fr" "(uid=testldap)" uidNumber gidNumber homeDirectory # Vérifier les objectClass d'un user / groupe ... "(uid=testldap)" objectClass ... "(cn=famille)" objectClass gidNumber member
WireGuard (prévu aussi pour les partages réseau). Le LDAPS reste fermé au WAN. Le cache offline sssd gère le “aucun réseau” ; WireGuard gère le “réseau distant” (rafraîchissement du cache + accès partages).