Déployer sur Kubernetes
Vue d'ensemble
Vaks PM est livré sous la forme d'un petit ensemble d'images de conteneur et est conçu pour fonctionner entièrement sur une infrastructure contrôlée par l'organisation, sans dépendance obligatoire à un service en ligne externe. Les seules connexions sortantes optionnelles sont un relais de messagerie (pour les notifications) et un fournisseur d'identité d'entreprise (pour l'authentification unique).
Ce guide couvre le parcours d'installation Kubernetes, destiné aux organisations qui exploitent déjà un cluster Kubernetes. L'autre parcours pris en charge, un installateur guidé qui provisionne un cluster Docker Swarm de zéro via SSH, est documenté séparément dans Déployer sur Docker Swarm. Les deux parcours exécutent la même application à partir des mêmes images ; le choix est dicté par l'outillage opérationnel existant, pas par des différences de fonctionnalités.
L'installation utilise Helm, le gestionnaire de paquets de Kubernetes. Un chart (paquet Helm) prêt à l'emploi est fourni dans le dépôt à deploy/helm/vaks-pm. Le chart déploie tous les services applicatifs sans état et, en option, les trois dépendances de support (base de données, cache, stockage objet) soit à l'intérieur du cluster (pour l'évaluation), soit en pointant vers des services externes existants (la recommandation de production).
Glossaire
Les acronymes et termes utilisés tout au long de ce guide, définis ici une fois pour référence.
| Terme | Signification |
|---|---|
| K8s | Kubernetes : une plateforme d'orchestration de conteneurs qui planifie, met à l'échelle et supervise des charges de travail conteneurisées. |
| Helm | Gestionnaire de paquets de Kubernetes. Une installation empaquetée est un chart ; ses réglages ajustables vivent dans un fichier de valeurs. |
| Chart | Un ensemble de modèles de ressources Kubernetes paramétrés par un fichier de valeurs. Le chart Vaks PM se trouve à deploy/helm/vaks-pm. |
| Fichier de valeurs | Un fichier YAML (YAML Ain't Markup Language) fournissant les paramètres du chart, passé avec -f au moment de l'installation. |
| Ingress | L'objet Kubernetes qui expose les services HTTP/HTTPS vers l'extérieur et route par nom d'hôte et par chemin. Un contrôleur Ingress doit être installé pour qu'il prenne effet. |
| PVC | PersistentVolumeClaim (demande de volume persistant) : une demande de disque durable dans Kubernetes, afin que les données d'un conteneur survivent aux redémarrages. |
| StorageClass | Un profil de stockage nommé que le cluster utilise pour satisfaire automatiquement un PVC. Une StorageClass par défaut est requise si les dépendances embarquées sont utilisées. |
| Secret | Un objet Kubernetes contenant des valeurs sensibles (clés, mots de passe, chaînes de connexion). Encodé en base64 dans le magasin de données du cluster, non chiffré sauf si le chiffrement du magasin de données est activé (voir Au repos). |
| CNI | Container Network Interface (interface réseau de conteneurs) : le plugin réseau du cluster (par exemple Cilium, Calico ou Flannel). Certains CNI peuvent chiffrer le trafic de pod à pod. |
| mTLS | Mutual Transport Layer Security (TLS mutuel) : un TLS où les deux extrémités présentent et vérifient un certificat ; utilisé par les maillages de services pour chiffrer le trafic de pod à pod. |
| TLS | Transport Layer Security : la couche de chiffrement derrière HTTPS. |
| KMS | Key Management Service (service de gestion de clés) : un magasin de clés géré (par exemple AWS KMS) qui réalise les opérations de clé sans exposer le matériel cryptographique. |
| HSM | Hardware Security Module (module matériel de sécurité) : un appareil inviolable qui détient les clés et réalise les opérations cryptographiques sur site. |
| KEK | Key Encryption Key (clé de chiffrement de clés) : une clé maîtresse qui enveloppe (chiffre) les clés de données. Elle peut être tournée sans rechiffrer aucune donnée. |
| DEK | Data Encryption Key (clé de chiffrement de données) : une clé par tenant qui chiffre les données ; elle est elle-même enveloppée par la KEK. |
| RPO | Recovery Point Objective (objectif de point de récupération) : la quantité maximale acceptable de perte de données, mesurée en temps, après une défaillance. Détermine la fréquence des sauvegardes. |
| JWT | JSON Web Token (jeton web JSON) : un jeton de session signé émis après la connexion. |
| SMTP | Simple Mail Transfer Protocol (protocole simple de transfert de courrier) : le protocole d'envoi des courriels ; utilisé pour les notifications. |
| Tenant | Une organisation isolée servie par l'instance. Chaque tenant dispose de sa propre base de données. |
Architecture déployée
Quelles que soient les options choisies, le système en exécution est le même ensemble de services. L'application est sans état là où elle peut l'être (les services api, web et worker ne détiennent aucune donnée locale et passent à l'échelle horizontalement) ; tout l'état durable vit dans trois magasins de support : PostgreSQL, Redis et le magasin d'objets S3.
| Composant | Rôle | Déployé |
|---|---|---|
api | Back end NestJS : l'API REST (préfixe /api/v1) plus le canal WebSocket temps réel. | Toujours |
web | Sert le front end React mono-page (ressources statiques, via nginx). | Toujours |
worker | Exécuteur de tâches d'arrière-plan (envoi de courriels, livraison de webhooks, transfert d'audit, crons planifiés), adossé à la file Redis. | Toujours |
controlplane | Porte d'authentification partagée qui émet un jeton estampillé du tenant. Même image que api, avec un drapeau de rôle différent. | tenancy pooled uniquement |
| PostgreSQL 17 | Base de données relationnelle principale. Une base par tenant. | Optionnel (embarqué ou externe) |
| Redis 7 | Cache, pub/sub et la file de tâches d'arrière-plan. | Optionnel (embarqué ou externe) |
| MinIO (S3) | Magasin d'objets compatible S3 pour les pièces jointes, le branding, les modèles et les archives d'audit. | Optionnel (embarqué ou externe) |
Une tâche de migration de schéma (un hook Helm nommé vaks-pm-dbpush) s'exécute automatiquement à chaque installation et mise à niveau, avant le démarrage des services, pour synchroniser la structure de la base de données. Aucune étape de migration manuelle n'est requise.
Routage réseau (Ingress)
Le chart génère un unique Ingress qui répartit le trafic d'un seul nom d'hôte par chemin de requête :
| Chemin | Service cible |
|---|---|
/api/v1/auth | controlplane pooled ; sinon api |
/socket.io | api (canal WebSocket temps réel) |
/api | api |
/ | web |
Modes de tenancy
Le chart prend en charge deux modèles de tenancy, sélectionnés par tenancy.mode. Ceci est indépendant de la décision mono-vs-HA de la section suivante.
| Mode | Signification |
|---|---|
| dedicated (par défaut) | Une organisation par instance, un nom de domaine. L'authentification est gérée par le service api ; il n'y a pas de plan de contrôle séparé. C'est le modèle d'une organisation qui déploie l'outil pour son propre usage. À choisir en cas de doute. |
| pooled | Plusieurs organisations (« tenants ») partagent une instance. L'organisation est résolue à partir du sous-domaine de la requête (<slug>.<baseDomain>) et un service controlplane séparé émet les jetons d'authentification. Nécessite un Ingress à domaine générique et le paramètre tenancy.baseDomain. Pertinent uniquement pour un fournisseur revendant l'application comme un service. |
Prérequis et état initial
Les listes ci-dessous décrivent le point de départ attendu avant l'exécution de toute commande d'installation. L'état initial attendu est un cluster Kubernetes opérationnel sur lequel l'administrateur dispose des pleins droits sur au moins un namespace.
Cluster et poste de travail
| Élément | Exigence |
|---|---|
| Cluster Kubernetes | Version 1.24 ou plus récente, en état de fonctionnement. |
| Droits sur le namespace | Accès administratif à au moins un namespace (les exemples utilisent vaks-pm). |
| Contrôleur Ingress | Un contrôleur opérationnel déjà installé (par exemple ingress-nginx ou Traefik). Le nom de sa classe est nécessaire au moment de l'installation. |
| StorageClass par défaut | Requise si les PostgreSQL / Redis / MinIO embarqués sont utilisés, car ils demandent des PVC. Inutile lorsque les trois dépendances sont externes. |
kubectl | Installé sur le poste de travail et configuré pour cibler le cluster (kubeconfig valide). |
helm | Helm 3.x installé sur le poste de travail. |
openssl | Disponible sur le poste de travail pour générer les secrets aléatoires. |
| Images applicatives | Les trois images vaks-pm/api, vaks-pm/web, vaks-pm/worker accessibles depuis un registre que le cluster peut tirer (avec des identifiants de pull si le registre est privé). |
Réseau et certificats
- Un nom d'hôte public pour l'instance (par exemple
pm.example.com) et l'autorité pour créer un enregistrement DNS le pointant vers le point d'entrée du contrôleur Ingress. - Pour la production, un certificat TLS pour ce nom d'hôte, fourni comme un Secret Kubernetes existant ou émis automatiquement par un gestionnaire de certificats (par exemple cert-manager).
- Seul le port HTTPS standard (443) doit être joignable depuis les clients ; tout le reste reste à l'intérieur du cluster.
Optionnel, configurable après l'installation
- Un relais de messagerie sortant (SMTP) pour les notifications.
- Un fournisseur d'identité d'entreprise (par exemple Microsoft Entra ID) uniquement si l'authentification unique est requise.
- Pour la production : des services PostgreSQL, Redis et S3 existants. Pour l'évaluation seulement, le chart peut embarquer les trois dans le cluster.
secrets.existingSecret afin qu'aucun texte en clair n'atterrisse dans un fichier de valeurs ni dans l'historique des releases Helm.Mono vs HA · choisir la topologie
Le chart peut s'installer sous deux formes très différentes. Ce choix est indépendant du mode de tenancy et constitue la décision la plus lourde de conséquences pour un déploiement de production. Lire le tableau attentivement : une installation mono ne convient qu'à l'évaluation.
| Aspect | MONO / évaluation | HA / production |
|---|---|---|
| Objet | Évaluation, démonstrations, essais hors ligne. | Production pour environ 200 utilisateurs et plus. |
Réplicas applicatifs (api/web/worker/controlplane) | replicaCount: 1 | replicaCount: 2 à 3 chacun. |
| Répartition des pods | Aucune. | Anti-affinité de pods via la valeur affinity de chaque service, afin que les réplicas atterrissent sur des nœuds différents. |
| PostgreSQL | PostgreSQL embarqué mono-nœud (postgresql.enabled=true) acceptable. | PostgreSQL externe / géré OBLIGATOIRE (postgresql.enabled=false + externalDatabase.*). |
| Redis | Redis embarqué mono-nœud (redis.enabled=true) acceptable. | Redis externe / géré OBLIGATOIRE (redis.enabled=false + externalRedis.url). |
| Stockage objet S3 | MinIO embarqué mono-nœud (minio.enabled=true) acceptable. | S3 externe / géré OBLIGATOIRE (minio.enabled=false + s3.external.*). |
| Sondes de santé | Intégrées au chart (liveness + readiness sur api/web/worker). | Identiques ; déjà présentes, et sur lesquelles repose la mise à niveau progressive sans interruption. |
postgresql.enabled=false, redis.enabled=false et minio.enabled=false, et pointer le chart vers les services gérés.affinity (api.affinity, web.affinity, worker.affinity) passée directement à la spécification du pod. Y définir une podAntiAffinity afin que les deux ou trois réplicas d'un service soient planifiés sur des nœuds distincts ; sinon Kubernetes peut les co-localiser et la défaillance d'un seul nœud supprime tout le service.Dimensionnement
Les valeurs ci-dessous sont les requests (réservations) par défaut du chart, indicatives pour un déploiement de production servant environ 200 utilisateurs. À ajuster à la charge observée. L'unité m est le milli-cœur de processeur (1000 m = 1 vCPU) ; Mi et Gi sont des mébioctets et des gibioctets de mémoire.
| Service | Réplicas (défaut) | Requête CPU | Requête mémoire | Limite CPU | Limite mémoire |
|---|---|---|---|---|---|
api | 2 | 250 m | 512 Mi | 1 | 1 Gi |
web | 2 | 50 m | 64 Mi | 250 m | 256 Mi |
worker | 2 | 100 m | 256 Mi | 500 m | 512 Mi |
controlplane (pooled) | 2 | 250 m | 512 Mi | 1 | 1 Gi |
postgresql (si embarqué) | 1 | 250 m | 512 Mi | 1 | 1 Gi |
redis (si embarqué) | 1 | 50 m | 64 Mi | 250 m | 256 Mi |
minio (si embarqué) | 1 | 100 m | 256 Mi | 500 m | 512 Mi |
resources et replicaCount. Sur un environnement de test aux ressources contraintes, réduire les réplicas à 1 et abaisser les réservations. Pour environ 200 utilisateurs sous charge soutenue, augmenter les réplicas api et worker à 3 et relever leurs limites avant d'ajouter des nœuds.Images · miroir et identifiants de pull
Trois images applicatives sont requises : vaks-pm/api, vaks-pm/web et vaks-pm/worker. Les recopier dans un registre que le cluster peut atteindre, puis noter l'adresse hôte de ce registre (par exemple registry.example.com). Définir l'hôte comme image.registry ; le chart ajoute le dépôt (vaks-pm/api et ainsi de suite), donnant registry.example.com/vaks-pm/api. Ne pas répéter le segment vaks-pm dans image.registry.
Si le registre stocke les images sous des noms différents, surcharger image.api.repository, image.web.repository et image.worker.repository. Les trois images partagent un seul tag (image.tag, par défaut l'appVersion du chart).
Créer le namespace et, pour un registre privé, un Secret de pull à référencer ensuite via imagePullSecrets :
kubectl create namespace vaks-pm
# Private registry only:
kubectl -n vaks-pm create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=<user> \
--docker-password=<password>
Secrets
L'application requiert quatre secrets obligatoires. Les générer avec de fortes valeurs aléatoires :
openssl rand -hex 32 # jwtAccessSecret
openssl rand -hex 32 # jwtRefreshSecret
openssl rand -base64 32 # mfaEncryptionKey
openssl rand -hex 32 # workerApiToken
| Secret | Sert à |
|---|---|
jwtAccessSecret / jwtRefreshSecret | Signer les jetons de session émis à la connexion. Doivent être de longues valeurs aléatoires. |
mfaEncryptionKey | Chiffrer au repos les secrets à deux facteurs par utilisateur. Également la KEK par défaut au niveau champ (voir Secrets au niveau champ). |
workerApiToken | Le jeton que le worker utilise pour appeler l'API interne lors des callbacks. |
smtpPassword (optionnel) | S'authentifier auprès du relais de messagerie, uniquement lorsque les notifications utilisent l'authentification par mot de passe. |
ssoOidcClientSecret (optionnel) | Le secret client OIDC, uniquement lorsque l'authentification unique est configurée. |
syslogClientKey (optionnel) | Clé privée PEM pour le transfert syslog des journaux d'audit en mTLS ; montée dans le worker. |
secrets.existingSecret. Ce Secret doit contenir les clés DATABASE_URL, REDIS_URL, JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, MFA_ENCRYPTION_KEY, WORKER_API_TOKEN, S3_SECRET_KEY, et le cas échéant SMTP_PASSWORD et SSO_OIDC_CLIENT_SECRET. Cela tient le texte en clair hors du fichier de valeurs et de l'historique Helm.Fichiers de valeurs
La configuration vit dans un fichier de valeurs passé avec -f. Deux exemples complets suivent : le Scénario A pour la production (dépendances externes, réplicas HA, anti-affinité) et le Scénario B pour l'évaluation (dépendances embarquées, réplicas uniques).
Scénario A · Production (PostgreSQL / Redis / S3 externes, HA)
Le Kubernetes propre à l'organisation, une base de données et un cache gérés, un service S3 existant, et des réplicas répartis entre les nœuds par anti-affinité. Mode mono-organisation (dedicated).
image:
registry: registry.example.com # host only; chart appends vaks-pm/api|web|worker
tag: "1.0.0"
imagePullSecrets:
- name: regcred
publicBaseUrl: https://pm.example.com
tenancy:
mode: dedicated
# --- HA replicas ---
api:
replicaCount: 3
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels: { app.kubernetes.io/component: api }
web:
replicaCount: 2
worker:
replicaCount: 3
# --- Database: managed / external ---
postgresql:
enabled: false
externalDatabase:
url: "postgresql://vakspm:<password>@pg.internal.example.com:5432/vakspm?schema=public"
# --- Cache: managed / external ---
redis:
enabled: false
externalRedis:
url: "redis://redis.internal.example.com:6379"
# --- Object storage: existing S3 service (e.g. MinIO / Ceph / AWS) ---
minio:
enabled: false
s3:
external:
endpoint: "https://s3.internal.example.com:9000"
region: us-east-1
useSSL: true
forcePathStyle: true # true for MinIO/Ceph/Garage
accessKey: "<access-key>"
secretKey: "<secret-key>" # prefer secrets.existingSecret
bucketPrefix: ""
createBuckets: true
ingress:
enabled: true
className: nginx
host: pm.example.com
tls:
enabled: true
secretName: pm-example-tls
# Prefer secrets.existingSecret in production (see Secrets section).
secrets:
jwtAccessSecret: "<openssl rand -hex 32>"
jwtRefreshSecret: "<openssl rand -hex 32>"
mfaEncryptionKey: "<openssl rand -base64 32>"
workerApiToken: "<openssl rand -hex 32>"
Scénario B · Évaluation (tout embarqué, réplicas uniques)
Toutes les dépendances à l'intérieur du cluster, adapté à une mise en place rapide ou hors ligne. Réplicas réduits à 1.
image:
registry: registry.example.com # host only; chart appends vaks-pm/api|web|worker
tag: "1.0.0"
publicBaseUrl: https://pm.example.test
tenancy:
mode: dedicated
api: { replicaCount: 1 }
web: { replicaCount: 1 }
worker: { replicaCount: 1 }
postgresql: { enabled: true }
redis: { enabled: true }
minio: { enabled: true }
ingress:
enabled: true
className: nginx
host: pm.example.test
tls: { enabled: false }
# Creates the demo organization and administrator account.
migrations:
seedDemo: true
secrets:
jwtAccessSecret: "<openssl rand -hex 32>"
jwtRefreshSecret: "<openssl rand -hex 32>"
mfaEncryptionKey: "<openssl rand -base64 32>"
workerApiToken: "<openssl rand -hex 32>"
Stockage S3
Vaks PM stocke les fichiers binaires (pièces jointes de tâches, logos de branding, modèles de documents, chartes de projet et archives d'audit compressées) dans un magasin d'objets compatible S3. Toute implémentation conforme convient (Amazon S3, MinIO, Ceph RGW, Garage, NetApp StorageGRID, Wasabi…). Le chart traduit la section de stockage en variables d'environnement ; seuls les paramètres ci-dessous changent d'un fournisseur à l'autre.
Paramètre (s3.external.*) | Signification |
|---|---|
endpoint | Vide → Amazon S3 natif (l'adresse est dérivée de la région). Une URL complète ou un nom d'hôte pour MinIO, Ceph, Garage, etc. |
port / useSSL | Utilisés lorsque endpoint est un simple nom d'hôte. useSSL: true (par défaut) sélectionne HTTPS vers le magasin. |
forcePathStyle | Style d'adressage des buckets. Définir true pour MinIO / Ceph / Garage ; false pour Amazon S3. Choisi automatiquement si laissé à null. |
region | Région du service (par exemple eu-west-1). |
accessKey / secretKey | Identifiants d'accès. Préférer fournir la clé secrète via secrets.existingSecret (S3_SECRET_KEY). |
bucketPrefix | Préfixe appliqué à tous les buckets. Indispensable sur Amazon S3, dont l'espace de noms des buckets est global ; permet aussi à plusieurs instances de partager un magasin. |
createBuckets | Définir à false lorsque les buckets sont précréés et que le compte n'a pas le droit de les créer. |
L'application utilise cinq buckets (noms indiqués avant l'application du préfixe) : charter-blobs, document-templates, vaks-pm-attachments, vaks-pm-branding, vaks-pm-audit-archive. Lorsque createBuckets vaut false, créer ces cinq buckets (préfixés) au préalable et accorder au compte de service les droits de lecture, écriture, suppression et liste sur eux. Au démarrage, l'API journalise une ligne S3 backend: … résumant la configuration en vigueur.
Installation Helm
Avant d'installer, valider le rendu du chart par rapport au fichier de valeurs :
helm lint ./deploy/helm/vaks-pm -f values-production.yaml
helm template vaks-pm ./deploy/helm/vaks-pm -f values-production.yaml \
| kubectl apply --dry-run=client -f -
Puis installer. Le --timeout laisse à la tâche de migration de schéma le temps de se terminer :
helm install vaks-pm ./deploy/helm/vaks-pm \
-n vaks-pm --create-namespace \
-f values-production.yaml \
--timeout 5m
Attendre que les pods deviennent prêts :
kubectl -n vaks-pm get pods
kubectl -n vaks-pm rollout status deploy/vaks-pm-api
Vérification
Surveiller le hook de migration de schéma pendant son exécution (il se supprime automatiquement en cas de succès), puis valider chaque chemin réseau. Remplacer pm.example.com par le nom d'hôte configuré.
# Schema-migration job. It is a Helm hook DELETED automatically on success,
# so watch it while it runs rather than after:
kubectl -n vaks-pm get pods -w # the vaks-pm-dbpush-* pod should reach Completed
# If it FAILS, the job is kept; read its logs:
kubectl -n vaks-pm logs job/vaks-pm-dbpush
# API readiness (expected response: {"status":"ok","db":"up"})
curl -s https://pm.example.com/api/v1/health/ready
# API liveness
curl -s https://pm.example.com/api/v1/health/live
# Home page (expected: HTTP 200)
curl -s -o /dev/null -w "%{http_code}\n" https://pm.example.com/
vaks-pm-dbpush s'exécute comme un hook post-installation/mise à niveau et est supprimée une fois réussie ; un NotFound à la lecture de ses logs après une installation réussie est donc attendu, et non une erreur. Une tâche en échec est conservée pour inspection.db:up, que la page d'accueil renvoie 200, que l'administrateur initialisé peut se connecter et que l'envoi d'une pièce jointe de tâche réussit (ce qui exerce le chemin S3 de bout en bout), le déploiement est fonctionnel. Si l'option de démonstration a été activée, un compte administrateur initial est créé (admin@vaks-pm.local, mot de passe ChangeMe123!) ; le changer immédiatement.Chiffrement sur Kubernetes · vue d'ensemble
Un déploiement complet comporte trois préoccupations de chiffrement indépendantes. Elles sont orthogonales (en activer une n'active pas les autres), et seules les deux premières sont prises en charge par les valeurs de démarrage rapide.
- Secrets au niveau champ. Les secrets applicatifs (mot de passe SMTP, graines multi-facteurs, secrets client d'authentification unique) sont chiffrés à l'intérieur de la base de données par l'application elle-même, via le chiffrement à enveloppe (une DEK par tenant enveloppée par une KEK). Activé par défaut.
- En transit. Le trafic réseau : navigateur↔cluster (la bordure externe), les sauts internes vers les dépendances (base de données, cache, stockage objet), et le trafic de pod à pod.
- Au repos. Les données sur disque : les volumes de dépendances (PVC) et les Secrets Kubernetes (qui détiennent le trousseau de KEK, les clés de signature et les identifiants) dans le magasin de données du cluster.
| Couche | Protège | Défaut (démarrage rapide) | Activer avec |
|---|---|---|---|
| Secrets au niveau champ (enveloppe) | Secrets SMTP / MFA / SSO dans la base | ACTIVÉ | intégré ; garde via crypto.* |
| TLS de bordure externe | navigateur ↔ cluster | ACTIVÉ (si TLS Ingress défini) | ingress.tls + certificat |
| TLS app ↔ PostgreSQL | trafic base de données | DÉSACTIVÉ | tls.postgres |
| TLS app ↔ Redis / S3 | trafic cache / objet | DÉSACTIVÉ | rediss:// / s3.external.useSSL |
| Réseau de pod à pod | tout le trafic intra-cluster | DÉSACTIVÉ | CNI chiffré / mTLS de maillage de services |
| Volumes de données (PVC) au repos | base / cache / objets complets sur disque | DÉSACTIVÉ | StorageClass chiffrée |
| Secrets Kubernetes au repos | trousseau de KEK, clés, identifiants | DÉSACTIVÉ | chiffrement du magasin de données / magasin externe |
Secrets au niveau champ (chiffrement à enveloppe)
Les champs sensibles sont chiffrés avec une DEK par tenant enveloppée par une KEK. C'est intégré et fonctionne d'emblée : la KEK est par défaut MFA_ENCRYPTION_KEY (enregistrée sous la ref env), un chemin qui fonctionne toujours à la fois pour le chiffrement et le déchiffrement. Rien de plus n'est requis pour une configuration de base.
Le chiffrement au niveau champ protège les secrets listés, pas le gros des données métier (projets, tâches, utilisateurs). Protéger l'ensemble du jeu de données au repos relève de la couche de chiffrement des volumes dans Au repos.
Options de garde (crypto.*)
Deux modèles de garde existent ; tous deux sont portables entre les parcours Swarm et Kubernetes, car l'application ne fait que lire un répertoire de trousseau monté (crypto.keysDir, par défaut /etc/vaks/keys) et ne se soucie pas de qui l'a monté.
- Trousseau de clés local. Monter un trousseau versionné de fichiers nommés
kek_v<N>(base64, 32 octets) et pointercrypto.activeKekvers la version que les nouvelles écritures doivent utiliser ; les clés plus anciennes restent utilisables pour déchiffrer les blobs hérités. Fournir les clés en ligne viacrypto.keks(le chart construit un Secret), ou, choix recommandé pour la production, référencer un Secret géré hors bande aveccrypto.existingSecret, monté en lecture seule dansapi/worker/controlplane. Cela permet la rotation de la KEK sans rechiffrer les données. - Garde par KMS / HSM externe. Définir
crypto.provideràkmsouhsmpour déplacer l'enveloppe/désenveloppe de la KEK hors de la machine, afin que la clé maîtresse ne réside jamais dans le cluster. Pour KMS, renseignercrypto.kms.keyId/region/endpoint(préférer l'identité de charge de travail aux clés statiques ; sinon un Secret viacrypto.kms.existingSecret). Pour un HSM sur site, déployer le sidecar courtier de clés PKCS#11 et définircrypto.hsm.brokerUrl/keyLabelavec le jeton partagé danscrypto.hsm.existingSecret. Seule la KEK quitte la machine ; la DEK par tenant reste locale. La migration est en ligne : déployer avec le fournisseur défini, puis ré-envelopper via Admin → Clés de chiffrement.
crypto.provider: local par défaut utilise le trousseau de fichiers (ou, à défaut, MFA_ENCRYPTION_KEY). Pour une séparation des tâches dans un déploiement réglementé, déplacer la garde vers un KMS ou un HSM. Le modèle et sa correspondance avec les contrôles DORA sont décrits dans Sécurité.En transit
Bordure externe (Ingress)
Terminer HTTPS au niveau de l'Ingress en fournissant un certificat comme un Secret Kubernetes existant, ou en laissant un gestionnaire de certificats en émettre un :
ingress:
enabled: true
className: nginx # or traefik
host: pm.example.com
tls:
enabled: true
secretName: pm-example-tls # cert-manager or supplied
Services internes (base de données, cache, stockage objet)
Ces sauts transportent les données de l'application vers ses dépendances et sont en clair sauf s'ils sont activés. Chacun est un réglage distinct :
| Saut | Réglage | Effet |
|---|---|---|
| app ↔ PostgreSQL | tls.postgres: true | Définit PG_TLS=1. Pour la vérification de certificat, ajouter tls.postgresCaSecret (un Secret avec ca.crt) et tls.postgresVerifyMode (verify-full ou verify-ca). |
| app ↔ Redis | externalRedis.url: rediss://… | Une connexion Redis en TLS. |
| app ↔ S3 | s3.external.useSSL: true + un endpoint https | Une connexion au stockage objet en TLS. |
prisma db push) ne lit pas PG_TLS ; si la base de données impose le TLS, ajouter aussi ?sslmode=require à la DATABASE_URL.Réseau de pod à pod
La façon la plus simple de chiffrer tout le trafic intra-cluster d'un coup, y compris les sauts vers la base de données, le cache, le stockage objet et tout courtier KMS/HSM, est de chiffrer le réseau de pods lui-même plutôt que chaque service individuellement :
- CNI chiffré. Par exemple un backend WireGuard (tel que k3s
--flannel-backend=wireguard-native), ou Cilium / Calico avec le chiffrement activé. Cela chiffre le trafic de pods de nœud à nœud sans aucun changement de l'application. - mTLS de maillage de services. Un maillage (Istio, Linkerd) impose le TLS mutuel entre les pods.
Au repos
Volumes de données (PVC)
Les volumes de la base de données, du cache et du stockage objet contiennent l'ensemble du jeu de données (pas seulement les secrets au niveau champ). Les chiffrer avec une StorageClass chiffrée ; seuls les volumes sont chiffrés, jamais le système d'exploitation du nœud :
postgresql:
persistence:
storageClass: gp3-encrypted # a cloud-KMS or CSI-encrypted class
# do the same for redis.persistence.storageClass / minio.persistence.storageClass,
# or use managed services with provider-side encryption
Options : une classe de stockage en bloc chiffrée d'un fournisseur cloud (adossée à un KMS), un pilote Container Storage Interface (CSI) avec chiffrement, ou des nœuds dont les disques sont chiffrés (par exemple LUKS, Linux Unified Key Setup). Lorsque les dépendances sont externes/gérées, utiliser plutôt le chiffrement du fournisseur (chiffrement RDS, chiffrement côté serveur S3, et ainsi de suite).
Secrets Kubernetes
Le trousseau de KEK, les clés de signature et les identifiants de base de données/stockage vivent dans des Secrets Kubernetes. Un Secret n'est qu'encodé en base64 dans le magasin de données du cluster, non chiffré par défaut. Le protéger avec l'un des éléments suivants :
- Chiffrement du magasin de données au repos. Une
EncryptionConfigurationavec un fournisseuraescbcou (mieux) un fournisseurkmssur le serveur d'API. Sur k3s, c'est lek3s secrets-encrypt enableintégré, qui requiert le magasin de données etcd embarqué (installer avec--cluster-init; il n'a aucun effet sur le magasin de données SQLite par défaut). - Magasin de secrets externe. L'External Secrets Operator, un pilote de secrets CSI ou un contrôleur de Secrets scellés, afin que le matériel cryptographique provienne d'un coffre plutôt que d'être stocké dans le magasin de données. Cela se combine avec
secrets.existingSecretetcrypto.existingSecret. - Garde de la KEK hors machine. Déplacer la KEK vers un KMS/HSM (voir Secrets au niveau champ) signifie que la clé la plus sensible n'est jamais dans un Secret Kubernetes du tout.
Exemple HA chiffré
Ce fichier de valeurs consolidé réunit la forme HA de production avec chaque contrôle de chiffrement au niveau applicatif activé. Il repose toujours sur des contrôles au niveau du cluster (CNI chiffré, chiffrement des Secrets du magasin de données) configurés en dehors de Helm, comme noté dans les commentaires.
image:
registry: registry.example.com
tag: "1.0.0"
imagePullSecrets:
- name: regcred
publicBaseUrl: https://pm.example.com
tenancy:
mode: dedicated
# --- HA replicas + anti-affinity ---
api: { replicaCount: 3 }
web: { replicaCount: 2 }
worker: { replicaCount: 3 }
# --- External, managed, TLS-terminating dependencies ---
postgresql:
enabled: false
externalDatabase:
url: "postgresql://vakspm:<password>@pg.internal.example.com:5432/vakspm?schema=public&sslmode=require"
redis:
enabled: false
externalRedis:
url: "rediss://redis.internal.example.com:6379" # rediss:// = TLS
minio:
enabled: false
s3:
external:
endpoint: "https://s3.internal.example.com:9000"
region: us-east-1
useSSL: true # TLS to object storage
forcePathStyle: true
accessKey: "<access-key>"
secretKey: "<secret-key>"
createBuckets: true
# --- In-transit TLS to PostgreSQL with CA verification ---
tls:
postgres: true # PG_TLS=1
postgresCaSecret: pg-ca # Secret holding ca.crt
postgresVerifyMode: verify-full
# --- External edge TLS ---
ingress:
enabled: true
className: nginx
host: pm.example.com
tls:
enabled: true
secretName: pm-example-tls
# --- Field-level KEK: versioned keyring with rotation ---
crypto:
existingSecret: vaks-pm-keyring # holds kek_v1, kek_v2, …
activeKek: v2 # new writes wrap with kek_v2
# --- All sensitive values from an out-of-band Secret ---
secrets:
existingSecret: vaks-pm-secrets
# Cluster-level controls configured OUTSIDE Helm (plan before building):
# * encrypted CNI (e.g. WireGuard) for pod-to-pod traffic
# * datastore Secret encryption (EncryptionConfiguration / k3s secrets-encrypt)
# * encrypted StorageClass for any in-cluster PVCs (none here: all deps external)
tls.postgres + rediss:// + S3 useSSL) · volumes de données au repos (StorageClass chiffrée ou chiffrement du service géré) · Secrets Kubernetes au repos (chiffrement du magasin de données ou magasin externe) · garde des clés (déplacer la KEK vers un KMS/HSM pour la séparation des tâches).Mise à niveau
Une mise à niveau pointe la release vers un tag d'image plus récent et réapplique le chart. Le client ne construit ni ne modifie jamais de code — seul le tag change. Comme les services applicatifs sont sans état et exécutent plusieurs réplicas, Kubernetes procède par rolling update (les nouveaux pods doivent passer leur sonde de readiness avant que les anciens ne soient retirés), donc l'opération est sans interruption. Le schéma de base de données voyage avec l'image et est appliqué par un Job de type hook Helm ; il n'y a donc pas de pack de migrations SQL à jouer séparément dans le cas courant.
helm upgrade avec le nouveau image.tag — le hook vaks-pm-dbpush aligne le schéma avant que les pods applicatifs ne basculent ; (4) vérifier la santé. Les étapes détaillées suivent.1 · Mettre le nouveau tag d'image en miroir
Le chart tire les trois images (api, web, worker) depuis le registre nommé dans image.registry, au tag indiqué dans image.tag. Mettre d'abord la nouvelle version en miroir dans ce registre (voir Images · miroir et identifiants de pull), pour que chaque nœud puisse la tirer. Épingler un tag de version unique (par ex. 1.4.0) plutôt qu'un tag mobile, afin que la révision déployée soit reproductible.
2 · Lancer la mise à niveau
Fixer le nouveau tag et réappliquer le même fichier de valeurs. Le --timeout laisse au hook de migration de schéma le temps de se terminer avant que Helm ne bascule les pods applicatifs :
helm upgrade vaks-pm ./deploy/helm/vaks-pm \
-n vaks-pm -f values-production.yaml \
--set image.tag=1.4.0 --timeout 5m
kubectl -n vaks-pm rollout status deploy/vaks-pm-api # new pods Ready
kubectl -n vaks-pm rollout status deploy/vaks-pm-web
Helm exécute le hook vaks-pm-dbpush (ci-dessous) en étape pre-upgrade, de sorte que le schéma est aligné avant le démarrage des nouveaux pods api. Si le hook échoue, la mise à niveau s'arrête et les pods en fonctionnement continuent de servir l'ancienne version.
3 · Schéma de base de données
Le schéma voyage dans l'image. Le chart embarque un Job de type hook Helm, vaks-pm-dbpush, qui exécute prisma db push à l'installation et à la mise à niveau (conditionné par migrations.enabled, actif par défaut). Il s'auto-supprime en cas de succès et est conservé en cas d'échec pour inspection — donc pour une mise à niveau de routine, il n'y a aucune étape de migration manuelle.
# observer le hook pendant qu'il tourne (supprimé en cas de succès) :
kubectl -n vaks-pm get pods -w # vaks-pm-dbpush-* doit atteindre Completed
kubectl -n vaks-pm logs job/vaks-pm-dbpush # présent uniquement s'il a ÉCHOUÉ
externalDatabase.url vers un rôle à privilèges minimaux sans droit DDL sur le schéma public, le hook échoue (permission denied for schema public / must be owner of table …). Dans ce cas, appliquer d'abord le changement de schéma avec un rôle disposant des droits DDL, puis lancer la mise à niveau avec migrations.enabled=false :
kubectl -n vaks-pm run dbpush --rm -it --restart=Never \
--image=<registre>/vaks-pm/api:1.4.0 \
--env DATABASE_URL='postgresql://<owner>:…@<host>:5432/<db>' \
-- sh -c 'cd /app/api && npx prisma db push --accept-data-loss'prisma db push est forward-only : il n'existe pas de migration inverse, et certains changements de type ou de contrainte peuvent supprimer des données. migrations.acceptDataLoss (défaut true) laisse le hook appliquer de tels changements sans demander — l'examiner avant une mise à niveau sur une base peuplée, et faire une sauvegarde d'abord (voir Exploitation). helm rollback ramène les charges de travail au tag d'image précédent, mais il n'annule pas les changements de schéma ; le retour arrière du schéma passe par une restauration de sauvegarde.<dbName>_<slug>) doit recevoir le changement de schéma avec un rôle DDL avant la bascule des pods applicatifs.4 · Retour arrière
Pour revenir à la release précédente (tag d'image, valeurs et manifestes) :
helm history vaks-pm -n vaks-pm # trouver la révision saine précédente
helm rollback vaks-pm <revision> -n vaks-pm --timeout 5m
5 · Vérifier après la mise à niveau
kubectl -n vaks-pm get pods # tous Running sur la nouvelle image
curl -s https://pm.example.com/api/v1/health/ready # {"status":"ok","db":"up"}
Tous les pods applicatifs Running sur le nouveau tag, le hook de migration Completed, la connexion fonctionne, et un envoi de pièce jointe réussit (exerce le chemin S3). Ajouter une organisation (mode pooled) n'est pas une mise à niveau : provisionner un nouveau tenant avec sa propre base de données et son nom d'hôte ; les tenants existants ne sont pas perturbés.
Désinstallation
helm uninstall vaks-pm -n vaks-pm
Référence des paramètres
Les paramètres les plus utilisés. Voir deploy/helm/vaks-pm/values.yaml pour la liste exhaustive et commentée.
| Paramètre | Défaut | Description |
|---|---|---|
image.registry | "" | Hôte du registre contenant les images (le chart ajoute le dépôt). |
image.tag | appVersion du chart | Tag commun aux trois images. |
imagePullSecrets | [] | Secrets pour un registre privé. |
publicBaseUrl | (requis) | URL publique de l'application (liens des courriels et CORS). |
tenancy.mode | dedicated | dedicated ou pooled. |
tenancy.baseDomain | "" | Domaine racine. Requis en mode pooled. |
api/web/worker.replicaCount | 2 | Nombre de réplicas par service (mettre 1 pour l'évaluation, 2 à 3 pour la HA). |
controlplane.replicaCount | 2 | Réplicas du plan de contrôle (pooled uniquement). |
api/web/worker.resources | voir Dimensionnement | Requêtes et limites CPU/mémoire par service. |
api/web/worker.affinity | {} | Passe-plat de l'affinité de la spécification du pod ; définir l'anti-affinité pour la répartition HA. |
secrets.existingSecret | "" | Nom d'un Secret existant fournissant les clés sensibles. Recommandé en production. |
secrets.jwtAccessSecret / jwtRefreshSecret | "" | Clés de signature JWT (lorsque existingSecret n'est pas utilisé). |
secrets.mfaEncryptionKey | "" | Clé MFA au repos ; également la KEK par défaut au niveau champ. |
secrets.workerApiToken | "" | Jeton utilisé par le worker pour appeler l'API interne. |
secrets.smtpPassword / ssoOidcClientSecret / syslogClientKey | "" | Optionnel : authentification courriel, secret client OIDC, clé mTLS syslog d'audit. |
postgresql.enabled | true | Déployer le PostgreSQL embarqué mono-nœud. Mettre false pour la production. |
externalDatabase.url | "" | Chaîne de connexion pour une base de données externe. |
redis.enabled | true | Déployer le Redis embarqué mono-nœud. Mettre false pour la production. |
externalRedis.url | "" | Chaîne de connexion pour un Redis externe (utiliser rediss:// pour le TLS). |
minio.enabled | false | Déployer le MinIO embarqué mono-nœud (évaluation). |
s3.external.endpoint / region / useSSL / forcePathStyle / accessKey / secretKey / bucketPrefix / createBuckets | voir Stockage S3 | Connexion à un service S3 externe. |
crypto.keysDir | /etc/vaks/keys | Chemin de montage du trousseau au niveau champ (CRYPTO_KEYS_DIR). |
crypto.activeKek | "" | Version de KEK avec laquelle les nouvelles écritures enveloppent (auto si vide). |
crypto.keks / crypto.existingSecret | {} / "" | Entrées de trousseau en ligne, ou un Secret existant de clés kek_v<N>. |
crypto.provider | local | local | kms | hsm : garde de la KEK pour les nouvelles enveloppes. |
crypto.kms.* / crypto.hsm.* | — | Réglages KMS (keyId/region/endpoint/existingSecret) ou HSM (brokerUrl/keyLabel/existingSecret). |
tls.postgres | false | Activer le TLS vers PostgreSQL (PG_TLS=1). |
tls.postgresCaSecret / tls.postgresVerifyMode | "" / verify-full | Secret de CA (ca.crt) et mode de vérification pour le TLS PostgreSQL. |
persistence.storageClass (par dépendance) | "" | StorageClass chiffrée pour le chiffrement des volumes au repos. |
ingress.enabled / className / host | true / "" / "" | Bascule Ingress, classe du contrôleur, nom d'hôte public. |
ingress.tls.enabled / secretName | true / "" | Bascule du TLS de bordure et nom du Secret TLS. |
migrations.enabled | true | Exécuter le hook de migration de schéma à l'installation/mise à niveau. |
migrations.acceptDataLoss | true | Autoriser prisma db push à appliquer des changements qui suppriment des données. Examiner avant d'activer sur une base peuplée. |
migrations.seedDemo | false | Créer l'organisation de démonstration et l'administrateur. Laisser false en production. |
Dépannage
| Symptôme | Cause probable et correctif |
|---|---|
Un pod reste Pending | Ressources de nœud insuffisantes, ou aucune StorageClass par défaut pour satisfaire un PVC. Inspecter avec kubectl -n vaks-pm describe pod <name> et kubectl get storageclass. |
Un pod en ImagePullBackOff | Image introuvable ou registre injoignable. Vérifier image.registry, image.tag et les imagePullSecrets requis. |
La santé affiche db:down | L'API ne peut pas atteindre la base de données. Vérifier externalDatabase.url (ou le pod postgresql) et la connectivité réseau depuis le namespace. Si la base impose le TLS, ajouter ?sslmode=require à l'URL. |
La tâche vaks-pm-dbpush échoue | La migration de schéma n'a pas pu être appliquée. Lire kubectl -n vaks-pm logs job/vaks-pm-dbpush ; généralement la base est injoignable au démarrage (la tâche réessaie). |
| L'envoi de pièce jointe ou la génération de rapport échoue | S3 mal configuré. Vérifier s3.external.*, que les cinq buckets existent (ou createBuckets=true), et les droits du compte d'accès. L'API journalise une ligne S3 backend: … au démarrage. |
| Les réplicas tous sur un seul nœud | Aucune anti-affinité définie. Ajouter une podAntiAffinity sous api.affinity / web.affinity / worker.affinity (voir Mono vs HA). |