Masquer les derniers auteurs
| author | version | line-number | content |
|---|---|---|---|
![]() |
5.1 | 1 | Tableau de bord d'accueil du serveur principal : liens vers les services auto-hébergés avec leur état, et widgets. Il compte quatre pages : l'accueil (##conf.yml##) et trois pages secondaires. |
![]() |
1.1 | 2 | |
![]() |
5.1 | 3 | {{toc/}} |
![]() |
1.1 | 4 | |
![]() |
5.1 | 5 | Les valeurs propres à l'infrastructure sont remplacées par des marqueurs ##<ENTRE_CHEVRONS>## : ##<DOMAINE>##, adresses IP, comptes, mots de passe et clés. |
![]() |
1.1 | 6 | |
![]() |
5.1 | 7 | == Déploiement == |
![]() |
1.1 | 8 | |
![]() |
5.1 | 9 | === Stack Portainer === |
| 10 | |||
![]() |
1.1 | 11 | {{code language="yaml"}} |
| 12 | services: | ||
| 13 | dashy: | ||
| 14 | image: lissy93/dashy:latest | ||
| 15 | container_name: dashy | ||
| 16 | environment: | ||
| 17 | - NODE_ENV=production | ||
| 18 | - UID=1000 | ||
| 19 | - GID=1000 | ||
| 20 | - NODE_TLS_REJECT_UNAUTHORIZED=0 | ||
| 21 | ports: | ||
| 22 | - "4000:8080" | ||
| 23 | volumes: | ||
| 24 | - <APPDATA>/Dashy/user-data:/app/user-data | ||
| 25 | restart: unless-stopped | ||
| 26 | healthcheck: | ||
| 27 | test: ['CMD', 'node', '/app/services/healthcheck'] | ||
| 28 | interval: 90s | ||
| 29 | timeout: 10s | ||
| 30 | retries: 3 | ||
| 31 | start_period: 40s | ||
| 32 | networks: | ||
| 33 | - proxy | ||
| 34 | |||
| 35 | networks: | ||
| 36 | proxy: | ||
| 37 | external: true | ||
| 38 | {{/code}} | ||
| 39 | |||
![]() |
5.1 | 40 | * ##NODE_TLS_REJECT_UNAUTHORIZED=0## : Dashy accepte les certificats auto-signés des services locaux (vérifications d'état, proxy CORS). |
| 41 | * ##user-data## contient toute la configuration. Dashy sert ce dossier à la racine du site. | ||
| 42 | * Le port ##4000## donne un accès direct depuis le LAN, sans SWAG ni Authelia. SWAG n'en a pas besoin : il joint le conteneur par le réseau ##proxy##. | ||
![]() |
1.1 | 43 | |
![]() |
5.1 | 44 | === SWAG === |
![]() |
4.1 | 45 | |
![]() |
5.1 | 46 | ##<APPDATA>/swag/config/nginx/proxy-confs/30-dashy.subdomain.conf## : |
![]() |
4.1 | 47 | |
| 48 | {{code language="nginx"}} | ||
| 49 | server { | ||
| 50 | listen 443 ssl; | ||
| 51 | listen [::]:443 ssl; | ||
![]() |
5.1 | 52 | server_name dash.<DOMAINE>; |
![]() |
4.1 | 53 | |
| 54 | include /config/nginx/ssl.conf; | ||
| 55 | include /config/nginx/snippets/robots-default.conf; | ||
| 56 | |||
| 57 | client_max_body_size 25m; | ||
| 58 | |||
![]() |
5.1 | 59 | # Authelia devant tout le domaine |
![]() |
4.1 | 60 | include /config/nginx/authelia-server.conf; |
| 61 | |||
![]() |
5.1 | 62 | # Upstream Dashy |
![]() |
4.1 | 63 | set $upstream_app dashy; |
| 64 | set $upstream_port 8080; | ||
| 65 | set $upstream_proto http; | ||
| 66 | |||
![]() |
5.1 | 67 | # (facultatif) proxy vers /pico/ |
![]() |
4.1 | 68 | location /pico/ { |
| 69 | include /config/nginx/authelia-location.conf; | ||
| 70 | proxy_pass http://<IP_PICO>/; | ||
| 71 | include /config/nginx/proxy.conf; | ||
| 72 | proxy_set_header Connection close; | ||
| 73 | } | ||
| 74 | |||
![]() |
5.1 | 75 | # API de Dashy (sauvegarde de la config, vérifications d'état) |
![]() |
4.1 | 76 | location ^~ /api/ { |
| 77 | include /config/nginx/authelia-location.conf; | ||
| 78 | include /config/nginx/proxy.conf; | ||
| 79 | include /config/nginx/resolver.conf; | ||
| 80 | proxy_pass $upstream_proto://$upstream_app:$upstream_port; | ||
| 81 | } | ||
| 82 | location = /cors-proxy { | ||
| 83 | include /config/nginx/authelia-location.conf; | ||
![]() |
5.1 | 84 | # Dashy envoie la cible dans ce header |
| 85 | set $target $http_target_url; | ||
| 86 | if ($target = "") { return 400; } | ||
![]() |
4.1 | 87 | |
![]() |
5.1 | 88 | add_header Access-Control-Allow-Origin "*" always; |
| 89 | add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; | ||
| 90 | add_header Access-Control-Allow-Headers "*" always; | ||
| 91 | if ($request_method = OPTIONS) { return 204; } | ||
![]() |
4.1 | 92 | |
![]() |
5.1 | 93 | # TLS (certs auto-signés possibles sur les cibles LAN, ex: Nextcloud) |
| 94 | proxy_ssl_server_name on; | ||
| 95 | proxy_ssl_verify off; | ||
![]() |
4.1 | 96 | |
![]() |
5.1 | 97 | # Par défaut, on ne fait pas confiance à l'input |
| 98 | set $auth_header ""; | ||
| 99 | set $accept_header ""; | ||
| 100 | set $ocs_header ""; | ||
| 101 | set $host_override ""; | ||
![]() |
4.1 | 102 | |
![]() |
5.1 | 103 | access_by_lua_block { |
| 104 | local target = ngx.var.target | ||
![]() |
4.1 | 105 | |
![]() |
5.1 | 106 | local host, port = target:match("^https?://([^/:]+):?(%d*)") |
| 107 | if not host then | ||
| 108 | ngx.status = 400 | ||
| 109 | ngx.header["Content-Type"] = "application/json" | ||
| 110 | ngx.say('{"error":"Target-URL invalide"}') | ||
| 111 | return ngx.exit(400) | ||
| 112 | end | ||
![]() |
4.1 | 113 | |
![]() |
5.1 | 114 | -- Cibles LAN explicitement autorisées (widgets Dashy internes) |
| 115 | local allowed_internal = { ["<IP_SERVEUR>"] = true, ["<IP_ADGUARD>"] = true, ["<IP_PICO>"] = true } | ||
| 116 | local function is_private_ip(h) | ||
| 117 | if h == "localhost" then return true end | ||
| 118 | local a, b = h:match("^(%d+)%.(%d+)%.%d+%.%d+$") | ||
| 119 | if not a then return false end -- pas une IPv4 littérale -> traité comme hôte public | ||
| 120 | a, b = tonumber(a), tonumber(b) | ||
| 121 | if a == 127 or a == 10 or a == 0 then return true end | ||
| 122 | if a == 172 and b >= 16 and b <= 31 then return true end -- 172.16.0.0/12 (docker inclus) | ||
| 123 | if a == 192 and b == 168 then return true end | ||
| 124 | if a == 169 and b == 254 then return true end -- link-local / metadata cloud | ||
| 125 | return false | ||
| 126 | end | ||
![]() |
4.1 | 127 | |
![]() |
5.1 | 128 | -- Anti-SSRF : on bloque tout le LAN/loopback/metadata, sauf les cibles explicitement whitelistées |
| 129 | if is_private_ip(host) and not allowed_internal[host] then | ||
| 130 | ngx.status = 403 | ||
| 131 | ngx.header["Content-Type"] = "application/json" | ||
| 132 | ngx.say('{"error":"Cible interne non autorisée"}') | ||
| 133 | return ngx.exit(403) | ||
| 134 | end | ||
![]() |
4.1 | 135 | |
![]() |
5.1 | 136 | ngx.var.host_override = (port ~= "" and port ~= nil) and (host .. ":" .. port) or host |
| 137 | |||
| 138 | -- Parse le JSON envoyé dans customheaders | ||
| 139 | local cjson = require "cjson.safe" | ||
| 140 | local hdrs = ngx.req.get_headers() | ||
| 141 | local ch = hdrs["customheaders"] | ||
| 142 | if ch then | ||
| 143 | local obj = cjson.decode(ch) | ||
| 144 | if obj then | ||
| 145 | if obj["Authorization"] then ngx.var.auth_header = obj["Authorization"] end | ||
| 146 | if obj["Accept"] then ngx.var.accept_header = obj["Accept"] end | ||
| 147 | if obj["OCS-APIREQUEST"] ~= nil then | ||
| 148 | ngx.var.ocs_header = tostring(obj["OCS-APIREQUEST"]) | ||
![]() |
4.1 | 149 | end |
![]() |
5.1 | 150 | end |
| 151 | end | ||
| 152 | } | ||
![]() |
4.1 | 153 | |
![]() |
5.1 | 154 | # Injecte les vrais headers vers la cible |
| 155 | proxy_set_header Authorization $auth_header; | ||
| 156 | proxy_set_header Accept $accept_header; | ||
| 157 | proxy_set_header OCS-APIREQUEST $ocs_header; | ||
![]() |
4.1 | 158 | |
![]() |
5.1 | 159 | proxy_set_header Host $host_override; |
| 160 | proxy_set_header X-Forwarded-Proto https; | ||
![]() |
4.1 | 161 | |
![]() |
5.1 | 162 | proxy_pass $target; |
| 163 | } | ||
| 164 | |||
| 165 | # Application (SPA Vue en history mode) | ||
![]() |
4.1 | 166 | location / { |
| 167 | include /config/nginx/authelia-location.conf; | ||
| 168 | include /config/nginx/proxy.conf; | ||
| 169 | include /config/nginx/resolver.conf; | ||
| 170 | proxy_pass $upstream_proto://$upstream_app:$upstream_port; | ||
| 171 | |||
![]() |
5.1 | 172 | # --- Fallback SPA : si l’upstream renvoie 404 sur /login, /settings, etc., |
| 173 | # on renvoie l'index de l'app pour laisser Vue router côté client. | ||
![]() |
4.1 | 174 | proxy_intercept_errors on; |
| 175 | error_page 404 = @dashy_fallback; | ||
| 176 | } | ||
| 177 | |||
![]() |
5.1 | 178 | # Cible du fallback : renvoyer l'index de l'app en conservant les en-têtes |
![]() |
4.1 | 179 | location @dashy_fallback { |
| 180 | proxy_set_header X-Original-URI $request_uri; | ||
| 181 | proxy_pass $upstream_proto://$upstream_app:$upstream_port/; | ||
| 182 | } | ||
| 183 | } | ||
| 184 | {{/code}} | ||
| 185 | |||
![]() |
5.1 | 186 | * Authelia protège tout le domaine, y compris les fichiers YAML, qui contiennent les identifiants des widgets. |
| 187 | * ##/cors-proxy## remplace le proxy CORS de Dashy pour les widgets en ##useProxy: true##. Il ne relaie que les en-têtes ##Authorization##, ##Accept## et ##OCS-APIREQUEST##, et refuse les IP privées absentes de ##allowed_internal## : tout nouveau widget qui vise une IP du LAN doit y être ajouté. | ||
| 188 | * ##/pico/## relaie l'interface web du Pico W. | ||
| 189 | * ##@dashy_fallback## renvoie l'application sur les routes internes de Vue (##/login##...), que le serveur de Dashy renvoie en 404. | ||
![]() |
4.1 | 190 | |
![]() |
5.1 | 191 | === Authelia === |
![]() |
4.1 | 192 | |
![]() |
5.1 | 193 | Règle dans la section ##access_control## de la configuration d'Authelia : |
![]() |
1.1 | 194 | |
![]() |
4.1 | 195 | {{code language="yaml"}} |
| 196 | access_control: | ||
| 197 | default_policy: deny | ||
| 198 | rules: | ||
![]() |
5.1 | 199 | - domain: "dash.<DOMAINE>" |
![]() |
4.1 | 200 | policy: one_factor |
| 201 | {{/code}} | ||
| 202 | |||
![]() |
5.1 | 203 | Aucune règle ##bypass## pour ce domaine ne doit la précéder : Authelia applique la première règle qui correspond. |
![]() |
4.1 | 204 | |
![]() |
5.1 | 205 | == Fichiers de configuration == |
![]() |
4.1 | 206 | |
![]() |
1.1 | 207 | Dans ##<APPDATA>/Dashy/user-data## : |
| 208 | |||
![]() |
5.1 | 209 | |=Page Dashy|=Fichier|=Sous-page|=Widgets avec identifiants |
| 210 | |Accueil|##conf.yml##|[[Accueil>>Wiki Batcave John.Infrastructure.ServeurPrincipal.Dashy.Accueil.WebHome]]|##adguard-stats##, ##adguard-filter-status##, ##adguard-top-domains## (compte AdGuard Home) ; ##nextcloud-system## (mot de passe d'application Nextcloud) | ||
| 211 | |bookmarks|##bookmark.yml##|[[Bookmarks>>Wiki Batcave John.Infrastructure.ServeurPrincipal.Dashy.Bookmarks.WebHome]]|— | ||
| 212 | |cpu|##cpu.yml##|[[CPU>>Wiki Batcave John.Infrastructure.ServeurPrincipal.Dashy.CPU.WebHome]]|— | ||
| 213 | |news|##news.yml##|[[News>>Wiki Batcave John.Infrastructure.ServeurPrincipal.Dashy.News.WebHome]]|##flight-data## (clé RapidAPI) ; ##news-headlines## (clé Currents API) | ||
![]() |
1.1 | 214 | |
![]() |
5.1 | 215 | ##item-icons/## contient les icônes locales et l'image de fond. |
![]() |
1.1 | 216 | |
![]() |
5.1 | 217 | === Multi-pages === |
![]() |
4.1 | 218 | |
![]() |
5.1 | 219 | La page d'accueil est ##conf.yml##. Elle déclare les pages secondaires dans ##pages:## : nom affiché dans la barre de navigation et chemin relatif à ##user-data##. |
| 220 | |||
![]() |
4.1 | 221 | {{code language="yaml"}} |
| 222 | pages: | ||
| 223 | - name: bookmarks | ||
| 224 | path: bookmark.yml | ||
| 225 | - name: cpu | ||
| 226 | path: cpu.yml | ||
| 227 | - name: news | ||
| 228 | path: news.yml | ||
| 229 | {{/code}} | ||
| 230 | |||
![]() |
5.1 | 231 | Chaque page secondaire ne contient que ##pageInfo##, ##sections## et la même liste ##pages:##, pour garder la navigation. ##appConfig## n'est lu que dans ##conf.yml## et s'applique à toutes les pages. |
![]() |
4.1 | 232 | |
![]() |
5.1 | 233 | Fichiers présents mais non référencés : ##cpu2.yml## (ancienne copie de ##cpu.yml## à l'indentation cassée), ##conf.yml.bak## et ##bookmark.yml.bak##. |
![]() |
4.1 | 234 | |
![]() |
5.1 | 235 | === Éléments communs === |
![]() |
4.1 | 236 | |
![]() |
5.1 | 237 | ##appConfig## est dans ##conf.yml##, dont le fichier complet est sur la sous-page [[Accueil>>Wiki Batcave John.Infrastructure.ServeurPrincipal.Dashy.Accueil.WebHome]]. |
![]() |
4.1 | 238 | |
![]() |
5.1 | 239 | * Thème ##dracula##, image de fond servie depuis ##item-icons/##. |
| 240 | * ##statusCheck: false## au niveau global : la vérification d'état est activée item par item. | ||
| 241 | * ##faviconApi: allesedv## pour les items en ##icon: favicon##. ##enableFontAwesome: true## pour les icônes ##fas##, ##fab## et ##fad##. Les icônes ##hl-...## viennent du jeu homelab intégré. | ||
| 242 | * ##widgetsAlwaysUseProxy: false## : chaque widget choisit avec ##useProxy## s'il passe par ##/cors-proxy##. Le réglage se met sur le widget : au niveau de la section, il est ignoré. | ||
| 243 | * Authentification : un compte ##admin## défini par son nom et le hash SHA-256 de son mot de passe (##echo -n '<MOT_DE_PASSE>' ~| sha256sum##). ##enableGuestAccess: true## : une fois passé Authelia, on voit les pages sans se connecter à Dashy. ##disableConfigurationForNonAdmin: true## : seul l'admin modifie la configuration. | ||
![]() |
4.1 | 244 | |
![]() |
5.1 | 245 | Vérifier la syntaxe après une modification à la main : |
![]() |
4.1 | 246 | |
![]() |
1.1 | 247 | {{code language="bash"}} |
| 248 | docker exec dashy yarn validate-config | ||
| 249 | {{/code}} | ||
![]() |
4.1 | 250 | |
![]() |
3.1 | 251 | == Statistiques GitHub : github-readme-stats == |
![]() |
1.1 | 252 | |
![]() |
3.1 | 253 | Instance locale de github-readme-stats, qui génère les cartes SVG de statistiques GitHub affichées par un widget de Dashy : statistiques du profil, langages les plus utilisés, dépôts épinglés. |
| 254 | |||
| 255 | {{code language="yaml"}} | ||
| 256 | services: | ||
| 257 | github-readme-stats: | ||
| 258 | image: github-readme-stats:local | ||
| 259 | container_name: github-readme-stats | ||
| 260 | restart: unless-stopped | ||
| 261 | ports: | ||
| 262 | - "9000:9000" | ||
| 263 | environment: | ||
| 264 | - PAT_1=${PAT_1} | ||
| 265 | - CACHE_SECONDS=14400 | ||
| 266 | {{/code}} | ||
| 267 | |||
| 268 | * ##PAT_1## : jeton d'accès personnel GitHub, défini dans les variables de la stack Portainer. Jeton classique avec les permissions ##repo## pour compter les dépôts privés. | ||
| 269 | * ##CACHE_SECONDS## : durée de mise en cache des cartes (4 heures), pour limiter les appels à l'API GitHub. | ||
| 270 | * Accessible uniquement en local, sur le port 9000. | ||
| 271 | |||
| 272 | Principales adresses : | ||
| 273 | |||
| 274 | |=Carte|=Adresse | ||
![]() |
5.1 | 275 | |Statistiques du profil|##/api?username=<UTILISATEUR>## |
| 276 | |Langages les plus utilisés|##/api/top-langs?username=<UTILISATEUR>## | ||
| 277 | |Dépôt épinglé|##/api/pin?username=<UTILISATEUR>&repo=<DEPOT>## | ||
![]() |
3.1 | 278 | |
![]() |
5.1 | 279 | Dans Dashy, les cartes s'affichent avec un widget de type ##image## qui pointe vers l'instance locale. |
![]() |
3.1 | 280 | |
| 281 | === Image locale === | ||
| 282 | |||
| 283 | Portainer ne sait pas construire une image dans une stack : l'image ##github-readme-stats:local## est construite sur l'hôte, depuis le dépôt cloné. Elle n'est donc pas mise à jour par Watchtower. Pour la mettre à jour, depuis le dossier du dépôt : | ||
| 284 | |||
| 285 | {{code language="bash"}} | ||
| 286 | git pull | ||
| 287 | docker build -t github-readme-stats:local . | ||
| 288 | {{/code}} | ||
| 289 | |||
| 290 | Puis redéployer la stack dans Portainer. | ||
| 291 | |||
![]() |
4.1 | 292 | == Maintenance == |
| 293 | |||
![]() |
5.1 | 294 | === Après une migration ou une réinstallation === |
![]() |
4.1 | 295 | |
![]() |
5.1 | 296 | |=Service|=À refaire |
| 297 | |Nextcloud|Créer un nouveau mot de passe d'application et le reporter dans ##nextcloud-system## : les mots de passe d'application ne suivent pas une migration. | ||
| 298 | |AdGuard Home|Reporter le compte de l'interface web dans les trois widgets ##adguard-*##. Si l'IP change : ##hostname## et ##allowed_internal## de SWAG. | ||
| 299 | |Pico W|Si l'IP change : ##news.yml## (item et script), ##location /pico/## et ##allowed_internal## de SWAG. | ||
| 300 | |SWAG, Authelia|Remettre ##30-dashy.subdomain.conf## complet et la règle ##one_factor##, puis vérifier en navigation privée que ##/conf.yml## redirige vers Authelia. | ||
| 301 | |Dashy|Restaurer ##user-data##. | ||
![]() |
4.1 | 302 | |
| 303 | === Recharger la configuration === | ||
| 304 | |||
![]() |
5.1 | 305 | * Dashy relit les fichiers YAML à chaque chargement : après une modification à la main, rafraîchir la page. Si l'ancienne version reste affichée, elle vient du stockage local du navigateur : réinitialiser les paramètres locaux depuis le menu de configuration. |
| 306 | * SWAG : ##docker exec swag nginx -t## puis ##docker exec swag nginx -s reload##. Authelia : ##docker restart authelia##. | ||
![]() |
4.1 | 307 | |
| 308 | === Un widget reste vide === | ||
| 309 | |||
![]() |
5.1 | 310 | Dans les outils de développement du navigateur (F12, onglet Réseau), regarder la requête du widget : |
![]() |
4.1 | 311 | |
![]() |
5.1 | 312 | |=Symptôme|=Cause |
| 313 | |Erreur CORS sur un appel direct|Passer le widget en ##useProxy: true## | ||
| 314 | |##/cors-proxy## : 302 ou 401|Session Authelia expirée : recharger la page | ||
| 315 | |##/cors-proxy## : 403 ##Cible interne non autorisée##|IP absente de ##allowed_internal## | ||
| 316 | |##/cors-proxy## : 401 ou 403 de la cible|Identifiants faux ou révoqués, compte sans droits, ou en-tête non relayé par le bloc Lua | ||
| 317 | |##/cors-proxy## : 502 ou 504|Service cible arrêté ou injoignable depuis SWAG | ||
![]() |
4.1 | 318 | |
![]() |
5.1 | 319 | Journaux : ##docker logs dashy## et ##<APPDATA>/swag/config/log/nginx/error.log##. |
![]() |
4.1 | 320 | |
![]() |
1.1 | 321 | == Sauvegarde == |
| 322 | |||
![]() |
5.1 | 323 | ##<APPDATA>/Dashy/user-data## est sauvegardé par le script de [[Sauvegarde>>Wiki Batcave John.Infrastructure.Sauvegarde.WebHome]], identifiants des widgets compris. Le fichier SWAG est aussi dans ##<APPDATA>##. |
![]() |
4.1 | 324 |
