Set-OPS-Public/docs/authentification.md
Daniel Allaire f9e45b1676 relais : la face publique d'un site sans adresse fixe, et le site de reprise de TechnoLibre
Le site de TechnoLibre n'a qu'une adresse DHCP. Trois adresses fixes sont louees sur un VPS
(le relais), qui repond a l'ARP pour elles et les route en /32 dans un tunnel WireGuard que la
frontiere initie. Aucune traduction : l'adresse du client arrive intacte.

- underlay.py : `tunnel: wireguard` dispense de VLAN ; role d'hote `relais`.
- devis_opnsense.py : `opnsense_face_publique`. Les entrees publiques s'y posent avec
  reply-to ; les sorties des locataires du pool passent par le relais ; une redirection n'y
  vise plus `wanip`. Le devis de Chezlepro est identique au caractere pres.
- appliquer_opnsense.py : replyto et gateway transmis et dans l'identite (seulement s'ils
  existent) ; le NAT sortant en mode automatic arrete l'application au lieu de dupliquer.
- role serveur_relais, inventorie depuis la carte comme un hyperviseur ; cle privee dans la
  voute du site. Deploye, idempotent.
- site_inventaire.py : les hyperviseurs ne sont plus filtres sur 192.168.11. ; site-appliquer
  charge la voute designee par SETOPS_UNDERLAY ; voute.py n'exige plus d'un locataire un
  secret du site.
- prouver.py : P67 lit le plan par underlay.plan_site() ; P53 couvre la face publique.
- Tests : test_underlay_tunnel, test_face_publique_relais, test_mode_nat_sortant.

95 preuves (make verifier), make test passe. CHANGELOG 144 et 145.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:20:04 -04:00

139 lines
7.8 KiB
Markdown

