====== Serveur d'authentification LLDAP + configuration réseau ======
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).
===== Architecture réseau =====
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 :**
* LAN filaire : 192.168.1.0/26 (serveur .9, proxy .25, routeur .90)
* WiFi invité : 192.168.115.0/24 (isolé)
* **''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'').
* Le LDAPS ne passe **PAS** par Caddy (Caddy ne fait que du HTTP) : le routeur relaie directement le 6360 vers LLDAP.
===== Déploiement LLDAP (Docker) =====
==== Structure ====
/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)
==== Secrets ====
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/*
==== Certificat LDAPS auto-signé ====
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)
==== docker-compose.yml ====
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.
===== Pare-feu serveur (UFW) =====
# 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.**
===== Reverse proxy Caddy =====
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).
===== Configuration OpenWrt (routeur .90) =====
==== Principe ====
''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).
==== Redirections de port (firewall) ====
Règles DNAT nécessaires, **par zone source** (wan + lan + invite) :
* **443** -> 192.168.1.25:443 (proxy) — une règle par zone
* **6360** -> 192.168.1.9:6360 (LLDAP) — zones lan + invite
* Cockpit 9090, git ssh 8329, etc. : redirections existantes conservées
**⚠️ 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).
==== Port admin du routeur déplacé ====
**⚠️ 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''
==== DNS local ciblé (dnsmasq) ====
**⚠️ 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.
===== Comptes et groupes (LLDAP) =====
Base DN : ''dc=beafrancois,dc=fr''. Users sous ''ou=people'', groupes sous ''ou=groups''.
==== Attributs POSIX — à créer manuellement ====
**⚠️ 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) :
* **Schéma User :** ''uidNumber'', ''gidNumber'', ''homeDirectory'' (+ ''unixShell'' optionnel)
* **Schéma Group :** ''gidNumber''
Puis remplir ces attributs pour chaque user et groupe.
==== Modèle de groupes retenu ====
**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.
==== Compte bind (lecture seule) ====
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.
===== Opérations courantes =====
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.
==== Ajouter un utilisateur ====
- **Create a user** : renseigner User ID (identifiant de login, ex. ''paul''), email (obligatoire, même interne), display name.
- Définir le **mot de passe** du compte (c'est lui qui servira au login système).
- Ouvrir la fiche du user et remplir les **attributs POSIX** (sinon sssd l'ignore) :
* ''uidNumber'' : prochaine valeur libre en plage haute (10004, 10005…)
* ''gidNumber'' : **302** (famille) — groupe primaire partagé, modèle retenu
* ''homeDirectory'' : ''/home/paul''
- Ajouter le user comme **membre du groupe ''famille''** (et ''partage''/''multimedia'' selon les accès voulus).
**⚠️ Rappels :**
* Le ''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).
* Ne PAS réutiliser un uidNumber déjà pris. Vérifier : ''ldapsearch ... "(uidNumber=10004)"''.
==== Changer le mot de passe d'un utilisateur ====
Deux méthodes selon qui fait le changement.
=== Par l'administrateur (interface web) ===
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é.
=== Par l'utilisateur lui-même (commande passwd) ===
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 :**
* **Doit être EN LIGNE** (le portable doit joindre LLDAP en LDAPS). ''passwd'' écrit dans l'annuaire — impossible hors réseau. Hors ligne, la commande échoue.
* La commande standard ''passwd'' suffit, rien de spécial à installer.
* Le nouveau mot de passe est actif immédiatement en ligne ; le cache offline du portable se met à jour au login suivant.
=== Propagation du cache offline (les deux méthodes) ===
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.
==== Supprimer / désactiver un utilisateur ====
* **Supprimer** : fiche user → Delete. L'accès en ligne est coupé immédiatement.
* **⚠️ Offline :** un portable qui ne se reconnecte jamais garde le cache (donc l'accès) indéfiniment avec ''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.
==== Ajouter un groupe ====
- **Create a group** (ex. ''projets'').
- Remplir l'attribut POSIX **''gidNumber''** (valeur cohérente avec les permissions serveur si lié à un partage).
- Ajouter les membres.
- Côté serveur, créer le groupe Unix correspondant si besoin pour les partages (''groupadd -g projets'').
==== Gérer l'appartenance aux groupes ====
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.
==== Après toute modification : rafraîchissement côté client ====
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.)
===== Vérifications utiles (depuis le serveur) =====
# 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
===== Accès distant (hors LAN) — à faire =====
**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).