From 1b605861865045ec9295dc769f3a293e72ce2cfe Mon Sep 17 00:00:00 2001 From: jsboige Date: Thu, 3 Sep 2026 07:28:04 +0200 Subject: [PATCH] fix(comfyui-qwen,#14382): credential API par fichier, plus d'interpolation compose Le credential ComfyUI-Login (bcrypt $2b$12$Iv..., compare litteralement au fichier bind-mounte) ne doit jamais traverser l'interpolation d'env du compose : docker compose interprete les '$' des valeurs et le conteneur recevait un token tronque (60c -> 57c, mesure ; warning "Iv is not set"). Le compose interpolait en outre des noms que render_envs.py ne gere pas -- vides sur un .env rendu par le seul render (#14382), copies non gerees qui driftent a la rotation (pattern #6901) -- et le sidecar idle-monitor recevait le mot de passe formulaire (RAW) comme bearer -> polls 401. - compose : lignes env credential supprimees ; idle-monitor bind-mounte .secrets/qwen-api-user.token en lecture seule - comfyui_idle_monitor.py : --token fallback fichier /secrets/ - workspace/install_comfyui.sh : PASSWORD copie du fichier monte (l'echo de l'env ecrivait le token tronque), erreur si absent - render_envs.py : COMFYUI_AUTH_TOKEN dans SECRET_KEYS + ALIASES (= COMFYUI_API_TOKEN, pattern HF) -- nom canonique notebook #16 gere depuis master, un AUTH perime non vide masquait le bon fallback - auth_manager.py : n'ecrit plus BEARER/RAW dans les .env (fin du second producteur) - .env.example + docs/genai/secrets-management.md : section source unique, procedure de rotation, retrait du brut des .env geres Co-Authored-By: Claude-Code --- .../services/comfyui-qwen/.env.example | 8 ++++--- .../services/comfyui-qwen/docker-compose.yml | 12 ++++++++-- .../comfyui-qwen/workspace/install_comfyui.sh | 10 ++++++++- .../services/shared/comfyui_idle_monitor.py | 22 +++++++++++++++++-- docs/genai/secrets-management.md | 21 ++++++++++++++++-- scripts/genai-stack/core/auth_manager.py | 6 +++-- scripts/secrets/render_envs.py | 13 +++++++++-- 7 files changed, 78 insertions(+), 14 deletions(-) diff --git a/docker-configurations/services/comfyui-qwen/.env.example b/docker-configurations/services/comfyui-qwen/.env.example index 849c45b03a..78633d5498 100644 --- a/docker-configurations/services/comfyui-qwen/.env.example +++ b/docker-configurations/services/comfyui-qwen/.env.example @@ -51,9 +51,11 @@ COMFYUI_URL=http://localhost:8188 COMFYUI_USERNAME=admin COMFYUI_PASSWORD=your_secure_password_here -# Token Bearer pour l'accès API (optionnel) -# Si non spécifié, un token sera généré automatiquement -COMFYUI_BEARER_TOKEN=your_bearer_token_here +# Credential ComfyUI-Login (meme valeur que .secrets/qwen-api-user.token, +# propage par scripts/secrets/render_envs.py depuis master.env #14382). +# Le compose NE l'interpole PAS (les '$' du bcrypt seraient tronques) : +# l'auth passe par le fichier bind-mounté, cette ligne sert au render. +COMFYUI_API_TOKEN=your_comfyui_api_token_here # Configuration du mode invité (true/false) # Si true, les utilisateurs non authentifiés peuvent accéder en mode invité diff --git a/docker-configurations/services/comfyui-qwen/docker-compose.yml b/docker-configurations/services/comfyui-qwen/docker-compose.yml index cd3e247b13..1bdd631d91 100644 --- a/docker-configurations/services/comfyui-qwen/docker-compose.yml +++ b/docker-configurations/services/comfyui-qwen/docker-compose.yml @@ -72,7 +72,6 @@ services: - COMFYUI_LOGIN_ENABLED=${COMFYUI_LOGIN_ENABLED:-true} - COMFYUI_USERNAME=${COMFYUI_USERNAME} - COMFYUI_PASSWORD=${COMFYUI_PASSWORD} - - COMFYUI_BEARER_TOKEN=${COMFYUI_BEARER_TOKEN} - GUEST_MODE_ENABLED=${GUEST_MODE_ENABLED:-false} - SECRET_KEY=${SECRET_KEY} # Configuration comportement @@ -120,12 +119,21 @@ services: - COMFYUI_URL=http://comfyui-qwen:8188 - IDLE_TIMEOUT=${IDLE_TIMEOUT:-1200} - CHECK_INTERVAL=${CHECK_INTERVAL:-60} - - COMFYUI_AUTH_TOKEN=${COMFYUI_RAW_TOKEN} - COMFYUI_USERNAME=${COMFYUI_USERNAME:-admin} - COMFYUI_PASSWORD=${COMFYUI_PASSWORD} - LOG_LEVEL=${LOG_LEVEL:-INFO} - TZ=${TZ:-Europe/Paris} + volumes: + # Credential ComfyUI-Login : par fichier, JAMAIS par env compose -- + # l'interpolation compose mange les '$' des bcrypt ($2b$12$Iv... -> le + # conteneur recevrait un token tronque, #14382). Le monitor lit le + # fichier en fallback si COMFYUI_AUTH_TOKEN n'est pas defini. + - type: bind + source: ../../../.secrets/qwen-api-user.token + target: /secrets/qwen-api-user.token + read_only: true + working_dir: /app command: ["python", "comfyui_idle_monitor.py"] diff --git a/docker-configurations/services/comfyui-qwen/workspace/install_comfyui.sh b/docker-configurations/services/comfyui-qwen/workspace/install_comfyui.sh index 93f5a49cc9..8f437e1eeb 100644 --- a/docker-configurations/services/comfyui-qwen/workspace/install_comfyui.sh +++ b/docker-configurations/services/comfyui-qwen/workspace/install_comfyui.sh @@ -107,7 +107,15 @@ fi if [ "${COMFYUI_LOGIN_ENABLED:-false}" = "true" ]; then echo "Configuration du token ComfyUI-Login..." mkdir -p custom_nodes/ComfyUI-Login - echo "${COMFYUI_BEARER_TOKEN}" > custom_nodes/ComfyUI-Login/PASSWORD + # Credential depuis le fichier bind-mounté, pas depuis l'env compose : + # l'interpolation compose tronque les '$' du bcrypt ($2b$12$Iv...), + # le PASSWORD serait corrompu (#14382). + if [ -f /workspace/ComfyUI/.secrets/qwen-api-user.token ]; then + cp /workspace/ComfyUI/.secrets/qwen-api-user.token custom_nodes/ComfyUI-Login/PASSWORD + else + echo "ERREUR: /workspace/ComfyUI/.secrets/qwen-api-user.token absent -- impossible de configurer ComfyUI-Login" >&2 + exit 1 + fi chmod 600 custom_nodes/ComfyUI-Login/PASSWORD fi diff --git a/docker-configurations/services/shared/comfyui_idle_monitor.py b/docker-configurations/services/shared/comfyui_idle_monitor.py index a942abecdc..0a94f8a989 100644 --- a/docker-configurations/services/shared/comfyui_idle_monitor.py +++ b/docker-configurations/services/shared/comfyui_idle_monitor.py @@ -21,9 +21,24 @@ import argparse import threading from datetime import datetime +from pathlib import Path from typing import Optional from functools import wraps +# Credential file bind-mounted read-only by the compose (#14382). The token +# never crosses compose env interpolation: the bcrypt form ($2b$12$Iv...) +# gets its '$' runs eaten there and the container would receive a truncated +# bearer (measured: 60c value -> 57c in-container). +_TOKEN_FILE = Path("/secrets/qwen-api-user.token") + + +def _read_token_file() -> Optional[str]: + try: + content = _TOKEN_FILE.read_text(encoding="utf-8").strip() + return content or None + except OSError: + return None + try: import requests except ImportError: @@ -449,8 +464,11 @@ def run_standalone(): ) parser.add_argument( "--token", - default=os.environ.get("COMFYUI_AUTH_TOKEN"), - help="Authentication token (optional, deprecated - use --username/--password)" + default=os.environ.get("COMFYUI_AUTH_TOKEN") or _read_token_file(), + help="Authentication token (optional, deprecated - use --username/--password). " + "Falls back to the bind-mounted /secrets/qwen-api-user.token (#14382) -- " + "the credential must not cross compose env interpolation, which " + "truncates the '$' runs of the bcrypt form." ) parser.add_argument( "--username", diff --git a/docs/genai/secrets-management.md b/docs/genai/secrets-management.md index c82de47f08..f035a66640 100644 --- a/docs/genai/secrets-management.md +++ b/docs/genai/secrets-management.md @@ -54,10 +54,10 @@ python scripts/secrets/render_envs.py --check | Hubs / git | `CIVITAI_TOKEN`, `GITHUB_TOKEN` = `GITHUB_ACCESS_TOKEN` | services + notebooks | Oui | | Client API keys (server↔client) | `WHISPER_API_KEY`, `VLLM_API_KEY`, `TTS_API_KEY`, `QWEN_ASR_API_KEY`, `MUSICGEN_API_KEY`, `DEMUCS_API_KEY`, `FUNASR_API_KEY` | 1 service + notebooks | Moyen | | Qdrant vector DB (client) | `QDRANT_API_KEY` | notebooks RAG / SemanticKernel / Argument_Analysis | Moyen (flip serveur = op inter-repo roo-extensions) | -| Tokens client ComfyUI | `COMFYUI_VIDEO_TOKEN`, `COMFYUI_API_TOKEN` | notebooks → services ComfyUI | Moyen | +| Tokens client ComfyUI | `COMFYUI_VIDEO_TOKEN`, `COMFYUI_API_TOKEN` = `COMFYUI_AUTH_TOKEN` | notebooks → services ComfyUI | Moyen | | Session | `SECRET_KEY` | 1 service | Oui | -**Alias :** `HF_TOKEN`/`HUGGINGFACE_TOKEN` et `GITHUB_TOKEN`/`GITHUB_ACCESS_TOKEN` désignent le MÊME secret sous deux noms (services vs notebooks). Le bootstrap vérifie leur cohérence (abort si divergence). +**Alias :** `HF_TOKEN`/`HUGGINGFACE_TOKEN`, `GITHUB_TOKEN`/`GITHUB_ACCESS_TOKEN` et `COMFYUI_API_TOKEN`/`COMFYUI_AUTH_TOKEN` désignent le MÊME secret sous deux noms (services vs notebooks). Le bootstrap vérifie leur cohérence (abort si divergence). **L'alias ne fabrique PAS le nom manquant — les deux doivent être écrits dans `master.env`.** `ALIASES` (render_envs.py) déclare la paire ; il ne dérive pas `HF_TOKEN` d'un `HUGGINGFACE_TOKEN` présent seul. Un `master.env` qui ne porte qu'un des deux noms laisse l'autre dans la liste `declared SECRET keys are absent from master.env (left untouched in services)` de `--check`, et **aucun consommateur ne le reçoit** — alors que le token existe et est valide. Mesuré le 2026-08-15 : `HUGGINGFACE_TOKEN` présent + `HF_TOKEN` absent, pour **655 sites de code lisant `HF_TOKEN`** contre 144 lisant `HUGGINGFACE_TOKEN`. Remède : écrire les deux lignes dans `master.env`, puis `render_envs.py`. @@ -65,6 +65,23 @@ python scripts/secrets/render_envs.py --check **Un 403 sur un repo *gated* n'est pas un défaut de token.** Discriminer avant d'escalader — repo non-gated **avec** token → `206` (le token lit) ; repo gated **sans** token → `401` ; repo gated **avec** token → `403` = authentifié mais non autorisé, c'est-à-dire licence non acceptée par le compte porteur **ou** scope « read gated repos » absent du token fine-grained. Les deux se règlent sur huggingface.co, pas dans `master.env`. +### ComfyUI-Login (comfyui-qwen) — source unique du credential API (#14382) + +Le middleware ComfyUI-Login valide le bearer **littéralement contre le fichier bind-mounté** `.secrets/qwen-api-user.token` (60 caractères, forme `$2b$` bcrypt — le hash *est* le token partagé). Avant #14382, le compose interpolait des noms que `render_envs.py` ne gérait pas (`COMFYUI_BEARER_TOKEN`, `COMFYUI_RAW_TOKEN`) : sur un `.env` rendu par le seul render, ils résolvent **vide** (mesure #14382) ; là où `auth_manager.py` les avait écrits, ce sont des copies non gérées qui dérivent à la prochaine rotation (pattern incident #6901). Le sidecar idle-monitor recevait en outre `COMFYUI_RAW_TOKEN` (le mot de passe formulaire, pas le bearer) → ses polls d'activité prenaient 401. + +Le câblage depuis #14382 ne fait plus dériver — et ne passe **jamais** par l'interpolation d'env du compose : docker compose **interprète les `$` des valeurs** (le bcrypt `$2b$12$Iv…` devient un token tronqué 60c → 57c, mesuré en conteneur). Le credential passe par le **fichier** (les fichiers bind-mountés ne sont pas interpolés) : + +| Consommateur | Voie | +|---|---| +| middleware ComfyUI-Login (`entrypoint.sh`) | fichier bind-mounté `.secrets/qwen-api-user.token` (déjà le chemin privilégié) | +| `workspace/install_comfyui.sh` → `ComfyUI-Login/PASSWORD` | `cp` du fichier monté (l'`echo` de l'env compose écrivait un token tronqué) | +| sidecar `idle-monitor` (poll `/system_stats`) | fichier bind-mounté `/secrets/qwen-api-user.token`, lu en fallback par `comfyui_idle_monitor.py` | +| notebooks GenAI (`os.getenv("COMFYUI_AUTH_TOKEN") or os.getenv("COMFYUI_API_TOKEN")`, flip #16) | `GenAI/.env`, les deux noms gérés master (alias `COMFYUI_AUTH_TOKEN` = `COMFYUI_API_TOKEN`) | + +**Rotation du credential comfyui-qwen** : éditer `COMFYUI_API_TOKEN` **et** `COMFYUI_AUTH_TOKEN` dans `master.env` (même valeur), régénérer `.secrets/qwen-api-user.token` avec cette valeur, `render_envs.py`, puis **recréer** le container (`docker compose up -d` — un `restart` ne relit ni les montages ni l'env d'interpolation ; cf règle du restart ci-dessous pour `COMFYUI_PASSWORD`). + +`COMFYUI_RAW_TOKEN` (le mot de passe en clair avant hash, utile seulement au login formulaire `COMFYUI_USERNAME`/`COMFYUI_PASSWORD`) **ne transite plus par aucun `.env` géré** — la forme brute ne doit pas vivre dans les `.env` (seule la forme hashée y circule). `auth_manager.py` n'écrit plus `COMFYUI_BEARER_TOKEN`/`COMFYUI_RAW_TOKEN` dans les `.env`. + ### Qdrant — convention client vs serveur (cross-repo) Qdrant expose la **même** clé API sous **deux noms** selon le côté : diff --git a/scripts/genai-stack/core/auth_manager.py b/scripts/genai-stack/core/auth_manager.py index 6d8855a3e2..5543296769 100644 --- a/scripts/genai-stack/core/auth_manager.py +++ b/scripts/genai-stack/core/auth_manager.py @@ -227,11 +227,13 @@ def _update_env_file(self, env_path: Path, raw_token: str, bcrypt_hash: str) -> updated_keys = set() # Mapping des clés à mettre à jour + # NB #14382 : COMFYUI_BEARER_TOKEN / COMFYUI_RAW_TOKEN ne sont plus + # ecrits -- le compose lit ${COMFYUI_API_TOKEN} (cle geree par + # master.env via render_envs.py) et la forme brute ne doit pas vivre + # dans le .env de service. updates = { 'COMFYUI_API_TOKEN': bcrypt_hash, - 'COMFYUI_BEARER_TOKEN': bcrypt_hash, # Alias souvent utilisé 'QWEN_API_USER_TOKEN': bcrypt_hash, # Legacy - 'COMFYUI_RAW_TOKEN': raw_token, 'COMFYUI_PASSWORD': raw_token # Pour login simple } diff --git a/scripts/secrets/render_envs.py b/scripts/secrets/render_envs.py index 632f369072..b4f7620b0a 100644 --- a/scripts/secrets/render_envs.py +++ b/scripts/secrets/render_envs.py @@ -161,8 +161,12 @@ "QDRANT_API_KEY", # OWUI native API (NB-20, #417) + TTS multi-voice gateway (#16, po-2023) "OWUI_API_KEY", "TTS_GATEWAY_API_KEY", - # ComfyUI client tokens (notebook client <-> service must agree) - "COMFYUI_VIDEO_TOKEN", "COMFYUI_API_TOKEN", + # ComfyUI client tokens (notebook client <-> service must agree). + # COMFYUI_AUTH_TOKEN is the canonical notebook-client name (#16 flip: + # notebooks read ``os.getenv("COMFYUI_AUTH_TOKEN") or os.getenv("COMFYUI_API_TOKEN")``). + # Aliased to COMFYUI_API_TOKEN -- both names carry the credential the + # ComfyUI-Login middleware validates (bind-mounted token file, #14382). + "COMFYUI_VIDEO_TOKEN", "COMFYUI_API_TOKEN", "COMFYUI_AUTH_TOKEN", # ComfyUI-Video web login password (user decision #10985, 2026-08-20): # centralized in master.env under an INSTANCE-SCOPED name -- plain # COMFYUI_PASSWORD must NOT enter SECRET_KEYS because comfyui-qwen carries @@ -203,6 +207,11 @@ # ``QWEN_API_USER_TOKEN`` in legacy code paths). Both names MUST # carry the same value; bootstrap enforces this on first sync. #10265. "QWEN_API_USER_TOKEN": "QWEN_API_TOKEN", + # ComfyUI-Login bearer, notebook-client canonical name (#14382): notebooks + # read COMFYUI_AUTH_TOKEN first; must equal COMFYUI_API_TOKEN (the + # credential the middleware validates). A stale non-empty AUTH_TOKEN + # shadows the correct API_TOKEN in the ``or`` fallback chain -> 401. + "COMFYUI_AUTH_TOKEN": "COMFYUI_API_TOKEN", }