Aller au contenu principal

Installation durcie pour la production bancaire

Ce guide décrit comment le Manager installe les services en production bancaire. Décision d'architecture : ADR-0171.

Principes​

  • Les quatre méthodes restent : binary, podman, python et package (répartition au moment de la décision dans l'ADR-0171). Les services ne sont pas tous conteneurisés : chaque méthode est durcie à sa façon.
  • L'installation reste hors ligne : tout vient du miroir / Trusted Hub.
  • systemd reste le pilote unique : navin-<service>-<composant>.service, quelle que soit la méthode.
  • Les services existants du client (PostgreSQL, Redis, Keycloak) restent connectés, jamais réinstallés.

1. Réseau des conteneurs​

Chaque composant Podman déclare sa politique réseau dans le catalogue (run.podman.network) :

PolitiqueEffetCondition
isolated (défaut)Réseau bridge propre au service, navin-<cluster>-<service>, créé sur l'hôte (podman network create --ignore, ou fichier Quadlet .network) : deux services ne partagent jamais un réseau, un flux entre services passe toujours par un port déclaréAucune
hostRéseau de l'hôte (--network host)network_justification obligatoire : le besoin technique de ce composant, sinon le catalogue est refusé au chargement
noneAucun réseauAucune

Ports d'un composant isolé (ports du catalogue, port de l'hôte = port du conteneur) :

DéclarationPublicationQui l'atteint
public: true--publish p:p/proto, toutes les adresses de l'hôteles hôtes autorisés par le pare-feu (firewall.open)
public: false--publish 127.0.0.1:p:p/protol'agent et les processus de l'hôte, aucun autre hôte
publish: falsejamais publiéles seuls conteneurs du même service, par leur nom
list_param et fieldun port par entrée de la liste (frontaux HAProxy), selon publicselon public
modespublié dans ces modes seulement (ports ringpop du mode cluster de Temporal)selon public

Règles vérifiées au chargement du catalogue :

  • --network, --net, --publish, -p, --expose sont interdits dans extra : le réseau passe uniquement par network et ports ;
  • les ports publiés restent filtrés par le pare-feu : seuls les ports public: true sont ouverts (firewall.open).

Les modèles du catalogue écrivent leurs adresses avec net du contexte de rendu, qui ne change rien pour les méthodes binary, python et package ni pour le réseau de l'hôte :

AideRéseau isoléAutres cas
net.listen(adresse)adresse non spécifiée (toutes les interfaces du conteneur)adresse
net.local(adresse)adresse non spécifiée, port publié sur 127.0.0.1 seulementadresse (127.0.0.1 par défaut)
net.peer(composant, adresse)nom du conteneur navin-<service>-<composant> sur le réseau du serviceadresse (127.0.0.1 par défaut)
net.check(adresse)127.0.0.1 (contrôle par l'agent d'un port non public)adresse

L'adresse annoncée aux pairs et aux clients reste celle de l'hôte (host.ip, host.fqdn) : les autres hôtes atteignent le port publié.

Politique réseau par service​

PolitiqueServicesRaison
isolatedTous les autres composants Podman du catalogue : bases et stockages (postgresql, mysql, redis, etcd, clickhouse, opensearch, qdrant, seaweedfs, nats), passerelles (apisix, haproxy avec un port publié par frontal), observabilité (jaeger, otel-collector), keycloak, temporal (tous les rôles, dans les deux modes), presidio, opik, navin-enterprise, ollama, clamav, docling, litellm, marquez, opa, openfga, opensearch-dashboards, promptfoo, temporal-ui, label-studio (application et interface), llama-factory (GPU par CDI) et vllm hors du mode distributedÉcoute sur l'adresse non spécifiée du conteneur, adresse de l'hôte annoncée aux pairs, composants du même hôte joints par leur nom de conteneur
host (sonde noyau)falco, tetragoneBPF, --privileged, --pid=host : profil spécifique justifié
host (besoin démontré)livekit (médias WebRTC, recommandation officielle de LiveKit), vllm en mode distributed seulement (host_network_modes : Ray et NCCL sur des ports dynamiques)Justification propre au composant, vérifiée au chargement du catalogue

