# Authentification unique Kerberos pour le client Windows KodeMed Ce guide s'adresse aux hôpitaux dont les postes de travail sont membres d'un domaine Active Directory et qui souhaitent que leurs codeurs ouvrent KodeMed depuis le SIH **sans voir de formulaire de connexion** : la session Windows déjà ouverte suffit. Le principe, en un paragraphe : le client KodeMed (`KodeMed.dll`, intégré au SIH) ouvre la page de connexion dans WebView2. Lorsque le serveur publie `ssoKerberos: true`, le client autorise WebView2 à envoyer des tickets Kerberos à l'hôte Keycloak. Keycloak répond à la première requête par `WWW-Authenticate: Negotiate`, Windows demande au contrôleur de domaine un ticket pour `HTTP/`, WebView2 le transmet, Keycloak le valide avec son **keytab**, retrouve l'utilisateur dans l'AD via son fournisseur LDAP, et le client reçoit son jeton. Aucun formulaire. Si un maillon de cette chaîne échoue, le formulaire de connexion s'affiche exactement comme avant. Kerberos est une voie d'accès supplémentaire, jamais la seule. > **Rien n'est installé sur les postes et rien ne change dans le SIH.** Le client lit le > réglage sur le serveur (`GET /api/v1/config`) à chaque démarrage. La mise en place se > résume à : un compte de service et un SPN dans l'AD, deux fichiers sur l'hôte KodeMed, > quelques champs dans Keycloak et une ligne dans `.env`. --- ## Prérequis | | Pourquoi | |---|---| | Les postes sont **membres du domaine** et les utilisateurs ouvrent leur session avec leur **compte de domaine** | Un compte local n'a aucun ticket Kerberos à présenter | | Keycloak est atteint par son **FQDN**, jamais par une adresse IP | Le ticket est demandé pour `HTTP/`. Aucun SPN ne peut désigner une adresse IP, et Windows n'essaie même pas | | Ce FQDN est un **enregistrement A, pas un CNAME** (recommandé) | Windows résout le CNAME et demande un ticket pour le nom **canonique**. Le SPN enregistré correspond à l'alias, le KDC ne connaît pas ce principal, le navigateur se replie sur NTLM et Keycloak journalise `Defective token detected`. Si un CNAME est inévitable : enregistrez le SPN sur le nom canonique, **ou** donnez aux postes la stratégie WebView2 `AdditionalBrowserArguments` avec `--disable-auth-negotiate-cname-lookup` (voir « Stratégies des postes » à la section 4). La stratégie Edge `DisableAuthNegotiateCnameLookup` ne s'applique **pas** à WebView2 | | Un **nom d'hôte complet** d'au moins trois parties (`kodemed-sso.hospital.local`, jamais `hospital.local`) | Le client et le serveur refusent un nom plus court : avec un navigateur qui compare sa liste d'autorisation par suffixe, `hospital.local` couvrirait tous les hôtes du domaine. Même règle pour les hôtes supplémentaires | | **HTTPS** | Dès qu'un hôte figure dans la liste d'autorisation de WebView2, Chromium répond à Negotiate aussi en http simple : le client écrit donc chaque entrée sous la forme `https://`, et le serveur publie `ssoKerberos: false` pour une URL de connexion en http. KodeMed sert Keycloak en https dans toutes les topologies prises en charge | | **Horloges synchronisées** (NTP) sur l'hôte KodeMed et les contrôleurs de domaine | Kerberos refuse un ticket dès que l'écart dépasse 5 minutes (`Clock skew too great`) | | Le Keycloak fourni (ou votre propre IdP) dispose d'un **fournisseur LDAP** pour cet AD | Kerberos prouve *qui* est l'utilisateur ; Keycloak doit encore le trouver dans son realm pour émettre un jeton avec ses rôles | Quel est « l'hôte Keycloak » ? Celui de l'URL de l'émetteur : `OIDC_ISSUER_URI` dans `.env`, `oauth2Url` dans `GET /api/v1/config`. Dans la configuration habituelle par domaine, c'est `sso.` ; en mode domaine unique (`--single-domain`), c'est l'unique hôte KodeMed, avec Keycloak sous `/auth`. Ci-dessous, il est noté `kodemed-sso.hospital.local`, et le realm AD `HOSPITAL.LOCAL`. > Derrière un reverse proxy ou un F5 : rien de particulier, tant que le proxy transmet sans > les modifier l'en-tête de requête `Authorization` et l'en-tête de réponse > `WWW-Authenticate`. C'est le cas par défaut pour la plupart. Le SPN est toujours le nom > qu'utilise **le poste de travail**. --- ## 1. Active Directory (un administrateur du domaine) ### 1.1 Le compte de service Créez un compte utilisateur simple, par exemple `svc-kodemed-kc` : mot de passe qui n'expire jamais, l'utilisateur ne peut pas changer de mot de passe, membre d'aucun groupe particulier. Il ne sert qu'à porter le SPN. **Avant toute autre chose**, cochez dans son onglet *Compte* l'option **« Ce compte prend en charge le chiffrement Kerberos AES 256 bits »**. Cette option détermine le type de clé que `ktpass` écrit ; cochée après coup, le keytab contient une clé du mauvais type, ce qui apparaît comme `Checksum failed` dans le journal de Keycloak. ### 1.2 Le SPN ```bat setspn -S HTTP/kodemed-sso.hospital.local HOSPITAL\svc-kodemed-kc setspn -Q HTTP/kodemed-sso.hospital.local ``` `-S` refuse les doublons. La requête doit renvoyer **un seul** compte : un SPN enregistré sur deux comptes empêche le KDC d'émettre des tickets pour lui, et tous les utilisateurs retombent silencieusement sur le formulaire. ### 1.3 Le keytab ```bat ktpass /out keycloak.keytab ^ /princ HTTP/kodemed-sso.hospital.local@HOSPITAL.LOCAL ^ /mapuser HOSPITAL\svc-kodemed-kc ^ /pass * ^ /crypto AES256-SHA1 ^ /ptype KRB5_NT_PRINCIPAL ``` - `/pass *` demande le mot de passe au lieu de le placer sur la ligne de commande. - **`ktpass` réinitialise le mot de passe du compte** avec celui que vous saisissez. Tout keytab plus ancien pour ce compte cesse immédiatement de fonctionner. Si le mot de passe est modifié par la suite, le keytab doit être exporté à nouveau. - La partie realm (`@HOSPITAL.LOCAL`) est le domaine AD en **majuscules**. - `ktpass` remplace aussi le `userPrincipalName` du compte par le principal. C'est normal. Le fichier `keycloak.keytab` obtenu est un **secret d'authentification** : quiconque le détient peut se faire passer pour le service. Transmettez-le comme un mot de passe (pas par e-mail) et supprimez ensuite la copie sur le poste d'administration. --- ## 2. Le répertoire `kerberos/` sur l'hôte KodeMed Le Keycloak fourni monte un **répertoire** de l'hôte, en lecture seule, sur `/opt/keycloak/conf/kerberos`. Par défaut, il s'agit de `kerberos/` dans le répertoire d'installation (`KC_KERBEROS_DIR` dans `.env`) ; le ZIP de déploiement, `setup.sh` et `kodemed-update.sh` le créent tous. C'est volontairement un répertoire : une source de montage de fichier unique absente est transformée en répertoire vide par Docker et refusée par podman. Si vous faites pointer `KC_KERBEROS_DIR` ailleurs, utilisez un répertoire **dédié** qui ne contient rien d'autre : le montage porte l'option `:z`, si bien que sur un hôte SELinux le moteur **réétiquette** tout le répertoire pour l'usage des conteneurs. Ne le faites jamais pointer vers `/etc`, un répertoire personnel ou un répertoire utilisé par d'autres logiciels. Déposez-y exactement deux fichiers : **`keycloak.keytab`** — issu de l'étape 1.3. **`krb5.conf`** — minimal : ```ini [libdefaults] default_realm = HOSPITAL.LOCAL dns_lookup_kdc = true dns_lookup_realm = false rdns = false ``` `dns_lookup_kdc` trouve les contrôleurs de domaine via les enregistrements SRV de l'AD, ce qui convient tant que l'hôte KodeMed utilise le DNS de l'AD. Sinon, indiquez-les : ```ini [realms] HOSPITAL.LOCAL = { kdc = dc1.hospital.local kdc = dc2.hospital.local } ``` La JVM trouve ce fichier grâce à `-Djava.security.krb5.conf=…` dans le `JAVA_OPTS_APPEND` de Keycloak, que le compose généré construit à partir de `KC_KERBEROS_KRB5_CONF` (par défaut `/opt/keycloak/conf/kerberos/krb5.conf`). Ne modifiez cette clé que si le fichier se trouve ailleurs. Vos propres options JVM pour Keycloak vont dans `KC_JAVA_OPTS_APPEND` (vide par défaut ; par exemple `-Dsun.security.krb5.debug=true` pendant un diagnostic) : le compose les place devant l'option krb5.conf. Keycloak s'exécute sous l'**uid 1000** : ```bash cd /opt/kodemed sudo chown -R 1000:1000 kerberos sudo chmod 0750 kerberos sudo chmod 0400 kerberos/keycloak.keytab sudo chmod 0640 kerberos/krb5.conf # podman rootless : podman unshare chown -R 1000:1000 kerberos ``` Recréez ensuite Keycloak pour qu'il voie les fichiers : `docker compose up -d kodemed-keycloak`. `kodemed-backup.sh` copie `kerberos/` dans la sauvegarde et signale que celle-ci contient désormais un secret. Lorsque Kerberos est activé et que le compose contient le Keycloak fourni, une sauvegarde **sans** keytab est signalée par un avertissement (le reste de la sauvegarde reste restaurable ; le keytab devrait être exporté à nouveau). --- ## 3. Keycloak : la section Kerberos du fournisseur LDAP Console d'administration Keycloak → realm **kodemed** → **User federation** → votre fournisseur LDAP → section **Kerberos integration** : | Champ | Valeur | |---|---| | Allow Kerberos authentication | **On** | | Kerberos realm | `HOSPITAL.LOCAL` | | Server principal | `HTTP/kodemed-sso.hospital.local@HOSPITAL.LOCAL` — identique à `ktpass /princ` | | Key tab | `/opt/keycloak/conf/kerberos/keycloak.keytab` (le chemin **dans** le conteneur) | | Kerberos principal attribute | voir ci-dessous | | Debug | Off (à activer uniquement pendant un diagnostic) | | Use Kerberos for password authentication | Facultatif. On = le formulaire vérifie les mots de passe auprès du KDC au lieu d'une liaison LDAP | **Kerberos principal attribute** détermine comment Keycloak retrouve l'utilisateur AD correspondant au principal du ticket (`jdoe@HOSPITAL.LOCAL`) : - **`userPrincipalName`** — Keycloak cherche un utilisateur dont l'UPN est égal au principal. Correct lorsque les UPN de vos utilisateurs sont de la forme `@`. - **Vide** — Keycloak prend la partie avant `@` et la recherche avec l'attribut *Username LDAP attribute* du fournisseur. Cela ne fonctionne que si cet attribut est **`sAMAccountName`**. Le piège : avec un fournisseur dont l'attribut de nom d'utilisateur est `cn` ou `userPrincipalName`, ou des utilisateurs dont l'UPN porte un autre suffixe (`j.doe@hospital.ch`), le ticket est parfaitement validé puis l'utilisateur est *introuvable* — il voit le formulaire et rien ne semble anormal. Pour vérifier : sur un poste, `klist` affiche le principal de l'utilisateur sur sa première ligne. Comparez-le avec le `userPrincipalName` de l'utilisateur dans l'AD et choisissez l'option qui correspond. L'enregistrement du fournisseur avec *Allow Kerberos authentication* activé place l'étape **Kerberos** du flux **browser** sur **Alternative**. Vérifiez-le sous **Authentication → Flows → browser** : l'exécution *Kerberos* doit indiquer *Alternative* — jamais *Required* (tout utilisateur dont le ticket échoue serait bloqué) ni *Disabled* (Kerberos ne serait jamais proposé). Si les utilisateurs proviennent de votre propre IdP (en mode broker, voir `KodeMed_Identity_Provider_Guide.md`) plutôt que d'un fournisseur LDAP, Kerberos se configure **dans cet IdP**, pas ici ; seule l'étape 4 concerne KodeMed. Deux points de plus dans ce cas : indiquez l'hôte de cet IdP (par exemple l'ADFS) dans `KODEMED_SSO_NEGOTIATE_EXTRA_HOSTS`, et faites en sorte que Keycloak y redirige directement — sinon sa propre page de connexion, avec l'IdP sous forme de bouton, s'affiche quand même. Soit vous réglez l'étape **Identity Provider Redirector** du flux **browser** sur cet IdP (*Authentication → Flows → browser → Identity Provider Redirector → ⚙ → Default Identity Provider*), soit la requête de connexion porte `kc_idp_hint=`. --- ## 4. Activation Dans `.env` : ```bash KODEMED_SSO_KERBEROS=true ``` puis `docker compose up -d kodemed-server` (une variable d'environnement est lue à la création du conteneur). Le même réglage se trouve dans l'administration, sous **Paramètres → Connexion Windows (Kerberos)** ; une valeur enregistrée là prévaut sur `.env` jusqu'à *Revenir à la valeur du .env*. `KODEMED_SSO_NEGOTIATE_EXTRA_HOSTS` reste vide dans presque toutes les installations. Elle liste d'autres hôtes, séparés par des virgules, auxquels le client peut envoyer des tickets — uniquement pour une connexion qui passe par un autre hôte protégé par Kerberos (par exemple un ADFS devant Keycloak). Uniquement des noms d'hôte complets (au moins trois parties). La liste est publiée par `GET /api/v1/config`, **sans authentification** : quiconque atteint le serveur peut la lire, n'y mettez que des noms que vous acceptez de divulguer. **Rien sur les postes, rien dans le SIH.** Le client lit `ssoKerberos` sur le serveur à chaque démarrage et autorise Negotiate pour l'hôte Keycloak (déduit de l'émetteur) ainsi que pour les hôtes supplémentaires, chacun sous la forme `https://`. ### Stratégies des postes (seulement si vous en utilisez déjà) WebView2 n'applique **pas** les stratégies du navigateur Edge : `AuthServerAllowlist`, `AuthNegotiateDelegateAllowlist` ou `DisableAuthNegotiateCnameLookup` définies pour Edge par GPO ne changent rien dans le client KodeMed. Ce qui s'applique à WebView2 : - la stratégie **`AdditionalBrowserArguments`** sous `HKLM\Software\Policies\Microsoft\Edge\WebView2\AdditionalBrowserArguments` (ou `HKCU\…`) : une valeur de type chaîne dont le **nom** est l'exécutable hôte (par exemple `SIH.exe`) ou `*` pour toutes les applications WebView2, et dont les **données** sont les arguments ; - la variable d'environnement **`WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`**. Leur contenu est **ajouté** aux arguments que l'application définit. Si l'hôpital déploie déjà son propre `--auth-server-allowlist=…` par l'un de ces moyens, c'est cette liste — et non celle du client — qui peut s'appliquer : ajoutez-y `https://` (et les éventuels hôtes supplémentaires), sinon la connexion Windows ne démarre jamais sur ces postes. Le même mécanisme transporte `--disable-auth-negotiate-cname-lookup` lorsqu'un CNAME est inévitable. ### Citrix / serveurs de terminaux Sur un serveur de terminaux, toutes les sessions atteignent le serveur depuis la **même** adresse IP, et `GET /api/v1/config` relève du compartiment `auth` du limiteur de débit (20 requêtes, puis 10 par minute, par IP). Lorsque de nombreux utilisateurs démarrent le client dans la même minute, le client réessaie une fois une requête refusée après 1,5 s (ou le `Retry-After` envoyé par le serveur, 5 s au plus), puis affiche le formulaire de connexion. Pour un hôte avec beaucoup de sessions, augmentez le compartiment dans `.env`, par exemple `KODEMED_RATE_LIMIT_AUTH_CAPACITY=200` et `KODEMED_RATE_LIMIT_AUTH_REFILL=100`, puis `docker compose up -d kodemed-server`. Il en va de même du compartiment par défaut utilisé par toutes les autres requêtes (100 + 100 par minute et par IP) : dimensionnez `KODEMED_RATE_LIMIT_CAPACITY` / `KODEMED_RATE_LIMIT_REFILL` pour toutes les sessions de l'hôte. C'est aussi le cas derrière un répartiteur de charge qui ne transmet pas l'adresse du client - voir `EXTERNAL-PROXY.md`, *Load balancers, NAT, RDS/Citrix* (#2164) ; `kodemed-doctor.sh` affiche les limites effectives et les 429 récents. --- ## 5. Vérification 1. Sur l'hôte KodeMed : `bash tools/kodemed-doctor.sh`. La section *Kerberos single sign-on* vérifie que le point de configuration publie le réglage, que l'hôte de connexion est en https et désigné par un nom, s'il s'agit d'un CNAME, et que Keycloak peut lire le keytab et `krb5.conf`. 2. Sur un poste, avec un utilisateur ordinaire : `klist purge`, puis fermez et rouvrez le SIH et ouvrez KodeMed. Il s'ouvre **sans formulaire**. 3. `klist` affiche maintenant un ticket pour `HTTP/kodemed-sso.hospital.local@HOSPITAL.LOCAL`. Ouvrir `https://kodemed-sso.hospital.local/realms/kodemed/account` dans Edge permet de tester le **côté serveur** uniquement (SPN, keytab, Keycloak). Edge a sa propre liste d'autorisation (et ses propres stratégies, que WebView2 ne lit pas) : un formulaire dans Edge ne signifie pas que le client échouera, et un succès dans Edge ne prouve pas que le client lit le réglage. Pour plus de détails, activez **Debug** dans la section Kerberos du fournisseur et lisez `docker logs kodemed-keycloak` pendant qu'un utilisateur se connecte. Désactivez-le ensuite. --- ## En cas de problème | Symptôme | Cause | |---|---| | Le formulaire s'affiche et Keycloak ne journalise rien concernant Kerberos/SPNEGO | Le client n'a pas été informé : `ssoKerberos` n'est pas `true` dans `GET /api/v1/config` (serveur non recréé ; compose plus ancien qui ne déclare pas la clé — lancez `kodemed-update.sh` ; URL de connexion en http ou hôte de moins de trois parties ; désactivé dans les Paramètres de l'administration), ou une stratégie WebView2 `AdditionalBrowserArguments` / `WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS` avec son propre `--auth-server-allowlist` sans l'hôte Keycloak | | `Defective token detected` dans le journal de Keycloak | Le navigateur a envoyé du NTLM et non du Kerberos : pas de SPN pour cet hôte, SPN sur deux comptes (`setspn -Q`), l'hôte est un CNAME, une adresse IP est utilisée, ou le poste/l'utilisateur n'est pas dans le domaine | | `Checksum failed` / `Cannot find key of appropriate type` | Le keytab et le compte ne concordent pas : l'option AES 256 a été cochée après `ktpass`, ou le mot de passe a changé depuis. Exportez à nouveau le keytab | | `Clock skew too great` | Plus de 5 minutes d'écart entre l'hôte KodeMed et les contrôleurs de domaine | | `Cannot locate default realm` / `krb5.conf` introuvable | Le `JAVA_OPTS_APPEND` de Keycloak ne contient pas `-Djava.security.krb5.conf` (un `JAVA_OPTS_APPEND` écrit à la main l'a remplacé — mettez plutôt vos options dans `KC_JAVA_OPTS_APPEND`), ou le fichier n'est pas dans `kerberos/` | | `Permission denied` sur le keytab | Illisible pour l'uid 1000 — voir le `chown` ci-dessus | | Ticket accepté, puis formulaire malgré tout | L'utilisateur est introuvable : *Kerberos principal attribute* ne correspond pas à la façon dont votre AD nomme les utilisateurs (section 3) | | Fonctionne sur certains postes, pas sur d'autres | Ces postes ne sont pas membres du domaine, l'utilisateur s'est connecté avec un compte local, ou ils n'atteignent aucun contrôleur de domaine (VPN) | | Fonctionne dans Edge, pas dans le client | Le réglage n'est pas publié (première ligne), le client est antérieur à #2047, ou Edge fonctionne grâce à une GPO Edge (`AuthServerAllowlist`) que WebView2 ne lit pas | Quelle que soit la cause, les utilisateurs ne sont jamais bloqués : lorsque Kerberos échoue, Keycloak affiche le formulaire de connexion. --- ## Kubernetes (chart Helm) `config.ssoKerberos: true` publie le réglage exactement comme la clé du `.env`. Le Keycloak de développement du chart **ne monte pas** de keytab : sur Kubernetes, Kerberos est assuré par l'IdP du client (ou par un Keycloak que vous configurez vous-même, avec le keytab dans un Secret monté en tant que répertoire).