fix(services): anki page de statut + proxy nginx (interface navigateur sur le domaine)

This commit is contained in:
Hermes Agent
2026-10-09 17:59:00 -04:00
parent 917ebe40d3
commit 9ec071dd67
+101 -15
View File
@@ -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 = ''
<!doctype html>
<html lang='fr'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Anki Sync Server</title>
<style>
body{background:#1e1e2e;color:#cdd6f4;font-family:system-ui,-apple-system,Segoe UI,sans-serif;margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center}
.card{background:#181825;border:1px solid #585b70;border-radius:12px;padding:2rem 2.5rem;max-width:34rem}
h1{color:#ffffff;font-size:1.35rem;margin:0 0 .6rem}
.badge{display:inline-block;background:#2a2a3d;color:#b4befe;border:1px solid #b4befe;border-radius:999px;padding:.15rem .6rem;font-size:.75rem;margin-bottom:1rem}
p{color:#a6adc8;line-height:1.55;font-size:.95rem;margin:.6rem 0}
code{background:#2a2a3d;color:#f1cae1;padding:.05rem .35rem;border-radius:4px;font-size:.85rem}
</style>
</head>
<body>
<div class='card'>
<h1>Anki Sync Server</h1>
<span class='badge'>en ligne</span>
<p>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.</p>
<p>Dans Anki (préférences, onglet <code>synchronisation</code>), renseignez <code>https://anki.marcpartensky.com</code> comme serveur de sync.</p>
<p>Santé du service : <code>GET /health</code></p>
</div>
</body>
</html>
'';
};
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).";
}
];
}