# Faire tourner le mot de passe de la base de données sans interruption : ce que vous repoussez depuis longtemps

Tout le monde est d'accord pour dire que le mot de passe de la base de données devrait tourner. Presque personne ne le fait, et la raison n'est pas la paresse. C'est que la première fois que vous essayez, quelque chose casse : un pool de connexions qui garde l'ancien mot de passe, une Lambda qui l'a mis en cache au démarrage à froid, un job d'arrière-plan qui l'a lu dans une variable d'environnement posée au déploiement. Alors la rotation est annulée, un ticket est ouvert, et le mot de passe reste le même pendant deux ans.

Le nôtre tourne tous les trente jours, automatiquement, et l'application ne s'en aperçoit pas. Voici le mécanisme, qui est surtout celui d'AWS, et les trois changements dans l'application qui l'ont rendu sûr, qui sont les nôtres.

## Pourquoi la rotation naïve casse des choses

La rotation avec un seul utilisateur de base de données se déroule ainsi : générer un nouveau mot de passe, `ALTER USER app PASSWORD 'new'`, mettre à jour le secret. Entre le `ALTER` et le moment où chaque consommateur a relu le secret, toute *nouvelle* connexion avec l'ancien mot de passe est refusée. Les connexions existantes survivent, parce que Postgres authentifie à la connexion, mais un pool qui ouvre une nouvelle connexion pendant cette fenêtre échoue, et un service qui ne lit le secret qu'au démarrage échoue à chaque reconnexion jusqu'à ce qu'il soit redémarré.

La fenêtre peut durer des secondes si tout relit rapidement. Elle peut durer des heures si quelque chose met en cache. En pratique, c'est « jusqu'au prochain déploiement », parce que c'est là que les variables d'environnement sont rafraîchies, et c'est ça la panne.

## Utilisateurs alternés : la rotation qui n'invalide jamais un mot de passe en usage

La stratégie de rotation multi-utilisateurs de Secrets Manager utilise deux utilisateurs de base de données, `app` et `app_clone`, avec des droits identiques. À tout moment, l'un est l'utilisateur *courant* dans le secret et l'autre est inactif. La rotation change le mot de passe de l'*inactif*, le teste, puis bascule le secret vers lui. L'utilisateur qui était courant, dont chaque consommateur pourrait tenir le mot de passe, n'est pas touché jusqu'à la rotation suivante trente jours plus tard, et d'ici là chaque consommateur a relu le secret de nombreuses fois.

<div class="article-figure">
<svg viewBox="0 0 900 250" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Chronologie de la rotation à utilisateurs alternés. Jour 0 : le secret pointe vers l'utilisateur app avec le mot de passe P1 ; app_clone a le mot de passe P0, inactif. Jour 30, rotation : la Lambda de rotation met le mot de passe d'app_clone à P2, teste une connexion, puis bascule le secret vers app_clone. Les consommateurs qui tiennent app P1 continuent de fonctionner ; les nouvelles connexions utilisent app_clone P2. Jour 60 : la rotation met le mot de passe d'app à P3 et rebascule. À aucun moment un mot de passe qu'un consommateur pourrait tenir n'est invalidé.">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="20" y="24" fill="#f1f3ff" font-size="14" font-weight="700">Utilisateurs alternés · le mot de passe en usage n'est jamais celui qu'on change</text>
<line x1="60" y1="120" x2="860" y2="120" stroke="#2a3150" stroke-width="2"/>
<text x="60" y="150" text-anchor="middle" fill="#9aa3c7" font-size="11">jour 0</text><text x="460" y="150" text-anchor="middle" fill="#9aa3c7" font-size="11">jour 30 · rotation</text><text x="860" y="150" text-anchor="middle" fill="#9aa3c7" font-size="11">jour 60 · rotation</text>
<rect x="60" y="60" width="400" height="22" rx="4" fill="#4fffb0" opacity="0.7"/><text x="260" y="75" text-anchor="middle" fill="#0d1120" font-size="11" font-weight="700">secret → app · P1 · chaque consommateur utilise ceci</text>
<rect x="60" y="88" width="400" height="22" rx="4" fill="#2a3150"/><text x="260" y="103" text-anchor="middle" fill="#9aa3c7" font-size="11">app_clone · P0 · inactif</text>
<rect x="460" y="60" width="400" height="22" rx="4" fill="#2a3150"/><text x="660" y="75" text-anchor="middle" fill="#9aa3c7" font-size="11">app · P1 · toujours valide, inactif, aucune connexion ne casse</text>
<rect x="460" y="88" width="400" height="22" rx="4" fill="#4fffb0" opacity="0.7"/><text x="660" y="103" text-anchor="middle" fill="#0d1120" font-size="11" font-weight="700">secret → app_clone · P2 · les nouvelles connexions utilisent ceci</text>
<line x1="460" y1="50" x2="460" y2="125" stroke="#ffd166" stroke-width="2" stroke-dasharray="5,3"/>
<text x="460" y="176" text-anchor="middle" fill="#ffd166" font-size="11">1 · mettre le mot de passe d'app_clone à P2   2 · tester la connexion   3 · basculer le secret</text>
<text x="450" y="206" text-anchor="middle" fill="#9aa3c7">Un consommateur qui tient P1 fonctionne encore 30 jours. D'ici là, il a relu le secret de nombreuses fois.</text>
<text x="450" y="228" text-anchor="middle" fill="#9aa3c7">Le jour 60 fait la même chose en sens inverse : app reçoit P3, le secret rebascule.</text>
</g>
</svg>
</div>

