Partie 1 : Globe 3D — Carte des connexions en temps réel
Mars 2026
Le besoin
J'avais déjà un dashboard Grafana Geomap pour visualiser les connexions entrantes sur le cluster. Ça fonctionne bien, mais c'est... Grafana. C'est un outil de monitoring, pas une vitrine.
L'idée : créer une page web publique directement sur home-fonta.fr, avec un globe terrestre 3D et des arcs animés style "cyber threat map" (comme les cartes Kaspersky ou Norse Attack Map). En gros, transformer les logs nginx en quelque chose de visuellement sympa, accessible à n'importe quel visiteur sans compte Grafana.
Les données existent déjà dans Loki — autant les réutiliser.

Architecture
La chaîne de données existante
Je ne repars pas de zéro. Toute la chaîne de collecte est déjà en place depuis la phase monitoring + Loki :
Visiteur externe → Nginx Ingress (logs JSON)
│
▼
Promtail (DaemonSet)
├── Parse JSON (remote_addr)
└── Enrichissement GeoIP (DB-IP MMDB)
├── geoip_location_latitude
├── geoip_location_longitude
├── geoip_country_name
└── geoip_city_name
│
▼
Loki (stockage)
│
▼
┌───────────┴───────────┐
│ │
Grafana Geomap Flask API (NOUVEAU)
(monitoring interne) │
▼
Globe.gl (WebGL)
(page publique)
La nouveauté c'est la branche droite : Flask interroge Loki directement via son API HTTP interne (loki.loki.svc.cluster.local:3100), transforme les résultats en JSON, et le navigateur du visiteur dessine le globe 3D.
Point clé : le rendu est côté client
Le globe 3D utilise WebGL (via Globe.gl / three.js). Tout le rendu graphique se fait dans le navigateur du visiteur, pas sur les Raspberry Pi. Le serveur Flask ne fait que :
- Servir les fichiers HTML/CSS/JS (via Nginx, cache 7 jours)
- Répondre à l'API
/api/map-data(une requête Loki, quelques ko de JSON)
Zéro impact GPU sur le cluster.
Les technologies utilisées
| Technologie | Rôle | Pourquoi ce choix |
|---|---|---|
| Globe.gl | Globe 3D WebGL | Spécialisé pour les globes avec arcs, basé sur three.js, open-source |
| three.js | Moteur 3D (sous Globe.gl) | Standard du WebGL, performant |
| Flask | Backend API | Déjà en place, léger, parfait pour un endpoint JSON |
| Loki | Source de données | Déjà en place, contient tous les logs nginx avec GeoIP |
| requests (Python) | Client HTTP | Pour que Flask interroge l'API Loki |
Implémentation
1. L'API backend (app.py)
Deux nouvelles routes ajoutées au Flask existant :
@app.route('/map')
def attack_map():
return render_template('map.html')
@app.route('/api/map-data')
def map_data():
# Interroge Loki, retourne du JSON
La route /api/map-data accepte un paramètre range (5m, 15m, 1h, 6h, 24h) qui contrôle la fenêtre temporelle. Par sécurité, seules ces valeurs sont acceptées — on ne veut pas qu'un visiteur injecte du LogQL arbitraire.
La requête LogQL envoyée à Loki :
sum by (geoip_location_latitude, geoip_location_longitude,
geoip_country_name, geoip_city_name)
(count_over_time(
{namespace="ingress-nginx", geoip_location_latitude=~".+"}
[1h]
))
C'est la même requête que le dashboard Grafana Geomap ! Elle :
- Sélectionne les logs nginx qui ont une géolocalisation (exclut les IPs privées)
- Compte le nombre de logs par position sur la période
- Regroupe par coordonnées + pays + ville
Le Flask transforme la réponse Loki en JSON propre :
{
"arcs": [
{"lat": 48.85, "lon": 2.35, "country": "France", "city": "Paris", "count": 42},
{"lat": 40.71, "lon": -74.01, "country": "United States", "city": "New York", "count": 15}
],
"total_ips": 23,
"total_hits": 187,
"top_countries": [
{"country": "France", "hits": 89},
{"country": "United States", "hits": 34}
],
"server": {"lat": 43.6, "lon": 1.44}
}
Le champ server contient les coordonnées de notre serveur (Toulouse). C'est la destination de tous les arcs.
2. Le globe 3D (map.js)
Globe.gl s'initialise en quelques lignes, mais chaque option compte :
const globe = Globe()
// Texture de la Terre (photo satellite de nuit — NASA)
.globeImageUrl("//unpkg.com/three-globe/example/img/earth-night.jpg")
// Relief 3D (montagnes, fosses océaniques)
.bumpImageUrl("//unpkg.com/three-globe/example/img/earth-topology.png")
// Fond étoilé
.backgroundImageUrl("//unpkg.com/three-globe/example/img/night-sky.png")
// Halo bleu autour du globe (atmosphère terrestre)
.atmosphereColor("rgba(100, 180, 255, 0.4)")
.atmosphereAltitude(0.2)
Les arcs animés
Chaque arc est une courbe 3D entre une source (l'IP géolocalisée) et la destination (mon serveur). Les propriétés clés :
// Couleurs : dégradé du cyan (source) vers l'orange (destination)
.arcColor(d => ["rgba(0, 255, 255, 0.9)", "rgba(255, 100, 50, 0.6)"])
// Épaisseur proportionnelle au nombre de hits (échelle log)
.arcStroke(d => Math.min(0.5 + Math.log2(d.count + 1) * 0.3, 3))
// Effet de pointillés animés (comme des données qui voyagent)
.arcDashLength(0.6)
.arcDashGap(0.3)
.arcDashAnimateTime(2000) // 2 secondes pour traverser le globe
Pourquoi Math.log2 ? Sans échelle logarithmique, un pays avec 10 000 hits aurait un arc 1000x plus épais qu'un pays avec 10 hits — illisible. Le logarithme "écrase" les grandes valeurs : log2(10) = 3.3, log2(10000) = 13.3. L'écart visuel est bien plus raisonnable.

Les points lumineux
À chaque source, un point cyan lumineux dont la taille dépend du nombre de hits :
.pointColor(() => "rgba(0, 255, 255, 0.8)")
.pointRadius(d => Math.min(0.3 + Math.log2(d.count + 1) * 0.15, 2))

3. Le panneau de statistiques
En bas à gauche, un panneau semi-transparent (backdrop-filter: blur) :
#stats-panel {
background: rgba(10, 10, 30, 0.85);
backdrop-filter: blur(12px);
border: 1px solid rgba(0, 255, 255, 0.2);
}

