====== 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).