{"slug": "grand-saut-dans-le-deploiement-sur-site-d-un-serveur-llm-a-moindres-privileges", "title": "Grand saut dans le déploiement sur site d'un serveur LLM à moindres privilèges", "summary": "A company has deployed an on-premises LLM server with strict data privacy controls, aiming to provide air-gapped, stateless inference instances that eliminate data exfiltration risks. The project prioritizes isolation between tenants and future scalability, targeting use cases like document translation, log analysis, and code review without cloud dependency.", "body_md": "# Grand saut dans le déploiement sur site d'un serveur LLM à moindres privilèges\n\n[Téléchargement](grand-saut-dans-le-deploiement-sur-site-dun-serveur-llm-a-moindres-privileges#)\n\nEn 1826, les enfants rêvaient de chevaucher à travers les grands espaces comme dans les romans d’aventure. En 1926, on s’imaginait en Arsène Lupin, maître du cambriolage. En 2026, gérer des serveurs d'inférence distribués sans fuiter l'intégralité des données de l'entreprise est sans nul doute un rêve universel pour la nouvelle génération. Cet article retrace notre parcours de déploiement d'un serveur LLM on-premise, avec un regard critique sur la sécurité de la stack sous-jacente.\n\nVous souhaitez améliorer vos compétences ? Découvrez nos sessions de **formation** ! [En savoir plus](../offres/formations)\n\n## Introduction\n\nCe n'est plus un secret pour personne : les LLMs sont devenus, pour beaucoup d'entre nous, des outils à avoir dans sa boîte à outils.\n\nBeaucoup de sociétés aimeraient exploiter leurs grandes capacités de traitement, tout en préservant un niveau strict de confidentialité pour leurs données métier.\n\nGalvanisés par la *hype* mondiale et par un brin de curiosité, nous avons décidé de nous joindre à l'aventure.\n\n## Pourquoi un LLM *« on-premise* » ?\n\nSi vous travaillez pour une entreprise soucieuse de la confidentialité de ses données, utiliser un LLM *cloud* n'est probablement pas une super option. Pour être absolument certain que vos secrets restent des secrets, mieux vaut le faire tourner chez soi.\n\nAvoir un serveur LLM *on-premise* nous offre un contrôle total sur les mesures de protection des données, l'auditabilité, et offre le luxe appréciable de travailler avec de vraies données d'entreprise sans dépendre d'un tiers. Ce qui est un grand enjeu concernant la confiance de nos clients et la confidentialité de nos travaux de recherche et développement.\n\n## Objectifs et non-objectifs\n\nDepuis le jour un du projet, l'infrastructure a été pensée avec des préoccupations de futur passage à l'échelle et d'isolation entre les différents tenants. Plusieurs équipes sont prêtes à investir dans des GPUs, même coûteux, car un LLM confidentiel représenterait un potentiel énorme gain de temps pour leur activité. Traduction de documents, relecture, analyse de logs, amélioration des outils, index hors-ligne de connaissances, analyse contextuelle de base de code — et autres cas d'usage.\n\nLa mission principale est de fournir des instances LLM complètement isolées du réseau (*air-gapped*) qui éliminent les risques d'exfiltration de données. L'isolation \"en profondeur\" des processus reste au demeurant une préoccupation majeure. En revanche, évoluer dans un environnement réseau contrôlé nous donne un peu plus de flexibilité concernant les compromis de sécurité que nous pourrions envisager.\n\nRome ne s'est pas construite en un jour, Claude Code non plus.\n\nCette première version se veut entièrement *stateless* (sans persistence) : pas de données en entrée, pas d'entraînement personnalisé, pas de *pipeline* RAG, pas d'agent, pas de connecteur. Un périmètre serré signifie une mise en production plus rapide, et une production rapide rend les développeurs heureux.\n\nPar souci de clarté, cet article utilisera uniquement des données accessibles sur internet.\n\nDans un contexte de production, un registre interne serait naturellement la voie à suivre.\n\nQuelques notions importantes\n\nSi vous êtes familier avec les concepts et le vocabulaire de base des LLMs, vous pouvez passer directement à la section suivante.\n\nLa plupart des concepts suivants sont fondamentaux pour comprendre en profondeur comment fonctionne le traitement de texte par les LLMs, en revanche nous n'avons pas besoin d'une compréhension approfondie du sujet, une simple introduction suffira pour l'instant.\n\n### Tokens\n\nLes modèles LLM traitent les corpus lexicaux par morceaux (grosso modo des mots ou fragments de mots) appelés \"tokens\". Convertir le texte d'entrée en tokens est l'une des premières étapes du *pipeline* d'inférence, appelée \"tokenisation\".\n\nOpenAI fournit [\"tokenizer\"](https://platform.openai.com/tokenizer), un outil en ligne pour visualiser la tokenisation de texte par GPT.\n\n### Fichier modèle\n\nPour faire simple, un modèle est un gros fichier, principalement composé de nombres à virgule flottante générés pendant la \"phase d'entraînement\". Ce modèle est ensuite utilisé pour \"deviner\" statistiquement le prochain token en fonction de tous les tokens précédents. Ce processus s'appelle \"l'inférence\". Ces nombres (poids) sont stockés avec une quantification de précision à *n* bits.\n\n### Fenêtre de contexte\n\nLe nombre total de tokens que le modèle peut prendre en compte pendant l'inférence.\n\nCe nombre diffère pour chaque modèle.\n\nPar exemple, `Mistral-Small-24B-Instruct-2501`\n\na une longueur de contexte maximale de [32 768 tokens](https://huggingface.co/mistralai/Mistral-Small-24B-Instruct-2501/blob/main/config.json#L13) (environ 32k).\n\n### Cache KV\n\nAfin de produire une inférence de qualité, chaque token doit être \"calculé\" en se basant sur tous les tokens précédemment générés.\n\nPour illustrer, supposons la séquence de tokens en entrée : *\"Tom est un super\"*. Deviner le prochain token en se basant uniquement sur le token *\"super\"* est une mission désespérée, car il a une relation causale évidente avec *\"Tom\"* (sujet, probablement humain masculin), *\"est\"* (verbe, descriptif) et *\"un\"* (article indéfini, appartenance).\n\nLe contexte compte, ce qui signifie que sans persistance de mémoire, tous les tokens précédents devraient être (re)calculés avant de générer les suivants. Cette opération a un impact dramatique sur les performances, qui empire à mesure que la taille du contexte augmente.\n\nAvec une vision simplifiée des maths sous-jacentes, on peut considérer que — sans mécanisme de cache — le calcul d'une séquence de *n* tokens a une complexité quadratique *0(n²)*. Ce qui n'est vraisemblablement pas génial.\n\nAvec un cache vectoriel, la valeur de chaque token n'est calculée qu'une seule fois puis stockée dans un *kv store* pour les inférences futures.\n\nLa complexité devient linéaire *O(n)*, ce qui est nettement mieux.\n\n**Exemple :** inférer *\"magicien\"* à partir de la séquence précédente ne coûterait que *n* lectures depuis le *kv cache* :\n\n```\nTom    → [0.12, -0.44, 1.03, ...]\nest    → [-0.31, 0.88, 0.02, ...]\nun     → [0.07, -0.12, 0.55, ...]\nsuper  → [1.21, 0.33, -0.91, ...]\n```\n\nNaturellement, les calculs son légèrement plus complexes que ça.\n\nMais cet exemple met en évidence l'importance de ce composant, et comment le sous-dimensionner pourrait sévèrement dégrader les performances de notre serveur.\n\nLa formule de taille du cache KV pour les modèles [transformer](https://en.wikipedia.org/wiki/Transformer_(deep_learning)) (le standard actuel) à attention multi-tête est :\n\n**Ok assez de théorie, passons à la pratique !**\n\n## Étape 1 : shopping matériel\n\nAprès une batterie de tests empiriques sur plusieurs modèles en conditions réelles de travail, `gpt-oss-120b`\n\ns'est avéré être le meilleur compromis performance-qualité pour nos cas d'usage. En quantification 4 bits, les fichiers de poids du modèle font environ `60 GiB (4bits x 120b paramètres) + métadonnées`\n\n, ce qui représente une quantité massive de données à faire tenir dans la mémoire de notre GPU, et nécessitera sans aucun doute du matériel haut de gamme.\n\nComme aucune donnée d'entreprise n'est\n\n(pour l'instant)fournie au modèle via une base de données vectorielle, il était important de choisir un modèle avec une taille limite de contexte d'entrée conséquente — soit 131 072 tokens pour gpt-oss-120b.\n\nNous avons débuté notre recherche avec plusieurs critères en tête :\n\n- Le modèle doit tenir dans la vRAM du GPU.\n- Le cache kv du contexte doit tenir dans la vRAM du GPU, ou au minimum bénéficier d'un mécanisme de migration de pages mémoire haute performance.\n- Nous avons un espace rack limité sur site, plus le format par GPU est compact, mieux ça sera.\n- Chaque équipe doit avoir son GPU dédié.\n\nLe GPU est, sans surprise, le composant matériel le plus important. Le premier critère à considérer est la taille de la vRAM (video RAM), la mémoire volatile intégrée au GPU, qui donne à l'unité de traitement un accès extrêmement rapide aux données une fois chargées. Trois choses doivent être en mémoire :\n\n- Les poids du modèle\n- Le cache kv de contexte\n- Les buffers de calcul\n\nLes poids estimés du modèle peuvent être obtenus en multipliant tous les tenseurs du modèle par leur précision associée :\n\n``` python\nimport math\nfrom gguf import GGUFReader\n\nshards = (\n    \"gpt-oss-120b-mxfp4-00001-of-00003.gguf\",\n    \"gpt-oss-120b-mxfp4-00002-of-00003.gguf\",\n    \"gpt-oss-120b-mxfp4-00003-of-00003.gguf\",\n)\n\nn_parameters = 0\nn_bytes = 0\n\nfor path in shards:\n    reader = GGUFReader(path)\n    for tensor in reader.tensors:\n        n_parameters += math.prod(tensor.shape)\n        n_bytes += tensor.data.nbytes\n\nprint(f\"{n_parameters=:,}\")\nprint(f\"{n_bytes=:,}\")\n```\n\nLa valeur approximative pour les poids est `~60 GiB`\n\n.\n\nLa fenêtre de contexte est une valeur fixe pour chaque modèle, pour `gpt-oss-120b`\n\nelle équivaut à `131 072 tokens`\n\n(cf. [métadonnées du modèle](https://huggingface.co/ggml-org/gpt-oss-120b-GGUF/blob/main/gpt-oss-120b-mxfp4-00001-of-00003.gguf)). Pour le cache kv, on applique simplement la formule du chapitre sur la théorie.\n\nLe nombre d'octets par élément du cache KV est une valeur configurable, FP16 est considéré comme un bon compromis entre précision et utilisation mémoire.\n\n**Note :** gpt-oss-120b est un \"modèle hybride\" dont la moitié des couches utilisent un mécanisme de *Sliding Window Attention* *( model card - 2.2 Attention)* en lieu et place de l'attention complète. En théorie, cette fonctionnalité pourrait réduire l'empreinte mémoire du cache kv jusqu'à 50%. Le support de cette fonctionnalité est partiellement implémenté par\n\n`llama.cpp`\n\n([#13194](https://github.com/ggml-org/llama.cpp/pull/13194))) mais il n'est pas encore tout à fait clair si elle est suffisamment mature pour interagir correctement avec le cache du prompt global. On a de ce fait décidé de l'ignorer dans un premier temps. Si à l'avenir on se retrouve un peu juste niveau mémoire, vLLM a sa propre implémentation (ref.\n\n[Jenga paper](https://arxiv.org/pdf/2503.18292)).\n\n*(source Efficient Streaming Language Models with Attention Sinks)*\n\n**Tout ceci étant dit,** on se retrouve avec une taille résidente en mémoire de `~70GiB`\n\n.\n\nPeu de GPUs sont capables de faire tenir autant de données en mémoire. La migration de pages reste une option de repli si elle s'avère utilisable. Un parti courant pour résoudre le manque de mémoire consiste à agréger la vRAM de plusieurs GPUs. C'est clairement une solution possible, mais pas vraiment commode du point de vue de l'administration système : plus d'espace physique consommé par instance de serveur LLM, attribution et administration du matériel fastidieuses, nécessite de hautes performances du bus PCI, etc.\n\nAvec un prix public d'environ 9 000 € et une énorme capacité de 96 Go de vRAM, la Nvidia RTX Pro 6000 Max-Q Blackwell était sans conteste le matériel idéal pour notre cas d'usage. Cela signifie que l'on pourrait faire tenir le modèle complet et 4 slots de cache kv en parallèle (et ainsi éviter le partage de données de cache entre utilisateurs d'un même GPU).\n\nLa taille mémoire exacte de la RTX Pro 6000 est de `97887 MiB`\n\n. Naïvement, il semblerait que tout rentre parfaitement mais rappelez-vous que les valeurs ci-dessus sont des estimations, sans alignement de padding ni buffer transitoire additionnel.\n\n```\n% nvidia-smi -q -d MEMORY\n\nAttached GPUs                   : 1\nGPU 00000000:01:00.0\n    FB Memory Usage\n        Total                   : 97887 MiB\n        Reserved                : 637 MiB\n        Used                    : 96084 MiB\n        Free                    : 1167 MiB\n```\n\nOn peut soit accepter une fraction des données en RAM, soit réduire la taille de chaque contexte, avec une *\"marge de sécurité\"* arbitraire de `2 GiB`\n\n. Réduire la taille de contexte à `126 000 tokens`\n\nest plus que suffisant pour notre usage et satisfait l'ensemble de nos critères.\n\nLe GPU étant élu, nous avons commandé un serveur GPU complet chez APY avec les spécifications suivantes :\n\n- AMD EPYC 9224\n- ASUS ESC4000A-E12 2U (4 slots GPU)\n- 128 Go DDR5 ECC\n- RTX Pro 6000 Blackwell Max-Q (1 pour qualification)\n\n*(source servers.asus.com)*\n\nParfait, récapitulatif de la topologie finale :\n\n- Réseau isolé → pas d'accès internet\n- GPUs isolés → un par projet\n- Contexte isolé en mémoire GPU → un par utilisateur\n- Modèle partagé → un par GPU\n\n## Étape 2 : déploiement naïf\n\nLorsque l'on déploie de nouveaux services, on adopte généralement une approche itérative : déployer une première fois avec la configuration minimale pour avoir une vision globale des interactions entre composants. Ensuite, on examine chaque composant de plus près avec un œil plus averti, et on applique le durcissement approprié pour aboutir à une configuration à privilèges réduits.\n\nNous avons commencé avec la stack la plus simple possible :\n\n**OS :** debian 13 avec*hardening*approprié**Serveur d'inférence :**`llama.cpp`\n\n**Modèle LLM :**`gpt-oss-120b`\n\nPour les besoins de l'article, le déploiement sera fait \"à la main\".\n\nBien entendu, tout cela est voué à être automatisé pour un déploiement fluide et sans pépin.\n\n### Drivers GPU\n\nNous avons maintenant en notre possession un GPU Nvidia, et le *backend* standard pour les GPUs Nvidia est CUDA.\n\nDonc commençons par le commencement, installons les drivers Nvidia open-source en mode *compute-only* (réf. [NVIDIA Driver Installation Guide](https://docs.nvidia.com/datacenter/tesla/driver-installation-guide/debian.html#compute-only-system)) :\n\n```\n% apt install linux-headers-$(uname -r)\n\n% export version=\"1.1-1\"\n% export repository=\"https://developer.download.nvidia.com/compute/cuda/repos/debian13/x86_64\"\n\n# Bien tenté nvidia, mais pour être honnête, on n'a pas vraiment envie d'installer\n# un paquet pour configurer des sources apt.\n% wget -O - \"${repository}/cuda-keyring_${version}_all.deb\" | dpkg -x /dev/stdin .\n% install -m 0644 ./usr/share/keyrings/cuda-archive-keyring.gpg /usr/share/keyrings/\n% install -m 0644 ./etc/apt/sources.list.d/cuda-debian13-x86_64.list /etc/apt/sources.list.d/\n\n% apt update\n% apt install nvidia-driver-cuda nvidia-kernel-open-dkms cuda-toolkit-13\n\n# Le paquet \"cuda-toolkit-13\" installe la dépendance \"nvidia-persistenced\".\n# Cet outil sert à réduire le démarrage à froid entre les chargements GPU, on n'en a pas besoin.\n% vim /usr/lib/systemd/system-preset/nvidia.preset\ndisable nvidia-persistenced.service\n\n% systemctl disable --now nvidia-persistenced.service\n```\n\nMaintenant on peut vérifier que notre GPU est détecté par CUDA :\n\n```\n% nvidia-smi -L\nGPU 0: NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition (UUID: GPU-xxx-xxx)\n```\n\n### llama.cpp\n\n`llama.cpp`\n\nest un serveur d'inférence LLM *batteries-included* très populaire, à faible maintenance opérationnelle. Peu de dépendances, *stack* simple, gestion mémoire personnalisable, large support de différents types de modèles, bon potentiel d'intégration avec des outils externes. `vLLM`\n\nétait également un sérieux candidat, mais c'est un choix plus opinioné, plus axé sur la haute performance que l'usage quotidien.\n\nCommençons par télécharger tous les fichiers du modèle :\n\n```\n% export repository=\"https://huggingface.co/ggml-org/gpt-oss-120b-GGUF/resolve/main\"\n\n% install -dm 0755 /var/lib/models\n% for i in {1..3}; do wget -P /var/lib/models \\\n  \"${repository}/gpt-oss-120b-mxfp4-0000${i}-of-00003.gguf\"; done\n```\n\nNote :le serveur`llama.cpp`\n\nn'est pas disponible dans les dépôts debian.\n\nCependant, les performances s'améliorent significativement à chaque version, donc par la force des choses, ce sera de l'inférencebleeding-edge.\n\nEnsuite, on doit compiler `llama.cpp`\n\ndepuis les sources, avec le backend CUDA :\n\n```\n% apt install git cmake build-essential libssl-dev\n% git clone --depth 1 https://github.com/ggml-org/llama.cpp\n% cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_COMPILER=/usr/local/cuda-13/bin/nvcc ./llama.cpp\n% cmake --build build [--parallel] --config Release\n\n% ./build/bin/llama-cli --list-devices\nggml_cuda_init: found 1 CUDA devices:\n  Device 0: NVIDIA RTX PRO 6000 Blackwell Server Edition, compute capability 12.0, VMM: yes\nAvailable devices:\n  CUDA0: NVIDIA RTX PRO 6000 Blackwell Server Edition (97252 MiB, 96694 MiB free)\n```\n\nC'est l'heure du benchmark !\n\n```\n% ./build/bin/llama-bench --model /mnt/models/gpt-oss-120b-mxfp4-00001-of-00003.gguf -t 1 -fa 1 -b 2048 -ub 2048 -p 2048,8192,16384,32768,65536,131072 -ngl 99\nggml_cuda_init: found 1 CUDA devices:\n  Device 0: NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition, compute capability 12.0, VMM: yes\n| model                          |       size |     params | backend    | ngl | threads | n_ubatch | fa |            test |                  t/s |\n| ------------------------------ | ---------: | ---------: | ---------- | --: | ------: | -------: | -: | --------------: | -------------------: |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | CUDA       |  99 |       1 |     2048 |  1 |          pp2048 |      8704.94 ± 20.84 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | CUDA       |  99 |       1 |     2048 |  1 |          pp8192 |      8980.29 ± 10.49 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | CUDA       |  99 |       1 |     2048 |  1 |         pp16384 |      8723.56 ± 13.68 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | CUDA       |  99 |       1 |     2048 |  1 |         pp32768 |      8274.81 ± 65.17 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | CUDA       |  99 |       1 |     2048 |  1 |         pp65536 |      7294.51 ± 10.20 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | CUDA       |  99 |       1 |     2048 |  1 |        pp131072 |       5514.09 ± 5.41 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | CUDA       |  99 |       1 |     2048 |  1 |           tg128 |        217.70 ± 0.55 |\n\nbuild: ecd99d6 (1)\n```\n\nCe sont globalement de très bons résultats étant donné que les [benchmarks du 18 août 2025](https://github.com/ggml-org/llama.cpp/discussions/15396#NVIDIA) ([f08c4c0](https://github.com/ggml-org/llama.cpp/commit/f08c4c0d8d0cb6caaf8b7ad316039232b9fa059c)), il y a environ 6 mois, avec exactement le même matériel, rapportaient pratiquement moitié moins de `t/s`\n\n.\n\nBien, faisons de ce pas un peu d'inférence :\n\nAssez simple finalement.\n\nÉtape 3 : orchestration et isolation\n\nFaire tourner `llama.cpp`\n\nsur l'hôte fonctionne très bien. Néanmoins, on aimerait ajouter une couche d'isolation, surtout pour une topologie multi-GPU. Beaucoup de méthodes d'isolation existent. Nous sommes des ingénieurs modernes vivant dans un monde moderne qui place grande foi dans les conteneurs OCI pour l'isolation des processus et des utilisateurs.\n\nVous pensez peut-être avec indignation que les conteneurs ne sont pas les boites les mieux isolées du catalogue, d'autant qu'ils partagent entre eux les mêmes modules kernels. Des VMs avec GPU *passthrough* seraient probablement plus appropriées point de vue isolation, mais elles ont aussi leurs inconvénients : performances dégradées, orchestration non triviale, procédures de mise à jour fastidieuses. On évoquait plus tôt de petits compromis de sécurité, et celui-ci en est un. Un réseau isolé et du code de driver open-source auditable, c'est un compromis que nous sommes disposés à accepter !\n\n**Podman** est un excellent outil pour gérer des conteneurs car il supporte nativement le mode *rootless*.\n\nEt ce qui est également formidable avec podman, c'est qu'il supporte nativement une spécification assez commode de *mapping* de périphériques appelée \"CDI\" (*Container Device Interface*).\n\n### Qu'est-ce que les CDI ?\n\nCDI est une extension de la spécification OCI intégrée pour les périphériques, [fournie par le CNCF](https://github.com/cncf-tags/container-device-interface), supportée depuis [podman v3.2.0](https://github.com/containers/podman/releases/tag/v3.2.0).\n\nTout fichier présent dans `/run/cdi`\n\nou `/var/run/cdi`\n\nsera chargé par les gestionnaires de conteneurs compatibles CDI et sera injecté dans l'appel au *runtime* du conteneur (conformément à la [spécification OCI runtime-spec](https://specs.opencontainers.org/runtime-spec/config-linux/)).\n\nLa spécification complète des champs CDI est disponible dans le dépôt * cncf-tags/container-device-interface* (réf.\n\n[SPEC.md](https://github.com/cncf-tags/container-device-interface/blob/main/SPEC.md))\n\nPar chance, nvidia a eu la grâce de fournir un outillage très complet pour les conteneurs, avec notamment la génération de fichiers CDI (ref. [NVIDIA/nvidia-container-toolkit](https://github.com/NVIDIA/nvidia-container-toolkit)).\n\nSur les systèmes debian, tout cela est livré par le paquet **nvidia-container-toolkit**.\n\n```\n# nvidia-container-toolkit-base est une dépendance de nvidia-container-toolkit\n# qui fournit la majorité de l'outillage.\n# Certains éléments ont été retirés pour la lisibilité.\n% dpkg -L nvidia-container-toolkit-base\n/etc/systemd/system/nvidia-cdi-refresh.service\n/usr/bin/nvidia-cdi-hook\n/usr/bin/nvidia-container-runtime\n/usr/bin/nvidia-ctk\n```\n\nLe paquet fournit 3 binaires :\n\n`nvidia-cdi-hook`\n\n: gère les fichiers et liens symboliques à l'intérieur du conteneur.`nvidia-container-runtime`\n\n:*wrapper*autour du runtime de conteneur pour monter les périphériques. Nous n'en avons pas besoin car on va exploiter les capacités CDI intégrées dans podman.`nvidia-ctk`\n\n: usages variés, y compris la génération de fichiers CDI.\n\nEt un service systemd `nvidia-cdi-refresh.service`\n\nqui maintient les fichiers CDI à jour.\n\n```\n; systemctl cat nvidia-cdi-refresh.service\n; Certains éléments ont été retirés pour la lisibilité.\n\n[Unit]\nDescription=Refresh NVIDIA CDI specification file\nConditionPathExists=|/usr/bin/nvidia-smi\nConditionPathExists=|/usr/sbin/nvidia-smi\nConditionPathExists=/usr/bin/nvidia-ctk\n\n[Service]\nExecStart=/usr/bin/nvidia-ctk cdi generate\nCapabilityBoundingSet=CAP_SYS_MODULE CAP_SYS_ADMIN CAP_MKNOD\n```\n\nAu démarrage du service, la commande `nvidia-ctk cdi generate`\n\ns'exécute et — sans surprise — génère le fichier CDI basé sur le matériel à disposition sur l'hôte.\n\n```\n# /run/cdi/nvidia.yaml\n# Certains éléments ont été retirés pour la lisibilité.\n\n---\ncdiVersion: 0.5.0\n# Identifiant de périphérique vendor et class\nkind: nvidia.com/gpu\n# Périphériques disponibles pour le gestionnaire de conteneurs\n# Ex. podman run --device nvidia.com/gpu=0\ndevices:\n    - name: \"0\"\n      containerEdits:\n        # Périphériques liés à l'intérieur du conteneur\n        deviceNodes:\n            - path: /dev/nvidia0\n    - name: all\n      containerEdits:\n        deviceNodes:\n            - path: /dev/nvidia0\n            - path: /dev/nvidia1\n# Configuration partagée entre tous les périphériques ci-dessus\ncontainerEdits:\n    env:\n        - NVIDIA_CTK_LIBCUDA_DIR=/usr/lib/x86_64-linux-gnu\n    deviceNodes:\n        - path: /dev/nvidia-uvm\n        - path: /dev/nvidia-uvm-tools\n        - path: /dev/nvidiactl\n    hooks:\n        - hookName: createContainer\n          path: /usr/bin/nvidia-cdi-hook\n          args:\n            - nvidia-cdi-hook\n            - create-symlinks\n            - --link\n            - ../libnvidia-allocator.so.1::/usr/lib/x86_64-linux-gnu/gbm/nvidia-drm_gbm.so\n            - --link\n            - libglxserver_nvidia.so.590.48.01::/usr/lib/xorg/modules/extensions/libglxserver_nvidia.so\n          env:\n            - NVIDIA_CTK_DEBUG=false\n    # Bind mounts entre l'hôte et le conteneur\n    mounts:\n        - hostPath: /usr/bin/nvidia-cuda-mps-control\n          containerPath: /usr/bin/nvidia-cuda-mps-control\n          options:\n            - ro\n            - nosuid\n            - nodev\n            - rbind\n            - rprivate\n```\n\nLes fichiers CDI sont quasi exclusivement utilisés par Nvidia pour monter des GPUs dans les conteneurs.\n\nLe support CDI de Podman a même été implémenté par un employé de chez Nvidia (réf. [#10081](https://github.com/containers/podman/pull/10081)).\n\n### Configuration de Podman\n\nPodman fournit [une documentation dédiée](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md) à la configuration en mode *rootless*, par 3 grandes étapes :\n\n#### 1. Installer podman\n\nLe runtime `runc`\n\n, développé par l'Open Container Initiative, est un standard dans l'écosystème des conteneurs.\n\n```\n% apt install --no-install-recommends podman runc\n\n% vim /etc/containers/storage.conf\n[storage]\ndriver = \"overlay\"\n\n[storage.options.overlay]\nignore_chown_errors = \"true\"\n\n% vim /etc/containers/containers.conf\n[engine]\nruntime = \"/usr/sbin/runc\"\n\n% vim /etc/modules-load.d/overlay.conf\noverlay\n\n% useradd \\\n    --uid 10000 \\\n    --create-home \\\n    --home-dir /var/lib/containers/container01 \\\n    --shell /usr/sbin/nologin \\\n    --password '!' \\\n    container01\n```\n\n*Note : à partir de maintenant, considérez que toutes les commandes podman sont exécutées en tant qu'utilisateur système \"container01\".*\n\n#### 2. Configuration réseau\n\nA user-mode networking tool for unprivileged network namespaces must be installed on the machine in order for Podman to run in a rootless environment.\n\nCe n'est pas tout à fait vrai. Un driver réseau en mode utilisateur doit être installé *si* l'on a besoin de réseau, comme *bind* un port par exemple. Bonne nouvelle pour nous, le serveur `llama.cpp`\n\npeut **écouter sur un socket UNIX**. Cela signifie que **nous n'avons pas besoin d'une pile réseau TCP/IP** dans notre conteneur, c'est donc un gain très appréciable en performances et en sécurité.\n\nPour que tout cela fonctionne, nous avons juste besoin de lancer la commande podman avec le flag `--network=none`\n\n(ou `netns=none`\n\ndans `containers.conf`\n\n).\n\n```\n% podman run hello-world\nError: could not find pasta, the network namespace can't be configured: exec: \"pasta\": executable file not found in $PATH\n\n% podman run --network=none hello-world    \n!... Hello Podman World ...!\n\n         .--\"--.           \n       / -     - \\         \n      / (O)   (O) \\        \n   ~~~| -=(,Y,)=- |         \n    .---. /`  \\   |~~      \n ~/  o  o \\~~~~.----. ~~   \n  | =(X)= |~  / (O (O) \\   \n   ~~~~~~~  ~| =(Y_)=-  |   \n  ~~~~    ~~~|   U      |~~\n```\n\n**3. Configuration du mapping subuid/subgid**\n\nRootless Podman requires the user running it to have a range of UIDs listed in the files /etc/subuid and /etc/subgid\n\nCe n'est pas tout à fait vrai non plus. *Mapper* votre utilisateur courant avec l'utilisateur root du conteneur `1:1`\n\nne nécessiterait pas forcément de plage uid ou gid supplémentaire.\n\nPar défaut, l'utilisateur root à l'intérieur d'un conteneur sera mappé sur l'utilisateur qui a démarré le conteneur sur l'hôte (en supposant `--userns=host`\n\n), donc si on utilise par ex. des volumes, les processus du conteneur pourront accéder à n'importe quel fichier avec les mêmes droits système que l'utilisateur hôte. C'est déjà mieux que le mode *\"rootful\"* par défaut de Docker, où l'utilisateur root dans le conteneur est le *vrai utilisateur root sur l'hôte* (pas de *namespacing* utilisateur). Toute faille de sécurité dans l'isolation du système de fichiers ou des processus serait un risque énorme d'escalade de privilèges.\n\nD'un point de vue sécurité, on préférerait probablement mapper l'utilisateur du conteneur vers un UID élevé non privilégié sur l'hôte.\n\nLe processus principal `llama.cpp`\n\nà l'intérieur du conteneur podman tourne en tant qu'utilisateur `debian (1000:1000)`\n\net sur l'hôte en tant qu'uid 101000. Il n'est pas root dans le conteneur, et n'a même pas les droits de l'utilisateur container01 sur l'hôte, ce qui rend l'escalade de privilèges nettement plus difficile.\n\nSi les\n\nhomede vos utilisateurs sont gérés par`systemd-homed`\n\n, les plages subuid doivent commencer à 524288 pour éviter les collisions (réf.[rootless containers])\n\n```\n% apt install uidmap\n\n# Ajouter la plage subid pour l'utilisateur container01\n% usermod \\\n  --add-subuids 100000-101000 \\\n  --add-subgids 100000-101000 \\\n  container01\n\n# Désactiver le provisionnement automatique de subuid à la création de nouveaux utilisateurs.\ngrep 'SUB_.ID_COUNT' /etc/login.defs \nSUB_UID_COUNT 0\nSUB_GID_COUNT 0\n\n% podman run --detach --rm --userns=keep-id --user 1000:1000 debian:trixie tail -f /etc/hosts\n% ps -ef | grep tail\nUID          PID    PPID  C STIME TTY          TIME CMD\n101000    167172  167169  0 Mar11 pts/0    00:00:00 bash\n\n% lsns --type user --tree\nNS             TYPE  NPROCS    PID USER    COMMAND\n4026531837     user     423      1 root    /sbin/init\n└─4026532736   user       6 167141  container01 podman run --detach --rm --userns=keep-id --user 1000:1000 debian:trixie\n  └─4026534507 user       1 167172 101000  tail -f /etc/hosts\n```\n\nConfiguration du reverse proxy\n\nRappelez-vous que le serveur `llama.cpp`\n\nécoute sur un fichier socket UNIX. Les sockets UNIX sont commodes pour deux raisons : aucune configuration réseau n'est nécessaire, et ils s'appuient sur les droits POSIX du système de fichiers (DACLs ou FACLs) pour fournir un contrôle d'accès clé en main à l'API sous-jacente.\n\nAfin d'exposer la WebUI aux utilisateurs distants, on a besoin d'une forme de reverse proxy. Beaucoup de choix sont disponibles, on va partir sur un bon vieux serveur **nginx** — bien que l'on aurait pu simplement utiliser l'intégration native de proxy socket par systemd [systemd-socket-proxyd(8)](https://man.archlinux.org/man/systemd-socket-proxyd.8.en).\n\nL'écosystème Linux offre un large éventail de solutions de durcissement des processus. Ce sujet sort du périmètre de l'étude, de la documentation de qualité est disponible un peu partout sur internet :\n\n- Systemd sandboxing (\n[wiki.archlinux.org](https://wiki.archlinux.org/title/Systemd/Sandboxing)) - Service sandboxing (\n[wiki.debian.org](https://wiki.debian.org/ServiceSandboxing)) - Systemd hardening made easy with SHH (\n[synacktiv.com](https://www.synacktiv.com/publications/systemd-hardening-made-easy-with-shh)) [apparmor(7)](https://man.archlinux.org/man/apparmor.7),[landlock(7)](https://man.archlinux.org/man/landlock.7.en),[chroot(1)](https://man.archlinux.org/man/chroot.1),[pivot_root(8)](https://man.archlinux.org/man/pivot_root.8),[unshare(1)](https://man.archlinux.org/man/unshare.1),[limits.conf(5)](https://man.archlinux.org/man/core/pam/limits.conf.5.en)\n\nUn fichier de configuration pour commencer à expérimenter pourrait être aussi simple que :\n\n```\nmap \"$request_method:$uri\" \"$whitelist\" {\n    default 0;\n\n    \"GET:/\"                     1;\n    \"GET:/health\"               1;\n    \"GET:/v1/health\"            1;\n    \"GET:/props\"                1;\n    \"GET:/models\"               1;\n    \"GET:/v1/models\"            1;\n\n    \"POST:/completions\"          1;\n    \"POST:/v1/completions\"       1;\n    \"POST:/chat/completions\"     1;\n    \"POST:/v1/chat/completions\"  1;\n    \"POST:/v1/messages\"          1;\n}\n\nupstream container01 {\n    server unix:/var/lib/containers/container01/socks/llama-cpp.sock;\n}\n\nserver {\n    listen 127.0.0.1:80;\n\n    location / {\n        if ($whitelist = 0) {\n            return 403;\n        }\n\n        proxy_pass http://container01;\n    }\n}\n```\n\nSeules les routes API nécessaires sont *whitelistées* via la [map nginx](https://nginx.org/en/docs/http/ngx_http_map_module.html). La postérité se souviendra de la route API `/slots`\n\nqui fournissait de but en blanc tous les prompts des autres utilisateurs, jusqu'à ce qu'il soit \"rendu sécurisé\" *(cf. *[0d161f0](https://github.com/ggml-org/llama.cpp/commit/0d161f021aa33ec0e90cce96f5d1a88925557327)).\n\n``` bash\n$ podman run -e LLAMA_SERVER_SLOTS_DEBUG=1 ghcr.io/ggml-org/llama.cpp:server-cuda13 --jinja [...]\n\n$ curl http://localhost:8080/slots | jq -r '.[].prompt'\n<|start|>system<|message|>You are Kevin Flynn<|end|>\n<|start|>user<|message|>\n\nAnalyze this code for vulnerabilities\n--- File: cve-2002-0639 ---\nnresp = packet_get_int();\n\nif (nresp > 0) {\n response = xmalloc(nresp*sizeof(char*));\n\n for (i = 0; i < nresp; i++)\n  response[i] = packet_get_string(NULL);\n}\n<|end|>\n```\n\nAfin de renforcer la sécurité d'accès, on s'appuie sur l'authentification ssh plutôt que d'exposer un port sur le réseau interne.\n\nC'est pour cette raison que le serveur écoute sur `127.0.0.1`\n\net non pas sur son IP d'interco standard.\n\n```\n# Forward du port 80 du serveur nginx vers le port 8080 de la machine locale\n% ssh -J bastion.local -NL 8080:127.0.0.1:80 server.local\n```\n\nLancer llama.cpp\n\nTout est prêt. L'ensemble des options du serveur `llama.cpp`\n\nsont [documentées sur le wiki du projet](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md).\n\nLa commande finale ressemblerait à quelque chose comme ça :\n\n```\n# Notez que l'on doit forcer le mode de création du socket à 770\n# pour conserver les droits d'accès appropriés\n% podman run --rm --network none --detach \\\n    --user=1000:1000 \\\n    --userns=keep-id \\\n    --umask=007 \\\n    --device nvidia.com/gpu=0 \\\n    --volume /var/lib/models:/models:ro \\\n    --volume /var/lib/containers/container01/socks:/run/llama-cpp \\\n    ghcr.io/ggml-org/llama.cpp:server-cuda13 \\\n        --host /run/llama-cpp/llama-cpp.sock \\\n        --model /models/gpt-oss-120b-mxfp4-00001-of-00003.gguf \\\n        --alias gpt-oss-120b \\\n        --ctx-size 504000  \\    # 126 000 tokens x 4 slots\n        --parallel 4 \\          # 4 slots (implique --no-kv-unified)\n        --no-slots \\            # Désactiver le monitoring de slots\n        --swa-full              # Désactiver le cache SWA réduit\n\n23ed38fddcd8\n\n% podman logs -f 23ed38fddcd8\nggml_cuda_init: found 1 CUDA devices:\n  Device 0: NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition, compute capability 12.0, VMM: yes\n[...]\nmain: server is listening on unix:///run/llama-cpp/llama-cpp.sock\n```\n\nDémarrage du conteneur\n\nL'étape suivante consiste à faire démarrer et à configurer automatiquement les conteneurs au démarrage système.\n\nParmi toutes les solutions disponibles, on a opté pour les quadlets podman.\n\nLes quadlets sont une version étendue des services systemd, conçue spécialement pour la gestion du cycle de vie des conteneurs (réf. [podman-systemd.unit](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html))\n\nIls offrent de nombreuses abstractions telles que `.container`\n\n, `.network`\n\n, `.image`\n\n, ...\n\nBasés sur les [générateurs systemd](https://manpages.debian.org/testing/systemd/systemd.generator.7.en), tout fichier de définition déclenchera le provisionnement sous-jacent d'un véritable service systemd.\n\nPour l'utilisateur `container01`\n\navec `uid=10000,gid=10000`\n\n, la commande podman précédente convertie au format quadlet donnerait :\n\n```\n; /etc/containers/systemd/users/10000/llama-cpp.container\n\n[Unit]\nDescription=llama.cpp CUDA server\n\n[Container]\nImage=ghcr.io/ggml-org/llama.cpp:server-cuda13\nAutoUpdate=disabled\nAddDevice=nvidia.com/gpu=0\nNetwork=none\n\nVolume=/var/lib/models:/models:ro\nVolume=/var/lib/containers/container01/socks:/run/llama-cpp\n\n; Commande de healthcheck simple\nHealthStartPeriod=1m\nHealthCmd=/usr/bin/test -S /run/llama-cpp/llama-cpp.sock\n\nUser=1000\nGroup=1000\nUserNS=keep-id\n\nNoNewPrivileges=true\n\n; Forcer le mode de création du socket\nPodmanArgs=--umask=007\n\nExec=\\\n  --host \"/run/llama-cpp/llama-cpp.sock\" \\\n  --model /models/gpt-oss-120b-mxfp4-00001-of-00003.gguf \\\n  --alias gpt-oss-120b \\\n  --ctx-size 504000 \\\n  --parallel 4 \\\n  --no-slots \\\n  --swa-full\n\n[Quadlet]\nDefaultDependencies=false\n\n[Service]\nRestart=on-failure\n\n[Install]\nWantedBy=default.target\n```\n\nActiver le [ linger mode](https://wiki.archlinux.org/title/Systemd/User#Automatic_start-up_of_systemd_user_instances) déclenchera les générateurs systemd pour l'utilisateur\n\n`container01`\n\net, espérons-le, démarrera notre serveur LLM conteneurisé.\n\n```\n% loginctl enable-linger container01\n\n% pstree -SctuZ\nsystemd(`unconfined')\n ├─nginx(mnt,uts,`nginx (enforce)')\n │  ├─nginx(www-data,`nginx (enforce)')\n │  ├─nginx(www-data,`nginx (enforce)')\n │  └─nginx(www-data,`nginx (enforce)')\n └─systemd(container01,mnt,`sd-pam (enforce)')\n    ├─conmon(mnt,user,`podman (enforce)')\n    │  └─llama-server(cgroup,ipc,mnt,net,pid,uts,`container (enforce)')\n    │     ├─{cuda-EvtHandlr}(`container (enforce)')\n    │     ├─{cuda00001400006}(`container (enforce)')\n    │     ├─{llama-server}(`container (enforce)')\n    │     ├─[...]\n    │     └─{llama-server}(`container (enforce)')\n    └─podman pause(mnt,user,`container (enforce)')\n\n% cat /run/user/10000/systemd/generator/llama-cpp.service\n# Cette commande affiche le service systemd généré pour l'utilisateur 10000\n# basé sur les fichiers de configuration podman et quadlet fournis.\n```\n\nÉtape 4 : améliorations\n\nIl semblerait que notre déploiement \"standard\" de serveur LLM embarque tout un tas de dépendances, dont nous aimerions certainement nous passer.\n\nDe ce fait, il conviendrait de prendre un peu de recul sur l'utilité de chacun des composants afin de réduire au mieux la surface d'attaque globale.\n\nCreusons un peu.\n\n### Modules kernel\n\nD'abord, un tour de magie :\n\n```\n% lsmod | grep nvidia\nnvidia              16248832  6 nvidia_modeset\n\n% ./build/bin/llama-cli --list-devices\n[...]\n\n% lsmod | grep nvidia\nnvidia_uvm           2048000  0\nnvidia              16248832  7 nvidia_uvm\n```\n\nSurprise, nouveau module kernel !\n\nComme prévu, chaque module est parfaitement bien documenté en ligne, n'est-ce pas nvidia ?\n\nNon. Rien du tout.\n\nVous pensez peut-être que l'on pourrait \"deviner\" en se basant sur leur nom, ce qui est une bonne remarque mais pas spécialement une approche très rigoureuse.\n\n```\n% man nvidia-\nnvidia-cuda-mps-control  nvidia-cuda-mps-server  nvidia-modprobe  nvidia-persistenced  nvidia-smi\n\n# Le mieux que l'on ait serait nvidia-modprobe\n% man nvidia-modprobe\nNAME\n  nvidia-modprobe - Load the NVIDIA kernel module and create NVIDIA character device files.\n\n% tree /usr/share/doc/nvidia-*\n/usr/share/doc/nvidia-driver-cuda\n├── changelog.Debian.gz\n├── changelog.gz\n└── copyright\n/usr/share/doc/nvidia-kernel-open-dkms\n├── changelog.Debian.gz\n└── copyright\n/usr/share/doc/nvidia-kernel-support\n├── changelog.Debian.gz\n├── changelog.gz\n└── copyright\n/usr/share/doc/nvidia-modprobe\n├── changelog.Debian.gz\n└── copyright\n/usr/share/doc/nvidia-opencl-icd\n├── changelog.Debian.gz\n├── changelog.gz\n└── copyright\n/usr/share/doc/nvidia-persistenced\n├── changelog.Debian.gz\n└── copyright\n```\n\nEh bien figurez-vous, après un peu d'archéologie de paquets, que la documentation n'est livrée qu'avec le paquet desktop `nvidia-driver`\n\n.\n\nSacré Nvidia !\n\n```\n% apt download nvidia-driver\n% dpkg --contents nvidia-driver*\n-rw-r--r-- root/root 10530 2025-12-08 12:36 ./usr/share/doc/nvidia-driver/html/index.html\n[...]\n```\n\nTrès bien, maintenant on a un chapitre * Chapter 5. Listing of Installed Components* qui nous fournit toutes les informations pertinentes.\n\n`nvidia.ko`\n\n: fournit un accès bas niveau à votre matériel Nvidia.`nvidia-modeset.ko`\n\n: responsable de la configuration du moteur d'affichage du GPU.`nvidia-peermem.ko`\n\n: permet aux HCA Mellanox d'accéder aux buffers de lecture/écriture de la mémoire GPU sans avoir à copier les données dans la mémoire hôte. (i.e. au lieu du RDMA via une interco de type infiniband)`nvidia-uvm.ko`\n\n: module kernel*Unified Memory*; ce module fournit la fonctionnalité de partage de mémoire entre le CPU et le GPU dans les programmes CUDA. Il est généralement chargé dans le kernel au démarrage d'un programme CUDA, et est utilisé par le driver CUDA sur les plateformes supportées.\n\n**Chapter 36. Direct Rendering Manager Kernel Modesetting (DRM KMS)**\n\n`nvidia-drm.ko`\n\n: enregistre un driver DRM auprès du sous-système DRM du kernel Linux. Crée les périphériques dri `/dev/dri/card*`\n\net `/dev/dri/renderD*`\n\n(voir [docs.kernel.org](https://docs.kernel.org/gpu/introduction.html), [wikipedia](https://en.wikipedia.org/wiki/Direct_Rendering_Manager)).\n\nRécapitulons :\n\n`nvidia.ko`\n\n*(requis)*: module principal`nvidia-modeset.ko`\n\n*(inutile)*: on n'a pas besoin du moteur d'affichage`nvidia-peermem.ko`\n\n*(inutile)*: on n'a pas besoin de RDMA`nvidia-uvm.ko`\n\n*(peut-être)*: on n'a pas besoin de Unified Memory puisqu'on a assez de vRAM pour faire tenir l'intégralité des données du LLM. Mais peut-être que`llama.cpp`\n\nl'utilise, on verra ça plus tard`nvidia-drm.ko`\n\n*(inutile)*: CUDA utilise les périphériques`/dev/nvidia*`\n\n, pas les périphériques dri\n\nEn poursuivant avec notre approche itérative, nous allons maintenant investiguer quels modules sont essentiels au bon fonctionnement de CUDA, puis nous bloquerons le reste.\n\n```\n# Désactiver le chargement de modules kernel est une mesure de sécurité capitale sur les serveurs de production.\n# Charger des modules est l'un des moyens préférés des attaquants pour compromettre un système\n# et élever ses privilèges. Voir sysctl.d(5)\n% echo 1 > /proc/sys/kernel/modules_disabled\n\n% lsmod | grep nvidia\nnvidia              16248832  0\n\n% ./build/bin/llama-cli --list-devices\nggml_cuda_init: failed to initialize CUDA: unknown error\nAvailable devices:\n\n% strace -f -e execve ./build/bin/llama-cli --list-devices 2>&1\nexecve(\"./build/bin/llama-cli\", [\"./build/bin/llama-cli\", \"--list-devices\"], 0x7ffec8576210 /* 19 vars */) = 0\n[pid 42346] execve(\"/sbin/modprobe\", [\"modprobe\", \"nvidia-uvm\"], 0x7f95e382e870 /* 1 var */) = 0\nggml_cuda_init: failed to initialize CUDA: unknown error\nAvailable devices:\n```\n\nCUDA essaie de charger opportunistement un module kernel dans notre dos.\n\nPas cool nvidia !\n\n```\n% lsmod | grep nvidia\nnvidia_uvm           2048000  0\nnvidia              16248832  7 nvidia_uvm\n\n% ./build/bin/llama-cli --list-devices\nggml_cuda_init: found 1 CUDA devices:\n  Device 0: NVIDIA RTX PRO 6000 Blackwell Server Edition, compute capability 12.0, VMM: yes\nAvailable devices:\n  CUDA0: NVIDIA RTX PRO 6000 Blackwell Server Edition (97252 MiB, 96694 MiB free)\n```\n\nJusqu'ici tout se présente bien, on a notre *setup* driver minimal.\n\nLa commande `strace`\n\nnous donne 3 informations :\n\n```\nopenat(AT_FDCWD, \"/dev/nvidiactl\", O_RDWR|O_CLOEXEC)\nopenat(AT_FDCWD, \"/dev/nvidia0\", O_RDWR|O_CLOEXEC)\nopenat(AT_FDCWD, \"/dev/nvidia-uvm\", O_RDWR|O_CLOEXEC)\n```\n\n- CUDA utilise\n`nvidiactl`\n\n(au minimum) pour énumérer les périphériques. - CUDA utilise le nœud de périphérique\n`/dev/nvidia*`\n\npour interagir avec les GPUs. - CUDA utilise le nœud de périphérique\n`/dev/nvidia-uvm`\n\n, certainement pour une bonne raison.\n\n#### Le problème du module nvidia-uvm\n\n*Unified Virtual Memory* est un système de gestion mémoire développé par Nvidia, basé sur la migration dynamique de pages, pour créer un espace mémoire \"unifié\" entre la RAM CPU et la vRAM GPU. En déchargeant la mémoire GPU en RAM, les programmes peuvent calculer des jeux de données plus volumineux que la capacité de la vRAM sans trop de dégradation de performance.\n\nC'est sans aucun doute une cible de premier choix pour un attaquant cherchant à se procurer des données confidentielles.\n\nPour nous, cette fonctionnalité semble naïvement parfaitement inutile puisque l'on fait tenir l'intégralité de la charge dans la vRAM. Donc pas besoin de migration de pages, pas vrai ?\n\nDans le code CUDA, l'utilisation d'uvm est assez explicite : l'appel de fonction `cudaMalloc()`\n\nest remplacé par `cudaMallocManaged()`\n\npour demander explicitement de la mémoire managée au moteur CUDA.\n\nCoup de chance, `llama.cpp`\n\nn'utilise pas uvm par défaut, et n'appelle `cudaMallocManaged()`\n\nque si la variable `GGML_CUDA_ENABLE_UNIFIED_MEMORY`\n\nest exportée. Problème résolu, non ?\n\nMalheureusement, d'après quelques personnes parfaitement anonymes sur de sombres forums dans les fins fonds d'internet, CUDA dépend d'uvm depuis la version 6 ou 7 ou 8 (toujours extensivement non-documenté par Nvidia), même si ce n'est jamais appelé explicitement dans le code. L'initialisation du *runtime* CUDA lance un processus enfant pour charger nvidia_uvm au lieu de tourner en mode dégradé. Il semblerait que le runtime exige inconditionnellement que nvidia_uvm soit présent, même pour des charges de travail non-UVM.\n\nEn se basant sur le [code du module kernel open](https://github.com/NVIDIA/open-gpu-kernel-modules/tree/main/kernel-open/nvidia-uvm), on peut supposer que CUDA s'appuie sur ce module non seulement pour la mémoire unifiée, mais également pour l'ensemble de sa gestion d'espace mémoire virtuel (VUA), la gestion des faults GPU et l'éviction mémoire.\n\nPériphériques système\n\nPar défaut, les nœuds de périphériques GPU sont lisibles et inscriptibles par tout le monde :\n\n```\n% ls -l /dev/nvidia0 \ncrw-rw-rw- 1 root root 195, 0 Feb 26 14:30 /dev/nvidia0\n```\n\nPas de panique au demeurant, cela ne signifie pas pour autant que tout le monde peut lire l'intégralité de la mémoire de notre GPU. Les périphériques sont un type spécial de fichiers qui réimplémentent [toutes les opérations standard sur fichiers](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/fs.h?h=v6.19.3#n1918) telles que `open()`\n\n, `write()`\n\n, et gèrent les appels `ioctl()`\n\ncomme ils le souhaitent.\n\nConcernant les nœuds de périphériques `nvidia*`\n\n, ils gèrent une table d'allocation mémoire dynamique dans la vRAM GPU.\n\nDeux problèmes :\n\n- Toute faille dans l'implémentation de ce périphérique mènerait à une compromission globale de la mémoire GPU.\n- Tout le monde peut écrire des données dans le GPU, et on ne veut pas partager la mémoire avec d'autres processus.\n\nVoyons si le module kernel `nvidia`\n\na quelques chouettes options à nous proposer :\n\n```\n% modinfo nvidia\n[...]\nparm:           NVreg_DeviceFileUID:int\nparm:           NVreg_DeviceFileGID:int\nparm:           NVreg_DeviceFileMode:int\n```\n\nParfait, on peut donc appliquer quelques restrictions un peu plus convenables :\n\n```\n# Restreindre l'accès aux périphériques à root\n% vim /etc/modprobe.d/nvidia-devices.conf\noptions nvidia NVreg_DeviceFileUID=0\noptions nvidia NVreg_DeviceFileGID=0\noptions nvidia NVreg_DeviceFileMode=0660\n\n# Blacklister les modules inutiles\n% vim /etc/modprobe.d/nvidia-blacklist.conf \nblacklist nvidia_drm\nblacklist nvidia_modeset\nblacklist nvidia_peermem\n\n# Charger automatiquement les modules nvidia au boot système\n% vim /etc/modules-load.d/nvidia.conf\nnvidia\nnvidia_uvm\n\n% update-initramfs -u\n% systemctl reboot\n\n% lsmod | grep nvidia\nnvidia_uvm           2048000  4\nnvidia              16273408  40 nvidia_uvm\n\n% ls -l /dev/nvidia*\ncrw-rw---- 1 root  root  195,   0 Feb 26 14:32 /dev/nvidia0\ncrw-rw---- 1 root  root  195, 255 Feb  9 14:26 /dev/nvidiactl\ncrw-rw-rw- 1 root  root  236,   0 Feb  9 14:26 /dev/nvidia-uvm\n```\n\n`nvidia-uvm`\n\nn'est pas créé avec le même utilisateur et mode que `nvidiaX`\n\nou `nvidiactl`\n\n.**En résumé :**\n\n`nvidia-modprobe`\n\nest appelé au chargement du module kernel `nvidia`\n\npar une règle `udev`\n\net crée ensuite tous les nœuds de périphériques.\n\n```\n# /usr/lib/udev/rules.d/60-nvidia.rules \n\nACTION==\"add|bind\", KERNEL==\"nvidia\", RUN+=\"/usr/bin/nvidia-modprobe\"\n```\n\n`nvidiaX`\n\net`nvidiactl`\n\nsont créés par la fonction`nvidia_mknod()`\n\n-[nvidia-modprobe-utils.c#L642](https://github.com/NVIDIA/nvidia-modprobe/blob/e5512245994c07cb0aaaa22c1d3cbf72f452cb01/modprobe-utils/nvidia-modprobe-utils.c#L642).`nvidia-uvm`\n\nest créé par la fonction`nvidia_uvm_mknod()`\n\n-[nvidia-modprobe-utils.c#L803](https://github.com/NVIDIA/nvidia-modprobe/blob/e5512245994c07cb0aaaa22c1d3cbf72f452cb01/modprobe-utils/nvidia-modprobe-utils.c#L803)\n\nCes deux fonctions appellent ensuite `mknod_helper()`\n\n(la fonction qui crée le nœud de périphérique), sauf que la première passe un argument supplémentaire `NV_PROC_REGISTRY_PATH`\n\n.\n\n```\n/* La majeure partie du code a été retirée pour la lisibilité */\n\n#define NV_PROC_REGISTRY_PATH \"/proc/driver/nvidia/params\"\n#define NV_DEVICE_FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)\n\nint nvidia_mknod(int minor)\n{\n  // mknod est appelé avec proc_path = \"/proc/driver/nvidia/params\"\n  return mknod_helper(NV_MAJOR_DEVICE_NUMBER, minor, path, NV_PROC_REGISTRY_PATH);\n}\n\nint nvidia_uvm_mknod(int base_minor)\n{\n  // mknod est appelé avec proc_path = NULL\n  return mknod_helper(major, base_minor, NV_UVM_DEVICE_NAME, NULL);\n}\n\nstatic int mknod_helper(int major, int minor, const char *path, const char *proc_path)\n{\n    mode_t mode;\n\n    init_device_file_parameters(&uid, &gid, &mode, &modification_allowed, proc_path);\n}\n\nstatic void init_device_file_parameters(uid_t *uid, gid_t *gid, mode_t *mode, int *modify, const char *proc_path)\n{\n    FILE *fp;\n    unsigned int value;\n\n    *mode = NV_DEVICE_FILE_MODE;  // valeur par défaut telle que définie ci-dessus\n\n    if (proc_path == NULL) {\n        return; // nvidia-uvm retourne sans mode défini\n    }\n\n    fp = fopen(proc_path, \"r\");\n\n    while (fscanf(fp, \"%31[^:]: %u\\n\", name, &value) == 2) {\n        if (strcmp(name, \"DeviceFileMode\") == 0) {\n            *mode = value;  // le mode est défini\n        }\n    }\n}\n```\n\nFin de digression. Pour changer le mode de `nvidia-uvm`\n\n, plusieurs options :\n\n- Ajouter une autre règle udev\n- Recompiler avec le flag\n`-DNV_DEVICE_FILE_MODE=0660`\n\n- Surveiller la création de nouveaux nœuds dans\n`/dev`\n\n**Solution 1.** n'est pas vraiment possible car les règles udev ne fonctionneront pas correctement étant donné que le mknod est géré directement par nvidia-modprobe\n\n**Solution 2.** est faisable mais assez fastidieux, et ne pourra pas vraiment résoudre l'ensemble du problème.\n\n**Solution 3.** est probablement notre meilleur candidate. Nous allons utiliser des *units* systemd de type `.path`\n\nqui surveillent un chemin sur le systeme de fichiers et lancer un service systemd associé en conséquence.\n\nLe but est de modifier le mode du *device* en `0660`\n\nafin d'empêcher toute interraction de la part d'un utilisateur qui ne serait pas explicitement autorisé. Une bonne option serait d'utiliser les ACLs Linux pour donner un accès fin à nos utilisateurs non-privilégiés.\n\n```\n# /etc/systemd/system/nvidia-uvm.path\n\n[Unit]\nDescription=Watch for nvidia-uvm device creation\n\n[Path]\nPathExists=/dev/nvidia-uvm\n\n[Install]\nWantedBy=multi-user.target\n\n# /etc/systemd/system/nvidia-uvm.service\n\n[Unit]\nDescription=Set nvidia-uvm device permissions\n\n[Service]\nType=oneshot\nExecStart=/bin/chmod 660 /dev/nvidia-uvm\nExecStart=/usr/bin/setfacl -m u:101000:rw /dev/nvidia-uvm\n```\n\nEt la même question se pose pour `nvidiactl`\n\net des périphériques génériques `nvidiaX`\n\n.\n\nNous pourrions alors modifier les *units* ci-avant pour les rendre un peu plus génériques.\n\n```\n# /etc/systemd/system/nvidia-devices@.path\n\n[Unit]\nDescription=Watch for /dev/%I device creation\n\n[Path]\nPathExists=/dev/%I\n\n[Install]\nWantedBy=multi-user.target\n\n# /etc/systemd/system/nvidia-devices@.service\n\n[Unit]\nDescription=Set /dev/%I permissions\n\n[Service]\nType=oneshot\nEnvironmentFile=-/etc/nvidia/%I.conf\nExecStart=/bin/chmod 660 /dev/%I\nExecStart=/usr/bin/sh -c \\\n  'for uid in ${OWNER_UIDS}; do /usr/bin/setfacl -m u:$uid:rw /dev/%I; done'\n```\n\nÀ partir de maintenant, il ne reste plus qu'à explicitement configurer les droits pour chaque *device* et lancer nos services génériques.\n\n```\n# Exemple avec deux utilisateurs qui partageraient l'accès à nvidiactl et nvidia-uvm\n% echo 'OWNER_UIDS=\"101000 201001\"' > /etc/nvidia/nvidia-uvm.conf\n% echo 'OWNER_UIDS=\"101000 201001\"' > /etc/nvidia/nvidiactl.conf\n% echo 'OWNER_UIDS=\"101000\"' > /etc/nvidia/nvidia0.conf\n% echo 'OWNER_UIDS=\"201001\"' > /etc/nvidia/nvidia1.conf\n\n% systemctl enable --now 'nvidia-devices@nvidia\\x2duvm.path'\n% systemctl enable --now 'nvidia-devices@nvidiactl.path'\n% systemctl enable --now 'nvidia-devices@nvidia0.path'\n% systemctl enable --now 'nvidia-devices@nvidia1.path'\n% getfacl /dev/nvidia*\n\n# file: dev/nvidia-uvm\n# owner: root\n# group: root\nuser::rw-\nuser:101000:rw-\nuser:201001:rw-\ngroup::rw-\nmask::rw-\nother::---\n\n# file: dev/nvidiactl\n# owner: root\n# group: root\nuser::rw-\nuser:101000:rw-\nuser:201001:rw-\ngroup::rw-\nother::---\n\n# file: dev/nvidia0\n# owner: root\n# group: root\nuser::rw-\nuser:101000:rw-\ngroup::rw-\nother::---\n\n# file: dev/nvidia1\n# owner: root\n# group: root\nuser::rw-\nuser:201001:rw-\ngroup::rw-\nother::---\n```\n\nTrès bien, à ce stade nous avons une installation CUDA minimale avec :\n\n`/dev/nvidia0`\n\n*(root:root + facl 101000)*`/dev/nvidia1`\n\n*(root:root + facl 201001)*`/dev/nvidiactl`\n\n*(root:root + facl 101000 + facl 201...)*`/dev/nvidia-uvm`\n\n*(root:root + facl 101000+ facl 201...)*\n\n#### Néanmoins\n\nBien que les périphériques `nvidiaX`\n\npuissent être liés à un groupe système unique (un par GPU), `nvidiactl`\n\net (bien pire) `nvidia-uvm`\n\ndoivent être partagés par tous les processus utilisant des GPUs. Et, d'un point de vue sécurité, ce n'est pas franchement une situation d'avenir.\n\nRappelez-vous que `/dev/nvidia-uvm`\n\nest un périphérique unique qui fournit l'accès à la gestion mémoire de tous les GPUs du système. N'importe qui avec la permission d'ouvrir le périphérique pourrait donc potentiellement forger un exploit pour accéder à l'espace d'adressage virtuel UVM global de tous les autres GPUs.\n\nCependant, peut-être qu'il nous reste encore une solution viable à explorer !\n\n#### Backend Vulkan\n\n`llama.cpp`\n\nsupporte également le backend Vulkan, une API de calcul et de rendu graphique standardisée, multi-plateforme et multi-constructeur.\n\nDéployer `llama.cpp`\n\navec vulkan est finalement assez similaire à CUDA.\n\n```\n% apt install nvidia-kernel-open-dkms nvidia-driver-libs [vulkan-tools]\n\n% vulkaninfo --summary\n==========\nVULKANINFO\n==========\n\nVulkan Instance Version: 1.4.309\n\nDevices:\n========\nGPU0:\n        apiVersion         = 1.4.325\n        driverVersion      = 590.48.1.0\n        vendorID           = 0x10de\n        deviceID           = 0x2bb5\n        deviceName         = NVIDIA RTX PRO 6000 Blackwell Server Edition\n        driverName         = NVIDIA\n        driverInfo         = 590.48.01\n```\n\nAvant nvidia-container-toolkit, il est nécessaire d'installer `nvidia-smi`\n\ncar `nvidia-cdi-refresh.service`\n\nen a besoin pour s'exécuter `ConditionPathExists=/usr/bin/nvidia-smi`\n\n(cf. Étape 3).\n\nUn paquet \"nvidia-smi\" existe dans les dépôts nvidia mais il ne fournit pas du tout le binaire `nvidia-smi`\n\n, probablement pour des raisons de rétrocompatibilité.\n\nThis is a transitional dummy package, it can be safely removed.\n\nLe vrai binaire est livré dans le paquet du driver CUDA :\n\n```\n% apt download nvidia-driver-cuda\n% dpkg -x nvidia-driver-cuda_590.48.01-1_amd64.deb .\n% install -m 0755 ./usr/bin/nvidia-smi /usr/bin\n\n% apt install nvidia-container-toolkit\n\n% podman run -it --rm --device nvidia.com/gpu=0 --entrypoint /app/llama-bench --volume /var/lib/models:/models:ro ghcr.io/ggml-org/llama.cpp:full-vulkan --model /mnt/models/gpt-oss-120b-mxfp4-00001-of-00003.gguf -t 1 -fa 1 -b 2048 -ub 2048 -p 2048,8192,16384,32768,65536,131072 -ngl 99\n\nggml_vulkan: Found 1 Vulkan devices:\nggml_vulkan: 0 = NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition | uma: 0 | fp16: 1 | bf16: 0 | warp size: 32 | shared memory: 49152 | int dot: 1 | matrix cores: NV_coopmat2\n| model                          |       size |     params | backend    | ngl | threads | n_ubatch | fa |            test |                  t/s |\n| ------------------------------ | ---------: | ---------: | ---------- | --: | ------: | -------: | -: | --------------: | -------------------: |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | Vulkan     |  99 |       1 |     2048 |  1 |          pp2048 |      7976.25 ± 48.33 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | Vulkan     |  99 |       1 |     2048 |  1 |          pp8192 |      8204.88 ± 10.63 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | Vulkan     |  99 |       1 |     2048 |  1 |         pp16384 |      8091.26 ± 61.02 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | Vulkan     |  99 |       1 |     2048 |  1 |         pp32768 |      7642.83 ± 10.28 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | Vulkan     |  99 |       1 |     2048 |  1 |         pp65536 |       6755.33 ± 8.83 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | Vulkan     |  99 |       1 |     2048 |  1 |        pp131072 |       5404.22 ± 3.21 |\n| gpt-oss 120B MXFP4 MoE         |  59.02 GiB |   116.83 B | Vulkan     |  99 |       1 |     2048 |  1 |           tg128 |        217.36 ± 0.03 |\n\nbuild: ecd99d6 (1)\n```\n\nPour être parfaitement honnêtes, nous pensions que CUDA oblitérerait littéralement les performances de Vulkan. Au final, il semblerait que l'écart ne soit pas si grand que ça !\n\n|\nVulkan\n|\nCUDA\n|\nTest\n|\nÉcart de performance\n|\n|---|---|---|---|\n|\n7976.25\n|\n8704.94\n|\npp2048\n|\n9.14%\n|\n|\n8204.88\n|\n8980.29\n|\npp8192\n|\n9.45%\n|\n|\n8091.26\n|\n8723.56\n|\npp16384\n|\n7.81%\n|\n|\n7642.83\n|\n8274.81\n|\npp32768\n|\n8.27%\n|\n|\n6755.33\n|\n7294.51\n|\npp65536\n|\n7.98%\n|\n|\n5404.22\n|\n5514.09\n|\npp131072\n|\n2.03%\n|\n|\n217.36\n|\n217.7\n|\ntg128\n|\n0.16%\n|\n\nPas de CUDA signifie pas de périphérique uvm à monter dans le conteneur.\n\nAvec un écart de performance aussi faible, surtout pour les longs contextes, **Vulkan apparaît comme un très bon compromis de performances au profit de la sécurité**.\n\n#### Et nvidiactl dans tout ça ?\n\nLes codes des drivers `nvidiaX`\n\net `nvidiactl`\n\nsont fortement liés. Ils utilisent par exemple le même dispatcher d'appels `ioctl()`\n\n(réf. [nv.c](https://github.com/NVIDIA/open-gpu-kernel-modules/blob/main/kernel-open/nvidia/nv.c#L2595)).\n\nMais `nvidiactl`\n\nest bien moins dangereux que `nvidia-uvm`\n\ncar aucune opération de calcul, ni aucun accès à la mémoire GPU ne peut être effectué via ce périphérique. Il sert aux objectifs suivants :\n\n- Découvrir les GPUs disponibles, obtenir les métadonnées (version, capacités, ...).\n- Demander la création de contexte sur un GPU spécifique.\n\nMême si un processus malveillant est en mesure d'interagir avec `/dev/nvidiactl`\n\n, il ne pourra pas contourner la vérification des permissions sur le périphérique `/dev/nvidiaX`\n\n.\n\n### AppArmor\n\nAppArmor is a kernel enhancement to confine programs to a limited set of resources.\n\nman apparmor(7)\n\nPour être ceinture et bretelles, AppArmor fournit une couche supplémentaire de défense en profondeur avec plusieurs niveaux d'isolation, et offre de l'auditabilité dans la mesure où toute tentative d'accès refusée sera journalisée.\n\nVoici un exemple d'abstraction pour confiner l'accès du conteneur aux seules ressources nécessaires :\n\n```\n# /etc/apparmor.d/abstractions/llama-cpp\n\n  # Refuser les capacités dangereuses\n  deny capability sys_admin,\n  deny capability sys_module,\n  deny capability sys_rawio,\n  deny capability sys_ptrace,\n  deny capability mac_admin,\n  deny capability mac_override,\n\n  # Accès aux bibliothèques partagées\n  /lib/x86_64-linux-gnu/** mr,\n  /usr/lib/x86_64-linux-gnu/** mr,\n  /lib64/** mr,\n\n  # Runtime et drivers Vulkan\n  /usr/lib/x86_64-linux-gnu/libvulkan.so* mr,\n  /usr/lib/x86_64-linux-gnu/vulkan/** mr,\n  /usr/share/vulkan/** r,\n\n  # Driver Vulkan NVIDIA\n  /usr/lib/x86_64-linux-gnu/libnvidia-*.so* mr,\n  /usr/lib/x86_64-linux-gnu/nvidia/** mr,\n  /usr/share/nvidia/** r,\n  /usr/share/glvnd/** r,\n\n  # Périphériques de contrôle partagés\n  /dev/nvidiactl rw,\n  /dev/nvidia0 rw,\n\n  # Adapter au bon numéro de périphérique PCI\n  /sys/devices/pci0000:00 rw,\n\n  # Home utilisateur\n  owner /var/lib/containers/container01 r,\n  owner /var/lib/containers/container01/** rw,\n\n  # Accès en lecture seule au modèle partagé\n  /var/lib/models r,\n  /var/lib/models/** r,\n```\n\nDernière étape : récapitulatif\n\n```\n#########################################\n# Désactiver le chargement de modules   #\n# pendant l'installation                #\n#########################################\n% echo 1 > /proc/sys/kernel/modules_disabled\n\n###################################################\n# Désactiver le provisionnement automatique de    #\n# subuid pour les nouveaux utilisateurs           #\n###################################################\n% gawk -i inplace '/^SUB_(UID|GID)_COUNT/{$2=0}1' /etc/login.defs\n\n###################################\n# Installer l'outillage conteneur #\n###################################\n% apt install --no-install-recommends podman runc uidmap\n% install -m 0644 /dev/stdin /etc/modules-load.d/overlay.conf << EOF\noverlay\nEOF\n\n####################################################\n# Ajouter l'utilisateur non privilégié container01 #\n####################################################\n% useradd \\\n    --uid 10000 \\\n    --create-home \\\n    --home-dir /var/lib/containers/container01 \\\n    --shell /usr/sbin/nologin \\\n    --password '!' \\\n    container01\n\n% usermod \\\n  --add-subuids 100000-101000 \\\n  --add-subgids 100000-101000 \\\n  container01\n\n% install -dm 770 -g www-data -o container01 /var/lib/containers/container01/socks\n% setfacl -m u:101000:rwx /var/lib/containers/container01/socks\n\n#####################################\n# Installer les drivers nvidia      #\n# et les dépendances vulkan         #\n#####################################\n% export version=\"1.1-1\"\n% export repository=\"https://developer.download.nvidia.com/compute/cuda/repos/debian13/x86_64\"\n% wget -O - \"${repository}/cuda-keyring_${version}_all.deb\" | dpkg -x /dev/stdin .\n% install -m 0644 ./usr/share/keyrings/cuda-archive-keyring.gpg /usr/share/keyrings/\n% install -m 0644 ./etc/apt/sources.list.d/cuda-debian13-x86_64.list /etc/apt/sources.list.d/\n% apt update\n% apt install linux-headers-$(uname -r)\n% apt install nvidia-kernel-open-dkms nvidia-driver-libs\n% install -m 0644 /dev/stdin /etc/modules-load.d/nvidia.conf << EOF\nnvidia\nEOF\n\n#############################################\n# Ajuster les permissions des périphériques #\n#############################################\n% install -m 0644 /dev/stdin /etc/modprobe.d/nvidia-devices.conf << EOF\noptions nvidia NVreg_DeviceFileUID=0\noptions nvidia NVreg_DeviceFileGID=0\noptions nvidia NVreg_DeviceFileMode=0660\nEOF\n\n% install -m 0644 /dev/stdin /etc/modprobe.d/nvidia-blacklist.conf << EOF\nblacklist nvidia_uvm\nblacklist nvidia_drm\nblacklist nvidia_modeset\nblacklist nvidia_peermem\nEOF\n\n% install -m 0644 /dev/stdin /etc/systemd/system/nvidia-devices@.path << EOF\n[Unit]\nDescription=Watch for /dev/%I device creation\n\n[Path]\nPathExists=/dev/%I\n\n[Install]\nWantedBy=multi-user.target\nEOF\n\n% install -m 0644 /dev/stdin /etc/systemd/system/nvidia-devices@.service << EOF\n[Unit]\nDescription=Set /dev/%I permissions\n\n[Service]\nType=oneshot\nEnvironmentFile=-/etc/nvidia/%I.conf\nExecStart=/bin/chmod 660 /dev/%I\nExecStart=/usr/bin/sh -c \\\n  'for uid in \\${OWNER_UIDS}; do /usr/bin/setfacl -m u:\\$uid:rw /dev/%I; done'\nEOF\n\n% install -dm 755 /etc/nvidia\n% echo 'OWNER_UIDS=\"101000\"' > /etc/nvidia/nvidia-uvm.conf\n% echo 'OWNER_UIDS=\"101000\"' > /etc/nvidia/nvidiactl.conf\n% echo 'OWNER_UIDS=\"101000\"' > /etc/nvidia/nvidia0.conf\n% systemctl enable --now 'nvidia-devices@nvidia\\x2duvm.path'\n% systemctl enable --now 'nvidia-devices@nvidiactl.path'\n% systemctl enable --now 'nvidia-devices@nvidia0.path'\n\n% update-initramfs -u\n% systemctl reboot\n\n##################################\n# Installer le toolkit conteneur #\n##################################\n% apt download nvidia-driver-cuda\n% dpkg -x nvidia-driver-cuda_*_amd64.deb .\n% install -m 0755 ./usr/bin/nvidia-smi /usr/bin\n\n% curl https://nvidia.github.io/libnvidia-container/gpgkey | gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg\n\n% install -m 0644 /dev/stdin /etc/apt/sources.list.d/nvidia-container-toolkit.list << EOF\ndeb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://nvidia.github.io/libnvidia-container/stable/deb/\\$(ARCH) /\nEOF\n\n% apt update\n% apt install nvidia-container-toolkit\n\n#################################\n# Configurer le runtime  podman #\n#################################\n% export repository=\"https://huggingface.co/ggml-org/gpt-oss-120b-GGUF/resolve/main\"\n% install -dm 0755 /var/lib/models\n% for i in {1..3}; do wget -P /var/lib/models \\\n  \"${repository}/gpt-oss-120b-mxfp4-0000${i}-of-00003.gguf\"; done\n\n% install -dm 755 /etc/containers/systemd/users/10000\n% install -m 0644 /dev/stdin /etc/containers/systemd/users/10000/llama-cpp.container << EOF\n[Unit]\nDescription=llama.cpp Vulkan server\n\n[Container]\nImage=ghcr.io/ggml-org/llama.cpp:server-vulkan\nAutoUpdate=disabled\nAddDevice=nvidia.com/gpu=0\nNetwork=none\n\nVolume=/var/lib/models:/models:ro\nVolume=/var/lib/containers/container01/socks:/run/llama-cpp\n\n; Commande de healthcheck simple\nHealthStartPeriod=1m\nHealthCmd=/usr/bin/test -S /run/llama-cpp/llama-cpp.sock\n\nUser=1000\nGroup=1000\nUserNS=keep-id\n\nNoNewPrivileges=true\n\n; Forcer le mode de création du socket\nPodmanArgs=--umask=007\n\nExec=\\\n  --host \"/run/llama-cpp/llama-cpp.sock\" \\\\\n  --model /models/gpt-oss-120b-mxfp4-00001-of-00003.gguf \\\\\n  --alias gpt-oss-120b \\\\\n  --ctx-size 504000 \\\\\n  --parallel 4 \\\\\n  --no-slots \\\\\n  --swa-full\n\n[Quadlet]\nDefaultDependencies=false\n\n[Service]\nRestart=on-failure\n\n[Install]\nWantedBy=default.target\nEOF\n\n% loginctl enable-linger container01\n```\n\n## Conclusion\n\nÀ nouveau, cette première itération est volontairement sobre : pas de base de données vectorielle, pas de *fine-tuning*, pas d'agents, pas de connecteurs externes. Les prochaines étapes impliqueront bien certainement de la persistance de données, des *plugins* pour les outils internes, du *fine-tuning* de modèle, de l'indexation de documentation et de base de code. Chacune de ces étapes rouvrira d'intéressantes interrogations de sécurité, que l'on a soigneusement conservées hors périmètre jusqu'à présent.\n\nIl convient toutefois de préciser que cette solution n'est pas 100% sans danger face à des attaques sophistiquées — quand bien même ce serait une réalité. Les conteneurs partagent le noyau de l'hôte, `nvidiactl`\n\nreste un périphérique partagé entre les GPUs, et certaines parties du code, par exemple Vulkan dont les sources ne sont pas consultables, échappent à toute possibilité d'audit, nous contraignant à adopter une approche « de confiance », en dépit de certaines petites réserves évoquées précédemment.\n\nComme toujours, c’est dans l’écart entre « ça fonctionne » et « nous sommes satisfaits du fonctionnement » que réside le véritable enjeu. Finalement, avec tout le sérieux qu’implique cette démarche (et l'émotion qui l'accompagne), nous avons désormais un serveur LLM de confiance à qui divulguer — avec grande sérénité — nos données confidentielles et commencer à en tirer le profit tant attendu ! 🙂", "url": "https://wpnews.pro/news/grand-saut-dans-le-deploiement-sur-site-d-un-serveur-llm-a-moindres-privileges", "canonical_source": "https://www.synacktiv.com/publications/grand-saut-dans-le-deploiement-sur-site-dun-serveur-llm-a-moindres-privileges.html", "published_at": "2026-07-07 17:03:29.869613+00:00", "updated_at": "2026-07-07 17:03:31.733314+00:00", "lang": "en", "topics": ["large-language-models", "ai-infrastructure", "ai-safety"], "entities": ["Claude Code"], "alternates": {"html": "https://wpnews.pro/news/grand-saut-dans-le-deploiement-sur-site-d-un-serveur-llm-a-moindres-privileges", "markdown": "https://wpnews.pro/news/grand-saut-dans-le-deploiement-sur-site-d-un-serveur-llm-a-moindres-privileges.md", "text": "https://wpnews.pro/news/grand-saut-dans-le-deploiement-sur-site-d-un-serveur-llm-a-moindres-privileges.txt", "jsonld": "https://wpnews.pro/news/grand-saut-dans-le-deploiement-sur-site-d-un-serveur-llm-a-moindres-privileges.jsonld"}}