authentification : SSO Keycloak devant, secours par sudo, formulaire local fermé
Directive : toute authentification web passe par Keycloak, LDAP est la source
unique des comptes, chaque service garde un accès de secours par sudo sur
l'hôte. Les trois sont indissociables — la chaîne service → Keycloak → LDAP est
en série, donc sans secours une panne exclut tout le monde, y compris pour
réparer. Portée : le web seulement ; IMAP/SMTP se lient à LDAP directement et
SSH est en clé seule.
Posture <rôle>_connexion_locale, false par défaut. Le compte local existe — il
ne peut pas dépendre de Keycloak — mais son formulaire n'est plus proposé au
repos : ouvert en permanence, il contourne la politique de mot de passe, le MFA
et surtout la révocation centrale.
Vérifié auprès de l'amont, puis par rendu réel des gabarits dans les deux
postures :
- Grafana GF_AUTH_DISABLE_LOGIN_FORM → ferme ;
- Forgejo ENABLE_INTERNAL_SIGNIN + ENABLE_BASIC_AUTHENTICATION → ferme, API
Basic comprise. N'existe que depuis la v10 (ticket amont 7476) ; le rôle
épingle 10.0.0 et un assert refuse la fermeture en deçà, car le réglage serait
ignoré sans erreur ;
- Nextcloud hide_login_form → MASQUE seulement : ?direct=1 reste le chemin de
secours documenté par l'amont. Écrit comme tel, sans prétendre à l'équivalence.
Défaut attrapé par le rendu : la condition Forgejo sans `| bool` n'émettait rien
dans aucune posture — une valeur en chaîne est vraie au sens Jinja, la connexion
locale serait restée ouverte en silence.
docs/authentification.md, décisions D-38 à D-41. ansible-lint (production) sans
échec, 28 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:50:36 -04:00
|
|
|
# Authentification : Keycloak devant, LDAP dessous, `sudo` en secours
|
|
|
|
|
|
|
|
|
|
> **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.
|
|
|
|
|
|
authentification : chaque rôle déclare sa position, gardé par P29
Une règle qu'aucune garde ne vérifie finit par ne plus être vraie — c'est ce qui
était arrivé aux 28 lignes d'intégration recopiées. Chaque rôle serveur_* porte
un meta/authentification.yml, confronté à son code par P29.
web-sso 5, socle-identite 2 (keycloak/openldap : ils SONT la chaîne d'identité),
ldap-direct 2, interne-sans-auth 2, sans-auth-humaine 12.
La preuve refuse l'oubli ET le mensonge. Éprouvée par sabotage sur sept cas :
déclaration supprimée, portée inventée, secours retiré, posture de formulaire
retirée, raison retirée, ldap-direct mensonger, réglage retiré des defaults.
Les deux derniers passaient dans la première version :
- le mensonge passait à cause d'un commentaire. Je cherchais le mot « ldap » dans
le rôle, et serveur_grafana/defaults/main.yml contient « désactiver quelqu'un
dans LDAP » : de la prose validait une déclaration fausse. La preuve exige
maintenant un indice nommé — variable <rôle>_oidc / <rôle>_ldap, ou URI ldap://
- le réglage retiré passait parce que le gabarit citait encore la variable alors
que plus rien ne lui donnait de valeur. La preuve lit defaults/main.yml en YAML
et exige que la clé y soit définie, pas mentionnée.
Elle a aussi forcé une valeur : oauth2-proxy était déclaré « formulaire local
fermé » alors qu'il n'a aucun compte local. D'où formulaire_local: aucun, qui
distingue « il n'y en a jamais eu » de « il y en a un, il est fermé ».
Correction d'une note de la veille : Prometheus et Loki ne sont PAS exposés
publiquement (aucun expose au plan). Seuls six groupes le sont. Le risque est
intra-tenant, pas frontalier. Les deux lacunes sont comptées à chaque exécution,
pas masquées.
AFF-111, D-42. 29 preuves OK, ansible-lint (production) sur 375 fichiers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:05:43 -04:00
|
|
|
## 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 | 12 |
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
> **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.
|