Vault & PKI¶
Rôle¶
Cluster HashiCorp Vault : coffre-fort de secrets applicatifs Indio (moteur KV-v2) et autorité de certification interne (PKI racine + intermédiaire, avec CRL/OCSP), utilisée notamment pour l'émission de certificats RDP/KDC/ carte à puce du domaine Active Directory (voir Durcissement Active Directory) et plus largement comme source de confiance TLS interne. Le scellement (unseal) est automatisé via AWS KMS (voir Cloud AWS).
Architecture¶
| VM | vault_node_id |
IP |
|---|---|---|
| INDSERV016 | vault-1 | 10.100.2.146 |
| INDSERV017 | vault-2 | 10.100.2.147 |
| INDSERV018 | vault-3 | 10.100.2.148 |
- 3 nœuds en zone
seg-service(admin), stockage Raft natif (HA sans dépendance externe type Consul), clonés depuistmpl-rocky96-hardened. - VIP HAProxy
10.100.2.130:8200, DNSvault.infra.indio, en TCP passthrough — HAProxy ne termine pas le TLS : un client voit directement le certificat du nœud Vault backend réel, jamais un certificat propre à la VIP. - Deux chaînes de confiance distinctes à ne pas confondre :
- une CA bootstrap (
vault-bootstrap-ca), autosignée, qui sécurise uniquement le listener/cluster Vault lui-même (TLS des API, duretry_joinRaft entre nœuds) ; - la PKI applicative que Vault opère en tant que service
(
pki= CA racine « Indio Root CA »,pki_int= CA intermédiaire « Indio Intermediate CA ») — c'est celle-ci que consomment les autres composants de l'infra (ex. AD pour les certificats RDP/KDC).
Topologie du cluster et auto-unseal¶
flowchart TB
subgraph AWS["AWS eu-west-3 (compte 340694419192)"]
KMS["KMS alias/vault-unseal"]
end
subgraph SEG["seg-service — 10.100.2.128/26"]
VIP["VIP HAProxy 10.100.2.130:8200\nvault.infra.indio — TCP passthrough"]
V1["INDSERV016 vault-1\n10.100.2.146:8200"]
V2["INDSERV017 vault-2\n10.100.2.147:8200"]
V3["INDSERV018 vault-3\n10.100.2.148:8200"]
V1 -. "Raft retry_join (mTLS, CA bootstrap)" .- V2
V2 -. "Raft retry_join" .- V3
V1 -. "Raft retry_join" .- V3
end
VIP -->|"httpchk GET /v1/sys/health\nexpect 200 (leader only)"| V1
VIP -.-> V2
VIP -.-> V3
V1 -. "auto-unseal (Decrypt/Encrypt)" .-> KMS
V2 -. "auto-unseal" .-> KMS
V3 -. "auto-unseal" .-> KMS
Le health-check HAProxy (GET /v1/sys/health, expect status 200) exploite
directement la sémantique de l'API Vault : seul le nœud actif (leader
Raft) répond 200, les standby répondent 429 par défaut. La VIP route donc
toujours vers le leader courant sans configuration supplémentaire — un
follower ne reçoit jamais de trafic applicatif via la VIP.
Hiérarchie de la PKI applicative (bootstrap, vault_pki_setup.sh)¶
sequenceDiagram
participant Op as Opérateur (root token, run_once)
participant Root as pki (Indio Root CA)
participant Int as pki_int (Indio Intermediate CA)
Op->>Root: pki/root/generate/internal (CN="Indio Root CA", RSA 4096, TTL 87600h)
Root-->>Op: CA racine auto-signée
Op->>Int: pki_int/intermediate/generate/internal (CN="Indio Intermediate CA", RSA 4096)
Int-->>Op: CSR intermédiaire
Op->>Root: pki/root/sign-intermediate (csr, TTL 43800h)
Root-->>Op: certificat intermédiaire signé
Op->>Int: pki_int/intermediate/set-signed (certificate)
Note over Int: pki_int est désormais opérationnelle —<br/>c'est ELLE qui signe tous les certificats leaf
Op->>Int: pki_int/config/urls + pki_int/config/crl (AIA/CDP/OCSP, auto_rebuild, delta CRL)
Émission d'un certificat (principe sign-only)¶
sequenceDiagram
participant C as Client (CSR généré localement)
participant V as Vault (pki_int/sign/<role>)
C->>C: génère sa propre paire de clés + CSR (clé privée jamais transmise)
C->>V: authentification (token, AppRole ou Kerberos selon le consommateur)
V-->>C: token + policies (ex. pki-sign-rdp, sign-only)
C->>V: POST pki_int/sign/ROLE {csr, common_name, ttl}
Note over V: JAMAIS pki_int/issue/* pour un consommateur externe —<br/>ce endpoint générerait ET transmettrait une clé privée
V-->>C: certificat signé (PEM) + chaîne
Provisioning Terraform¶
Fichiers : main.tf, variables.tf, versions.tf, terraform.tfvars.example.
- Providers
hashicorp/vsphereete-breuninger/netbox, même schéma que les autres dépôts VM de l'infra (clone de template + réservation IP NetBox par nom DNS). vsphere_virtual_machine.vaultenfor_eachsurvar.nodes(défaut : INDSERV016/017/018), réseauseg-service(gateway10.100.2.190/26), 2 vCPU / 4096 Mo par défaut.output vms: map nom → IP.- Variables sensibles (
vsphere_password,netbox_token) marquéessensitive = true; le vraiterraform.tfvarsest gitignoré, seulterraform.tfvars.example(valeursCHANGE_ME) est versionné.
Configuration Ansible¶
inventory.ini: groupe[vault]avecvault_node_idpar hôte.group_vars/all.yml:vault_vip(10.100.2.130), région KMS (eu-west-3), identifiant de la clé KMS dédiée à l'auto-unseal (vault_kms_key_id, ARN complet), et variables de proxy sortant (Squid,no_proxyincluant les domaines internes) — nécessaires car le processvaulta besoin de ses propres variables de proxy pour joindre l'API KMS (leenvironment:Ansible d'un play ne configure que l'exécution des tâches, pas le processvaultlui-même une fois démarré).group_vars/vault.yml: fichier chiffré ansible-vault — non consulté pour cette page (contient a priori les identifiants AWS du KMS). Valeurs gérées séparément.templates/vault.hcl.j2→/etc/vault.d/vault.hcl: blocseal "awskms"(région +kms_key_id),cluster_addr/api_addrsur l'IP du nœud,storage "raft"avecretry_joinvers les deux autres nœuds (mTLS via la CA bootstrap),listener "tcp"avec certificat de nœud ettls_client_ca_filepour le mTLS inter-cluster.templates/kms.env.j2→/etc/vault.d/kms.env(mode 0600) : variables d'environnement AWS + proxy, référencées par un drop-in systemd (EnvironmentFile=,/etc/systemd/system/vault.service.d/kms-unseal.conf) sur le servicevault.files/tls/: certificat/clé par nœud (indservXXX.crt/.key) + CA bootstrap (vault-bootstrap-ca.crt/.key/.srl) — matériel TLS non lu ni reproduit ici (clés privées). Les tâches de copie de ces fichiers ont unnotify: redémarrer vault— sans ce handler, un certificat mis à jour sur disque continue d'être ignoré par le process déjà démarré (piège rencontré et corrigé lors de l'ajout du SANvault.infra.indio, voir Procédure manuelle).files/vault_pki_setup.sh: script shell idempotent, exécuté une seule fois sur le leader (run_once) après déblocage du cluster. Active le moteur KV-v2 au pathsecret, la CA racine au pathpki(TTL max 87600h = 10 ans, RSA 4096, CN « Indio Root CA »), la CA intermédiaire au pathpki_int(TTL max 43800h = 5 ans, RSA 4096, CN « Indio Intermediate CA », CSR signé par la racine), les URLsissuing_certificates/crl_distribution_points/ocsp_servers(sur la VIP) et la config CRL (auto_rebuild=true,auto_rebuild_grace_period=12h,enable_delta=true, OCSP activé, expiry 72h), puis un rôle d'émission génériqueinfra-indio(allowed_domains=infra.indio,allow_subdomains,max_ttl=8760h/1 an, RSA 2048). Les rôles spécifiques (KDC, smart-card) ne sont pas créés par ce script — voir Procédure manuelle.site.yml: trois plays —- installation du paquet
vault(dépôt YUM HashiCorp), dépôt du TLS, génération devault.hcl, drop-in systemd KMS, ouverture firewalld (8200/8201), démarrage du service ; - initialisation (
vault operator init -key-shares=5 -key-threshold=3, une seule fois sur le premier nœud) puis déblocage (vault operator unseal -migrate, harmless en unseal normal, indispensable seulement le temps d'une migration de seal Shamir → awskms) ; - exécution du script de setup PKI (
run_once, sortieno_log).
Procédure manuelle¶
Trois familles d'actions réalisées directement contre l'environnement AWS ou le cluster Vault vivant, absentes du Terraform/Ansible versionné.
1. Prérequis AWS pour l'auto-unseal (hors Terraform)¶
La clé KMS et l'IAM user utilisés par seal "awskms" ne sont pas
créés par ce dépôt ni par le projet Cloud AWS — provisionnés
manuellement par l'opérateur AWS, credentials transmis séparément :
- Vérifier si un alias KMS dédié existe déjà (
aws kms list-aliases) avant d'en créer un nouveau — un aliasvault-unsealpeut préexister d'un provisioning antérieur. - Créer un utilisateur IAM dédié à cet usage unique (ne pas réutiliser un utilisateur IAM existant pour un autre usage AWS de l'infra).
- Attacher une policy strictement scopée à l'ARN de cette clé
(
kms:Encrypt,kms:Decrypt,kms:DescribeKey— pasResource: "*"). - Fournir la région, l'ARN de la clé et les credentials au playbook via
group_vars/vault.yml(chiffré ansible-vault).
2. Correction du SAN TLS des nœuds (ajout de vault.infra.indio)¶
Les certificats ansible/files/tls/indserv0NN.crt ont été générés avant
que le nom vault.infra.indio n'existe. Le listener HAProxy faisant du TCP
passthrough, un client qui se connecte via ce nom voit directement le
certificat du nœud — sans ce SAN, la validation TLS échoue côté client
malgré un certificat par ailleurs valide.
# CSR + re-signature avec la CA bootstrap existante (clé privée déjà
# présente localement, pas de nouvelle CA ni nouvelle paire de clés)
openssl req -new -key indservXXX.key -out indservXXX.csr \
-subj "/CN=indservXXX.infra.indio" \
-addext "subjectAltName=DNS:indservXXX.infra.indio,DNS:vault.infra.indio,DNS:localhost,IP:10.100.2.14X"
openssl x509 -req -in indservXXX.csr -CA vault-bootstrap-ca.crt -CAkey vault-bootstrap-ca.key \
-CAcreateserial -out indservXXX.crt -days 825 -extfile <(echo "subjectAltName=...")
Déployé nœud par nœud (ansible-playbook site.yml --limit INDSERVxxx,
jamais tous en même temps — pas de serial: dans le playbook). Toujours se
connecter via vault.infra.indio, jamais l'IP de la VIP : celle-ci n'est
dans aucun SAN.
3. Configuration Vault pour les consommateurs PKI de l'AD (RDP/KDC)¶
Réalisée en API directe sur le cluster vivant, absente de
vault_pki_setup.sh. Détail exhaustif (avec chacun des blocages réels
rencontrés) dans le README.md de
Durcissement Active Directory ; résumé ici du côté Vault
uniquement :
# Rôles PKI dédiés (le rôle serveur "infra-indio" existant suffit au RDP serveur)
vault write pki_int/roles/infra-indio-kdc server_flag=true client_flag=true ext_key_usage=ServerAuth,ClientAuth ...
vault write pki_int/roles/infra-indio-smartcard client_flag=true ext_key_usage=ClientAuth,SmartCardLogon ...
# Policies sign-only (jamais pki_int/issue/*)
vault policy write pki-sign-rdp - <<< 'path "pki_int/sign/infra-indio" { capabilities = ["create","update"] }'
vault policy write pki-sign-kdc - <<< 'path "pki_int/sign/infra-indio-kdc" { capabilities = ["create","update"] }'
vault policy write pki-sign-smartcard - <<< 'path "pki_int/sign/infra-indio-smartcard" { capabilities = ["create","update"] }'
# Authentification Kerberos/SPNEGO (pas de secret partagé)
vault auth enable kerberos
vault write auth/kerberos/config keytab=@svc-vault-kerb.keytab service_account=svc-vault-kerb add_group_aliases=true
vault write auth/kerberos/config/ldap url="ldaps://<DC>:636" userattr=sAMAccountName \
groupfilter="(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))" groupattr=cn
vault write sys/auth/kerberos/tune passthrough_request_headers="Authorization"
# Mapping identity group AD -> policy (noms localisés FR de ce domaine)
vault write identity/group-alias name="Vault-KDC-Cert-Issuers" mount_accessor=<kerberos accessor> canonical_id=<...>
Points durement appris à connaître avant de rejouer cette configuration
ailleurs (détail complet dans le README ad-hardening) : upndomain doit
rester vide sur le mount LDAP (un POST omettant le champ ne le
réinitialise pas — merge partiel de l'API) ; passthrough_request_headers
est obligatoire sur le mount kerberos (Vault Core ne transmet pas
Authorization par défaut) ; les groupes intégrés « Contrôleurs de
domaine »/« Ordinateurs du domaine » sont des primary groups implicites,
invisibles à LDAP_MATCHING_RULE_IN_CHAIN — nécessite des groupes AD dédiés
avec appartenance explicite.
Procédure de déploiement¶
terraform apply— provisionne les 3 VM surseg-service.ansible-playbook site.yml— installe et configure Vault sur les 3 nœuds, initialise puis débloque le cluster Raft (auto-unseal awskms dès le premier boot sivault.hclest déjà configuré en conséquence — plus besoin de migration manuelle pour un déploiement neuf), active KV-v2 et la PKI (racine/intermédiaire/CRL/OCSP).- Les clés de déblocage (5, seuil 3, valables comme recovery keys en mode
auto-unseal) et le root token générés par
vault operator initsont écrits uniquement dans/root/.vault_init.jsonsur le nœud de contrôle Ansible (hors dépôt git, mode 0600) — à sécuriser/archiver séparément selon la procédure du coffre-fort de secrets Indio, jamais versionnés. - Prérequis AWS (Procédure manuelle §1) — à faire avant l'étape 2 pour un
déploiement strictement dans l'ordre, ou en migration a posteriori sur un
cluster déjà en Shamir pur (
vault operator unseal -migrate). - Intégrations consommatrices (RDP/KDC/carte à puce AD — Procédure manuelle §3) — configuration à réaliser directement sur le cluster vivant après l'étape 2.
Contrôle de santé / Vérification¶
# État individuel de chaque nœud (l'unseal ne se propage PAS entre nœuds Raft)
curl -sk https://10.100.2.146:8200/v1/sys/health
curl -sk https://10.100.2.147:8200/v1/sys/health
curl -sk https://10.100.2.148:8200/v1/sys/health
# sealed:false attendu sur les 3 ; un seul retourne 200 (le leader), les
# standby retournent 429 (comportement normal, cf. diagramme topologie)
# Toujours via le nom DNS, jamais l'IP de la VIP (absente de tout SAN)
curl -sk https://vault.infra.indio:8200/v1/sys/health
# Composition du cluster Raft
VAULT_ADDR=https://vault.infra.indio:8200 vault operator raft list-peers
# OCSP de la PKI applicative
openssl ocsp -issuer indio-intermediate-ca.pem -cert <certificat-a-verifier.pem> \
-url https://vault.infra.indio:8200/v1/pki_int/ocsp -CAfile indio-root-ca.pem
# -> "Response verify OK" / "good"
# Test d'émission (rôle générique, ne consomme pas pki_int/issue en usage réel)
vault write pki_int/issue/infra-indio common_name=test.infra.indio ttl=1h
Points d'attention¶
Aucune valeur sensible dans cette page
Ni token root, ni clé de déblocage, ni mot de passe, ni clé privée TLS
n'ont été lus ou reproduits pour documenter ce composant
(group_vars/vault.yml, .vault_init.json et les fichiers *.key du
dépôt Ansible sont volontairement exclus de cette analyse). Ces valeurs
sont gérées séparément (coffre-fort de secrets Indio / fichiers non
versionnés).
Historique : deux incidents de cluster scellé avant l'auto-unseal (07-12)
Avant l'activation de l'auto-unseal AWS KMS, le cluster a été retrouvé
scellé sur les 3 nœuds à deux reprises (probable redémarrage VM sans
reprise d'unseal manuel, cause racine du redémarrage non investiguée).
Symptôme trompeur observé : la VIP HAProxy semblait en panne TLS
(unexpected eof while reading dès le Client Hello) alors que le port
TCP restait ouvert — en réalité HAProxy marquait les 3 backends DOWN
(Vault renvoie 503 quand scellé) et coupait la connexion immédiatement.
Réflexe de diagnostic : toujours tester .../v1/sys/health
directement sur un nœud Vault avant de suspecter HAProxy. C'est cette
récurrence qui a motivé l'auto-unseal du 2026-07-15.
- Auto-unseal AWS KMS actif depuis le 2026-07-15 : a mis fin aux
incidents ci-dessus. Vérifié end-to-end (redémarrage d'un nœud standby
sans fournir aucune clé Shamir →
Sealed: falseen moins de 5s). - TCP passthrough HAProxy : toujours utiliser le nom DNS
vault.infra.indio(jamais l'IP de la VIP) pour toute connexion cliente — l'IP de la VIP n'apparaît dans aucun SAN de certificat de nœud, la validation TLS échoue si on s'y connecte directement. - Deux CA à ne pas confondre : la CA bootstrap (TLS du cluster Vault
lui-même) et la PKI applicative
pki/pki_int(ce que Vault émet pour le reste de l'infra). Un déploiement de confiance côté client (ex. AD) doit distribuer les deux chaînes, pas seulement l'une des deux. - Principe sign-only pour les consommateurs externes : les intégrations
(ex. AD) envoient un CSR déjà généré localement à
pki_int/sign/<role>; Vault ne génère et ne transmet jamais de clé privée pour ces usages, et aucun endpointpki_int/issue/*ne doit être exposé à ces intégrations. - Les URLs AIA/CDP/OCSP (
pki_int/config/urls) sont embarquées dans chaque certificat au moment de l'émission, pas relues dynamiquement — un changement de cette config (ex. passage d'une IP brute àvault.infra.indio, cf. incident documenté dans Durcissement Active Directory) n'a aucun effet sur les certificats déjà émis ; seul un réenrôlement forcé les met à jour. - Les fichiers
ansible/files/tls/*.keyet la CA bootstrap constituent du matériel sensible présent dans le dépôt Ansible — à exclure de toute republication ou export de ce dépôt en dehors de son usage opérationnel normal. - La clé KMS et l'IAM user d'auto-unseal appartiennent au même compte AWS que le bucket de tfstate/archivage — voir Cloud AWS pour le contexte complet (deux IAM users distincts par usage, jamais partagés).