Ce dépôt d'infrastructure décrit la machine ; le déploiement de ce site-ci le regarde lui, et le tenir ailleurs garantissait qu'il divergerait du code qu'il décrit. Repris tel quel depuis alpinux-owni/docs/, où il est supprimé en retour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFbwnJurBwTs7x7t93ecku |
||
|---|---|---|
| app | ||
| error | ||
| logo | ||
| scripts | ||
| stats | ||
| wiki | ||
| .env.example | ||
| .gitignore | ||
| favicon.ico | ||
| INFRASTRUCTURE.md | ||
| README.md | ||
| robots.txt | ||
| standard_index.html | ||
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 — 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…)
# 1. Modifier les fichiers à la racine du dépôt (logo/, wiki/, error/…)
# 2. Versionner (optionnel mais recommandé)
git add <fichiers>
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.shexclut automatiquementapp/,scripts/,.git/,.env,README.md. Il envoie :logo/,wiki/,stats/,error/,favicon.ico,robots.txt,standard_index.html.
Modifier l'app Flask (app/)
# 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.shrsyncapp/vers/opt/static-cdn/, met à jour le venv Python, et redémarre le service systemdstatic-cdn.
Configuration locale (.env)
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=<login> — les scripts construisent alors USER@HOST:PATH.
Scripts
# 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 :
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 :
# 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
# 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
./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:neteffectue 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 :
[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