Le bouton « 📁 Nouveau dossier » ouvre une saisie en ligne à droite du fil d'Ariane (Entrée valide, Échap annule) et crée le sous-dossier dans le répertoire affiché. Endpoint POST /mkdir, réservé aux admins : nom nettoyé par secure_filename, refus des noms réservés ou cachés (app, scripts, .git…), des traversées de chemin, et des dossiers déjà existants (409). Le README porte aussi les chemins locaux remis à jour après le déménagement du dépôt dans ~/Projects/alpinux.static. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtgiPNG3inkbM8k7suMtjL
260 lines
8.9 KiB
Markdown
260 lines
8.9 KiB
Markdown
# 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 <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/`)
|
|
|
|
```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=<login>` — 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`
|