Security and Access FR - Kheopsian/Hydranos GitHub Wiki

🇬🇧 English · 🇫🇷 Français

Sécurité et accès

S'applique à Hydranos 4.4.

Cette page s'adresse à quiconque fait tourner Hydranos à un endroit que d'autres peuvent joindre : sur un réseau local, derrière un reverse proxy, ou avec d'autres machines enrôlées comme nœuds. Elle explique comment Hydranos contrôle chaque requête et quelles adresses répondent sans clé. Elle explique aussi comment récupérer un mot de passe perdu ou faire la rotation de la clé API, et comment exposer l'interface web sans la livrer à des inconnus.

Un compte admin, une clé API

Hydranos a exactement deux identifiants, tous deux stockés dans le fichier de configuration (/config/default.toml sauf si vous passez --config ; sous Windows, default.toml à côté de hydranos.exe, ou, s'il n'y en a pas, un \config\default.toml laissé par la 4.3, sans rien dire : son avis est affiché avant qu'aucune console soit attachée, voir Installer sous Windows) :

Identifiant Où Ce que c'est
Compte admin [auth] username, [auth] password_hash Un seul utilisateur pour l'interface web. Le mot de passe est stocké sous forme de hash bcrypt (coût 12, la valeur par défaut de la bibliothèque, qu'il vienne de l'écran de premier démarrage, de Changer le mot de passe (Change password) ou de hydranos reset-password). Le mot de passe fait au moins 8 caractères partout. Il n'y a pas d'autres utilisateurs ni de rôles.
Clé API [daemon] api_key L'unique secret pour l'interface web, l'API REST, le shim qBittorrent et le serveur MCP.

La clé API est générée au démarrage quand api_key est vide ou contient encore la valeur d'exemple publiée change-me-in-production. La nouvelle clé fait 48 caractères hexadécimaux (24 octets aléatoires) et est réécrite sur place dans le TOML, donc vos commentaires sont préservés. Si le fichier ne peut être ni lu ni écrit, rien n'est remplacé, et le journal indique « the API stays closed ». Ce n'est vrai que pour une clé vide : elle reste vide et toutes les requêtes authentifiées sont refusées, puisqu'une clé vide n'autorise personne. Une clé d'exemple reste change-me-in-production et continue de fonctionner, donc quiconque connaît la valeur publiée entre. Dans les deux cas, corrigez les permissions du fichier et redémarrez.

L'interface web affiche la clé à un seul endroit : Config › Paramètres avancés (default.toml) (Advanced settings (default.toml)) › Général (General) › api_key, avec les boutons ↻ (générer) et ⧉ (copier). La même page liste les lignes [auth] sous le sous-onglet Other, password_hash compris. GET /api/settings renvoie tout le TOML à quiconque détient la clé, clé API et password_hash compris. Les mots de passe de proxy ([proxy] socks5_pass, et socks5_outbound_pass dans [race], [hoard] ou la session d'un moteur) et la clé API de gluetun y valent ********, comme dans les routes de l'onglet Réseau (Network) (GET /api/network/mode, GET /api/network/egress). Un enregistrement qui renvoie le masque garde la valeur enregistrée.

