Configuration¶
Le pas-à-pas a peuplé Vault et posé le ClusterSecretStore. Cette page revient sur ce qui
arrive ensuite : la forme que doit prendre chaque ExternalSecret, ce qui distingue un secret
Vault d'une variable ConfigMap et d'une variable figée dans une image, et l'inventaire des
sources par service. Elle ne répète pas l'installation d'External Secrets Operator ni le
descellement de Vault, détaillés à l'étape 6 du Pas-à-pas.
Le chemin d'un secret, de Vault au pod¶
opérateur ─vault kv patch─▶ Vault KV v2, mount « secrets », chemin prod/<service>
─auth kubernetes, rôle eso─▶ ClusterSecretStore vault-store
─ExternalSecret (dataFrom: extract)─▶ Secret Kubernetes ─envFrom─▶ pod
Ce dépôt ne porte que le maillon du milieu, l'ExternalSecret. Les deux extrémités, la valeur
dans Vault et la variable dans le pod, ne sont jamais écrites en clair dans les manifests.

Un seul issuer doit apparaitre partout : dans KC_HOSTNAME, dans les clés Vault qui portent
l'issuer côté portails et APIs, et dans le realm Keycloak. La figure suit ce chemin d'un bout à
l'autre ; c'est l'endroit où repérer un maillon qui porterait encore l'ancienne forme du
domaine.
La forme obligatoire d'un ExternalSecret¶
Tout ExternalSecret qui alimente des variables d'environnement doit prendre cette forme :
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: trade-classification-svc-secret
namespace: api
spec:
refreshInterval: 1m
secretStoreRef:
kind: ClusterSecretStore
name: vault-store
target:
name: trade-classification-svc-secret
creationPolicy: Owner
deletionPolicy: Retain
dataFrom:
- extract:
key: prod/classification
dataFrom: extract injecte dans le Secret toutes les clés présentes au chemin Vault, telles
quelles. Une liste data: énumérée oblige au contraire à déclarer chaque clé une par une dans
le manifest, et l'oubli d'une seule est silencieux : le pod démarre normalement, la variable
manque seulement au runtime. Le cas est déjà arrivé sur CONTENT_SERVICE_URL, avec pour
symptôme un proxy média en 404. Avec extract, ajouter une clé côté Vault suffit ; il reste à
redémarrer les pods pour qu'ils la lisent.
creationPolicy: Owner place l'ExternalSecret en propriétaire du Secret produit. C'est la
propriété qui surprend le plus un opérateur qui découvre ce dépôt : elle signifie qu'un Secret
posé à la main, par exemple avec kubectl create secret ou kubectl edit, est écrasé dès la
synchronisation suivante, silencieusement. Le seul point d'écriture valable est Vault.
deletionPolicy: Retain décide de ce qui arrive au Secret quand une clé disparait côté Vault :
il garde sa dernière valeur connue au lieu d'être vidé. Une erreur de manipulation dans Vault
devient ainsi une dérive silencieuse plutôt qu'une panne immédiate : le Secret survit à la
disparition de sa source, et l'écart ne se voit que dans le statut de l'ExternalSecret.
apps/keycloak/keycloak-manifests.yaml ne déclare pas deletionPolicy : il prend alors le
défaut Retain de l'API, le bon comportement, mais sans l'intention écrite.
Écrire dans Vault se fait avec vault kv patch, jamais vault kv put, sur tout chemin partagé
par plusieurs clés. put remplace l'intégralité du contenu du chemin : toute clé absente de la
commande disparait, donc disparait du Secret à la synchronisation suivante, sans erreur ni
alerte. put reste acceptable sur un chemin dédié à une seule clé, comme celui du tunnel
Cloudflare.
Un ExternalSecret du dépôt s'écarte de cette forme sans y être obligé, et c'est exactement le
défaut qui vient d'être décrit : keycloak-db-external-secret
(apps/keycloak/keycloak-manifests.yaml) énumère username et password en data:, pas en
extract, pour produire le Secret keycloak-db-secret. Une clé ajoutée par la suite au chemin
Vault prod/keycloak-db-secret n'atteindra donc jamais ce Secret tant que le manifest n'est
pas modifié pour l'énumérer explicitement.
Un objet déroge légitimement, pour une raison précise et non pour une préférence : le Secret du
tunnel Cloudflare, cloudflared-credentials, matérialise un fichier, pas un jeu de variables
d'environnement. Un point dans un nom de clé Vault se lit mal en extract, donc cet objet
déclare une entrée data: avec property, puis un target.template qui recompose la clé de
fichier attendue par le conteneur. core/vpn/externalsecret-wireguard.example.yaml montre la
même construction pour un futur Secret WireGuard, mais porte le suffixe .example.yaml : il
n'est pas appliqué sur l'instance de référence, ce n'est qu'un modèle. Cette exception ne vaut
que pour un secret-fichier à clé unique.
Rotation d'un secret¶
External Secrets Operator met à jour le Secret Kubernetes à chaque refreshInterval, une
minute pour la quasi-totalité des objets du dépôt. Il ne met à jour aucun pod déjà démarré : les
variables d'environnement sont figées au moment où le conteneur démarre, exactement comme les
variables NEXT_PUBLIC_* sont figées au moment où l'image est construite, plus bas sur cette
page. Une valeur modifiée dans Vault n'a donc d'effet qu'après :
Sans ce redémarrage, le Secret Kubernetes porte la nouvelle valeur, mais les pods en cours
continuent de servir avec l'ancienne, sans qu'aucune erreur ne le signale.
Inventaire des sources par service¶
Chaque service applicatif combine jusqu'à trois sources : un ExternalSecret Vault, une
ConfigMap de confiance CA montée en fichier, et pour les portails des variables figées à la
construction de l'image. Aucune de ces trois sources ne se substitue aux deux autres. Ce dépôt
ne versionne nulle part la liste des noms de variables que chaque service lit dans son Secret :
le tableau ci-dessous donne l'ExternalSecret et le chemin Vault de chaque service, pas les clés
qu'il contient. Pour obtenir ces clés sur une installation existante, sans jamais afficher leur
valeur :
kubectl get secret <nom-du-secret> -n <namespace> -o jsonpath='{.data}' | tr ',' '\n' | cut -d'"' -f2
Avant que le Secret n'existe, le même inventaire se lit directement sur le chemin Vault, sans afficher les valeurs :
| Service | Namespace | Secret Vault (ExternalSecret, extract) |
Notes |
|---|---|---|---|
hub-portail |
web |
hub-portail-secret, chemin prod/hub-portail-secret |
Variables NEXT_PUBLIC_* figées au build, voir plus bas |
portal-portail |
web |
portal-portail-secret, chemin prod/portal-portail-secret |
Variables NEXT_PUBLIC_* figées au build |
internal-portail |
internal |
internal-portail-secret, chemin prod/internal-portail-secret |
Variables NEXT_PUBLIC_* figées au build |
trade-classification-svc |
api |
trade-classification-svc-secret, chemin prod/classification |
Runtime .NET |
trade-declaration-svc |
api |
trade-declaration-svc-secret, chemin prod/declaration |
Runtime .NET |
trade-declaration-admin-svc |
api |
trade-declaration-admin-svc-secret, chemin prod/declaration-admin |
Runtime .NET |
trade-notification-svc |
api |
trade-notification-svc-secret, chemin prod/notification |
Runtime .NET |
trade-registration-svc |
api |
trade-registration-svc-secret, chemin prod/registration |
Runtime .NET |
trade-valuation-svc |
api |
trade-valuation-svc-secret, chemin prod/valuation |
Runtime .NET |
trade-content-service-svc |
api |
trade-content-service-svc-secret, chemin prod/content-service |
Runtime Node, PORT=3000 fixé dans le manifest ; deux pièges spécifiques, voir plus bas |
keycloak |
auth |
keycloak-db-external-secret, chemin prod/keycloak-db-secret, mode data (username, password), pas extract, voir plus haut |
KC_HOSTNAME fixé dans apps/keycloak/keycloak.yaml à la forme URL complète du host auth, substituée par render-config.sh avec ${TV_DOMAIN} |
cloudflare-dns-token (cert-manager) |
cert-manager |
cloudflare-dns-token, chemin prod/cloudflare-dns |
Consommé par le ClusterIssuer Let's Encrypt |
cloudflared-credentials (cloudflare) |
cloudflare |
cloudflared-credentials, chemin prod/cloudflared, mode data + template |
Secret-fichier, exception à extract documentée plus haut |
Une colonne « ConfigMap CA montée » figurait ici jusqu'au 2026-09-10. Elle a disparu avec le
CA interne : plus rien n'est à monter pour faire confiance à *.${TV_DOMAIN}.
Les variables NEXT_PUBLIC_*, figées au build¶
Les portails Next.js (hub-portail, portal-portail, internal-portail) lisent leurs
variables NEXT_PUBLIC_* au moment où l'image est construite, jamais au démarrage du
conteneur. C'est le cas par exemple de NEXT_PUBLIC_CONTENT_SERVICE_URL, dont la valeur
canonique est écrite dans apps/api/content-service/content-ingress.yaml :
https://content.${TV_DOMAIN}. Ce host ne fait pourtant pas partie des trois enregistrements DNS
publics autorisés (hub, portal, auth, voir l'étape 13 du
Pas-à-pas), alors que les portails qui l'appellent
sont eux-mêmes publics : ce dépôt ne dit nulle part comment un navigateur situé hors du réseau
interne doit atteindre ce host. Confirmer ce point avant de fixer la variable et de lancer le
build, plutôt que de supposer qu'un enregistrement public supplémentaire est la réponse. Une
valeur fausse ou périmée n'est pas rattrapable en changeant
une variable d'environnement du Deployment, ni en redéployant : la seule correction possible est
de reconstruire l'image avec la bonne valeur, puis de redéployer cette nouvelle image. Un
changement de domaine (${TV_DOMAIN}) qui ne s'accompagne pas de ce rebuild laisse les portails
appeler l'ancien domaine, sans qu'aucune erreur Kubernetes ne le signale : les pods démarrent, la
sonde de disponibilité passe, seules les requêtes émises depuis le navigateur échouent.
Ce point rejoint les clés Vault AUTH_KEYCLOAK_ISSUER, NEXTAUTH_URL et NEXTAUTH_URL_INTERNAL
déjà signalées en Paramètres : elles
changent toutes ensemble avec KC_HOSTNAME, faute de quoi tous les jetons sont rejetés avec
l'erreur IDX10205.
Les deux pièges du content-service¶
Le content-service (Payload) porte deux variables à ne jamais définir, chacune pour une raison distincte, toutes deux déjà rencontrées en conditions réelles.
SERVER_URL (et PAYLOAD_PUBLIC_SERVER_URL) doit rester absente du secret Vault
prod/content-service. Quand elle est définie, Payload renvoie des URLs de média absolues,
que le navigateur appelle alors directement sur le content-service, sans passer par le proxy
/api/media des portails. Cet appel direct est anonyme, sans clé API ni cookie, et l'accès aux
médias exige une autorisation : la lecture échoue en 403, de façon intermittente selon le poste
qui affiche la page. Absente, Payload émet des URLs relatives, que le navigateur résout depuis
l'origine du portail, donc à travers le proxy qui ajoute la clé API côté serveur.
PAYLOAD_AUTO_SEED ne doit pas être définie. Sa présence relance un seed complet des taux de
change à chaque démarrage de pod. Ce seed entrait en conflit avec les taux déjà actifs et les
marquait expirés alors que leur période de validité restait ouverte, un incident déjà survenu :
la section des taux de change devenait vide sur les deux portails à la fois. Absente, le
comportement par défaut est sûr. Si le cas se reproduit malgré tout, seules les lignes expirées
sans intervention humaine (expired_by_id vide) doivent être réactivées ; une expiration décidée
par un administrateur ne se réactive jamais automatiquement.
Confiance CA : plus rien à régler¶
Il n'y a plus de variable de confiance CA à poser. *.${TV_DOMAIN} est servi par un certificat
Let's Encrypt, dont la chaîne est déjà dans le magasin par défaut de chaque image applicative.
Cela n'a pas toujours été le cas, et l'histoire vaut d'être connue avant d'en réintroduire une :
jusqu'au 2026-09-10, un CA interne était distribué par trust-manager, avec deux variables
différentes pour deux runtimes, SSL_CERT_FILE côté .NET et OpenSSL, NODE_EXTRA_CA_CERTS
côté Node.js. Node ne lit ni l'une ni le magasin OpenSSL, ce qui rendait les deux non
interchangeables, et les confondre ne produisait aucun effet visible avant une connexion TLS qui
échouait.
Elles étaient devenues redondantes dès le passage à Let's Encrypt et ont survécu plusieurs semaines à cet état, « par précaution ». Une confiance dont personne ne se sert ne se remarque pas : c'est ce qui rend ce genre de reste durable.
ATTENTION: si un composant revenait un jour à un certificat interne, la vérification à faire
avant de poser SSL_CERT_FILE est que l'image possède bien un magasin par défaut. Mesuré ici :
les images .NET en portent 150, contre 151 dans le bundle monté. Pointer SSL_CERT_FILE sur un
fichier qui ne contiendrait que le CA interne ferait perdre d'un coup toute confiance publique,
sans erreur au démarrage.
Ce que ce paramétrage ne couvre pas¶
Le nom exact des clés attendues par chaque service, au-delà des variables déjà citées sur cette
page et de la commande donnée plus haut pour les retrouver, vit dans le code applicatif, hors du
périmètre de ce dépôt. Confirmer ces noms auprès de l'éditeur avant d'écrire dans Vault : une clé
absente reproduit le même symptôme que CONTENT_SERVICE_URL plus haut, un service qui démarre
normalement et échoue seulement à l'usage.