Migrer un service vers isolated : écrire ses adresses d'écoute avec net.listen ou net.local, ses accès à un autre composant du même hôte avec net.peer (et déclarer same_host_as sur le composant : l'installation refuse alors un placement où l'autre composant manque sur l'hôte), ses contrôles de ports non publics avec net.check, annoncer host.ip ou host.fqdn aux pairs, déclarer tous les ports utilisés, puis passer network: isolated et retirer la justification. Le test tests/test_catalog_podman_policy.py vérifie qu'aucun composant n'utilise le réseau hôte sans justification.

L'onglet Sécurité de la fiche du service montre, pour un service installé avec Podman, l'isolation de chaque conteneur telle que le catalogue la déclare : politique réseau et justification, réseau du service, ports publiés et leur portée (sur l'hôte ou sur 127.0.0.1 seulement), privilèges (aucun nouveau privilège, capacités retirées, système de fichiers en lecture seule, capacités ajoutées) et quotas. Cette vue est en lecture seule : l'isolation se change dans le catalogue, où chaque affaiblissement porte sa justification écrite.

Flux entre hôtes​

Un port public que seul le cluster utilise est ouvert aux hôtes du cluster et au Manager, jamais à toutes les sources (« communications internes autorisées explicitement »). Le Manager tire la liste de la matrice des flux (network/flows.py, page Réseau) : un port est restreint quand chaque flux qui l'atteint sur son hôte vient d'un hôte, d'un composant ou du Manager du cluster (transport, pairs, réplication, gossip, liaisons entre services, Prometheus du cluster), ou quand aucun flux ne l'atteint encore et que la matrice le classe entre les nœuds du service (service à un seul nœud). Un port atteint par clients ou par une source externe (points d'accès de provides, https, media, bundle, ports d'un lien rapide, UDP) reste ouvert à toutes les sources.

Pare-feuPort restreintPort client
firewalldipset permanent navin-<12 hex de l'id du cluster> (hash:ip, famille inet, plus <set>-v6 en inet6 pour les membres IPv6) et une règle riche source ipset=<set> port port=<p> protocol=<proto> accept par port--add-port=<p>/<proto>
ufwallow from <membre> to any port <p> proto <proto> comment <set> par membre et par portallow <p>/<proto>

Les membres sont les adresses des hôtes du cluster qui ne sont pas retirés et l'adresse du Manager quand NM_AGENT_URL ou NM_PUBLIC_URL contient une IP (un Manager connu par son seul nom n'atteint aucun port restreint, ce dont il n'a pas besoin). Chaque firewall.open porte tous les ports restreints de l'hôte : l'agent retire la règle --add-port ou ufw allow <p>/<proto> d'un port restreint, les règles de l'ensemble qui ne correspondent plus à un port ou à un membre, et n'ajoute rien quand l'hôte est déjà conforme (deux exécutions ne changent rien). La tâche périodique network.firewall_members (toutes les 60 s) lance une opération network.firewall_members quand un hôte arrive, part ou change de cluster, pour que chaque hôte du cluster reçoive le nouvel ensemble ; un rafraîchissement en échec est retenté après 15 minutes, un cluster sous verrou de sécurité n'est pas touché. Les agents sans root et les hôtes sans firewalld ni ufw gardent leur comportement (aucune tâche, ou backend: none). Les groupes de sécurité du cloud ne changent pas : les règles de la page Réseau les couvrent.

2. Quadlet​

catalog.render.quadlet(service, composant, ctx) produit, à partir de la même spécification que podman run :

  • navin-<service>-<composant>.container : Image= par digest, Pull=never, Network=, PublishPort=, Volume=, EnvironmentFile=, NoNewPrivileges=, DropCapability=, ReadOnly=, AddDevice= (CDI), SecurityLabelDisable= (GPU), PidsLimit=, quotas, LogDriver=journald ;
  • navin-<cluster>-<service>.network pour les composants isolés (un réseau par service).

Une option rendue vide (option propre au CPU sur un hôte GPU) est retirée : une clé Quadlet vide remettrait toute sa liste à zéro (PodmanArgs= vide effacerait les options de sécurité et les quotas déjà posés). Le point d'entrée passe par PodmanArgs=--entrypoint= : Quadlet n'a pas de clé Entrypoint= avant Podman 5. Le fichier ne pose pas Notify=true : la disponibilité vient de conmon, comme --sdnotify conmon de l'unité podman run ; Notify=true attendrait un signal sd_notify que les images n'envoient pas.

Les fichiers vont dans /etc/containers/systemd/ ; le générateur Quadlet produit le même nom d'unité navin-<service>-<composant>.service : supervision, logs et service.control ne changent pas.

Bascule​

Les composants Podman passent sur Quadlet par défaut, hôte par hôte (NM_PODMAN_QUADLET, activé par défaut ; false garde podman run partout) :

  • un hôte systemd dont Podman est en version 4.9 ou plus récente et dont l'agent annonce la tâche quadlet.unit dans ses faits (task_types, agents Python et Go) reçoit quadlet.unit à la place de systemd.unit ; elle écrit le .container et le .network du service, désactive puis retire l'unité podman run qu'elle remplace (une unité de /etc/systemd/system masquerait l'unité générée) et recharge systemd ;
  • les autres hôtes (Podman plus ancien ou pas encore annoncé, agent qui ne connaît pas la tâche) gardent l'unité podman run, avec les mêmes options d'isolation : le Manager ne planifie jamais une tâche que l'agent refuserait ;
  • une unité générée ne s'active pas avec systemctl enable : le Manager la démarre et la section [Install] du fichier la démarre au boot ;
  • les fichiers Quadlet sont suivis par la détection de dérive comme les unités systemd ; la désinstallation retire le .container et l'unité, le .network du service reste.