Bug connu en 4.4 : tous les secrets ne sont pas masqués — une URL announce_proxy qui contient user:pass ([race], [hoard] ou la session d'un moteur) est renvoyée en clair par GET /api/settings — considérez que la clé API livre aussi cet identifiant.

Les fichiers WireGuard gérés font exception : chaque .conf de fournisseur est écrit dans <data dir>/wireguard/ (dossier en mode 0700) avec le mode 0600 dès le premier octet, et l'API ne renvoie jamais ses clés. La liste ne donne que le nom du fichier, l'adresse, l'endpoint et la clé publique du pair (WireGuard géré). Sous Windows, les modes ne s'appliquent pas, et WireGuard géré n'y tourne de toute façon pas.

Comment les requêtes sont authentifiées

Appelant Ce qu'il envoie Comment il l'obtient
Interface web en-tête X-Api-Key (et ?apikey= sur les flux en direct et les envois de formulaire) POST /api/login avec le nom d'utilisateur et le mot de passe admin renvoie la clé dans son corps JSON ; il ne pose aucun cookie. Le navigateur garde la clé dans localStorage sous hydra_api_key.
Scripts, clients REST en-tête X-Api-Key: YOUR_API_KEY, ou ?apikey=YOUR_API_KEY Copiée depuis le fichier de configuration ou l'onglet Config.
Sonarr, Radarr, autobrr, cross-seed (shim qBittorrent) cookie de session SID POST /api/v2/auth/login, la seule route qui pose ce cookie, avec soit le nom d'utilisateur et le mot de passe admin, soit la clé API saisie comme mot de passe (n'importe quel nom d'utilisateur).
Clients MCP Authorization: Bearer YOUR_API_KEY (les formes en-tête, paramètre de requête et cookie fonctionnent aussi) Copiée depuis la configuration.
Un nœud en cours d'enrôlement jeton d'enrôlement à usage unique POST /api/nodes/register uniquement ; voir Nœuds et identifiants ci-dessous.

Règles qui s'appliquent à tous :

  • La clé est comparée en temps constant. L'en-tête l'emporte sur le paramètre de requête.
  • Un cookie SID déverrouille toute l'API native, pas seulement /api/v2. Les sessions vivent uniquement en mémoire. Une session expire après 1 heure sans requête (chaque requête la prolonge), il en existe 256 au plus (la plus ancienne est supprimée), et un redémarrage les termine toutes.
  • /api/login et /api/v2/auth/login n'ont ni limitation de débit ni verrouillage. Un mauvais nom d'utilisateur et un mauvais mot de passe reçoivent la même réponse.

Ces chemins répondent sans aucun identifiant :

Chemin Ce qu'il révèle ou fait
GET /health statut, version, durée de fonctionnement, et par moteur : identifiant, nombre de torrents, s'il écoute, s'il est retenu par start_paused ou bloqué par le kill switch (avec la raison)
GET /metrics toutes les séries de Métriques Prometheus : identifiants de moteur, nombres de torrents, débits, mémoire, la version (hydra_build_info), et le nom d'hôte de chaque tracker auquel vous annoncez (hydra_tracker_announces_total{engine,tracker,result}, hydra_tracker_errors_last_hour{engine,tracker,class})
GET /api/startup totaux de torrents, par moteur pendant leur chargement
GET /api/setup, POST /api/setup état du premier démarrage et création du compte (protégée, voir la section suivante)
POST /api/login, POST /api/v2/auth/login, POST /api/v2/auth/logout connexion, et déconnexion du shim
GET /, GET /static/…, GET /changelog.md les fichiers de l'interface web et le changelog
GET /install.sh le script d'enrôlement des nœuds (aucun secret dedans)
POST /api/nodes/register exige un jeton d'enrôlement valide à la place de la clé
POST /api/auth/api-key/confirm exige la clé en attente d'une rotation à la place de la clé (voir Rotation de la clé API)

Toutes les autres routes passent par le contrôle ci-dessous, POST /mcp compris. GET /mcp et DELETE /mcp évitent le contrôle mais ne renvoient rien : ils répondent toujours 405 avec Allow: POST.

flowchart TD
    R["Requête entrante"] --> P{"Chemin public ?<br/>/health, /metrics, /, /static/*, /changelog.md,<br/>/install.sh, /api/setup, /api/login, /api/startup,<br/>/api/nodes/register, /api/auth/api-key/confirm,<br/>/api/v2/auth/login, /api/v2/auth/logout"}
    P -->|oui| H["Le handler s'exécute<br/>setup, les deux logins, register et confirm<br/>font leurs propres contrôles"]
    P -->|non| E{"api_key vide dans la config ?"}
    E -->|oui| X["Refusée"]
    E -->|non| K{"En-tête X-Api-Key ou paramètre apikey<br/>égal à la clé ?"}
    K -->|oui| OK["Autorisée"]
    K -->|non| C{"Cookie SID d'une session active ?"}
    C -->|oui| OK
    C -->|non| M{"Chemin /mcp et<br/>Authorization Bearer égal à la clé ?"}
    M -->|oui| OK
    M -->|non| X
    X --> W{"Quel chemin ?"}
    W -->|"/api/v2/..."| F["403, corps Forbidden."]
    W -->|"/mcp"| B["401 avec WWW-Authenticate Bearer"]
    W -->|"/api/selection/*, /api/network/egress"| V["401, unauthorized"]
    W -->|autre| U["401, Invalid or missing API key"]
Loading

Le shim répond 403 plutôt que 401 exprès ; le commentaire du code en donne la raison : Sonarr et Radarr interrogent /api/v2/app/webapiVersion avant de se connecter et attendent le 403 de qBittorrent (comportement tiers, non vérifié ici). L'interface web rouvre sa fenêtre de connexion à chaque 401.

Il n'y a pas de prise en charge de CORS : Hydranos n'envoie aucun en-tête Access-Control-*, donc une page web d'une autre origine ne peut pas lire les réponses de son API.

La fenêtre de configuration au premier démarrage

Tant que [auth] password_hash ne contient pas de hash, l'interface web affiche Bienvenue dans Hydranos (Welcome to Hydranos) et Créer le compte (Create account). POST /api/setup crée le compte (mot de passe d'au moins 8 caractères, un nom d'utilisateur vide devient admin) et renvoie la clé API. Elle répond 409 dès qu'un compte existe. Tant que les moteurs se chargent, le routeur de démarrage ne route que GET /api/setup, donc un POST est refusé : créez le compte une fois l'écran de démarrage disparu.

La route de configuration ne répond qu'à un appelant local, jugé ainsi :

  1. L'adresse du pair TCP décide d'abord. Si ce n'est ni le loopback ni un réseau privé, la requête est refusée, quels que soient ses en-têtes.
  2. Si ce pair est privé et que la requête porte X-Forwarded-For (un reverse proxy sur votre réseau local), la première adresse de cet en-tête doit être privée elle aussi.
  3. Un pair privé sans X-Forwarded-For compte comme local.

« Privé » signifie 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 et 169.254.0.0/16 en IPv4 ; ::1, fc00::/7 et fe80::/10 en IPv6. Un appelant refusé reçoit 403 first-run setup only answers a local caller; run `hydranos reset-password <password>` on the host.

Deux conséquences découlent du code (non testées) :

  • Un client sur une adresse CGNAT ou Tailscale (100.64.0.0/10) n'est pas privé et est refusé. Utilisez plutôt hydranos reset-password sur l'hôte (Changer ou récupérer le mot de passe).
  • Tout ce qui remet à Hydranos une connexion depuis une adresse privée sans ajouter X-Forwarded-For rend chaque appelant local : un reverse proxy qui ne pose pas l'en-tête, un NAT ou une redirection de port qui réécrit la source. Créez le compte avant que le port soit joignable de l'extérieur, et posez X-Forwarded-For dans votre proxy (l'exemple nginx ci-dessous le fait).

Si vous devez configurer une machine distante, gardez l'interface sur loopback et passez par un tunnel :

[daemon]
api_host = "127.0.0.1"
ssh -L 8199:127.0.0.1:8199 [email protected]
# then open http://localhost:8199 on your own computer

Sous Docker, ne touchez pas à api_host (le loopback à l'intérieur du conteneur est injoignable depuis l'hôte) et publiez plutôt le port sur le loopback de l'hôte : "127.0.0.1:8199:8199/tcp".

Quand la configuration répond 403, faites-la depuis le réseau local sur le port 8199, ou suivez son message : hydranos reset-password <password> sur l'hôte écrit le mot de passe du compte dans le fichier (recette ci-dessous). Elle ne touche pas à [auth] username, que le modèle livré met à admin.

Changer ou récupérer le mot de passe

Connecté : Config › Compte (Account) › Changer le mot de passe (Change password) (« Définit [auth] password_hash (bcrypt). Vous restez connecté. »), tapez le nouveau mot de passe (texte indicatif Nouveau mot de passe (min. 8)), cliquez sur Modifier (Change). POST /api/auth/password refuse moins de 8 caractères (400 password must be at least 8 characters ; la page affiche Mot de passe trop court (8 minimum). avant l'envoi). Sinon, elle écrit le hash bcrypt dans [auth] password_hash de default.toml et recharge la configuration : l'ancien mot de passe est refusé dès la connexion suivante, sans redémarrage. La clé API ne change pas, donc ce navigateur, les *arr et tous les autres clients restent connectés.

Bloqué dehors : hydranos reset-password. Sur l'hôte, avec le binaire de l'installation :

# Docker (config at /config/default.toml in the container; add the path if you changed HYDRANOS_CONFIG_DIR)
docker exec hydranos hydranos reset-password 'your-new-password'
docker restart hydranos

# Linux, packaged layout
sudo /opt/hydranos/hydranos reset-password 'your-new-password' /etc/hydranos/default.toml
sudo systemctl restart hydranos

La commande exige au moins 8 caractères (hydranos: the password must be at least 8 characters, code de sortie 2), écrit le hash dans [auth] password_hash, vérifie que le fichier se lit encore (sinon … would not parse after the edit; nothing written), affiche hydranos: password set in <path>. Restart Hydranos for it to apply. et se termine. Sans chemin, elle modifie /config/default.toml (Windows : default.toml à côté de hydranos.exe, ou le \config\default.toml laissé par une installation 4.3 quand il n'y en a pas). Le démon en cours garde l'ancien hash jusqu'à son redémarrage. username et api_key ne changent pas. Le mot de passe est un argument de ligne de commande : il reste dans l'historique de votre shell et apparaît dans ps pendant l'exécution. Plus de détails : Exploiter Hydranos.

Sous Windows, hydranos.exe est un programme fenêtré et, d'après le code, la sortie de reset-password et de hash-password n'atteint pas la console (non testé sous Windows). reset-password écrit quand même le fichier, donc préférez-la là : quittez Hydranos depuis la zone de notification, lancez hydranos.exe reset-password "your-new-password" dans le dossier de hydranos.exe, redémarrez Hydranos et connectez-vous.

Procédure A : écrire un nouveau hash. Tout hash bcrypt avec le préfixe $2b$, $2y$, $2a$ ou $2x$ est accepté. hydranos hash-password '<password>' en affiche un (8 caractères minimum), tout comme des outils courants :

# Hydranos itself
hydranos hash-password 'your-new-password'

# Apache's htpasswd (package apache2-utils / httpd-tools); prints a $2y$ hash
htpasswd -nbBC 12 "" 'your-new-password' | tr -d ':\n'; echo

# or Python with the bcrypt package (pip install bcrypt); prints a $2b$ hash
python3 -c 'import bcrypt,sys; print(bcrypt.hashpw(sys.argv[1].encode(), bcrypt.gensalt(12)).decode())' 'your-new-password'

Sous Windows, htpasswd et tr ne sont pas disponibles et hash-password n'affiche rien de visible : utilisez reset-password ci-dessus, la procédure B ci-dessous (seul le Bloc-notes est nécessaire), ou installez Python depuis python.org et exécutez ceci dans PowerShell ou une invite de commandes (cela affiche un hash $2b$) :

py -m pip install bcrypt
py -c "import bcrypt,sys; print(bcrypt.hashpw(sys.argv[1].encode(), bcrypt.gensalt(12)).decode())" "your-new-password"

Ensuite, au choix :

  • Toujours connecté : ouvrez Config › Paramètres avancés (default.toml) › Other, collez le hash dans password_hash, cliquez sur Enregistrer les réglages (Save settings). La connexion suivante l'utilise immédiatement. Le bandeau demande un redémarrage complet, mais le mot de passe n'en a pas besoin.
  • Bloqué dehors : arrêtez Hydranos, mettez password_hash = "$2b$12$…" sous [auth] dans default.toml, redémarrez-le. Les modifications à la main ne sont lues qu'au démarrage.

[auth] username se change des deux mêmes façons. La connexion le compare exactement (sensible à la casse).

Procédure B : repartir de zéro. Arrêtez Hydranos, mettez password_hash = "" dans default.toml (n'importe quel éditeur de texte, Bloc-notes compris), démarrez-le. L'écran de premier démarrage revient et vous créez à nouveau le compte. La clé API ne change pas, donc l'automatisation continue de fonctionner. Tant que le hash est vide, la fenêtre de configuration décrite plus haut est de nouveau ouverte, avec le même risque d'exposition. Ne videz jamais password_hash depuis l'onglet Config sur une instance joignable : la modification s'applique immédiatement.

Quiconque détient la clé API garde un accès complet pendant tout cela, y compris les connexions au shim avec la clé comme mot de passe.

Rotation de la clé API

  1. Choisissez la nouvelle clé :
    • dans l'interface : Config › Paramètres avancés (default.toml) › Général › api_key, cliquez sur ↻, puis Enregistrer les réglages ;
    • ou dans le fichier : mettez votre propre valeur (par exemple issue de openssl rand -hex 24), ou mettez api_key = "" pour en obtenir une générée, puis redémarrez.
  2. Une clé enregistrée depuis l'interface s'applique immédiatement à toute nouvelle requête ; aucun redémarrage n'est nécessaire pour la clé. Le navigateur qui l'a enregistrée met à jour sa propre copie. Les autres navigateurs reçoivent la fenêtre de connexion ; se reconnecter leur donne la nouvelle clé.
  3. Mettez à jour tout ce qui détient l'ancienne clé :
    • les *arr et autobrr, si vous avez saisi la clé comme mot de passe du shim (ceux qui utilisent le mot de passe admin ne sont pas concernés) ;
    • les clients MCP et les scripts ;
    • chaque contrôleur qui a déclaré cette instance comme nœud : sur l'onglet Nodes du contrôleur, Modifier (Edit) le nœud et lui donner la nouvelle clé (PATCH /api/nodes/:name avec api_key ; la clé est testée avant d'être enregistrée).

Un contrôleur peut aussi faire la rotation de la clé d'un nœud pour vous : Changer la clé (Rotate key) sur son onglet Nodes (POST /api/nodes/:name/rotate-key). Il exécute la rotation en deux temps du nœud lui-même, que vous pouvez appeler vous-même sur n'importe quelle instance :

  1. POST /api/auth/api-key/rotate, avec la clé actuelle, répond {"pending_key", "expires_in", "confirm"}. Rien n'est écrit ; la clé actuelle continue de fonctionner. La clé en attente est valable 10 minutes (600 s) et oubliée au redémarrage.
  2. POST /api/auth/api-key/confirm avec la clé en attente dans X-Api-Key l'écrit dans [daemon] api_key. Dès la requête suivante, l'ancienne clé est refusée, et le journal indique API key rotated: the previous key is no longer accepted.

Le contrôleur enregistre la nouvelle clé dès que le nœud a confirmé, et sa réponse affiche la clé une fois, pour les autres clients du nœud (*arr, navigateurs).

Les sessions du shim (cookies SID) déjà émises restent valables après une rotation jusqu'à leur expiration par inactivité ou au redémarrage de Hydranos. Redémarrez si vous avez fait la rotation parce qu'une clé a fuité. Ne videz pas api_key depuis l'onglet Config : une clé vide refuse tout le monde, interface comprise, jusqu'à ce que le prochain redémarrage en génère une nouvelle.

Un redémarrage via l'interface ou POST /api/settings/restart est le même arrêt propre qu'un SIGTERM, suivi du code de sortie 75 (Exploiter Hydranos).

Se déconnecter

Le bouton Se déconnecter (Sign out), l'icône porte-et-flèche à droite de l'en-tête, déconnecte ce navigateur : il termine la session du shim de ce navigateur, s'il en a une (POST /api/v2/auth/logout), supprime la clé de son stockage (hydra_api_key dans localStorage) et recharge la page, qui redemande le mot de passe.

Se déconnecter retire la clé de ce navigateur seulement ; la clé elle-même reste valable, puisque c'est la clé unique de l'instance, partagée avec les *arr. Pour couper tous les navigateurs d'un coup, faites la rotation de la clé API. Les clients du shim peuvent terminer leur propre session avec POST /api/v2/auth/logout.

Exposer Hydranos

Le port 8199 sert du HTTP simple : l'interface web, l'API REST, le shim qBittorrent et /mcp sur un seul port d'écoute (tableau des ports : Installation & premier démarrage). Hydranos n'a pas de TLS propre.

Recommandations, dans l'ordre :

  1. Gardez le 8199 hors d'Internet. Utilisez-le depuis votre réseau local, un VPN entre vos machines, ou un tunnel SSH. Mettez api_host = "127.0.0.1" quand seuls des clients locaux en ont besoin.
  2. Créez d'abord le compte admin (voir La fenêtre de configuration au premier démarrage).
  3. Si vous avez besoin d'un accès distant, placez devant un reverse proxy TLS, à la racine de son propre nom d'hôte (https://hydranos.example.org/). L'interface charge /static/… et appelle /api/… avec des chemins absolus, donc un sous-chemin comme /hydranos/ ne fonctionne pas.
  4. Pensez à ce qui reste public : /metrics, /health, /api/startup et /install.sh ne demandent pas de clé. /metrics nomme chaque tracker auquel vous annoncez (noms d'hôte dans le label tracker) et /health donne vos moteurs, leurs nombres de torrents et l'état de leur kill switch : quiconque les atteint apprend quels trackers vous utilisez. Bloquez-les au niveau du proxy si vous ne voulez pas qu'ils soient joignables, et ne laissez que votre hôte Prometheus collecter /metrics.
  5. Pensez aux journaux : l'interface envoie la clé sous forme de ?apikey= dans certaines requêtes, donc les journaux d'accès du proxy peuvent la contenir.

Un exemple nginx minimal :

server {
    listen 443 ssl;
    server_name hydranos.example.org;
    ssl_certificate     /etc/ssl/hydranos.example.org.crt;
    ssl_certificate_key /etc/ssl/hydranos.example.org.key;

    location / {
        proxy_pass http://192.0.2.10:8199;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;   # overwrite, never append
        proxy_http_version 1.1;
        proxy_buffering off;          # live progress streams
        proxy_read_timeout 1h;
        client_max_body_size 4g;      # Transmission import uploads its folder
        proxy_cookie_flags SID secure;
    }
    location = /metrics { deny all; }
}

Le cookie SID est posé avec Path=/; HttpOnly; SameSite=Lax et sans Secure, délibérément : les clients *arr parlent au shim en HTTP simple sur le réseau local, et un cookie Secure ne leur reviendrait jamais. Derrière TLS, proxy_cookie_flags ajoute l'attribut pour les clients qui passent par le proxy. Faites pointer vos applications *arr vers le port 8199 sur le réseau local plutôt qu'à travers le proxy (réglages : Shim qBittorrent & automatisation).

Si le store ne peut pas être ouvert, Hydranos démarre en mode de secours sur le port d'écoute qu'il a déjà ouvert, donc sur vos api_host et api_port. Il sert alors /health, /, GET /api/setup, GET/POST /api/store/repair et POST /api/settings/restart, tous sans clé (Exploiter Hydranos). Quiconque atteint le port peut lancer la conversion du store et redémarrer Hydranos, donc la même règle s'applique : ne laissez le 8199 joignable que depuis des hôtes de confiance.

Le point d'accès MCP utilise la même clé ; ses deux outils destructeurs sont masqués sauf si [mcp] allow_destructive = true (Serveur MCP).

Nœuds et identifiants

  • Un contrôleur stocke l'URL et la clé API en clair de chaque nœud dans son store (hydra.db, table nodes). Traitez les sauvegardes de hydra.db comme des secrets.
  • L'enrôlement distribue un jeton à usage unique valable 30 minutes. La commande d'enrôlement utilise du http:// simple, donc le jeton puis la clé du nœud circulent en clair. Enrôlez et faites tourner les nœuds sur un réseau privé ou un VPN. Pour du HTTPS, placez le nœud derrière un reverse proxy et déclarez-le avec son URL https://.
  • Le nœud envoie sa propre clé au contrôleur ; il ne reçoit jamais la clé du contrôleur.
  • Le bouton Open de l'onglet Nodes vous connecte à un nœud en passant la clé du nœud dans le fragment de l'URL (#key=…). La page stocke la clé, puis la retire de la barre d'adresse.

Enrôlement et matrice des actions entre nœuds : Nœuds & plusieurs machines.

Signaler une vulnérabilité

Utilisez un avis de sécurité privé sur github.com/Kheopsian/Hydranos (onglet Security), pas une issue publique. Seule la dernière release reçoit des correctifs.

Voir aussi

Installation & premier démarrage · Shim qBittorrent & automatisation · Serveur MCP · Nœuds & plusieurs machines · Config : démon, auth & global · Supervision & journaux · Métriques Prometheus · WireGuard géré · Dépannage · Limites connues de la 4.4

⚠️ **GitHub.com Fallback** ⚠️