[Unit] Description=MaBibli — gestion de bibliothèque personnelle Documentation=https://git.akbar.nohost.me/mathieu/mabibli After=network.target [Service] Type=simple User=__APP__ Group=__APP__ # WorkingDirectory n'est PAS décoratif ici. ASP.NET Core déduit son *content root* # du répertoire courant, et c'est sous ce content root qu'il cherche `wwwroot/`, # c'est-à-dire tout le client Blazor WebAssembly. Vérifié en local : lancé depuis un # autre répertoire, le service démarre, répond 200 sur `/`… et journalise # « The WebRootPath was not found », l'application n'étant jamais servie. WorkingDirectory=__INSTALL_DIR__ ExecStart=__INSTALL_DIR__/MaBibli.Api # --------------------------------------------------------------------------- # CONTRAINTE DURE : écoute sur 127.0.0.1 uniquement # --------------------------------------------------------------------------- # L'application fait confiance à l'en-tête `YNH_USER` pour savoir qui la consulte. # Cet en-tête n'est digne de confiance que parce que SSOwat l'écrase à chaque # requête traversant nginx. Un service qui écouterait sur 0.0.0.0 serait joignable # en direct sur le port interne, sans passer par nginx : n'importe qui sur le réseau # pourrait alors forger `YNH_USER: mathieu` et se faire passer pour un membre du # foyer. L'adresse ci-dessous est donc une frontière de sécurité, pas un réglage. # # Ne jamais remplacer par `http://*:__PORT__`, `http://0.0.0.0:__PORT__` # ni `http://+:__PORT__`. Environment=ASPNETCORE_URLS=http://127.0.0.1:__PORT__ Environment=ASPNETCORE_ENVIRONMENT=Production # La base vit dans le répertoire de données, pas à côté du binaire : c'est ce qui # permet à `upgrade` de remplacer tout `install_dir` sans perdre la bibliothèque. # `ConnectionStrings__MaBibli` surcharge la valeur d'`appsettings.json` # (`Data Source=mabibli.db`, relative au content root). # # ⚠️ LES GUILLEMETS SONT OBLIGATOIRES — ne pas les retirer. # `Environment=` découpe sur les espaces. Sans guillemets, cette ligne ne définit pas # une variable mais DEUX : `ConnectionStrings__MaBibli=Data` et # `Source=__DATA_DIR__/mabibli.db`. L'application reçoit alors la chaîne de connexion # `Data` et meurt au démarrage sur : # ArgumentException: Format of the initialization string does not conform to # specification starting at index 0. # Constaté à la première installation réelle (2026-08-18), et reproduit isolément sur # une unité de test : sans guillemets `[Data]`, avec `[Data Source=/tmp/x.db]`. Environment="ConnectionStrings__MaBibli=Data Source=__DATA_DIR__/mabibli.db" # Pas de socket de diagnostic .NET, pas de télémétrie sortante. Environment=DOTNET_EnableDiagnostics=0 Environment=DOTNET_CLI_TELEMETRY_OPTOUT=1 # Certains chemins .NET cherchent un HOME ; le pointer vers le répertoire de données # évite toute tentative d'écriture dans un home inexistant. Environment=HOME=__DATA_DIR__ Restart=on-failure RestartSec=5 TimeoutStopSec=20 StandardOutput=journal StandardError=journal SyslogIdentifier=__APP__ # --------------------------------------------------------------------------- # Durcissement # --------------------------------------------------------------------------- NoNewPrivileges=yes PrivateTmp=yes PrivateDevices=yes DevicePolicy=closed # ⚠️ `yes` ne convient PAS ici, et c'est contre-intuitif. # Le `data_dir` de YunoHost vit sous `/home/yunohost.app/__APP__`. `ProtectHome=yes` # rend `/home` inaccessible et vide dans le namespace du service : la base devient # introuvable, et le démarrage meurt sur « SQLite Error 14: unable to open database # file ». `ReadWritePaths=` ne suffit pas à percer cette protection — mesuré sur une # unité de test : `yes` + `ReadWritePaths` échoue, `tmpfs` + `BindPaths` réussit. # # `tmpfs` conserve l'essentiel du bénéfice : les répertoires personnels du serveur # restent invisibles au service, seul son propre répertoire de données est remonté. ProtectHome=tmpfs BindPaths=__DATA_DIR__ ProtectHostname=yes ProtectClock=yes ProtectControlGroups=yes ProtectKernelModules=yes ProtectKernelTunables=yes ProtectKernelLogs=yes ProtectProc=invisible RestrictNamespaces=yes RestrictRealtime=yes RestrictSUIDSGID=yes LockPersonality=yes RemoveIPC=yes SystemCallArchitectures=native # Tout le système en lecture seule, sauf le répertoire de données. `install_dir` # appartient à root : le service ne peut donc pas réécrire ses propres binaires. ProtectSystem=strict ReadWritePaths=__DATA_DIR__ # AF_INET/AF_INET6 restent nécessaires : le service écoute en TCP local *et* appelle # les API BnF et OpenLibrary en sortie. Pour cette raison, pas de `IPAddressDeny=any`. RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 # Aucune capacité : le port alloué par YunoHost est > 1024, donc même # CAP_NET_BIND_SERVICE est inutile. CapabilityBoundingSet= AmbientCapabilities= # Liste reprise de radarr_ynh, autre application .NET packagée sans Docker, donc # éprouvée sur ce runtime. SystemCallFilter=~@clock @debug @module @mount @obsolete @reboot @setuid @swap # ⚠️ Ne PAS ajouter MemoryDenyWriteExecute=yes : le JIT de .NET a besoin de pages # exécutables inscriptibles, le service ne démarrerait pas. [Install] WantedBy=multi-user.target