Un hôte déjà installé passe sur Quadlet à la prochaine application de configuration du service, une fois son agent à jour ; le conteneur tourne avec sa nouvelle unité à son prochain redémarrage.

3. Profil de sécurité par service​

Champ run.podmanOption PodmanPar défaut
(toujours, hors privilégié)--security-opt no-new-privilegesactif
cap_drop_all: true--cap-drop ALL (puis cap_add)inactif, à activer après validation de l'image
cap_add--cap-addexige privileged_justification
read_only: true--read-only (tmpfs /tmp, /var/tmp, /run)inactif
security_opt--security-opt (seccomp, AppArmor) ; label=disable exige une justificationvide
user--userimage
devices--device nvidia.com/gpu=<index> (CDI)aucun
extra: [--privileged, --pid=host, ...]namespaces de l'hôteexige privileged_justification

Contrôles de santé : chaque composant permanent (kind: master ou worker) déclare ses health ; le chargeur du catalogue refuse un composant permanent sans contrôle de santé, quelle que soit sa méthode. Le Manager les exécute après chaque démarrage, à chaque mise à niveau (hôte canari puis chaque hôte) et en continu pour la supervision.

Quotas (limits du composant), appliqués à toutes les méthodes :

ChampPodmansystemd (binary, python)
cpus--cpusCPUQuota=
memory--memoryMemoryMax=
pids--pids-limitTasksMax=

Les secrets restent hors de la ligne de commande : fichier d'environnement 0640 rendu par le Manager (valeurs issues d'OpenBao) ; avec Quadlet, Secret= remplacera ce fichier.

Serveurs MCP des tenants​

Les serveurs MCP stdio des projets des tenants tournent sur les hôtes dans le service mcp-host (ADR-0171 ; sur Kubernetes, le MCP Runtime les isole par utilisateur et par projet). Son paramètre isolation décide comment :