En CDK, sur un cluster dont les identifiants viennent d'un secret généré, ce sont quelques lignes :

```ts
cluster.addRotationMultiUser('Rotation', {
  secret: appUserSecret,                 // le secret de l'utilisateur 'app', avec masterarn défini
  automaticallyAfter: Duration.days(30),
  vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
```

CDK déploie la Lambda de rotation d'AWS depuis le Serverless Application Repository dans le VPC, la relie au secret et la planifie. La Lambda doit atteindre à la fois la base de données (elle est dans le VPC) et Secrets Manager (un [interface endpoint ou un NAT](/fr/blog/nat-gateway-the-most-expensive-line-you-do-not-see) ; nous utilisons l'endpoint). L'utilisateur `app_clone` est créé par la Lambda à la première rotation s'il n'existe pas, avec les mêmes droits qu'`app`, et c'est la partie qui mérite d'être vérifiée à la main : si `app` reçoit des droits ajoutés plus tard par une migration, `app_clone` en a besoin aussi. Nous gérons cela en accordant les droits à un *rôle* dont les deux utilisateurs sont membres, de sorte que les grants sont faits une seule fois.

## Les trois changements dans l'application

Le mécanisme de rotation est sûr par construction. L'application doit tout de même lire le *nouveau* secret à un moment donné, et la façon dont elle le fait décide si la rotation est invisible ou une panne au ralenti.

**1. Lire le secret à la connexion, pas au démarrage.** La fabrique de connexions du pool appelle Secrets Manager (via un cache avec un TTL de cinq minutes) chaque fois qu'elle ouvre une connexion. Un service à longue durée de vie récupère un nouveau secret dans les cinq minutes suivant une bascule, sans redémarrage. Les `runtimeEnvironmentSecrets` d'App Runner injectent la valeur au démarrage de l'instance, ce qui convient pour un service redéployé chaque semaine et ne convient pas pour un qui tourne un mois, donc pour les identifiants de base de données spécifiquement, nous lisons depuis Secrets Manager dans le code plutôt que depuis l'environnement.

**2. Sur une erreur d'authentification, relire une fois et réessayer.** Ceinture et bretelles : si une tentative de connexion échoue avec `28P01` (mot de passe invalide), invalider le cache, récupérer à nouveau le secret et réessayer une fois. Cela couvre le cas où le TTL du cache n'a pas expiré au moment exact de la bascule. Ce sont dix lignes et elles se sont déclenchées en production exactement autant de fois que nous avons fait tourner : environ une fois par mois, en silence, dans les logs.

```ts
async function connect(): Promise<Client> {
  try {
    return await open(await creds.get());
  } catch (e) {
    if (isAuthError(e)) { creds.invalidate(); return open(await creds.get()); }
    throw e;
  }
}
```

**3. Laisser RDS Proxy s'en charger pour les Lambdas.** Les fonctions dans le VPC se connectent via RDS Proxy, et le proxy s'authentifie auprès de la base avec le secret lui-même : il surveille Secrets Manager et suit les rotations tout seul. Les fonctions s'authentifient auprès du *proxy* avec IAM, donc elles ne détiennent jamais de mot de passe de base de données. La rotation, pour elles, est un non-événement par conception. Cela règle aussi le problème des tempêtes de connexions que les Lambdas ont avec Postgres, qui est [la raison pour laquelle nous recommanderions le proxy de toute façon](/fr/blog/aurora-serverless-v2-review).

