Post-mortem : mcp-installer régénérait les tokens à chaque relance

Le bug
mcp-installer est un script bash que j’ai écrit pour automatiser l’installation du mcp-oauth-proxy (FastAPI + nginx + systemd) sur un nouveau host. Workflow standard : clone, run, c’est tout.
Sauf qu’en relançant le script sur un host déjà installé (par exemple pour mettre à jour la version), j’ai découvert un bug d’idempotence : tous les secrets étaient régénérés.
$ sudo ./install.sh
[+] Generating MCP_TOKEN...
[+] Generating CLIENT_ID...
[+] Generating CLIENT_SECRET...
[+] Generating TOKEN_SECRET...
[+] Writing /etc/mcp-oauth-proxy/.env...Si tu as déjà un .env avec des tokens en service, l’installer les écrase. Tous les clients OAuth déjà enregistrés (Claude.ai dans mon cas) se retrouvent avec des credentials invalides. La connexion casse.
Pourquoi c’est arrivé
Le script utilisait cette logique :
MCP_TOKEN=$(openssl rand -hex 32)
CLIENT_ID=$(openssl rand -hex 16)
CLIENT_SECRET=$(openssl rand -hex 32)
TOKEN_SECRET=$(openssl rand -hex 32)
cat > /etc/mcp-oauth-proxy/.env <<EOF
MCP_TOKEN=$MCP_TOKEN
CLIENT_ID=$CLIENT_ID
CLIENT_SECRET=$CLIENT_SECRET
TOKEN_SECRET=$TOKEN_SECRET
EOFLe pattern « génère puis écris » est correct pour une première install. Mais en relance, il ignore complètement le .env existant.
C’est l’erreur classique : l’auteur (moi) a testé le script uniquement sur un host fresh. Le mode “réinstall sur host existant” n’a jamais été testé.
Reproduction du bug
$ sudo ./install.sh
[OK] Installation complete
$ cat /etc/mcp-oauth-proxy/.env | head -1
MCP_TOKEN=a7f3e9d2c4b8a1...
$ sudo ./install.sh
[OK] Installation complete
$ cat /etc/mcp-oauth-proxy/.env | head -1
MCP_TOKEN=8c2f6a1b9e4d7c... ← DIFFÉRENTTest reproductible en 30 secondes. C’est presque comique que je ne l’aie pas vu plus tôt.
La leçon : “idempotent” est un contrat, pas un espoir
Un script d’installation doit être idempotent par défaut. C’est-à-dire : ./install.sh 1 fois, 2 fois, 5 fois → le système est dans le même état stable.
J’avais ce contrat dans ma tête. Je ne l’avais pas dans le code.
Le fix : pre-flight checks + force flag explicite
Version corrigée en v1.2.0 :
preflight_check_existing_install() {
if [ -f /etc/mcp-oauth-proxy/.env ]; then
echo "[!] Existing installation detected at /etc/mcp-oauth-proxy/.env"
echo "[!] Tokens will be PRESERVED (use --force-rotate-tokens to regenerate)"
return 0
fi
return 1
}
generate_or_load_secrets() {
if [ "$FORCE_ROTATE_TOKENS" = "true" ]; then
echo "[!] --force-rotate-tokens : regenerating all secrets"
MCP_TOKEN=$(openssl rand -hex 32)
CLIENT_ID=$(openssl rand -hex 16)
CLIENT_SECRET=$(openssl rand -hex 32)
TOKEN_SECRET=$(openssl rand -hex 32)
elif [ -f /etc/mcp-oauth-proxy/.env ]; then
echo "[+] Loading existing secrets from /etc/mcp-oauth-proxy/.env"
source /etc/mcp-oauth-proxy/.env
else
echo "[+] Generating new secrets..."
MCP_TOKEN=$(openssl rand -hex 32)
CLIENT_ID=$(openssl rand -hex 16)
CLIENT_SECRET=$(openssl rand -hex 32)
TOKEN_SECRET=$(openssl rand -hex 32)
fi
}3 modes désormais explicites :
- Première install (
.envabsent) → génération - Réinstall (
.envprésent) → tokens préservés (sécurité par défaut) - Rotation explicite (
--force-rotate-tokens) → régénération volontaire
Bonus : dry-run
Tant que je modifiais l’installer, j’ai ajouté un --dry-run qui montre ce qui serait fait sans rien modifier :
$ sudo ./install.sh --dry-run
[DRY-RUN] Would create directory /etc/mcp-oauth-proxy/
[DRY-RUN] Would copy oauth_proxy.py to /usr/local/lib/mcp-oauth-proxy/
[DRY-RUN] Would write systemd unit /etc/systemd/system/mcp-oauth-proxy.service
[DRY-RUN] Existing .env detected — tokens would be PRESERVED
[DRY-RUN] Would reload systemd and restart mcp-oauth-proxy.serviceUtile pour vérifier qu’un upgrade ne va rien casser avant de l’exécuter en prod.
Tests reproductibles
Pour éviter une régression similaire, j’ai ajouté un script de test :
#!/bin/bash
# tests/test-idempotence.sh
# Première install
sudo ./install.sh
TOKEN1=$(grep MCP_TOKEN /etc/mcp-oauth-proxy/.env | cut -d= -f2)
# Seconde install (devrait préserver)
sudo ./install.sh
TOKEN2=$(grep MCP_TOKEN /etc/mcp-oauth-proxy/.env | cut -d= -f2)
if [ "$TOKEN1" != "$TOKEN2" ]; then
echo "FAIL: tokens regenerated on second install"
exit 1
fi
# Force rotate (devrait régénérer)
sudo ./install.sh --force-rotate-tokens
TOKEN3=$(grep MCP_TOKEN /etc/mcp-oauth-proxy/.env | cut -d= -f2)
if [ "$TOKEN3" = "$TOKEN1" ]; then
echo "FAIL: --force-rotate-tokens didn't regenerate"
exit 1
fi
echo "PASS"Trois cas couverts. Si quelqu’un (futur moi inclus) modifie generate_or_load_secrets(), ce test attrape la régression.
Lessons learned
1. Tester sur un host pré-existant, pas seulement sur un fresh
C’est la première chose à faire pour valider l’idempotence d’un script. Snapshot VM, run install, snapshot, restore, run install à nouveau, compare l’état.
2. Les secrets méritent un traitement spécial
Tous les autres fichiers (config, scripts, units) peuvent être réécrits en confiance — leur valeur est dans le repo. Les secrets sont uniques et persistants par instance. Les écraser = casser quelque chose en aval.
Règle : tout secret généré par un installer doit avoir un mode “préserve si existe” par défaut, et un flag explicite pour régénérer.
3. Dry-run paye toujours
Le dev d’un mode --dry-run prend ~30 minutes. Le ROI est immédiat dès le premier sudo ./install.sh --dry-run sur un host de prod : tu vois exactement ce qui va se passer avant de t’engager.
Conclusion
Bug trouvé et fixé en v1.2.0 du mcp-installer. Tag GPG-signé, release GitHub publiée. Le dry-run est devenu pour moi une pratique systématique sur tous les scripts qui touchent à /etc/, /usr/local/, ou des secrets.
Le bug a duré ~3 semaines en prod sans qu’aucun tier ne s’en rende compte (j’étais le seul user). Mais si l’installer avait été utilisé par d’autres, ça aurait pu casser plusieurs déploiements en une fois. Le coût d’un fix tardif aurait été beaucoup plus élevé.
Conclusion banale mais vraie : un script “marche” jusqu’à ce qu’on le relance.