# Authentification : Keycloak devant, LDAP dessous, `sudo` en secours
> **Pour qui :** le **mainteneur** — la doctrine d'authentification, avant de brancher un service.
> **Directive d'architecture du 2026-08-03.** Trois règles, décidées ensemble et
> volontairement indissociables — chacune crée le problème que la suivante résout.
1. **Toute authentification web passe par Keycloak.** Aucun service ne tient son propre
répertoire de comptes.
2. **LDAP est la source de vérité unique** des comptes d'un tenant. Keycloak le fédère ;
les protocoles qui ne parlent pas OIDC s'y lient directement.
3. **Chaque service garde un accès de secours**, atteint par `sudo` sur l'hôte.
## 1. Pourquoi les trois vont ensemble
La chaîne est en série : `service → Keycloak → LDAP`. C'est ce qui donne « une identité, un
mot de passe » — et c'est aussi trois points de panne alignés. Si LDAP tombe, plus personne
n'entre nulle part, y compris pour réparer.
La règle 3 n'est donc pas une entorse au SSO : c'est ce qui rend les deux premières
tenables. Sans elle, la panne d'un maillon devient une exclusion totale.
## 2. Portée : le web, et seulement le web
| Protocole | Chemin d'authentification | Pourquoi |
|---|---|---|
| **HTTP(S)** — interfaces humaines | **Keycloak** (OIDC natif, ou `oauth2-proxy` devant) | c'est là que la directive s'applique |
| **IMAP / SMTP** | **LDAP direct** (Dovecot, Postfix) | ces protocoles ne parlent pas OIDC ; le flux est prouvé de bout en bout |
| **SSH** | **clé publique** (`PasswordAuthentication no`) | pas de mot de passe du tout, donc rien à fédérer |
| **PostgreSQL, Redis** | secrets applicatifs en voûte | comptes de service, pas d'humains |
Ce qui est interdit partout, c'est qu'un service tienne **son propre répertoire d'humains**.
Le chemin varie ; la source ne varie pas.
## 3. La posture retenue : la porte de secours n'est pas annoncée
Le compte local de chaque service **existe** — il ne peut pas dépendre de Keycloak, sinon il
tomberait avec lui. Mais son formulaire de connexion **n'est pas proposé au repos**.
Ce n'est pas une préférence esthétique. Un formulaire local ouvert en permanence :
- contourne la politique de mot de passe et le MFA ;
- contourne surtout la **révocation centrale** — désactiver quelqu'un dans LDAP laisserait
son compte local valide, sans que rien ne le signale ;
- offre une cible de bourrage d'identifiants sur `admin`, en continu, pour un usage qui
devrait être exceptionnel.
Le coût de la fermeture est nul depuis qu'on a tranché que **`sudo` suffit** : `sudo` *est*
le mécanisme de réouverture.
### Ce que chaque service sait vraiment faire
Vérifié auprès de l'amont le 2026-08-03, puis par **rendu réel des gabarits** dans les deux
postures — pas seulement lu.
| Service | Réglage | Effet réel | Secours par `sudo` |
|---|---|---|---|
| **Grafana** | `GF_AUTH_DISABLE_LOGIN_FORM=true` | ferme le formulaire | `grafana-cli admin reset-admin-password` |
| **Forgejo** ≥ 10 | `ENABLE_INTERNAL_SIGNIN=false` + `ENABLE_BASIC_AUTHENTICATION=false` | ferme la connexion interne **et** l'API en Basic | `forgejo admin user change-password` |
| **Nextcloud** | `hide_login_form=true` | **masque seulement** | `occ user:resetpassword`, ou `…/login?direct=1` |
> **Nextcloud est une exception, et il faut la dire.** `hide_login_form` **masque** le
> formulaire ; `…/login?direct=1` y accède encore, et l'amont le documente comme voulu —
> c'est ainsi qu'un administrateur entre. La protection est donc de **ne plus l'annoncer**,
> pas de l'interdire. Le présenter comme équivalent à Grafana ou Forgejo donnerait un faux
> confort.
Le réglage s'appelle `<rôle>_connexion_locale` dans les trois rôles, et vaut `false` par
défaut. Le passer à `true` rouvre la porte, explicitement.
**Garde de version.** `ENABLE_INTERNAL_SIGNIN` n'existe **que depuis Forgejo v10** (suivi
amont, ticket 7476 : *« This option was added to Forgejo v10 »*). Sur une version
antérieure il serait ignoré **sans erreur** — le rôle croirait avoir fermé la porte. Un
`assert` refuse donc `connexion_locale: false` sous 10.0.0 plutôt que de laisser croire.
## 4. La chaîne de secours
Trois maillons, aucun ne dépendant d'une porte web ouverte :
```
1. SSO Keycloak → LDAP ← la voie normale
2. sudo via SSH occ / grafana-cli / forgejo admin ← Keycloak ou LDAP en panne
3. console Proxmox de la VM ← SSH inaccessible
```
Un accès de secours qu'on ne sait pas emprunter n'existe pas : les commandes exactes sont
dans le tableau ci-dessus, et les mots de passe locaux vivent dans la voûte unique de
l'instance.
## 5. La garde : chaque rôle déclare sa position
Une directive qu'aucune garde ne vérifie finit par ne plus être vraie. Chaque rôle
`serveur_*` porte donc un `meta/authentification.yml`, et **P29** le confronte à son code.
| Portée | Sens | Rôles |
|---|---|---|
| `web-sso` | authentification humaine web → Keycloak (`mecanisme: natif` ou `oauth2-proxy`) | 5 |
| `socle-identite` | **est** la chaîne d'identité, ne peut pas se déléguer à elle-même | keycloak, openldap |
| `ldap-direct` | protocole non-OIDC lié à LDAP | dovecot, postfix |
| `interne-sans-auth` | interface joignable **dans** le tenant sans authentification, non exposée | prometheus, loki |
| `sans-auth-humaine` | aucun point d'authentification humaine | 24 |
La preuve refuse **l'oubli et le mensonge** : un rôle sans déclaration, une portée
inventée, un `web-sso` sans accès de secours ou sans posture de formulaire, un `web-sso
natif` sans réglage `<rôle>_connexion_locale` **défini dans `defaults`**, et une
déclaration que le code contredit.
**Et depuis le 2026-09-07, sa sœur `P58` garde les habilitations** — ce que P29 ne fait
pas : elle tient les *positions* (qui s'authentifie comment), pas les *droits* (qui obtient
quoi). Voir [`autorisation.md`](autorisation.md) §5.
**Depuis le 2026-09-06, elle refuse aussi que ce tableau mente.** Les chiffres et les listes
de la troisième colonne sont confrontés aux déclarations réelles. Ils avaient cessé d'être
vrais sans que rien ne le signale : la ligne `sans-auth-humaine` annonçait 12 rôles, il y en
avait 21 — la preuve lisait les déclarations depuis le début, mais ne regardait pas ce que
le document en disait.
> **Les indices doivent être nommés.** Une première version cherchait les mots « ldap » et
> « oidc » dans le rôle. Le mot *LDAP*, présent dans un commentaire de `serveur_grafana`,
> suffisait alors à valider une déclaration `ldap-direct` mensongère. La preuve exige
> désormais une **variable du namespace du rôle** (`<rôle>_oidc`, `<rôle>_ldap`) ou une URI
> `ldap://` — ce qu'une phrase en prose ne produit pas.
Une valeur mérite d'être expliquée : `formulaire_local: aucun`. Elle distingue un service
qui **n'a pas de compte local du tout** — `oauth2-proxy` est une passerelle — de celui qui
en a un et l'a fermé. Sans elle, on écrirait « fermé », ce qui laisserait croire qu'une
porte a été close alors qu'il n'y en a jamais eu.
### Les lacunes sont comptées, pas masquées
`interne-sans-auth` ne fait pas échouer la preuve : c'est une lacune **assumée et nommée**.
Mais elle est comptée à chaque exécution, donc visible dans chaque rapport. La refuser
bloquerait le harnais sur une décision déjà prise ; la taire la ferait oublier.
Prometheus et Loki sont dans ce cas. À noter, contre une idée reçue de la veille : **ils ne
sont pas exposés publiquement** — aucun `expose` au plan. Seuls six groupes le sont
(collabora, forgejo, grafana, keycloak, nextcloud, oauth2-proxy). Le risque est donc
intra-tenant, pas frontalier — plus petit qu'annoncé, réel quand même. `oauth2-proxy` est
déjà éprouvé devant Icinga Web 2 : le patron existe, il reste à l'appliquer.