<div class="article-figure">
<svg viewBox="0 0 900 220" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Deux chemins de consommateurs. Service App Runner : le pool lit le secret depuis Secrets Manager via un cache de 5 minutes à chaque nouvelle connexion, et sur une erreur d'authentification invalide le cache et relit une fois. Fonctions Lambda : elles s'authentifient auprès de RDS Proxy avec IAM et ne détiennent aucun mot de passe ; le proxy lit le secret lui-même et suit les rotations. Les deux chemins atteignent Aurora. La Lambda de rotation change le mot de passe de l'utilisateur inactif tous les 30 jours.">
<defs><marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#4fffb0"/></marker></defs>
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<rect x="15" y="30" width="200" height="70" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="115" y="54" text-anchor="middle" fill="#f1f3ff" font-weight="700">service App Runner</text><text x="115" y="72" text-anchor="middle" fill="#9aa3c7" font-size="11">secret à la connexion · cache 5 min</text><text x="115" y="90" text-anchor="middle" fill="#9aa3c7" font-size="11">erreur d'auth → relire une fois</text>
<rect x="15" y="130" width="200" height="70" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="115" y="154" text-anchor="middle" fill="#f1f3ff" font-weight="700">fonctions Lambda</text><text x="115" y="172" text-anchor="middle" fill="#9aa3c7" font-size="11">auth IAM auprès du proxy</text><text x="115" y="190" text-anchor="middle" fill="#9aa3c7" font-size="11">ne détiennent jamais de mot de passe</text>
<rect x="330" y="30" width="200" height="70" rx="10" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="430" y="54" text-anchor="middle" fill="#4fffb0" font-weight="700">Secrets Manager</text><text x="430" y="72" text-anchor="middle" fill="#9aa3c7" font-size="11">app / app_clone · AWSCURRENT</text><text x="430" y="90" text-anchor="middle" fill="#9aa3c7" font-size="11">Lambda de rotation tous les 30 jours</text>
<rect x="330" y="130" width="200" height="70" rx="10" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="430" y="154" text-anchor="middle" fill="#f1f3ff" font-weight="700">RDS Proxy</text><text x="430" y="172" text-anchor="middle" fill="#9aa3c7" font-size="11">lit le secret lui-même</text><text x="430" y="190" text-anchor="middle" fill="#9aa3c7" font-size="11">suit les rotations · met les connexions en pool</text>
<rect x="680" y="80" width="200" height="70" rx="10" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="780" y="104" text-anchor="middle" fill="#f1f3ff" font-weight="700">Aurora</text><text x="780" y="122" text-anchor="middle" fill="#9aa3c7" font-size="11">utilisateurs app et app_clone</text><text x="780" y="140" text-anchor="middle" fill="#9aa3c7" font-size="11">mêmes droits via un rôle</text>
<line x1="217" y1="65" x2="328" y2="65" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrR)"/>
<line x1="217" y1="165" x2="328" y2="165" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrR)"/>
<line x1="430" y1="102" x2="430" y2="128" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrR)"/>
<path d="M217,80 C300,115 560,115 678,110" fill="none" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrR)"/>
<line x1="532" y1="165" x2="678" y2="125" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrR)"/>
</g>
</svg>
</div>

## La première rotation, en staging, chronomètre en main

Nous ne l'avons pas activée en production d'abord. En staging, nous avons déclenché une rotation manuellement (`aws secretsmanager rotate-secret`), regardé le log de la Lambda, et fait tourner un générateur de charge contre l'API pendant tout ce temps. Résultats du premier essai :

| Moment | Ce qui s'est passé |
|---|---|
| T+0 s | La rotation démarre ; la Lambda définit le mot de passe d'`app_clone`, le teste, bascule le secret |
| T+4 s | Bascule terminée. Chaque connexion existante toujours OK (elles tiennent `app`) |
| T+0 à T+300 s | Les nouvelles connexions du pool App Runner utilisent encore les identifiants `app` en cache. Toujours OK, parce qu'`app` est encore valide |
| T+300 s | Le cache expire ; la prochaine nouvelle connexion récupère `app_clone`. Ça marche |
| T+5 min à T+30 jours | Les deux utilisateurs valides. Aucun consommateur ne peut être pris de court |

Zéro erreur dans le générateur de charge. La seule chose qui a échoué, c'est un cron dans un autre compte qui avait le mot de passe dans une variable d'environnement : il a continué de fonctionner trente jours (son utilisateur était encore valide) puis a cassé à la *deuxième* rotation, ce qui est exactement l'échec différé que la stratégie alternée produit pour les consommateurs qui ne relisent pas. C'est tout l'intérêt de le faire en staging d'abord. Le cron lit maintenant le secret.

## La checklist

- Rotation multi-utilisateurs, jamais mono-utilisateur. L'utilisateur de base de données supplémentaire est gratuit et fait toute la différence.
- Les deux utilisateurs reçoivent leurs droits via un rôle partagé, pour qu'une migration qui accorde des droits au rôle couvre les deux.
- La Lambda de rotation vit dans le VPC et a besoin d'un chemin vers Secrets Manager : interface endpoint ou NAT.
- Chaque consommateur lit le secret à la connexion via un cache à TTL court, ou passe par RDS Proxy avec IAM.
- Relire une fois sur `28P01`.
- Première rotation en staging, déclenchée manuellement, sous charge, avec le log ouvert.
- Puis grepper chaque variable d'environnement, `.env`, cron et script à la recherche de l'ancien mot de passe. Ce qui l'a encore cassera dans trente à soixante jours. Mieux vaut le trouver maintenant.

Rotation à trente jours, six mois, six rotations, zéro incident. Le mot de passe que personne n'a lu a changé six fois et personne ne s'en est aperçu, ce qui est exactement ce à quoi un secret est censé ressembler.

Si le mot de passe de votre base de données a une date d'anniversaire, [nous pouvons le faire tourner en une journée](/contact), staging d'abord.