Le panneau contient :
- Sélecteur de période : boutons 5m / 15m / 1h / 6h / 24h
- Compteurs : IPs uniques et nombre total de requêtes
- Top 5 pays : classés par nombre de hits
- Feed des dernières connexions : les sources les plus actives
Les données se rafraîchissent automatiquement toutes les 10 secondes via polling :
fetchData(); // Premier chargement immédiat
setInterval(fetchData, REFRESH_INTERVAL); // Puis toutes les 10s
4. Le mode plein écran
Un bouton en haut à droite permet de passer en fullscreen via l'API Fullscreen du navigateur :
document.getElementById("fullscreen-btn").addEventListener("click", () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
});

Fichiers créés / modifiés
Nouveaux fichiers
| Fichier | Taille | Rôle |
|---|---|---|
templates/map.html |
~2 ko | Page HTML (conteneur globe + panneau stats + menu) |
static/map.js |
~5 ko | Logique Globe.gl + polling API + gestion UI |
static/map.css |
~4 ko | Styles dark theme + glassmorphism + responsive |
Fichiers modifiés
| Fichier | Modification |
|---|---|
app.py |
+2 routes (/map, /api/map-data) + imports + config Loki |
requirements.txt |
+requests==2.32.3 |
templates/menu.html |
+lien "Carte" dans la sidebar |
tests/test_app.py |
+2 tests (test_map_page, test_map_api) |
Déploiement
Aucune modification d'infrastructure nécessaire. Le pipeline CI/CD existant gère tout :
git push (repo home-fonta)
│
▼
GitHub Actions CI (flake8 + pytest)
│ 8 tests passed ✓
▼
GitHub Actions Docker Build
│ Multi-arch (amd64 + arm64)
│ Push sur Docker Hub
▼
GitHub Actions Update Helm
│ Met à jour le tag image dans ansible-k3s
▼
ArgoCD détecte le changement
│ Auto-sync
▼
Nouveau pod déployé sur le cluster K3s
Zéro intervention manuelle. Le push déclenche toute la chaîne, toujours aussi incroyable à regarder faire.
Comparaison avec le dashboard Grafana
| Aspect | Grafana Geomap | Globe 3D |
|---|---|---|
| Accès | Compte Grafana requis | Public, accessible à tous |
| Animations | Marqueurs statiques | Arcs animés avec traînée |
| Données | Via datasource Loki (Grafana) | Via API Flask → Loki |
| Refresh | Configurable (1min par défaut) | 10 secondes |
| Rendu | Côté serveur (Grafana) | Côté client (WebGL) |
| Impact cluster | Charge Grafana | Quasi nul |
| Fullscreen | Oui (natif Grafana) | Oui (API Fullscreen) |
| Stats | Pas sur le même panneau | Panneau intégré |


