Skip to content

🐛 [BUG] - Race condition sur les secrets Vault lors du provisionnement concurrent (symptîme : SONAR_PASSWORD = 'not initialized') — server-nestjs #2402

Description

@KepoParis

Description

Sur le nouveau serveur server-nestjs, il arrive de façon intermittente que le secret Vault SONAR d'un projet contienne SONAR_PASSWORD: 'not initialized' au lieu du vrai mot de passe. Avec l'ancien plugin plugins/sonarqube, ce problÚme n'apparaßt jamais.

La valeur 'not initialized' n'est pas un bug en soi : c'est un repli lĂ©gitime lorsque l'utilisateur SonarQube existe mais que son secret Vault a rĂ©ellement disparu (SonarQube ne permet pas de relire un mot de passe). Le problĂšme est que, dans le nouveau serveur, cette valeur est Ă©crite alors qu'un mot de passe valide vient d'ĂȘtre créé — il y a donc perte de donnĂ©es.

Cause : une race condition, pas un bug de chemin. La logique et le chemin Vault (<PROJECTS_ROOT_DIR>/<slug>/SONAR) sont identiques entre l'ancien plugin et le nouveau serveur. La différence est la concurrence :

  • l'Ă©vĂ©nement project.upsert est Ă©mis depuis de nombreux endroits (project, project-roles, environment, deployment, project-hooks
) ;
  • certaines Ă©missions se font sans await (fire-and-forget, ex. deployment.service.ts:98, environment.service.ts:76), donc deux rĂ©conciliations du mĂȘme projet peuvent se chevaucher ;
  • rien ne sĂ©rialise le traitement par projet (emitAsync n'a aucun verrou par projet).

ensureUser effectue une sĂ©quence lecture → vĂ©rification → Ă©criture non atomique sur le secret. Sur un projet fraĂźchement créé, deux exĂ©cutions concurrentes se dĂ©roulent ainsi :

Étape Run A Run B
1 lit le secret → 404 lit le secret → 404
2 utilisateur absent —
3 crĂ©e l'utilisateur + Ă©crit le vrai mot de passe —
4 — utilisateur dĂ©sormais trouvĂ© (créé par A), lecture initiale = 404 → branche else
5 — Ă©crit SONAR_PASSWORD: 'not initialized' ❌ Ă©crase le mot de passe de A

Le rĂ©sultat dĂ©pend de l'ordre d'exĂ©cution → comportement non dĂ©terministe (« parfois »).

⚠ PortĂ©e plus large. SonarQube n'est que le symptĂŽme le plus visible. Le vrai sujet est l'absence de sĂ©rialisation par projet dans le nouveau modĂšle de gestion des plugins : tout plugin faisant un cycle lecture-modification-Ă©criture sur un secret Vault (ou toute autre ressource) est exposĂ© au mĂȘme type de race lors de provisionnements concurrents. À traiter idĂ©alement de façon transverse.

Etapes de reproduction

# Reproduction déterministe (illustre la branche de code fautive)
1. Provisionner un projet (secret SONAR créé avec un vrai mot de passe)
2. Supprimer manuellement tous les secrets Vault du projet
   (l'utilisateur SonarQube, lui, n'est PAS supprimé)
3. Reprovisionner le projet
4. Constater SONAR_PASSWORD = 'not initialized' :
   le secret est lu en 404, mais l'utilisateur existe dĂ©jĂ  → branche de repli

# Note : cette reproduction dĂ©clenche la mĂȘme branche sur l'ancien plugin.
# Ce qui est propre au nouveau serveur, c'est d'atteindre cette branche
# SANS suppression manuelle, via une race condition (perte de données) :

# Reproduction de la régression (intermittente)
5. DĂ©clencher plusieurs provisionnements rapprochĂ©s d'un mĂȘme projet neuf
   (création du projet + ajout d'un rÎle / environnement / déploiement quasi simultanés)
6. Laisser les événements project.upsert se chevaucher
7. Constater par intermittence SONAR_PASSWORD = 'not initialized'
   alors qu'un vrai mot de passe venait d'ĂȘtre Ă©crit

Piste technique (oĂč se trouve l'erreur & suggestions)

SymptÎme localisé dans apps/server-nestjs/src/modules/sonarqube/sonarqube.service.ts, méthode ensureUser (lecture ligne 201, branche else ligne 213-216, écriture ligne 220).

Suggestions, du plus ciblé au plus transverse :

  1. Écriture non destructrice : ne jamais Ă©craser un SONAR_PASSWORD valide existant. Relire le secret juste avant d'Ă©crire, ou n'Ă©crire 'not initialized' qu'aprĂšs confirmation que le secret est rĂ©ellement absent (idĂ©alement via un check-and-set / CAS Vault).
  2. SĂ©rialisation par projet : introduire un verrou/mutex par slug autour de la rĂ©conciliation d'un projet, afin que deux project.upsert du mĂȘme projet ne se chevauchent pas.
  3. Transverse (recommandĂ©) : traiter la sĂ©rialisation par projet au niveau du bus d'Ă©vĂ©nements / gestionnaire de plugins (app-events.service.ts), plutĂŽt que plugin par plugin — cela protĂšge l'ensemble des plugins (GitLab, Nexus, Registry, Vault
) contre la mĂȘme classe de bug. Corollaire : les Ă©missions project.upsert en fire-and-forget (deployment.service.ts:98, environment.service.ts:76) devraient passer par ce mĂ©canisme sĂ©rialisĂ©.

Version de la console impactée

v9.23.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions