static.alpinux.org — CDN assets logos et favicons
Find a file
Alpinux ab0cc146ea Reprendre la documentation de déploiement, qui vivait dans alpinux-owni
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
2026-09-27 13:00:19 +02:00
app Déploiement : ne plus effacer les rapports GoAccess, ne plus sauter le restart 2026-09-20 09:05:29 +02:00
error feat: v2.0.0 — 14 tickets implémentés 2026-05-06 21:04:03 +02:00
logo feat: v2.0.0 — 14 tickets implémentés 2026-05-06 21:04:03 +02:00
scripts Déploiement : ne plus effacer les rapports GoAccess, ne plus sauter le restart 2026-09-20 09:05:29 +02:00
stats feat: v2.0.0 — 14 tickets implémentés 2026-05-06 21:04:03 +02:00
wiki feat: v2.0.0 — 14 tickets implémentés 2026-05-06 21:04:03 +02:00
.env.example Le dépôt vit maintenant dans ~/Projects/alpinux.static 2026-09-20 08:39:11 +02:00
.gitignore Le dépôt vit maintenant dans ~/Projects/alpinux.static 2026-09-20 08:39:11 +02:00
favicon.ico initial commit — migration depuis monorepo alpinux.site.2026 2026-05-03 17:48:11 +02:00
INFRASTRUCTURE.md Reprendre la documentation de déploiement, qui vivait dans alpinux-owni 2026-09-27 13:00:19 +02:00
README.md Parcourir : créer un dossier depuis le fil d'Ariane (v2.1.0) 2026-09-20 08:39:05 +02:00
robots.txt feat: v2.0.0 — 14 tickets implémentés 2026-05-06 21:04:03 +02:00
standard_index.html feat: v2.0.0 — 14 tickets implémentés 2026-05-06 21:04:03 +02:00

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.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/)

# 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)

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: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 :

[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