isolationChaque serveur tourne commeServeurs hébergés
process (par défaut)un processus enfant de l'utilisateur de l'hôte (dossiers 0700, environnement propre, rlimits)serveurs validés du catalogue de l'entreprise seulement
containerun conteneur Podman sans root : un réseau par projet de tenant, racine en lecture seule, /tmp en tmpfs, --cap-drop=ALL, no-new-privileges, --userns=keep-id, quotas de mémoire et de processus, --pull=nevertous les serveurs, non fiables compris

Un serveur est fiable quand son préréglage figure dans le réglage du Manager mcp_validated_presets (NM_MCP_VALIDATED_PRESETS, les préréglages que l'entreprise a publiés dans son catalogue MCP) avec sa commande et ses arguments inchangés. Tout autre serveur (autre préréglage de la bibliothèque Navin, serveur écrit par un utilisateur) est refusé sur un mcp-host process avec 409 mcp_untrusted_needs_container, et l'environnement d'exécution le refuse aussi. L'isolation container demande podman et des plages d'uid et de gid subordonnés pour l'utilisateur du service (contrôles préalables podman_present et subordinate_ids ; l'agent ajoute les plages avec usermod lors de user.ensure). Les images d'exécution node et python sont fixées par empreinte dans le catalogue, livrées par le miroir hors ligne et chargées par l'agent dans le magasin d'images en lecture seule /opt/navin/mcp-host/images, listé par le containers-storage.conf de l'utilisateur du service.

4. Trusted Hub​

Avant installation, chaque artefact (binaire, image OCI, wheel, paquet) doit être : présent dans le manifest signé du Hub, vérifié par sha256, signé (Cosign, vérifiable hors ligne), accompagné d'un SBOM, scanné (CVE), conforme à la politique de licences (ADR-0101) et approuvé pour cette version. Le Hub est développé séparément ; le Manager refusera un artefact qui ne remplit pas ces conditions.

Avec le paramètre hub.enforce activé, le Manager vérifie chaque artefact avec le dernier document signé vérifié (Ed25519, clés épinglées, expiration, pas de retour arrière) avant qu'aucune tâche ne s'exécute ; un refus arrête le plan avec 409 :

NatureValeur vérifiéeRefus
Image (image.pull, image.load)empreinte du manifeste de l'entrée du miroirhub_package_not_approved
Binaire, archive (artifact.fetch)sha256:<hex> du fichierhub_artifact_not_approved
Wheel (python.venv)sha256:<hex> de chaque fichier wheel du dépôthub_artifact_not_approved

Une empreinte est approuvée quand elle est le digest d'une cible non révoquée ou le digest d'un de ses files, et qu'aucune révocation ne la cite. Ce que le Manager attend du document du Hub pour les natures autres que les images (contrat pour le Hub) : une cible {project, name, version, kind: bundle, ref, digest, service, image, files: [{name, digest: "sha256:&lt;hex&gt;"}]} dont files liste le sha256 de chaque binaire, archive et fichier wheel du bundle (le digest d'un bundle OCI est l'empreinte de son manifeste, pas celle du fichier) ; une révocation peut reprendre files pour que les fichiers révoqués restent refusés. Une empreinte de fichier mal formée rend tout le document invalide (hub_document_invalid). Le profil de cluster banking-secure est bloqué (règle hub_enforced) tant que hub.enforce est désactivé ou qu'aucun document valide n'est enregistré.

5. Mise à niveau et rollback​

MéthodeDurcissement
binaryRépertoire immuable par version, lien current basculé ; rollback = rebascule ; signature Cosign du fichier
pythonWheels du Hub listés avec leur sha256 dans le manifeste du miroir (wheels), versions figées, pip install --no-index --require-hashes --only-binary=:all: -r navin-requirements.txt, un venv par version {opt}/venvs/<version> ; {opt}/venv est un lien basculé par link.ensure, les chemins {opt}/venv/bin/... du catalogue restent valides et le rollback rebascule le lien. La première bascule sur un vrai répertoire {opt}/venv le renomme {opt}/venv.nm-legacy (jamais supprimé)
packageDépôts signés du Hub, versions figées dans le catalogue avec nom==version (dnf install nom-version, apt-get install nom=version) ; les vérifications de signature des dépôts du système ne sont jamais désactivées
podmanImage signée par digest, Quadlet, réseau isolé

Cycle de vie d'une mise à niveau​

Chaque étape du cycle cible est portée par le Manager et prouvée par un test :

ÉtapeCe que fait le ManagerCode et preuve
1. Prérequis et droitsPermission services:install, verrou de sécurité, modes et profil du cluster, approbation sous banking-secure, contrôles préalables sur chaque hôtenavin-manager/server/src/navin_manager/services/planner.py, navin-manager/server/src/navin_manager/services/profiles.py ; navin-manager/server/tests/test_lifecycle_profiles.py
2. Artefact signéBinaire, archive, wheel ou image : une cible approuvée et non révoquée du document signé du Trusted Hub quand hub.enforce est actifnavin-manager/server/src/navin_manager/hub.py, navin-manager/server/src/navin_manager/mirror.py ; navin-manager/server/tests/test_hub_client.py
3. DépendancesLiaisons exigées par la version cible liées, fournisseur installé et disponible, connexion encore présente ; refus avant toute tâchenavin-manager/server/src/navin_manager/services/upgrades.py ; test test_upgrade_checks_the_dependencies_of_the_target_release
4. Sauvegarde de la configuration et de l'étatSauvegarde d'un service à état avant la mise à niveau (upgrade.backup_before) ; version précédente (artefacts, fichiers, unités, contrôles, version de configuration) scellée avant toute tâchenavin-manager/server/src/navin_manager/services/backups.py ; test test_stateful_upgrade_backup_migration_refused_rollback_and_restore
5. Installation de la version demandéeHôte canari d'abord, puis un hôte à la fois : récupération, arrêt, migrations, fichiers et unités, démarrage ; un venv par version pour Pythontests test_stateless_upgrade_with_canary_then_rollback, test_python_upgrade_installs_a_venv_per_release_and_rollback_switches_the_link_back
6. Démarrage et santéContrôles de santé de chaque instance après son démarrage, obligatoires pour tout composant permanentnavin-manager/server/src/navin_manager/catalog/loader.py ; test test_a_permanent_component_without_health_check_is_refused
7. Validation applicativeTest de fumée sur l'hôte canari, contrôle du service, test de fumée finalnavin-manager/server/src/navin_manager/services/checks.py
8. Confirmation ou retour arrièreVersion enregistrée quand tout passe ; en cas d'échec avec auto_rollback, le Manager rejoue aussitôt la version précédente (acteur système, audité) ; après une migration irréversible, le retour passe par la restauration de la sauvegardetest test_failed_upgrade_with_auto_rollback_brings_the_previous_release_back

Les tests des étapes 3 à 8 sont dans navin-manager/server/tests/test_lifecycle_upgrades.py (étape 6 : navin-manager/server/tests/test_catalog_podman_policy.py). Quand une mise à niveau migre un schéma de base, le rollback restaure la sauvegarde avant de revenir au binaire précédent.

6. Actions privilégiées​

Tâches signées par le Manager et autorisées par rôle, journal d'audit de chaque installation, compte de service à privilèges minimaux sur chaque hôte. La restriction aux unités navin-* est une garde, pas une barrière : la liste blanche des tâches, le path guard de l'agent et la signature des tâches restent indispensables.

Haute disponibilité​

systemd et Podman gèrent un hôte. Quorums, réplicas et mises à jour progressives entre hôtes restent orchestrés par le Manager (disponibilité).