Ce qu'on voit en pratique
En situation réelle, le globe affiche typiquement :
- Des arcs depuis la France (connexions légitimes — moi)
- Des arcs depuis les USA (bots, crawlers Google/Bing)
- Des arcs depuis la Chine/Russie (scans automatisés)
- Parfois des arcs depuis des pays inattendus (Brésil, Indonésie...)

Le sélecteur de période permet de passer rapidement de "les 5 dernières minutes" à "les dernières 24 heures" pour voir l'évolution. Sur 24h, le globe est généralement couvert d'arcs — sur 5 minutes, on voit les connexions en quasi temps réel.
Sécurité
Quelques précautions prises :
-
Validation du paramètre
range: seules les valeurs5m,15m,1h,6h,12h,24hsont acceptées. Impossible d'injecter du LogQL. -
Pas d'exposition de données sensibles : l'API retourne des coordonnées GPS (précision ville), des noms de pays/villes et des compteurs. Pas d'IPs, pas de user-agents, pas d'URLs visitées.
-
Loki non exposé : l'API Loki n'est accessible que depuis l'intérieur du cluster (service ClusterIP). Le visiteur ne peut pas interroger Loki directement.
-
Graceful degradation : si Loki est down, l'API retourne un JSON vide. Le globe s'affiche normalement, juste sans arcs.
Pièges rencontrés
Piège 1 : import requests vs flask.request
Python a un package requests (pour faire des requêtes HTTP) et Flask a un objet request (la requête HTTP du visiteur). Si on fait from flask import request et import requests, pas de conflit. Mais pour la lisibilité, j'ai renommé : import requests as http_client.
Piège 2 : Promtail CPU throttled
Pendant le développement, j'ai découvert que le Promtail sur rpi4-master était bloqué à 499m/500m CPU — complètement throttlé. Les push vers Loki échouaient tous. J'ai augmenté la limite à 750m, ce qui a réglé le problème. Sans Promtail fonctionnel, pas de logs dans Loki, et donc pas de données pour le globe.
Piège 3 : Globe.gl et les versions CDN
Globe.gl évolue régulièrement. J'ai pin la version 2.35.1 dans le script tag pour éviter les breaking changes. Les textures (earth-night.jpg, etc.) sont aussi hébergées sur unpkg — si ce CDN tombe, le globe s'affiche sans texture mais reste fonctionnel.
Accès
La carte est accessible sur :
https://home-fonta.fr/map
Ou via le menu latéral du site (lien "Carte").
Récap
| Élément | Détail |
|---|---|
| URL | home-fonta.fr/map |
| Backend | Flask + requests → Loki API |
| Frontend | Globe.gl (three.js WebGL) |
| Données | Logs nginx enrichis GeoIP via Promtail |
| Refresh | 10 secondes (polling) |
| Périodes | 5m, 15m, 1h, 6h, 24h |
| Stats | IPs uniques, total requêtes, top 5 pays, feed connexions |
| Fullscreen | Oui (bouton + API Fullscreen) |
| Impact cluster | Quasi nul (rendu WebGL côté client) |
| Déploiement | Automatique via CI/CD + ArgoCD |
La prochaine partie sera sur la même chose mais avec la carte 2D.