diff --git a/CHANGELOG.md b/CHANGELOG.md index d81b147..127db95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ ## 2026-08-06 — le chemin nord-sud devient dérivable +### L'autorisation devient une doctrine (D-65, D-66) + +L'authentification était résolue et gardée (P29) ; l'**autorisation** n'existait nulle part. +Constat mesuré : `ou=groups` est créé par `serveur_openldap` depuis le début, et **aucun des +29 rôles ne le lit** — pas un `memberOf`, pas un filtre de groupe. Toute personne authentifiée +obtient le défaut du service : tout, ou rien, jamais ce qu'on avait décidé — puisqu'on n'avait +rien décidé. + +**D-65 — les groupes LDAP portent l'autorisation, Keycloak les projette.** L'argument est +mécanique, pas doctrinal : Dovecot et Postfix ne savent pas lire un rôle Keycloak. L'y loger +rendrait la moitié courriel du système aveugle et imposerait deux modèles de permissions — +donc leur divergence. + +**D-66 — un service nomme un groupe, jamais une personne.** C'est ce qui rend une arrivée ou +un départ instantané : une ligne au plan, aucun service touché. Nommer quelqu'un crée une dette +qu'on découvre le jour du départ, service par service. + +Le vocabulaire d'habilitation reste **celui du service** (`Admin` chez Grafana, `owner` chez +Forgejo) : inventer une échelle commune obligerait à la traduire partout, et la traduction est +précisément l'endroit où une habilitation se perd. + +Deux points tranchés en écrivant. Le groupe `personnel` mérite d'exister plutôt que d'être +sous-entendu — sans lui, « avoir un compte » et « avoir une boîte aux lettres » sont la même +chose, et on ne peut désactiver l'un sans l'autre. Et le mot de passe initial n'est pas *le* +mot de passe de quelqu'un : généré, en voûte, **changement forcé** à la première ouverture — +sans quoi l'exploitant connaîtrait celui de chacun. + +**Rien n'est construit.** Le registre `plan/acces.yml`, le rôle qui peuple LDAP, les +`meta/acces.yml` et la preuve P31 restent à écrire. + + ### Le courriel interne, et l'annuaire qui ne désignait personne `infra-mail-01` (Dovecot) et `edge-mta-01` (Postfix + rspamd) déployées — **dix-neuf diff --git a/docs/autorisation.md b/docs/autorisation.md new file mode 100644 index 0000000..b98a4c6 --- /dev/null +++ b/docs/autorisation.md @@ -0,0 +1,116 @@ +# Autorisation : le groupe LDAP est l'unité d'habilitation + +> **Directive d'architecture du 2026-08-07.** L'authentification dit *qui tu es* +> (`authentification.md`). Ce document dit *ce que tu peux faire*. Les deux sont +> distincts, et le second n'existait pas. + +## 1. Le constat, mesuré + +`ou=groups` est créé par `serveur_openldap` depuis le début. Au 2026-08-07, c'est un +**conteneur vide qu'aucun service ne lit** : pas un `memberOf`, pas un filtre de groupe +dans les 29 rôles du catalogue. + +Conséquence : toute personne qui s'authentifie obtient ce que le service accorde par +défaut. Selon le service, c'est *tout* ou *rien* — jamais ce qu'on a décidé, puisqu'on +n'avait rien décidé. + +Ce n'est pas une négligence : l'authentification était le problème à résoudre d'abord. +Mais une chaîne d'identité sans habilitations n'est qu'une porte d'entrée. + +## 2. La décision : LDAP porte les habilitations, pas Keycloak + +**Les groupes LDAP sont la source unique de l'autorisation.** Keycloak les *projette* en +rôles pour le web ; les services non-OIDC les lisent directement. + +L'argument n'est pas doctrinal, il est mécanique : **Dovecot et Postfix ne savent pas lire +un rôle Keycloak.** Si l'habilitation vivait dans Keycloak, la moitié courriel du système +y serait aveugle, et il faudrait tenir deux modèles de permissions — donc les voir diverger. + +C'est le prolongement de ce qui était déjà décidé pour les comptes (D-39), étendu aux +habilitations : une identité, un mot de passe, **et un seul endroit qui dit ce qu'elle +peut**. + +| Ce que porte LDAP | Ce qui le consomme | +|---|---| +| l'identité (`ou=people`) | Keycloak (fédération), Dovecot, Postfix | +| l'habilitation (`ou=groups`) | Keycloak → rôles OIDC ; Dovecot/Postfix directement | + +## 3. Un service nomme un groupe, jamais une personne + +C'est la règle qui rend une arrivée ou un départ instantané : une ligne au plan, **aucun +service touché**. Un service qui nomme quelqu'un crée une dette qu'on découvre le jour du +départ, service par service. + +Chaque rôle déclare donc son modèle dans `meta/acces.yml` — même patron que `meta/flux.yml` +et `meta/authentification.yml` : + +```yaml +acces: + - groupe: obs-admin # groupe LDAP, déclaré au plan + accorde: Admin # ce que ça vaut DANS ce service + raison: >- + Édition des tableaux de bord et des sources de données. +``` + +Le champ `accorde` est **le vocabulaire du service**, pas un niveau abstrait : `Admin` +chez Grafana, `owner` chez Forgejo, `admin` chez Nextcloud. Inventer une échelle commune +obligerait à la traduire partout, et la traduction est exactement l'endroit où une +habilitation se perd. + +## 4. Les groupes se déclarent au plan + +Un registre `instance/plan/acces.yml` déclare les personnes et les groupes, et le rôle qui +peuple LDAP le dérive — comme les VMID, les adresses et le supernet. Le plan reste la +seule chose qu'un opérateur édite. + +**Trois ou quatre groupes, pas un modèle RBAC.** Chezlepro a un sysadmin et quatorze VM ; +une hiérarchie fine coûterait plus qu'elle ne protège. Affiner plus tard est facile, +l'inverse ne l'est pas. + +| Groupe | Qui | Ce que ça ouvre | +|---|---|---| +| `sysadmin` | l'exploitant | administration des services, supervision | +| `personnel` | toute personne de l'organisation | courriel, nuage, espace de travail | +| `dev` | qui écrit du code | forge | + +Le groupe `personnel` mérite d'être nommé plutôt que sous-entendu : sans lui, « avoir un +compte » et « avoir une boîte aux lettres » seraient la même chose, et on ne saurait pas +désactiver l'un sans l'autre. + +## 5. Le premier mot de passe + +Une personne est un **tiers** : elle choisit son mot de passe, le dépôt ne l'invente pas. +La règle du dépôt est constante — un secret se **génère** quand le dépôt en est la source, +et se **saisit** quand un tiers l'est. Mais il faut bien un secret pour la première connexion. + +**Généré, déposé en voûte, changement forcé à la première ouverture.** Le secret généré +n'est donc jamais *le* mot de passe de quelqu'un : c'est un jeton d'amorçage à durée de vie +d'un usage. La distinction compte — sans le changement forcé, l'exploitant connaîtrait le +mot de passe de chacun, ce qui contredit « une identité, une personne ». + +## 6. La garde + +Une directive qu'aucune garde ne vérifie finit par ne plus être vraie — c'est ce que +`authentification.md` §5 a déjà appris. **P31** confronterait les deux sens : + +- tout groupe nommé dans un `meta/acces.yml` **existe** au plan ; +- toute personne déclarée appartient à **au moins un** groupe ; +- aucun rôle `web-sso` n'accorde d'accès sans déclarer son modèle ; +- aucun `meta/acces.yml` ne nomme une personne. + +Le dernier point est le plus important : c'est la règle du §3, et c'est celle qu'on +enfreint sans le remarquer, un service à la fois. + +## 7. Ce que ce document ne couvre pas + +**L'autorisation machine-à-machine.** Les comptes de service (PostgreSQL, Redis, jetons +d'API) ne sont pas des humains et ne passent pas par LDAP — ils vivent en voûte, liés à un +service et non à une personne. Les mêler aux groupes humains rendrait les deux illisibles. + +**Les permissions internes à un service.** Qui peut écrire dans quel dépôt Forgejo, qui +voit quel dossier Nextcloud : ça se règle dans le service, à partir du groupe qu'il a reçu. +Set-OPS accorde l'entrée et le niveau ; il ne réimplémente pas le modèle interne de chaque +application. + +**Rien n'est construit.** Ce document fixe la direction ; le registre, le rôle qui peuple +LDAP, les `meta/acces.yml` et la preuve P31 restent à écrire. diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md index 0dd6733..fa4e0f9 100644 --- a/docs/carte-set-ops.md +++ b/docs/carte-set-ops.md @@ -50,6 +50,7 @@ Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanis | Exposition → edge | app expose un FQDN public servi par un edge | `plan/domaines.yml` + `expose` (applications) | `bindings-conception.md` §4 | | Exploitation de l'hébergeur | ses **opérations** (supervision de la fabric, sauvegarde des configs, DNS d'underlay) n'appartiennent à aucun tenant et restent **hors overlay** | décidé, **non construit** : aucun équipement d'hébergeur n'est encore dans un inventaire | `hebergeur-exploitation.md` | | Authentification | web → Keycloak ; LDAP source unique ; secours par `sudo`, formulaire local non annoncé | `_connexion_locale: false` (grafana, forgejo, nextcloud) ; garde de version Forgejo ≥ 10 | `authentification.md` | +| Autorisation | les **groupes LDAP** sont l'unité d'habilitation ; un service nomme un groupe, jamais une personne | **décidé, non construit** : `ou=groups` existe et reste vide, aucun rôle ne le lit | `autorisation.md` | | SDN EVPN | ajouter un tenant implique **1 zone + 6 VNets + 6 sous-réseaux**, tous dérivés du seed | `scripts/devis_sdn.py` (`make devis-sdn`) ; nommage dérivé du tenant (`CHEZ17`, `chez174`), ≤ 8 caractères ; garde **P30** | `sdn-evpn.md` §2 | | Pools Proxmox | un pool par tenant : les noms courts de VM sont **volontairement identiques** d'un tenant à l'autre (même fonction, même nom), et seule la console Proxmox en souffrait | `scripts/devis_proxmox_pools.py` (`make devis-proxmox-pools`) ; nom dérivé de l'`index` ; garde de collision = preuve **P28** | `decisions-architecture.md` D-37 | | Routage | **aucun commutateur ne route** : la frontière est le seul équipement L3 ; les switches commutent | `passerelle` dit qui porte la passerelle, le SVI se dérive du rôle du porteur | `decisions-architecture.md` D-49/50 | diff --git a/docs/decisions-architecture.md b/docs/decisions-architecture.md index 2ad3ae8..816d4cc 100644 --- a/docs/decisions-architecture.md +++ b/docs/decisions-architecture.md @@ -79,6 +79,8 @@ sont les seules vérifiables. | **D-40** | L'accès de secours passe par **`sudo` sur l'hôte**, pas par un compte web permanent | `service → Keycloak → LDAP` est en série : sans secours, une panne exclut tout le monde, y compris pour réparer | `authentification.md` §4 | — | | **D-41** | Le formulaire de connexion locale n'est **pas proposé au repos** (`_connexion_locale: false`) | il contourne la politique de mot de passe, le MFA et surtout la **révocation centrale** ; `sudo` est le mécanisme de réouverture | `authentification.md` §3 | — | +| **D-65** | Les **groupes LDAP** portent l'autorisation ; Keycloak les *projette*, il ne les détient pas | Dovecot et Postfix ne savent pas lire un rôle Keycloak : l'y loger rendrait la moitié courriel du système aveugle, et imposerait deux modèles de permissions — donc leur divergence | `autorisation.md` §2 | P31 | +| **D-66** | Un service nomme un **groupe**, jamais une personne | une arrivée ou un départ devient une ligne au plan, sans toucher un seul service ; nommer quelqu'un crée une dette qu'on découvre le jour du départ | `autorisation.md` §3 | P31 | | **D-42** | Chaque rôle **déclare sa position** d'authentification (`meta/authentification.yml`), gardée par une preuve | une directive qu'aucune garde ne vérifie finit par ne plus être vraie — c'était le cas des 28 lignes d'intégration recopiées | `authentification.md` §5 | P29 | ## 3. Les secrets