# static.alpinux.org CDN pour les fichiers binaires et assets statiques de l'association : logos, favicons, images Open Graph. Tableau de bord d'administration accessible sur `https://static.alpinux.org` (authentification AlpID). Dépôt : [alpinux-static](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-static) — localement `~/Projects/alpinux.static/` (un domaine, un dépôt). --- ## Flux de publication Il y a **deux cibles** distinctes sur le serveur, gérées par des scripts séparés : | Quoi | Destination serveur | Script | Passe par git ? | |------|--------------------|---------|----| | Assets CDN (logo/, wiki/, stats/, error/…) | `/var/www/clients/client1/web17/web` | `push-assets.sh` | Non — rsync direct | | App Flask (`app/`) | `/opt/static-cdn/` + redémarrage service | `deploy-app.sh` | Non — rsync direct | Git sert à **versionner et sauvegarder** le code sur Gitea. Il n'est pas dans la boucle de déploiement : les scripts rsynchent depuis le dépôt local, pas depuis Gitea. ### Modifier les assets CDN (logo, wiki, stats…) ```bash # 1. Modifier les fichiers à la racine du dépôt (logo/, wiki/, error/…) # 2. Versionner (optionnel mais recommandé) git add git commit -m "..." git push # sauvegarde sur gitea.alpinux.org # 3. Pousser sur le serveur ./scripts/push-assets.sh # aperçu + confirmation ./scripts/push-assets.sh -y # sans confirmation ``` > `push-assets.sh` exclut automatiquement `app/`, `scripts/`, `.git/`, `.env`, `README.md`. > Il envoie : `logo/`, `wiki/`, `stats/`, `error/`, `favicon.ico`, `robots.txt`, `standard_index.html`. ### Modifier l'app Flask (`app/`) ```bash # 1. Modifier les fichiers dans app/ # 2. Versionner git add app/ git commit -m "..." git push # sauvegarde sur gitea.alpinux.org # 3. Déployer sur le serveur ./scripts/deploy-app.sh # rsync + pip install + restart service ./scripts/deploy-app.sh -n # dry-run ``` > `deploy-app.sh` rsync `app/` vers `/opt/static-cdn/`, met à jour le venv Python, > et redémarre le service systemd `static-cdn`. --- ## Configuration locale (`.env`) ```bash cp .env.example .env ``` | Variable | Obligatoire | Description | |----------|-------------|-------------| | `STATIC_HOST` | oui | Alias SSH ou nom d'hôte (`alpinux.org`) | | `STATIC_PATH` | oui | Chemin absolu du web root sur le serveur | | `LOCAL_ASSETS_DIR` | oui | Chemin absolu du dépôt local (`~/Projects/alpinux.static`) | | `STATIC_USER` | non | Login SSH — laisser vide si `~/.ssh/config` définit l'utilisateur | Si l'hôte est déclaré dans `~/.ssh/config` (recommandé), laisser `STATIC_USER` vide. Sinon, renseigner `STATIC_USER=` — les scripts construisent alors `USER@HOST:PATH`. --- ## Scripts ```bash # Synchroniser les assets CDN ./scripts/pull-assets.sh # serveur → local (aperçu + confirmation) ./scripts/pull-assets.sh -y # sans confirmation ./scripts/pull-assets.sh -n # dry-run ./scripts/push-assets.sh # local → serveur (aperçu + confirmation) ./scripts/push-assets.sh -y # sans confirmation ./scripts/push-assets.sh -n # dry-run # Déployer l'app Flask ./scripts/deploy-app.sh # déploie et redémarre le service ./scripts/deploy-app.sh -n # dry-run ``` --- ## Tableau de bord Flask (`app/`) Application Flask déployée sur le serveur via `deploy-app.sh`. Accessible sur `https://static.alpinux.org` (proxy Apache → `127.0.0.1:5003`). Authentification SSO via AlpID (Keycloak). ### Fonctionnalités | Route | Description | |-------|-------------| | `/` | Tableau de bord — statistiques CDN par dossier | | `/browse/` | Navigateur de fichiers — aperçu, suppression, **upload**, **création de dossier** | | `/search` | Recherche par nom / contenu / date | | `/stats/` | Rapport de trafic GoAccess | ### Création de dossier Le bouton « 📁 Nouveau dossier », à droite du fil d'Ariane de `/browse/`, crée un sous-dossier dans le répertoire affiché (Entrée pour valider, Échap pour annuler). Le nom est nettoyé par `secure_filename` — accents et espaces sont normalisés. Sont refusés : les noms réservés ou cachés (`app`, `scripts`, `.git`, …), les traversées de chemin et les dossiers déjà existants. ### Upload de fichiers Dans le navigateur (`/browse/`), une zone de dépôt est affichée en bas de chaque dossier. Elle accepte plusieurs fichiers à la fois (glisser-déposer ou sélection). Les chemins protégés (`.git`, `app`, `scripts`, etc.) sont refusés avec 403. Pour limiter la taille des uploads, ajouter dans `app/app.py` : ```python app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50 Mo ``` ### Variables d'environnement de l'app Le fichier de référence est `app/.env.example`. Sur le serveur : `/opt/static-cdn/.env`. **Auth & Flask** | Variable | Obligatoire | Description | |----------|-------------|-------------| | `SECRET_KEY` | oui | Clé secrète Flask (chaîne aléatoire longue) | | `ALPID_CLIENT_ID` | oui | Client Keycloak (`static-cdn`) | | `ALPID_CLIENT_SECRET` | oui | Secret client Keycloak | | `ALPID_DISCOVERY_URL` | oui | URL OIDC discovery AlpID | | `ADMIN_GROUPS` | non | Groupes Keycloak autorisés (défaut : `admins`) | | `ADMIN_EMAILS` | non | Fallback si le claim `groups` est absent du token | | `ASSETS_ROOT` | oui | Racine du CDN (`/var/www/clients/client1/web17/web` en prod) | **Stats GoAccess** | Variable | Description | |----------|-------------| | `STATS_FILE` | Rapport HTML GoAccess servi dans `/stats/` (défaut : `/opt/static-cdn/goaccess.html`) | | `STATS_JSON` | Rapport JSON GoAccess pour les badges "Vues" (défaut : `/opt/static-cdn/goaccess.json`) | | `STATS_LOG_FILE` | Log Apache à analyser pour la génération à la demande | | `STATS_GENERATE_CMD` | Commande GoAccess complète — remplace la commande par défaut si renseigné | --- ## Première installation (nouveau serveur) ### 1. Créer le site dans ISPConfig `https://owni.alpinux.org:8080` → *Sites → Ajouter un site web* - Domaine : `static.alpinux.org` - Activer **Let's Encrypt SSL** - DocumentRoot : `/var/www/clients/client1/web17/web` ### 2. Configurer le proxy Apache Dans ISPConfig → onglet **Directives Apache SSL** du site : ```apache # CDN public → Apache sert directement depuis DocumentRoot ProxyPass /logo/ ! ProxyPass /wiki/ ! ProxyPass /error/ ! ProxyPass /favicon.ico ! ProxyPass /robots.txt ! # Tableau de bord → Flask RequestHeader set X-Forwarded-Proto "https" ProxyPreserveHost On ProxyPass / http://127.0.0.1:5003/ ProxyPassReverse / http://127.0.0.1:5003/ ``` ### 3. Déployer l'app Flask ```bash # Depuis la machine locale ./scripts/deploy-app.sh # Sur le serveur — créer /opt/static-cdn/.env (voir app/.env.example) ssh alpinux.org nano /opt/static-cdn/.env # Activer le service sudo systemctl enable --now static-cdn ``` ### 4. Pousser les assets CDN ```bash ./scripts/push-assets.sh -y ``` --- ## Bannissement fail2ban (`/errors/`) ### Architecture La page `/errors/` permet de bannir des IPs ou des AS entiers. Les bans sont **permanents** (`bantime = -1`). | Composant | Valeur | |-----------|--------| | Jail fail2ban | `global-blacklist` | | Backend | `iptables-ipset-net-allports` (ipset `hash:net`, O(1)) | | Ipset kernel | `f2b-global-blacklist` | | Persistance bans | Base SQLite fail2ban (`bantime = -1` → jamais expirés) | | Lecture bans (app) | `sudo /usr/sbin/ipset list f2b-global-blacklist` | | Écriture bans (app) | `sudo /usr/bin/fail2ban-client set global-blacklist banip/unbanip` | > **Pourquoi ipset ?** Avec `iptables-allports`, chaque ban ajoutait une règle individuelle. À partir de ~5 000 entrées, les bans en rafale (ban AS = plusieurs /24) dépassaient le timeout de 60 s. `hash:net` effectue lookup et insertion en O(1) quel que soit le nombre d'entrées. ### Sudoers Fichier : `/etc/sudoers.d/static-cdn-fail2ban` (user `static-cdn`) ``` static-cdn ALL=(ALL) NOPASSWD: /usr/bin/fail2ban-client set global-blacklist banip * static-cdn ALL=(ALL) NOPASSWD: /usr/bin/fail2ban-client set global-blacklist unbanip * static-cdn ALL=(ALL) NOPASSWD: /usr/bin/fail2ban-client status global-blacklist static-cdn ALL=(ALL) NOPASSWD: /usr/sbin/ipset list f2b-global-blacklist ``` ### Configuration fail2ban `/etc/fail2ban/jail.local` : ```ini [global-blacklist] enabled = true filter = global-blacklist bantime = -1 action = iptables-ipset-net-allports[name=global-blacklist] logpath = /etc/fail2ban/badips.list ``` Action custom : `/etc/fail2ban/action.d/iptables-ipset-net-allports.conf` (hérite de `iptables.conf`, crée un ipset `hash:net family inet`) ### Restauration après redémarrage fail2ban Avec `bantime = -1`, fail2ban restaure **tous** les bans de sa DB SQLite au démarrage — il n'y a pas d'expiration. Le redémarrage du service prend quelques secondes même avec 20 000+ entrées (ipset = insertion kernel rapide). --- ## Voir aussi Vue d'ensemble des projets, procédures de déploiement globales et accès : `~/Projects/org.alpinux.owni/README.md`