Architectures IA locales pour PME suisses : RTX Spark, DGX Spark, RTX PRO 6000, H100. TCO réel, ingénierie RAG, conformité nLPD. Procédures opérationnelles publiées progressivement.
View the Project on GitHub doit4everyone/ia-locale-pme-suisse
| Retour au sommaire | Section suivante : §1 Prérequis et création de la VM |
Statut : validé en lab, octobre 2026, avec les deux moteurs du serveur de modèles, Ollama et vLLM (§2). La procédure a été rejouée deux fois de bout en bout sur une VM Ubuntu 26.04 neuve (disque chiffré, certificats, deploy.sh, indexation, droits, Open WebUI, n8n, test de cloisonnement, redémarrages, sauvegarde et restauration de Qdrant). Le second rejeu, du 15 au 17 octobre 2026, a fait corriger le script et ce guide (version 2.20.4). La version 2.21.1, qui ajoute le frontal HTTPS (Nginx), l’identité signée et les versions figées, a été rejouée de bout en bout le 19 octobre 2026 avec Ollama : déploiement, certificat de l’autorité interne, indexation, droits, Open WebUI, n8n, cloisonnement (72 verdicts, aucune fuite), usurpation refusée, garde-fou, ports exposés, redémarrage et supervision. Ce rejeu a fait corriger quelques points (version 2.21.2). La version 2.22.2 a été rejouée de bout en bout les 23 et 24 octobre 2026 sur une VM neuve à un seul disque (§1.5.1), avec Ollama et un certificat délivré par l’autorité interne : déploiement par deploy.sh, indexation (1 692 extraits), synchronisation, Open WebUI en LDAPS, n8n et courriel d’alerte, refus par adresse IP, usurpation refusée, redémarrage avec la stack en service, supervision, cloisonnement (81 verdicts : 79 OK, 2 INFO, aucune fuite). Ce rejeu a fait corriger quelques points (version 2.22.3). L’essai avec vLLM sur une VM neuve reste à refaire.
Un assistant comparable à ChatGPT, mais installé chez vous. Vos collaborateurs lui posent des questions dans leur navigateur ; il répond à partir des documents de l’entreprise (le serveur de fichiers, et en option SharePoint), cite ses sources, et ne montre à chacun que les documents qu’il a déjà le droit d’ouvrir sur le serveur de fichiers. Les questions et les réponses sont traitées sur vos machines : aucun service d’IA externe n’est appelé.
Ce guide mène d’une installation vierge à un assistant en service. Comptez une journée pour un premier déploiement : deux à trois heures pour la stack elle-même, plus l’installation du serveur de modèles (une demi-heure avec Ollama, une à deux heures avec vLLM) et le téléchargement des modèles (environ 12 Go). Microsoft 365, en option, vient ensuite.
| Machine | Rôle | Ce qui y tourne |
|---|---|---|
| Serveur Windows existant | L’annuaire et les documents | Active Directory, serveur de fichiers (partage réseau) |
| VM de la stack, Ubuntu Server 26.04 | L’application | Open WebUI (l’interface de discussion), la RAG API (le cœur : identité, droits, recherche, contrôle des réponses), Qdrant (l’index des documents), n8n (les tâches planifiées) |
| Serveur de modèles, avec une carte graphique NVIDIA | Les modèles d’IA | Le moteur, Ollama ou vLLM, et trois modèles : rédaction, vérification, recherche (§2.1) |
Utilisateur (navigateur)
│
▼
VM de la stack ── Nginx (HTTPS) → Open WebUI → RAG API → Qdrant (index des documents)
│
├→ Active Directory : qui est l'utilisateur, quels sont ses groupes
└→ Serveur de modèles : rédaction, vérification, recherche
Dans le lab, la VM de la stack tourne dans VMware Workstation sur le poste Windows qui porte la carte graphique. Ce peuvent aussi être deux machines distinctes.
C’est la seule décision à prendre avant de commencer. Le détail et les mesures sont au §2.2.
| Ollama | vLLM | |
|---|---|---|
| Questions simultanées | Une à la fois : la suivante attend | Plusieurs en même temps |
| Pour qui | Un à deux utilisateurs simultanés, un essai, un poste unique | Plusieurs utilisateurs simultanés : une PME en service |
| Système | Windows ou Linux | Linux ; sous Windows, dans WSL2 (essai seulement, pas la production) |
| Installation | Simple, quelques minutes | Plus technique, une à deux heures |
Mesuré dans le lab : avec vLLM, deux personnes qui posent leurs questions en même temps sont servies presque aussi vite qu’une seule (+6 %). Recommandation : vLLM pour une PME en service, Ollama pour un essai ou un à deux utilisateurs. Le choix n’est pas définitif : on passe de l’un à l’autre sans réindexer ni réinstaller la stack (§2.5).
rag.votre-domaine.ch : un enregistrement A vers l’adresse de la VM, dans la zone du domaine (§9.8.2). C’est par ce nom que les utilisateurs ouvriront l’assistant, en HTTPS.Conventions de ce guide. Les blocs
powershellse lancent sur Windows, dans PowerShell ouvert en administrateur. Les blocsbashse lancent sur Linux. Sur la VM, toutes les commandes se lancent en root : ouvrir d’abord une session root avecsudo -i. Les valeurs entre chevrons,<comme-ceci>, sont à remplacer par les vôtres, chevrons compris. Chaque étape se termine par un résultat attendu : ne pas passer à la suite tant qu’il n’est pas obtenu.
| Étape | Où | Quoi |
|---|---|---|
| 1 | Windows, Active Directory | Créer le compte de service svc-rag |
| 2 | Windows, Active Directory | Exporter le certificat de l’autorité de certification |
| 3 | Serveur de modèles | Installer Ollama (3A) ou vLLM (3B) |
| 4 | VM | Préparer la VM |
| 5 | VM | Monter le partage de fichiers |
| 6 | VM | Récupérer les scripts du dépôt |
| 7 | VM | Remplir le fichier de configuration .env |
| 8 | VM | Vérifier que la VM joint le serveur de modèles |
| 9 | VM | Lancer la stack |
| 10 | VM | Vérifier que la stack joint le serveur de modèles |
| 11 | VM | Préparer Python pour l’indexation |
| 12 | VM | Première indexation des documents |
| 13 | VM | Synchronisation des droits d’accès |
| 14 | Navigateur | Configurer Open WebUI |
| 15 | Navigateur | Configurer n8n |
| 16 | VM et navigateur | Contrôles finaux |
| 17 | Microsoft 365, facultatif | Entra ID, Teams, SharePoint |
svc-rag est le compte avec lequel la stack lit les fichiers et leurs droits d’accès (les ACL NTFS) sur le serveur de fichiers, et interroge l’Active Directory. Il a accès à l’intégralité du corpus : son mot de passe est un secret à privilèges.
# Adapter le chemin de l'OU à votre organisation
New-ADUser -Name "svc-rag" `
-SamAccountName "svc-rag" `
-UserPrincipalName "svc-rag@votre-domaine.ch" `
-AccountPassword (ConvertTo-SecureString "<mot-de-passe-fort-20-caracteres>" -AsPlainText -Force) `
-PasswordNeverExpires $false `
-CannotChangePassword $true `
-Enabled $true `
-Path "OU=COMPTES-SERVICE,DC=votre-domaine,DC=ch" `
-Description "Compte de service indexeur RAG - lecture seule"
Donner la lecture par un groupe dédié, pas par les groupes métier. Sur le serveur de fichiers, en administrateur, le script Set-AccesIndexationRAG.ps1 crée le groupe GRP-RAG-Indexation, y ajoute svc-rag, et accorde la lecture seule sur chaque dossier à indexer, y compris les sous-dossiers dont l’héritage est coupé :
# Vérification seule : affiche ce qui manque, ne modifie rien
.\Set-AccesIndexationRAG.ps1 -Racine "D:\FileService" -Exclure "UTILISATEURS" -Verifier
# Application, puis nouvelle vérification : 0 fichier sans lecture attendu
.\Set-AccesIndexationRAG.ps1 -Racine "D:\FileService" -Exclure "UTILISATEURS"
Adapter la racine du partage et la liste des dossiers exclus (les dossiers personnels notamment). Ne pas ajouter svc-rag aux groupes métier (GRP-RH, GRP-Finances, etc.) : il hériterait de tous leurs droits, et son retrait d’un de ces groupes lui couperait la lecture sans avertissement. Explications et cas réel : §5.2.4.
Get-ADPrincipalGroupMembership -Identity "svc-rag" | Select Name
Attendu : seulement GRP-RAG-Indexation et les utilisateurs du domaine.
Mot de passe :
PasswordNeverExpires $falseimpose sa rotation selon la politique du domaine. À chaque rotation, reporter le nouveau mot de passe sur la VM (étapes 5 et 7, procédure au §9.5.1) : sinon, le montage du partage tombe au prochain redémarrage, sans message clair.
La VM vérifie l’identité du contrôleur de domaine avant de lui envoyer le mot de passe de svc-rag (connexion LDAP chiffrée). Pour cela, elle a besoin du certificat de l’autorité de certification du domaine. Sans lui, tout le monde est refusé (erreur 403).
Commencer par lister les certificats, sans rien exporter, pour choisir le bon en connaissance de cause :
# Autorités racines, puis autorités intermédiaires
Get-ChildItem Cert:\LocalMachine\Root | Where-Object { $_.Subject -like "*votre-domaine*" } |
Format-List Subject, Issuer, Thumbprint, NotAfter
Get-ChildItem Cert:\LocalMachine\CA | Where-Object { $_.Subject -like "*votre-domaine*" } |
Format-List Subject, Issuer, Thumbprint, NotAfter
Une autorité racine a un Issuer identique à son Subject. Une autorité intermédiaire (émettrice) a pour Issuer la racine. Exporter ensuite chaque certificat par son empreinte (Thumbprint), jamais par « le premier de la liste » : la racine peut apparaître à plusieurs endroits des magasins de certificats, et un filtre sur le nom ne désigne alors pas un certificat unique.
# Ou par l'interface : certlm.msc → clic droit sur le certificat → Toutes les tâches → Exporter
$racine = Get-ChildItem Cert:\LocalMachine\Root | Where-Object { $_.Thumbprint -eq "<EMPREINTE-RACINE>" } |
Select-Object -First 1
Export-Certificate -Cert $racine -FilePath "C:\Temp\ad-root-ca.cer" -Type CERT
Copier ad-root-ca.cer sur la VM, dans /tmp (par scp ou par un partage). Il sera converti à l’étape 4.
PKI à deux niveaux (autorité racine et autorité émettrice) : exporter de la même façon le certificat de l’autorité intermédiaire (
C:\Temp\ad-sub-ca.cer, magasinCert:\LocalMachine\CA). Les deux seront réunis dans le même fichier à l’étape 4. Le lab compte deux niveaux : le certificat du contrôleur de domaine y est délivré directement par la racine, et le fichier de chaîne contient la racine et l’autorité intermédiaire.
Suivre 3A ou 3B selon le moteur choisi. Les explications de chaque réglage sont au §2.
1. Installer Ollama depuis ollama.com, dans sa dernière version.
2. Le régler, dans PowerShell ouvert avec le compte qui fera tourner Ollama :
[Environment]::SetEnvironmentVariable("OLLAMA_HOST", "0.0.0.0:11434", "User")
[Environment]::SetEnvironmentVariable("OLLAMA_FLASH_ATTENTION", "1", "User")
[Environment]::SetEnvironmentVariable("OLLAMA_KV_CACHE_TYPE", "q8_0", "User")
[Environment]::SetEnvironmentVariable("OLLAMA_NUM_PARALLEL", "1", "User")
[Environment]::SetEnvironmentVariable("OLLAMA_KEEP_ALIVE", "1h", "User")
[Environment]::SetEnvironmentVariable("OLLAMA_NO_CLOUD", "1", "User")
Puis quitter Ollama depuis la zone de notification et le relancer : les réglages ne sont lus qu’au démarrage.
3. Télécharger les trois modèles :
ollama pull qwen3:14b
ollama pull qwen3:4b-instruct
ollama pull nomic-embed-text
ollama list
Attendu : les trois modèles dans la liste. La vérification depuis la VM vient à l’étape 8.
Ollama tourne dans la session Windows ouverte. Se déconnecter de la session l’arrête. Fermer simplement la fenêtre du Bureau à distance le laisse tourner. Ollama sous Linux : §2.3.
Sur un poste Windows, vLLM tourne dans WSL2, ce qui convient au lab et à l’essai mais pas à la production (§2.4.1). Sur un serveur Linux, commencer au point 2 (configuration non testée en lab, §2.4.1).
1. Préparer WSL2 (poste Windows seulement). Dans PowerShell en administrateur :
wsl --install -d Ubuntu-26.04
Au premier lancement, Ubuntu demande de créer un utilisateur Linux. Créer ensuite le fichier C:\Users\<utilisateur>\.wslconfig :
[wsl2]
memory=16GB
swap=8GB
networkingMode=mirrored
Puis, toujours en administrateur, appliquer et ouvrir les ports de vLLM dans le pare-feu de WSL2 :
wsl --shutdown
New-NetFirewallHyperVRule -Name "vllm-entrant" -DisplayName "vLLM entrant 8000 et 8001" -Direction Inbound -VMCreatorId '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}' -Protocol TCP -LocalPorts 8000,8001 -Action Allow
Rouvrir Ubuntu et vérifier que la carte est visible : nvidia-smi. N’installer aucun pilote NVIDIA dans Ubuntu : WSL2 utilise celui de Windows. Avant d’activer WSL2 sur un poste qui fait aussi tourner VMware Workstation, lire la mise en garde du §2.4.2.
2. Installer vLLM, dans Ubuntu :
sudo apt update && sudo apt install -y build-essential
sudo mkdir -p /opt/vllm && sudo chown $USER /opt/vllm
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.local/bin/env
cd /opt/vllm
uv venv --python 3.12 --seed --managed-python
source .venv/bin/activate
uv pip install vllm==0.31.0 --torch-backend=auto
python -c "import torch; print(torch.cuda.is_available())"
Attendu : True (la carte est accessible).
3. Télécharger les modèles et créer la clé d’API :
source /opt/vllm/.venv/bin/activate
sudo mkdir -p /opt/hf /etc/vllm && sudo chown $USER /opt/hf
export HF_HOME=/opt/hf
hf download Qwen/Qwen3-14B-AWQ
hf download nomic-ai/nomic-embed-text-v1.5
echo "VLLM_API_KEY=$(openssl rand -hex 32)" | sudo tee /etc/vllm/vllm.env > /dev/null
sudo chown root:$USER /etc/vllm/vllm.env && sudo chmod 640 /etc/vllm/vllm.env
La clé protège vLLM, visible depuis tout le réseau local. Elle ne s’affiche jamais : ne pas la coller dans un message ou un ticket.
4. Lancer les deux serveurs, chacun dans son propre terminal Ubuntu, en commençant par le premier :
# Terminal 1 : rédaction et juge, port 8000
cd /opt/vllm && source .venv/bin/activate
set -a; source /etc/vllm/vllm.env; set +a
export HF_HOME=/opt/hf VLLM_USE_FLASHINFER_SAMPLER=0
vllm serve Qwen/Qwen3-14B-AWQ --served-model-name qwen3:14b \
--max-model-len 16384 --gpu-memory-utilization 0.87 --max-num-seqs 4 \
--reasoning-parser qwen3 --default-chat-template-kwargs '{"enable_thinking": false}' \
--host 0.0.0.0 --port 8000 --api-key "$VLLM_API_KEY"
Attendre Application startup complete (une à trois minutes), puis :
# Terminal 2 : embedding, port 8001
cd /opt/vllm && source .venv/bin/activate
set -a; source /etc/vllm/vllm.env; set +a
export HF_HOME=/opt/hf
vllm serve nomic-ai/nomic-embed-text-v1.5 --runner pooling --trust-remote-code \
--served-model-name nomic-embed-text --max-model-len 2048 --gpu-memory-utilization 0.06 \
--host 0.0.0.0 --port 8001 --api-key "$VLLM_API_KEY"
5. Vérifier, dans un troisième terminal : curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8000/v1/models, puis la même chose avec le port 8001. Attendu : 401 deux fois (le serveur répond, et exige la clé). L’essai complet, qui contrôle aussi la qualité des réponses, est au §2.4.6.
Les deux serveurs s’arrêtent si on ferme leur terminal. Les laisser ouverts pendant tout le déploiement. Le démarrage automatique par des services systemd est décrit au §2.4.7 ; validé dans WSL2, il ne dispense pas de garder un terminal Ubuntu ouvert, car WSL2 s’arrête quand le dernier se ferme. Si un démarrage échoue, les messages rencontrés et leur solution sont au §2.4.9.
Ouvrir une session root (sudo -i). Docker doit déjà être installé (§1.4), et le volume chiffré monté (§1.5) : les deux prérequis se sautent facilement, et le rejeu du 23 octobre 2026 en a oublié un. Contrôle :
docker compose version && systemctl show docker -p DropInPaths && findmnt /srv/rag-donnees
Attendu : la version de Compose, le fichier rag-donnees.conf de §15.3.3, et /srv/rag-donnees monté depuis /dev/mapper/rag-donnees. Puis :
apt update && apt install -y \
cifs-utils smbclient samba-common-bin \
ldap-utils curl wget git \
python3 python3-pip python3-venv
Le DNS du domaine, pour que la VM trouve le contrôleur de domaine et le serveur de fichiers par leur nom. Commencer par contrôler l’existant :
getent hosts <NOM-DC>.votre-domaine.ch
getent hosts <NOM-FILESERVER>
resolvectl status | grep -E "DNS Servers|DNS Domain"
Attendu : une adresse pour chaque nom, et uniquement les DNS du domaine dans DNS Servers (l’adresse IP de votre contrôleur de domaine, par exemple). Si c’est le cas, ne rien modifier.
Jamais de DNS public (8.8.8.8, 1.1.1.1…) à côté de celui du domaine. systemd-resolved bascule sur le serveur public dès que celui du domaine tarde une seule fois, au démarrage par exemple, et n’en revient pas : les noms du domaine deviennent introuvables, et la connexion LDAP comme la lecture des droits tombent en panne au hasard d’un redémarrage. Cas vécu dans le lab : un DNS public ajouté pour contourner une lenteur de téléchargement a provoqué la panne au deuxième redémarrage suivant. Si resolvectl en affiche un, le retirer de la configuration réseau, en général /etc/netplan/*.yaml (liste nameservers: addresses:), puis :
netplan generate && netplan apply
resolvectl status | grep -E "DNS Servers"
Si les noms ne se résolvent pas du tout (aucun DNS du domaine configuré), l’ajouter dans /etc/systemd/resolved.conf (avec nano), section [Resolve], puis systemctl restart systemd-resolved et refaire les deux getent :
[Resolve]
DNS=<IP-DC>
Domains=votre-domaine.ch
Le certificat de l’étape 2, converti au format attendu par Linux :
openssl x509 -inform DER -in /tmp/ad-root-ca.cer -out /etc/ssl/certs/ad-chain.pem
openssl x509 -in /etc/ssl/certs/ad-chain.pem -noout -subject -dates
Attendu : le nom de l’autorité de certification et ses dates de validité.
Avec une PKI à deux niveaux, convertir aussi le certificat intermédiaire, puis réunir les deux dans le fichier de chaîne :
openssl x509 -inform DER -in /tmp/ad-root-ca.cer -out /tmp/ca-root.pem
openssl x509 -inform DER -in /tmp/ad-sub-ca.cer -out /tmp/ca-sub.pem
cat /tmp/ca-root.pem /tmp/ca-sub.pem > /etc/ssl/certs/ad-chain.pem
# L'intermédiaire seul sert aussi au certificat de Nginx (§9.8.4)
cp /tmp/ca-sub.pem /etc/ssl/certs/ad-subca.pem
grep -c "BEGIN CERTIFICATE" /etc/ssl/certs/ad-chain.pem
Attendu : 2. Contrôler ensuite que le contrôleur de domaine présente un certificat que cette chaîne valide :
openssl s_client -connect <NOM-DC>.votre-domaine.ch:636 -CAfile /etc/ssl/certs/ad-chain.pem </dev/null 2>/dev/null | grep -E "Verify return code|Verification"
Attendu : Verify return code: 0 (ok). Une autre valeur indique un certificat manquant dans la chaîne, ou un nom de contrôleur qui ne correspond pas à celui du certificat.
Le mot de passe de svc-rag va dans un fichier lisible par root seulement, jamais dans une ligne de commande, et sur le volume chiffré : comme le .env, le fichier réel est dans /srv/rag-donnees/secrets, avec un lien à l’emplacement habituel. Jusqu’à la 2.22.2, ce fichier était créé dans /etc/smbcredentials, sur le disque système, en clair (constat du rejeu du 23 octobre 2026).
mkdir -p /etc/smbcredentials
ln -s /srv/rag-donnees/secrets/smb-svc-rag /etc/smbcredentials/svc-rag
nano /srv/rag-donnees/secrets/smb-svc-rag
Contenu :
username=svc-rag
password=<mot-de-passe-svc-rag>
domain=VOTREDOMAINE
chmod 600 /srv/rag-donnees/secrets/smb-svc-rag
ls -l /etc/smbcredentials/svc-rag /srv/rag-donnees/secrets/smb-svc-rag
mkdir -p /mnt/corpus-root
mount -t cifs //<NOM-FILESERVER>/FileService /mnt/corpus-root \
-o credentials=/etc/smbcredentials/svc-rag,vers=3.1.1,uid=1000,gid=1000,ro
ls /mnt/corpus-root/
Attendu : le lien vers le volume chiffré, le fichier réel en -rw-------, puis les dossiers du partage. Le domaine est donné par la ligne domain= du fichier d’identifiants : sans elle, le montage échoue sur un domaine Active Directory. Le montage est en lecture seule (ro) : la stack ne modifie jamais le partage. Rendre ensuite le montage permanent :
umount /mnt/corpus-root
cat >> /etc/fstab << 'FSTAB'
# Partage du corpus RAG (monté avec svc-rag, identifiants sur le volume chiffré)
//<NOM-FILESERVER>/FileService /mnt/corpus-root cifs credentials=/etc/smbcredentials/svc-rag,vers=3.1.1,uid=1000,gid=1000,ro,soft,nofail,_netdev,x-systemd.after=/srv/rag-donnees 0 0
FSTAB
systemctl daemon-reload && mount -a && findmnt /mnt/corpus-root
x-systemd.after=/srv/rag-donnees fait monter le partage après le volume chiffré, qui porte ses identifiants, sans déclencher lui-même le déverrouillage ; _netdev le désigne comme un montage réseau. Si le volume reste fermé (TPM refusé, §15.3.3), le partage n’est pas monté et nofail laisse le démarrage se poursuivre : rag-deverrouiller, puis mount /mnt/corpus-root. Validé au redémarrage avec la stack en service, le 24 octobre 2026 : volume chiffré monté, puis le partage une seconde plus tard.
Les scripts sont publiés dans ce dépôt GitHub. Ceux qui contiennent des valeurs d’exemple portent le préfixe anon_ : on les renomme en les copiant.
cd /root
git clone https://github.com/doit4everyone/ia-locale-pme-suisse.git
cd ia-locale-pme-suisse/scripts/stack-ia-locale
# Dossiers de la stack (déjà présents, sous forme de liens, si le disque chiffré du §1.5 est prêt)
mkdir -p /root/rag-stack/{qdrant_data,n8n_data,openwebui_data} /root/rag-pipeline /var/log/rag
mkdir -p /etc/rag-certs && chmod 700 /etc/rag-certs
# n8n tourne sous l'utilisateur « node » (uid 1000) : sans ce droit, il redémarre en boucle (EACCES).
# readlink -f : suivre le lien du §1.5 jusqu'au vrai dossier, sinon chown change le lien et pas le dossier.
chown -R 1000:1000 "$(readlink -f /root/rag-stack/n8n_data)"
# La stack : Compose, configuration d'exemple, RAG API, service Purview
cp docker-compose.yml env.example /root/rag-stack/
mkdir -p /root/rag-stack/api /root/rag-stack/mip-service /root/rag-stack/nginx
cp -r api/. /root/rag-stack/api/
mv /root/rag-stack/api/anon_main.py /root/rag-stack/api/main.py
mv /root/rag-stack/api/anon_auth.py /root/rag-stack/api/auth.py
cp -r mip-service/. /root/rag-stack/mip-service/
# Les scripts d'indexation, des droits et de contrôle
cp anon_indexer.py /root/rag-pipeline/indexer.py
cp anon_acl_resolver.py /root/rag-pipeline/acl_resolver.py
cp sp_indexer.py test_cloisonnement.py test_citations.py test_sid.py diag_sid.py cas_cloisonnement.exemple.json /root/rag-pipeline/
ls /root/rag-stack /root/rag-stack/api /root/rag-pipeline
Attendu : main.py, auth.py, teams.py, teams_graph.py, Dockerfile et requirements.txt dans api/ ; indexer.py, acl_resolver.py, sp_indexer.py, test_cloisonnement.py, test_citations.py, test_sid.py et diag_sid.py dans /root/rag-pipeline. teams.py, teams_graph.py, sp_indexer.py et mip-service servent à Microsoft 365 : ils restent inactifs tant que leurs réglages sont vides.
Variante automatisée :
deploy.sh. Le script du dépôt enchaîne les étapes 5 à 11 (montage, fichiers,.env, lancement, Python) en posant des questions, dont le choix du moteur ; il vérifie que les noms du contrôleur de domaine et du serveur de fichiers se résolvent, signale un DNS public dans la configuration de la VM, et contrôle que la stack joint le serveur de modèles. Il demande aussi le nom de la stack (RAG_FQDN), vérifie qu’il se résout vers la VM, et utilise le certificat placé dans/etc/rag-tlsou, à défaut, génère un certificat auto-signé (point 5 de l’étape 7). Il demande de confirmer par o (ououi) : toute autre réponse annule le déploiement, avec un message. À la fin, il contrôle que les cinq conteneurs tournent sans redémarrage et que la RAG API répond à travers Nginx ; sinon il liste les problèmes et se termine en erreur au lieu d’annoncer « Déploiement terminé ». Il propose enfin de lancer la première indexation et la synchronisation des droits (étapes 12 et 13), avec un journal dans/var/log/rag; un échec à ce stade n’annule pas le déploiement. Il peut être relancé après une erreur passagère (téléchargement interrompu, par exemple). S’il trouve un.envexistant, il demande confirmation avant de le remplacer et en garde une copie datée (2.21.1) : le remplacer change tous les jetons, donc l’identifiant n8n et la clé de la connexion d’Open WebUI sont à refaire, et efface les réglages Microsoft 365. Ne pas relancerdeploy.shsur une installation en service. Statut : rejoué quatre fois sur une VM neuve, en dernier lieu en version 2.22.2 le 24 octobre 2026, avec un certificat fourni par l’autorité interne ; une relance par erreur a été annulée par le garde-fou du.env, sans rien modifier. Lancement, depuis ce dossier, en root, avec un journal complet :chmod +x deploy.sh && ./deploy.sh 2>&1 | tee -a /root/deploy.log. L’option-aajoute au journal au lieu de l’écraser : sans elle, une relance efface la trace du premier déploiement (cas vécu).
Le fichier .env réunit tous les réglages et secrets de la stack. Il ne doit jamais être publié ni copié dans un document.
1. Créer le fichier et générer les secrets automatiquement :
cd /root/rag-stack
# Avec le disque chiffré du §1.5, .env est un lien : on travaille sur le vrai fichier.
# (sed -i remplace le fichier : appliqué au lien, il le détruirait et laisserait le .env en clair sur le disque système.)
ENVF=$(readlink -f /root/rag-stack/.env)
cp env.example "$ENVF"
chmod 600 "$ENVF"
for v in API_TOKEN ADMIN_TOKEN WEBUI_SECRET_KEY QDRANT_API_KEY LOG_HMAC_KEY MIP_TOKEN OWUI_JWT_SECRET; do
sed -i "s|^${v}=.*|${v}=$(openssl rand -hex 32)|" "$ENVF"
done
grep -c changeme "$ENVF"
Attendu : 0 : plus aucune valeur d’exemple pour les secrets. C’est indispensable : depuis la 2.21.0, la RAG API refuse de démarrer sur une valeur d’exemple, un secret vide ou trop court, ou deux jetons identiques (§16.6). Sans disque chiffré, readlink -f rend simplement le chemin de .env : la suite est identique.
Pour toute modification ultérieure du .env, utiliser le même chemin ($ENVF, ou nano /srv/rag-donnees/secrets/.env), puis docker compose up -d <service>, jamais docker compose restart : restart ne relit pas le .env et garde l’ancienne valeur, sans erreur.
2. Compléter le reste à la main avec nano .env :
| Réglage | Valeur | Exemple |
|---|---|---|
| Bloc du serveur de modèles | Voir le point 3 ci-dessous | |
ORG_NAME, ORG_OWNER |
Le nom de votre organisation, le même dans les deux | Exemple SA |
LDAP_HOST |
Le nom DNS complet du contrôleur de domaine, pas son adresse IP | dc01.votre-domaine.ch |
LDAP_BASE_DN |
Le domaine, en notation LDAP | DC=votre-domaine,DC=ch |
LDAP_BIND_DN |
Le compte svc-rag, avec son OU |
CN=svc-rag,OU=COMPTES-SERVICE,DC=votre-domaine,DC=ch |
LDAP_BIND_PWD, SMB_PASSWORD |
Le mot de passe de svc-rag, le même aux deux endroits et dans le fichier de l’étape 5 |
|
LDAP_DOMAIN, SMB_DOMAIN |
Le nom NetBIOS du domaine | VOTREDOMAINE |
SMB_SHARE |
Le partage à indexer | //FILESERVER/FileService |
DOCUMENTATION_PATHS |
Facultatif. Le ou les dossiers de documentation technique (guides, procédures, manuels), à la racine du partage, séparés par des virgules. Leur contenu va dans la collection documentation, tout le reste dans documents ; les droits NTFS s’appliquent de la même façon aux deux. Vide : tout le partage va dans documents. Changer ce réglage après l’indexation exige une réindexation complète (§5.9) |
DOCUMENTATION (lab : DOIT4EVERYONE) |
RAG_FQDN |
Le nom DNS de la stack, sans https:// (§9.8.2). Il fixe aussi l’adresse de n8n, https://<RAG_FQDN>:5678 |
rag.votre-domaine.ch |
Les valeurs qui contiennent un espace sont entre apostrophes dans env.example (par exemple SP_EXCLUDE_DRIVES='Preservation Hold Library'). Les conserver : sans elles, source .env échoue avec « command not found » sur le mot qui suit l’espace.
3. Le bloc du serveur de modèles, en haut du fichier. Il contient deux blocs : A pour Ollama, actif par défaut, et B pour vLLM, désactivé (chaque ligne commence par #). Un seul bloc doit être actif.
<IP-SERVEUR-MODELES> par l’adresse de la machine Ollama.# au début de chaque ligne du bloc A, retirer le # des lignes du bloc B, remplacer <IP-SERVEUR-MODELES> par l’adresse du poste ou du serveur vLLM, et <clé-vllm> par la clé créée à l’étape 3B. Pour la copier sans l’afficher depuis un poste Windows : §2.4.4.Plus bas dans le fichier, OLLAMA_URL prend la même valeur que EMBED_BASE_URL : port 11434 avec Ollama, port 8001 avec vLLM. Malgré son nom, cette ligne désigne le serveur d’embedding, avec les deux moteurs.
Jamais
localhostdans ces adresses, même si la VM et le serveur de modèles tournent sur le même poste : à l’intérieur d’un conteneur,localhostdésigne le conteneur lui-même.
4. Contrôler qu’il ne reste aucune valeur à remplacer :
grep -nE "^[A-Z_]+=.*<" .env
Attendu : seulement des lignes ENTRA_, TEAMS_, SP_ et MIP_TENANT_DOMAINE, qui concernent Microsoft 365 et restent inactives tant qu’il n’est pas configuré.
5. Le nom de la stack et son certificat (§9.8). Vérifier d’abord le nom, puis préparer la configuration de Nginx et le certificat :
cd /root/rag-stack
FQDN=$(grep '^RAG_FQDN=' .env | cut -d= -f2)
getent hosts "$FQDN"
# Attendu : l'adresse IP de la VM
# Configuration de Nginx, au nom de la stack
sed "s/<NOM-STACK>/${FQDN}/g" /root/ia-locale-pme-suisse/scripts/stack-ia-locale/nginx/rag.conf > nginx/rag.conf
grep -c "$FQDN" nginx/rag.conf
# Dossier du certificat : sur le disque chiffré si le lien du §1.5 existe
[ -e /etc/rag-tls ] || mkdir -p /etc/rag-tls
chmod 700 "$(readlink -f /etc/rag-tls)"
ls -l /etc/rag-tls/
Attendu : l’adresse de la VM, puis un nombre supérieur à 0.
rag.crt (certificat suivi de l’autorité intermédiaire) et rag.key, et les placer dans /etc/rag-tls, la clé en 600.cd /etc/rag-tls
FQDN=$(grep '^RAG_FQDN=' /root/rag-stack/.env | cut -d= -f2)
openssl req -x509 -newkey rsa:3072 -nodes -days 825 \
-keyout rag.key -out rag.crt \
-subj "/CN=${FQDN}" -addext "subjectAltName=DNS:${FQDN}"
chmod 600 rag.key
Attendu : rag.crt et rag.key dans /etc/rag-tls. Sans eux, Nginx s’arrête au démarrage et la stack n’est pas joignable.
Avec Ollama :
curl -s http://<IP-SERVEUR-MODELES>:11434/api/tags | python3 -m json.tool | grep '"name"'
Attendu : qwen3:14b, qwen3:4b-instruct et nomic-embed-text. Sans réponse : le réglage OLLAMA_HOST (étape 3A) ou le pare-feu Windows (§2.3).
Avec vLLM :
set -a; source /root/rag-stack/.env; set +a
curl -s -H "Authorization: Bearer $VLLM_API_KEY" http://<IP-SERVEUR-MODELES>:8000/v1/models | python3 -m json.tool | grep '"id"'
curl -s -H "Authorization: Bearer $VLLM_API_KEY" http://<IP-SERVEUR-MODELES>:8001/v1/models | python3 -m json.tool | grep '"id"'
Attendu : qwen3:14b, puis nomic-embed-text. Si curl reste bloqué : la règle de pare-feu de l’étape 3B. Si la réponse est vide : la clé du .env ne correspond pas à celle du serveur.
cd /root/rag-stack
docker compose build --no-cache rag-api
docker compose up -d
docker compose ps
Attendu : qdrant, n8n et nginx en Up, rag-api et open-webui en Up (healthy) après une minute environ. Seuls ces deux services déclarent un contrôle de santé : Up sans mention est normal pour les trois autres. Seul nginx publie des ports sur le réseau (80, 443, 5678) ; qdrant publie le sien sur 127.0.0.1 seulement, pour les scripts d’indexation lancés depuis la VM (étapes 12 et 13), et il n’est pas joignable d’une autre machine. Si rag-api redémarre en boucle : docker logs rag-api 2>&1 | tail -30 montre la cause (le plus souvent un secret refusé, ligne [SÉCURITÉ], une variable manquante dans .env, ou le certificat de l’étape 4 absent). Si nginx s’arrête : docker logs nginx (certificat absent de /etc/rag-tls, ou nginx/rag.conf manquant).
curl -sk https://$(grep '^RAG_FQDN=' .env | cut -d= -f2)/sante
Attendu : {"status":"ok"} (avec -k, le certificat n’est pas vérifié ; contrôle complet à l’étape 16).
Que la VM joigne le serveur de modèles ne suffit pas : c’est le conteneur rag-api qui l’interroge, par le réseau interne de Docker. Ce contrôle l’interroge depuis le conteneur, avec les réglages qu’il a réellement reçus :
docker exec rag-api python3 -c "
import os, httpx
api = os.environ.get('LLM_API', 'ollama')
cle = os.environ.get('VLLM_API_KEY', '')
h = {'Authorization': 'Bearer ' + cle} if cle else {}
for nom in ('LLM_BASE_URL', 'EMBED_BASE_URL'):
u = os.environ[nom] + ('/v1/models' if api == 'openai' else '/api/tags')
print(nom, httpx.get(u, headers=h, timeout=10).status_code)
"
Attendu : LLM_BASE_URL 200, puis EMBED_BASE_URL 200. En cas d’erreur No route to host alors que l’étape 8 a réussi : voir « En cas de problème », en fin de page.
L’indexation et la synchronisation des droits se lancent depuis la VM, dans un environnement Python isolé :
cd /root/rag-pipeline
python3 -m venv .venv
source .venv/bin/activate
pip install --retries 10 --timeout 60 qdrant-client python-docx pdfplumber python-pptx httpx \
ldap3 impacket rank-bm25 smbprotocol
Les neuf paquets sont tous nécessaires : sans smbprotocol ou rank-bm25, par exemple, la synchronisation ou l’indexation échoue au premier import.
Tester ensuite les deux accès de svc-rag, à l’annuaire et au partage :
set -a && source /root/rag-stack/.env && set +a
LDAPTLS_CACERT=/etc/ssl/certs/ad-chain.pem \
ldapwhoami -H ldaps://${LDAP_HOST}:${LDAP_PORT} -D "${LDAP_BIND_DN}" -w "${LDAP_BIND_PWD}" 2>&1
smbclient //<NOM-FILESERVER>/FileService -W "${SMB_DOMAIN}" -U "svc-rag%${SMB_PASSWORD}" -c "ls" 2>&1 | head -10
Attendu : dn:CN=svc-rag,..., puis la liste des dossiers du partage. Sur un domaine Active Directory, smbclient exige -W <DOMAINE> : sans lui, l’erreur est NT_STATUS_LOGON_FAILURE alors que le mot de passe est correct.
L’indexeur lit chaque document du partage, le découpe en extraits, calcule leurs vecteurs avec le modèle d’embedding, et les range dans Qdrant.
cd /root/rag-pipeline && source .venv/bin/activate
set -a && source /root/rag-stack/.env && set +a
python indexer.py --corpus /mnt/corpus-root --rapport /var/log/rag/rapport_indexation_initial.json
echo "Code de sortie : $?"
Attendu : au début, Vérification du serveur d'embedding (ollama), ou (openai) avec vLLM, puis Modèle d'embedding : nomic-embed-text ✓ ; à la fin, le code de sortie 0. Compter les extraits indexés :
for c in documents documentation; do
curl -s -H "api-key: $QDRANT_API_KEY" http://localhost:6333/collections/$c | \
python3 -c "import sys,json; print('$c :', json.load(sys.stdin)['result']['points_count'])"
done
Quarantaine : un fichier dont l’organisation propriétaire (client ou organisation interne) n’est pas identifiable est mis en quarantaine et n’est pas indexé ; il apparaît dans le rapport avec le statut
quarantaine. Un PDF scanné sans texte ou un fichier corrompu ne produit aucun extrait (statutvide).
L’outil de résolution des ACL recopie, dans chaque extrait indexé, la liste des groupes et des comptes autorisés à ouvrir son document. C’est ce qui permet ensuite de ne montrer à chacun que ses documents.
python acl_resolver.py \
--share //<NOM-FILESERVER>/FileService \
--mount /mnt/corpus-root \
--rapport /var/log/rag/rapport_acl_initial.json
echo "Code de sortie : $?"
Attendu : code 0 et Fichiers sans ACL : 0. Vérifier qu’un extrait porte bien ses droits :
curl -s -H "api-key: $QDRANT_API_KEY" -H "Content-Type: application/json" \
http://localhost:6333/collections/documents/points/scroll \
-d '{"limit":1,"with_payload":["source","autorises","interdits"]}' | python3 -m json.tool | head -15
Attendu : un champ autorises non vide.
Ouvrir https://<RAG_FQDN> (par exemple https://rag.votre-domaine.ch) depuis un poste du réseau. Le premier compte créé devient administrateur. Avec le certificat auto-signé, le navigateur affiche un avertissement : l’accepter pour ce poste, en sachant que tous les utilisateurs le verront jusqu’au remplacement du certificat (§9.8.6). Par l’adresse IP, la connexion est refusée : c’est voulu (§9.8.7).
Raccorder la RAG API :
http://rag-api:8080/v1API_TOKEN du .env (grep ^API_TOKEN /root/rag-stack/.env)rag-api apparaît dans la liste. Une erreur de connexion à cette étape vient presque toujours d’une adresse ou d’une clé erronée, ou d’une connexion créée dans la mauvaise section.Authentification par l’Active Directory (facultative en lab, obligatoire en production) : Settings → Admin → Authentication → LDAP, activer LDAP, puis remplir les champs. Open WebUI affiche leurs libellés dans la langue de l’interface ; la deuxième colonne donne ceux de l’interface en français (fichier de traduction de la version 0.11.3).
| Champ (interface en anglais) | Libellé en français | Valeur |
|---|---|---|
| Label | Étiquette | Un nom libre, par exemple Active Directory |
| Host | Hôte | <NOM-DC>.votre-domaine.ch, port 636, TLS activé |
| Attribute for Mail | Attribut pour l’e-mail | userPrincipalName |
| Attribute for Username | Attribut pour le nom d’utilisateur | sAMAccountName |
| Application DN | DN de l’application | CN=svc-rag,OU=COMPTES-SERVICE,DC=votre-domaine,DC=ch |
| Application DN Password | Mot de passe DN de l’application | Le mot de passe de svc-rag |
| Search Base | Base de recherche | DC=votre-domaine,DC=ch |
| Search Filters | Filtres de recherche | (objectClass=user) |
| Certificate Path | Chemin du certificat | /etc/ssl/certs/ad-chain.pem |
| Validate Certificate | Validate Certificate (non traduit) | Activé : la chaîne de l’étape 4 permet la validation (le §5.6.2 décrit le cas d’un lab sans certificat) |
| Ciphers | Chiffres | Valeur par défaut |
L’attribut du courriel doit être
userPrincipalName. Open WebUI transmet l’adresse de l’utilisateur à la RAG API, qui la cherche dans ce champ de l’annuaire. Avec un autre attribut, la recherche échoue et tout le monde est refusé (403). L’attribut du nom d’utilisateur est celui que la personne tape pour se connecter :sAMAccountNamepermet de saisirblaiseplutôt queblaise@votre-domaine.ch.
Rendre le modèle accessible : Settings → Admin → Models → rag-api → Make Public, puis enregistrer.
Les tâches automatiques d’Open WebUI (titres, étiquettes, questions de suivi) interrogent le modèle de la conversation, donc rag-api : une seule question de l’utilisateur déclenchait quatre appels dans le lab (un pour le titre, trois pour les suggestions), chacun avec sa recherche complète et son entrée dans le journal nLPD (§14.8.2). Le docker-compose.yml du dépôt les désactive (ENABLE_TITLE_GENERATION, ENABLE_TAGS_GENERATION, ENABLE_FOLLOW_UP_GENERATION à false). Ces variables ne fixent que l’état initial : Open WebUI les recopie dans sa base au premier démarrage, puis la base décide. Si la base existe déjà (Open WebUI démarré avant ce réglage), le faire une fois dans l’interface : Settings → Admin → Interface, désactiver les trois générations, et enregistrer.
Réglages d’administration et réglages personnels sont distincts. Chaque utilisateur dispose aussi, dans ses propres paramètres (menu de son compte), d’une rubrique Interface avec des options de titre, d’étiquettes et de suggestions de suivi. Elles ne remplacent pas le réglage d’administration : dans le lab, les modifier n’a rien changé au nombre d’appels. Le seul réglage qui compte est celui de Settings → Admin → Interface, vérifié par le contrôle ci-dessous.
Contrôle de la base :
docker exec -i open-webui python3 - <<'PYEOF'
import sqlite3
c = sqlite3.connect('/app/backend/data/webui.db')
for k, v, _ in c.execute("select * from config where key in ('task.title.enable','task.tags.enable','task.follow_up.enable','task.autocomplete.enable')"):
print(k, v)
PYEOF
Attendu : false aux quatre lignes. Puis poser une question et comparer wc -l /var/log/rag/rag-queries.jsonl avant et après : une seule ligne ajoutée par question.
n8n lance la synchronisation du corpus toutes les heures, et rappelle chaque mois la rotation du mot de passe de svc-rag. Ouvrir https://<RAG_FQDN>:5678 sans attendre : la première ouverture crée le compte propriétaire de n8n, et tant qu’il n’existe pas, n’importe qui joignant l’adresse peut le créer (§7.1.1). Mot de passe fort, conservé dans le gestionnaire de mots de passe de l’organisation. Si la page ne répond pas et que docker logs n8n montre EACCES: permission denied, le dossier n8n_data n’appartient pas à l’utilisateur 1000 : corriger par chown -R 1000:1000 "$(readlink -f /root/rag-stack/n8n_data)" puis docker compose up -d n8n. deploy.sh et l’étape 6 le font déjà.
1. Créer les deux identifiants (Credentials → Add credential). Le jeton d’administration n’est écrit dans aucun workflow : il reste dans l’identifiant (§16.7).
| Identifiant | Type | Réglages |
|---|---|---|
RAG API - ADMIN_TOKEN |
Header Auth | Name : Authorization ; Value : Bearer suivi de la valeur de ADMIN_TOKEN du .env (grep ^ADMIN_TOKEN /root/rag-stack/.env) ; domaines autorisés : Specific Domains, rag-api |
| Compte d’envoi des courriels | SMTP | Serveur, port, utilisateur et mot de passe de la boîte d’envoi ; SSL/TLS désactivé avec le port 587 (STARTTLS), Disable STARTTLS désactivé, Client Host Name vide |
Restreindre RAG API - ADMIN_TOKEN au domaine rag-api empêche n8n d’envoyer le jeton à une autre adresse, par exemple dans un workflow modifié par erreur. Avec Exchange Online, le serveur est smtp.office365.com, port 587, et l’authentification SMTP doit être autorisée pour la boîte d’envoi.
2. Importer les deux workflows du dossier scripts/N8N/ du dépôt (Import from File) : n8n-sync-corpus.json et n8n-rappel-rotation-svc-rag.json.
3. Rattacher les identifiants dans chaque nœud. Les identifiants référencés dans les fichiers importés n’existent pas chez vous : n8n affiche Credentials not found ou laisse le champ vide. Ouvrir chaque nœud concerné et y sélectionner l’identifiant créé à l’étape 1 : le nœud « POST /admin/sync » (Authentication : Generic Credential Type, Header Auth, RAG API - ADMIN_TOKEN), et chaque nœud de courriel (identifiant SMTP).
4. Renseigner les adresses dans chaque nœud de courriel, à la place de <EMAIL_EXPEDITEUR> et <EMAIL_DESTINATAIRE> :
554 5.2.252 SendAsDenied).5. Vérifier le nœud « POST /admin/sync » : URL http://rag-api:8080/admin/sync, méthode POST, authentification par l’identifiant RAG API - ADMIN_TOKEN. Aucun jeton n’est à recopier dans le nœud.
6. Publier les deux workflows, puis lancer une exécution manuelle de la synchronisation :
docker logs rag-api 2>&1 | grep "\[SYNC\]" | tail -10
Attendu : [SYNC] Terminé : success=True. Le courriel de la synchronisation ne part qu’en cas d’erreur, de quarantaine ou d’alerte de cloisonnement. Pour tester son envoi sans provoquer de panne : épingler des données d’erreur sur le nœud « POST /admin/sync » (Pin data, avec par exemple "success": false), lancer le workflow, vérifier la réception, puis retirer l’épingle avant de publier. Modifier la sortie d’un nœud en aval (Set mock data) ne teste rien : le nœud de courriel suivant reçoit toujours les données réelles.
7. Facultatif : le signal de supervision. Si l’organisation surveille la stack depuis une autre machine (§16.10), le nœud « Signal supervision (Push) » du workflow de synchronisation, désactivé à l’import, prévient l’outil après chaque synchronisation. Remplacer <NOM-COMPLET-SUPERVISION> et <CLE-PUSH> dans son URL, l’activer, publier. Le nom de la machine de supervision doit être un nom DNS complet : le conteneur n8n ne résout pas les noms courts.
Une synchronisation occupe la stack et le serveur de modèles. Tant qu’elle tourne, les réponses sont nettement plus lentes : dans le lab, trois à cinq fois plus lentes pendant une synchronisation lancée juste avant une série de questions (§10.3). Ne pas juger la vitesse de la stack à ce moment-là.
Une question de test, dans Open WebUI, sur un document que vous connaissez : la réponse doit le citer entre crochets.
Le test de cloisonnement, qui prouve que chacun ne voit que ses documents. Il demande quelques comptes de test créés dans l’Active Directory, et un fichier qui décrit, pour quelques documents, qui doit y avoir accès : partir de cas_cloisonnement.exemple.json, l’adapter à vos documents et le copier en /root/rag-pipeline/cas_cloisonnement.json (format au §16.3). Puis :
docker exec rag-api python3 /rag-pipeline/test_cloisonnement.py /rag-pipeline/cas_cloisonnement.json
Attendu : uniquement des OK, et en aucun cas une FUITE. Une fois ce fichier en place, la RAG API rejoue ces cas après chaque synchronisation (§16.4). Ce test vérifie les droits (le filtre sur les groupes, les refus nominatifs, Purview), pas la réponse du modèle : il dure quelques secondes. Dans le lab, sur le déploiement neuf : 72 verdicts, 70 OK, 2 INFO, 0 FUITE (§16.3).
Le test du garde-fou ne doit pas verrouiller
svc-rag. Le contrôle « Garde-fou des droits » ci-dessous désigne un partage qui n’existe pas : le compte s’authentifie normalement, chaque lecture échoue, et le garde-fou doit abandonner sans toucher à Qdrant. Ne pas le tester avec un mot de passe faux : chaque essai compte dans le seuil de verrouillage de l’Active Directory, et un comptesvc-ragverrouillé bloque la connexion de tous les utilisateurs (cas vécu dans le lab, avant la 2.20.4 qui arrêteacl_resolver.pyau premier refus d’authentification). Déverrouillage, sur un contrôleur de domaine :Unlock-ADAccount -Identity svc-rag. Validé lors du rejeu du 19 octobre 2026 :[GARDE-FOU] 70/70 fichiers en ACL illisible, abandon sans modification, code 1, Qdrant intact, compte non verrouillé.
Le frontal HTTPS et l’identité signée (§9.8, §16.6), sur la VM :
FQDN=$(grep '^RAG_FQDN=' /root/rag-stack/.env | cut -d= -f2)
# Certificat de l'autorité interne ; avec l'auto-signé, remplacer --cacert ... par -k
curl --cacert /etc/ssl/certs/ad-chain.pem https://${FQDN}/sante; echo
# Par l'adresse IP : refusé
curl -sk https://<IP-VM>/; echo "code $?"
Attendu : {"status":"ok"}, puis code 35. Puis le contrôle d’usurpation de §16.6 : une identité transmise sans signature est refusée (HTTP Error 403). Depuis un autre poste, seuls les ports 80, 443 et 5678 doivent répondre (§9.8.8).
Avant la mise en service : un snapshot de la VM arrêtée, une fois tous les contrôles passés. Arrêter la VM (shutdown -h now) avant le snapshot : un snapshot à chaud d’une VM dont le disque est chiffré et où tourne une base de données peut être incohérent. Prendre ensuite un snapshot Qdrant et le copier hors de la VM (§3.7).
| Contrôle | Commande ou action | Attendu |
|---|---|---|
| Serveur de modèles, depuis la stack | Étape 10 | 200 deux fois |
| Collections Qdrant | curl -s -H "api-key: $QDRANT_API_KEY" http://localhost:6333/collections |
documents et documentation |
| Extraits indexés | Étape 12 | Plus de 0 dans les deux collections |
| Droits recopiés | Étape 13 | autorises non vide |
| Santé de la RAG API | docker compose exec n8n wget -qO- http://rag-api:8080/health |
"status":"ok" |
| Frontal HTTPS | curl --cacert /etc/ssl/certs/ad-chain.pem https://<RAG_FQDN>/sante |
{"status":"ok"} (auto-signé : -k) |
| Refus par adresse IP | curl -sk https://<IP-VM>/; echo $? |
35 |
| Identité signée | Contrôle d’usurpation (§16.6) | HTTP Error 403 |
| Ports exposés | docker compose ps |
nginx : 80, 443, 5678 ; qdrant : 127.0.0.1:6333 seulement |
| Contrôle des citations | docker exec -w /app rag-api python3 /rag-pipeline/test_citations.py |
20 tests OK |
| Droits par SID | docker exec -w /app rag-api python3 /rag-pipeline/test_sid.py |
32 tests OK |
| SID d’un compte | docker exec -w /app rag-api python3 /rag-pipeline/diag_sid.py <compte> |
Lecture de tokenGroups : OK (§16.5.3) |
| Recherche par mots-clés chargée | docker logs rag-api \| grep BM25 |
Un nombre d’extraits |
| Accès autorisé | Question sur un document accessible | Réponse avec citation |
| Accès refusé | Compte sans droit, question sur un document restreint | « Cette information ne figure pas… » |
| Test de cloisonnement | Étape 16 | Aucune FUITE |
| Journal nLPD | tail -1 /var/log/rag/rag-queries.jsonl |
Une entrée avec user_id et ancree |
| Synchronisation horaire | n8n, dernière exécution | Réussie |
| Échéance du certificat | openssl x509 -in /etc/rag-tls/rag.crt -noout -enddate |
Notée au calendrier des rotations (§17.3) |
| Garde-fou des droits | python acl_resolver.py --share //<NOM-FILESERVER>/PartageInexistant --mount /mnt/corpus-root --rapport /var/log/rag/rapport_acl_gardefou.json |
Code 1, [GARDE-FOU], Qdrant intact |
| Lecture du partage | .\Set-AccesIndexationRAG.ps1 ... -Verifier |
0 fichier sans lecture |
| Avec vLLM : la clé est exigée | Étape 3B, point 5 | 401 sans la clé |
| Redémarrage complet | reboot, puis §15.3.5 et getent hosts <NOM-DC>.votre-domaine.ch <NOM-FILESERVER> |
Disque ouvert sans saisie, conteneurs Up, les deux noms résolus |
| Sauvegarde | Snapshot de la VM arrêtée, puis snapshots Qdrant copiés hors de qdrant_data (§3.7) |
Fichiers .snapshot présents dans la copie |
| Microsoft 365, si déployé | Checklists de §11.8, §12.9 et §13.9 | Tous les points validés |
Cette étape relie la stack à un tenant Microsoft 365 synchronisé avec l’Active Directory par Entra Connect. Les trois modules sont indépendants, mais le premier est requis par les deux autres. Le docker-compose.yml du dépôt transmet déjà toutes les variables : il suffit de les renseigner dans .env, puis de redémarrer la RAG API :
cd /root/rag-stack && docker compose up -d rag-api
Chaque application Microsoft 365 s’authentifie par certificat, jamais par secret. Les certificats sont créés sur la VM, dans /etc/rag-certs :
cd /etc/rag-certs
openssl req -x509 -newkey rsa:2048 -nodes -keyout <nom>.key -out <nom>.crt -days 730 -subj "/CN=<Nom-Application>"
chmod 600 <nom>.key
openssl x509 -in <nom>.crt -noout -fingerprint -sha1 | tr -d ':' | cut -d= -f2 # empreinte pour .env
rag-identity ;RAG-Identity-Resolver : certificat chargé, autorisations d’application Microsoft Graph User.Read.All et GroupMember.Read.All, consentement administrateur ;.env : ENTRA_ENABLED=true, ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CERT_THUMBPRINT (chemins par défaut conservés) ;docker logs rag-api 2>&1 | grep "\[AUTH\] Entra" affiche le nombre d’identifiants Entra de l’utilisateur.Détail : §11 Prérequis Microsoft 365.
GRP-Teams-CompteRendu-IA, de type sécurité à extension messagerie, créé en PowerShell Exchange Online, avec les organisateurs concernés ;rag-teams et App Registration RAG-Teams-Reader : autorisation d’application OnlineMeetingTranscript.Read.All, consentement ;New-CsApplicationAccessPolicy, puis Grant-CsApplicationAccessPolicy -Group <ID-objet-du-groupe>) ;.env : TEAMS_CLIENT_ID, TEAMS_CERT_THUMBPRINT, TEAMS_GROUP_ID ;n8n-teams-sync.json, expéditeur = compte SMTP authentifié ;docker exec -e PYTHONPATH=/app -w /app rag-api python3 /rag-pipeline/test_teams_graph.py (script de scripts/teams-test/, copié dans /root/rag-pipeline).Détail : §12 Synthèse des réunions Teams.
scripts/sharepoint/ : bibliothèques de conservation, membres atypiques, partages ;rag-sharepoint et App Registration RAG-SharePoint-Indexer : autorisations d’application Sites.Selected pour Microsoft Graph et SharePoint, consentement ;Accorder-SitesSelected.ps1 ;.env : SP_CLIENT_ID, SP_CERT_THUMBPRINT, SP_SITES (exactement les sites accordés) ;docker exec -e PYTHONPATH=/app -w /app rag-api python3 /rag-pipeline/sp_indexer.py --dry-run, puis contrôle des autorises[] dans le rapport /var/log/rag/rapport_sharepoint_*.json ;/admin/sync (workflow n8n), qui enchaîne le partage de fichiers et SharePoint ;Détail : §13 Connecteur SharePoint Online. Pour les documents protégés par Purview : §14.
La RAG API redémarre en boucle
docker logs rag-api 2>&1 | tail -30
Le plus souvent : une variable manquante dans .env, le certificat /etc/ssl/certs/ad-chain.pem absent, ou Qdrant pas encore prêt (attendre 30 secondes, puis docker compose restart rag-api).
Tout le monde est refusé (403)
LDAPTLS_CACERT=/etc/ssl/certs/ad-chain.pem ldapwhoami -H ldaps://<NOM-DC>.votre-domaine.ch:636 \
-D "CN=svc-rag,OU=COMPTES-SERVICE,DC=votre-domaine,DC=ch" -w "<mot-de-passe>" 2>&1
Causes : mot de passe de svc-rag erroné ou changé, compte verrouillé, certificat de l’autorité de certification incorrect, attribut LDAP d’Open WebUI autre que userPrincipalName, ou nom du contrôleur de domaine qui ne se résout plus (DNS public dans la configuration de la VM, voir plus bas).
Tout fonctionnait, puis tout le monde est refusé après un redémarrage, et la synchronisation échoue
Journal de rag-api : Erreur LDAP ... invalid server address ; synchronisation : NT_STATUS_INVALID_PARAMETER sur chaque fichier, ou le garde-fou qui signale un nom de serveur introuvable. Les noms du domaine ne se résolvent plus :
resolvectl status | grep -E "Current DNS Server|DNS Servers"
getent hosts <NOM-DC>.votre-domaine.ch <NOM-FILESERVER>
Si Current DNS Server est un DNS public, le retirer de la configuration réseau (étape 4). Le partage peut rester monté et lisible : il a été monté au démarrage, par son adresse ; ce sont les connexions suivantes, par le nom, qui échouent.
Le compte svc-rag est verrouillé
Journal : NT_STATUS_ACCOUNT_LOCKED_OUT, ou [GARDE-FOU] Authentification refusée. Le plus souvent, un mot de passe changé dans l’Active Directory et pas sur la VM (rotation, §9.5.1). Corriger le .env et le fichier de l’étape 5, puis déverrouiller sur un contrôleur de domaine :
Unlock-ADAccount -Identity svc-rag
Get-ADUser svc-rag -Properties LockedOut,BadLogonCount | Select-Object LockedOut,BadLogonCount
deploy.sh ou l’étape 11 s’arrête sur un délai dépassé pendant l’installation des paquets Python
Coupure passagère vers pypi.org. Relancer deploy.sh : il signale le .env du premier essai et demande de le remplacer ; répondre o, puisque ni Open WebUI ni n8n ne sont encore configurés. L’installation essaie désormais dix fois avant d’abandonner. Si l’échec se répète, tester l’accès en IPv4 puis en IPv6 : curl -4 -sS -o /dev/null -w "%{http_code}\n" https://pypi.org/simple/, puis la même chose avec -6. Ne pas ajouter de DNS public pour contourner : c’est la cause de la panne décrite plus haut.
La VM ne joint pas le serveur de modèles (étape 8)
OLLAMA_HOST absent (Ollama n’écoute que sur sa propre machine) ou port 11434 fermé dans le pare-feu Windows (§2.3).ss -ltn | grep -E ':800[01]', deux lignes attendues).La VM joint le serveur de modèles, mais pas la stack (étape 10 : No route to host)
Constaté une fois dans le lab, sans cause établie : le réseau interne de Docker avait perdu son adresse IPv4. Le conteneur ne pouvait plus rien joindre hors de la VM, alors que la VM elle-même joignait le serveur. Vérifier :
ip -br addr show br-$(docker network inspect rag-stack_default --format '' | cut -c1-12)
Si la ligne ne montre aucune adresse IPv4 (seulement une adresse qui commence par fe80::), redémarrer Docker, qui recrée l’adresse :
systemctl restart docker
cd /root/rag-stack && docker compose up -d
Puis refaire le contrôle de l’étape 10.
Les questions échouent avec une erreur 503, journal : « Échec de la recherche documentaire »
Le serveur d’embedding ne répond pas : sans vecteur de la question, pas de recherche. Avec vLLM, le serveur du port 8001 est arrêté (son terminal a été fermé, par exemple) ; avec Ollama, le modèle nomic-embed-text manque.
vLLM refuse de démarrer
Les messages rencontrés en lab et leur solution sont au §2.4.9.
Les réponses sont soudain très lentes
Une synchronisation est peut-être en cours (docker logs rag-api 2>&1 | grep "\[SYNC\]" | tail -3). Si les lenteurs persistent au repos : la mémoire de la carte déborde (§10.2).
n8n redémarre en boucle (EACCES: permission denied)
Le dossier n8n_data n’appartient pas à l’utilisateur 1000, sous lequel tourne n8n. Avec le disque chiffré, c’est le dossier réel qu’il faut corriger, pas le lien :
chown -R 1000:1000 "$(readlink -f /root/rag-stack/n8n_data)"
cd /root/rag-stack && docker compose up -d n8n
source .env répond « command not found »
Une valeur du .env contient un espace sans apostrophes (SP_EXCLUDE_DRIVES le plus souvent). Mettre la valeur entre apostrophes, comme dans env.example.
Une modification du .env n’est pas prise en compte
docker compose restart ne relit pas le .env. Utiliser docker compose up -d <service>, qui recrée le conteneur.
La RAG API ne répond pas aux contrôles de santé depuis la VM
Son port 8080 n’est pas publié sur le réseau : tester depuis l’intérieur de Docker.
docker exec rag-api python3 -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8080/health', timeout=5).read().decode())"
smbclient : NT_STATUS_LOGON_FAILURE avec un mot de passe correct
Ajouter le domaine : smbclient //<NOM-FILESERVER>/FileService -W <DOMAINE> -U "svc-rag%<mot-de-passe>".
Le montage du partage tombe après un redémarrage
mount -a && mount | grep corpus-root
Vérifier le mot de passe dans /etc/smbcredentials/svc-rag (rotation non reportée ?) et la ligne de /etc/fstab.
La recherche par mots-clés est vide au démarrage (BM25)
Avant la première indexation, l’index est vide : le journal dit « Aucun chunk dans Qdrant : index vide », ce qui est normal. Il se remplit à la première synchronisation (/admin/sync). Si l’index reste vide alors que Qdrant contient des extraits, Qdrant a démarré après la RAG API. Attendre 30 secondes, puis :
docker compose restart rag-api
docker logs rag-api 2>&1 | grep BM25
Le garde-fou des droits s’est déclenché (code 1)
Plus de 20 % des fichiers étaient illisibles : le résolveur refuse alors de toucher à Qdrant. Vérifier que svc-rag n’est pas verrouillé (sur Windows : Get-ADUser svc-rag -Properties LockedOut, puis Unlock-ADAccount -Identity svc-rag), que le partage est monté (ls /mnt/corpus-root), et que le contrôleur de domaine répond (ldapwhoami, ci-dessus).
| Retour au sommaire | Section suivante : §1 Prérequis et création de la VM |
Validé en lab sur VM-RAG-LAB (Ubuntu Server 26.04) et LABO-G9 (NVIDIA RTX 5060 Ti 16 Go), avec Ollama puis vLLM 0.31.0, octobre 2026.
ℹ️ Références, structuration et aide à la rédaction assistées par IA, avec validation humaine finale.