diff --git a/services/anki-sync-server/default.nix b/services/anki-sync-server/default.nix index 1cd9945..2a998e7 100644 --- a/services/anki-sync-server/default.nix +++ b/services/anki-sync-server/default.nix @@ -4,19 +4,68 @@ # standalone depuis le repo ankitects/anki (même version que le desktop Anki, # 25.09.4 dans la nixpkgs épinglée). Zéro Python. # -# Écoute en loopback (127.0.0.1:27701), exposé uniquement via le blueprint -# Pangolin/newt sur anki.marcpartensky.com. PAS de SSO Pangolin : les clients -# Anki (desktop, AnkiMobile, AnkiDroid) ne savent pas suivre une redirection -# navigateur ; l'authentification repose sur le couple utilisateur/mot de passe -# du serveur Anki lui-même (SYNC_USER), transmis en HTTPS par le reverse proxy. +# Le serveur écoute en loopback (127.0.0.1:27701) et ne sert AUCUNE page web +# (404 vide sur /, ce qui veut dire « rien » dans un navigateur). Une façade +# nginx en loopback (127.0.0.1:27702) sert une page de statut sur / et proxifie +# les routes de sync (/sync/*, /msync/*, /health) vers 27701. Le blueprint +# Pangolin/newt expose la façade sur anki.marcpartensky.com. +# PAS de SSO Pangolin : les clients Anki (desktop, AnkiMobile, AnkiDroid) ne +# savent pas suivre une redirection navigateur ; l'authentification repose sur +# le couple utilisateur/mot de passe du serveur Anki lui-même (SYNC_USER), +# transmis en HTTPS par le reverse proxy. # # Mot de passe du compte "marc" dans secrets/anki.yml (sops). Le module nixpkgs # lit passwordFile via LoadCredential systemd (jamais en clair dans le store). { config, lib, + pkgs, ... -}: { +}: let + syncPort = 27701; # port du serveur Anki (loopback) + proxyPort = 27702; # port de la façade nginx (loopback) + statusPage = pkgs.stdenv.mkDerivation { + pname = "anki-sync-status"; + version = "1"; + src = pkgs.writeTextFile { + name = "anki-sync-status.html"; + text = '' + + + + + + Anki Sync Server + + + +
+

Anki Sync Server

+ en ligne +

Ce domaine expose uniquement l'API de synchronisation Anki pour les clients de bureau et mobiles. Il n'y a pas d'interface web à afficher ici.

+

Dans Anki (préférences, onglet synchronisation), renseignez https://anki.marcpartensky.com comme serveur de sync.

+

Santé du service : GET /health

+
+ + + ''; + }; + dontUnpack = true; + installPhase = '' + runHook preInstall + mkdir -p $out + cp $src $out/index.html + ''; + meta.description = "Page de statut du serveur de sync Anki"; + }; +in { sops.secrets."anki-sync-server/password" = { key = "password"; sopsFile = ../../secrets/anki.yml; @@ -34,12 +83,47 @@ ]; }; + # Façade nginx, loopback uniquement : page de statut sur / (le serveur Anki + # n'a pas d'UI), proxy des routes de sync vers 27701. Timeouts longs (grosses + # collections), pas de limite de taille de corps (uploads de médias : images, + # vidéos). + services.nginx.virtualHosts."anki-sync-status" = { + listen = [ + { + addr = "127.0.0.1"; + port = proxyPort; + } + ]; + extraConfig = "client_max_body_size 0;"; + locations = { + "/sync" = { + proxyPass = "http://127.0.0.1:${toString syncPort}"; + extraConfig = '' + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + ''; + }; + "/msync" = { + proxyPass = "http://127.0.0.1:${toString syncPort}"; + extraConfig = '' + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + ''; + }; + "/health" = { + proxyPass = "http://127.0.0.1:${toString syncPort}"; + }; + "/" = { + root = statusPage; + }; + }; + }; + # Ressource publique Pangolin (newt, site "tower") : anki.marcpartensky.com - # -> 127.0.0.1:27701. Cle `proxy-resources` + champ `protocol` = schema - # réellement compris par le Pangolin du VPS (cf. services/newt). Pas d'auth - # Pangolin (clients natifs). Pas de healthcheck http : le serveur de sync ne - # sert aucun GET 2xx (les routes /sync/* et /msync/* sont du POST), donc un - # check sur "/" le marquerait unhealthy et Pangolin le sortirait du LB. + # -> 127.0.0.1:27702 (façade nginx). Cle `proxy-resources` + champ + # `protocol` = schema réellement compris par le Pangolin du VPS (cf. + # services/newt). Pas d'auth Pangolin (clients natifs). Pas de healthcheck + # déclaré : la façade répond 200 sur /, donc le check par défaut passe. services.newt.blueprint.proxy-resources."anki-sync-server" = { name = "anki-sync-server"; protocol = "http"; @@ -48,17 +132,19 @@ targets = [ { hostname = "127.0.0.1"; - port = config.services.anki-sync-server.port; + port = proxyPort; method = "http"; } ]; }; - # Garde-fou : le port local ne doit jamais être ouvert au firewall/LAN. + # Garde-fou : les ports locaux ne doivent jamais être ouverts au + # firewall/LAN. assertions = [ { - assertion = !(lib.elem config.services.anki-sync-server.port config.networking.firewall.allowedTCPPorts); - message = "anki-sync-server : le port local ne doit pas être ouvert au firewall (exposition uniquement via Pangolin/newt)."; + assertion = !(lib.elem syncPort config.networking.firewall.allowedTCPPorts) + && !(lib.elem proxyPort config.networking.firewall.allowedTCPPorts); + message = "anki-sync-server : les ports locaux ne doivent pas être ouverts au firewall (exposition uniquement via Pangolin/newt)."; } ]; } \ No newline at end of file