#06 — rgz-radius
PLANIFIÉ
Priorité: 🔴 CRITIQUE · Type: TYPE A · Conteneur: rgz-radius · Code: config/radius/
Dépendances: #04 rgz-db, #01 rgz-api
Description
rgz-radius est le moteur d'authentification et de comptabilité WiFi de la plateforme RGZ. Il s'agit de FreeRADIUS 3.2.3 configuré avec le module REST, ce qui lui permet de déléguer toutes les décisions d'authentification et de contrôle d'accès à rgz-api via des appels HTTP. Cette architecture "RADIUS-as-proxy" simplifie la logique réseau tout en centralisant la business logic dans l'API Python.
Le flux d'authentification WiFi commence lorsque le point d'accès CPE LiteBeam envoie une requête RADIUS Access-Request sur le port 1812/UDP. FreeRADIUS appelle immédiatement http://rgz-api:8000/api/v1/radius/authorize avec les attributs RADIUS (User-Name = subscriber_ref, NAS-Identifier, Calling-Station-Id = MAC). L'API vérifie la session, le forfait, et retourne Access-Accept ou Access-Reject avec les attributs QoS (Session-Timeout, WISPr-Bandwidth-Max-Down). L'accounting (ouverture/fermeture de sessions, octets transférés) est enregistré via les requêtes Accounting-Request sur 1813/UDP.
L'identité réseau d'un abonné est son subscriber_ref (format RGZ-MSISDN, ex: RGZ-0197979964). La MAC de l'appareil est transmise dans Calling-Station-Id et stockée dans subscriber_devices à titre de tracking — elle n'est jamais le critère de blocage ou d'acceptation. La limite de sessions simultanées est gérée par l'attribut Simultaneous-Use=2 (ou =4 pour un abonné avec 2 forfaits actifs) : FreeRADIUS vérifie automatiquement les sessions actives et refuse le 3e appareil sans intervention de l'API.
Le CoA (Change-of-Authorization, RFC 5176) sur le port 3799/UDP permet à l'outil #26 (DBA daemon) d'envoyer des paquets CoA pour modifier les paramètres QoS (MIR bandwidth) d'une session active sans la déconnecter. Le secret RADIUS partagé est injecté exclusivement via variable d'environnement (SEC-05).
Architecture Interne
CPE LiteBeam (NAS)
│ Access-Request (User-Name=RGZ-MSISDN, NAS-ID, MAC)
▼ port 1812/UDP
rgz-radius (FreeRADIUS 3.2.3)
│
├── Module REST → POST http://rgz-api:8000/api/v1/radius/authorize
│ │
│ ▼
│ rgz-api → vérifie session, forfait, Simultaneous-Use
│ │
│ ▼
│ Réponse: { "control:Auth-Type": "Accept", "reply:Session-Timeout": 3600,
│ "reply:Simultaneous-Use": 2, "reply:WISPr-Bandwidth-Max-Down": 5120 }
│
├── Access-Accept → retourné au CPE avec attributs QoS
│
├── Accounting (1813/UDP) → POST http://rgz-api:8000/api/v1/radius/accounting
│ └── Start / Interim-Update / Stop → radius_sessions en DB
│
└── CoA (3799/UDP) ← push depuis rgz-api (outil #26 DBA)
└── Modifier MIR bandwidth d'une session active
Simultaneous-Use (géré par FreeRADIUS, pas l'API):
1 forfait actif → Simultaneous-Use = 2
2 forfaits actifs → Simultaneous-Use = 4
Si 2 sessions déjà actives + Simultaneous-Use=2 → Access-Reject automatiqueConfiguration
Variables d'environnement
| Variable | Exemple | Description |
|---|---|---|
RADIUS_SECRET | changeme-radius-secret-strong | Secret partagé NAS ↔ FreeRADIUS (SEC-05) |
REST_URL | http://rgz-api:8000/api/v1/radius/ | URL de base module REST |
RADIUS_DB_URL | postgresql://rgz:pass@rgz-db:5432/rgz | DB pour les sessions simultanées |
COA_SECRET | changeme-coa-secret | Secret CoA (même que RADIUS_SECRET ou différent) |
Fichiers de configuration
config/radius/
├── clients.conf # Déclaration des NAS (CPE) autorisés + secrets
├── radiusd.conf # Config globale FreeRADIUS
├── mods-enabled/
│ ├── rest # Module REST → rgz-api
│ ├── sql # SQL pour Simultaneous-Use check (via rgz-db)
│ └── detail # Logging accounting détaillé
├── sites-enabled/
│ ├── default # Auth flow: authorize → authenticate → post-auth
│ └── inner-tunnel # EAP inner auth (si applicable)
└── dictionary # Attributs custom RGZ (WISPr-Bandwidth-*)
docker/radius/
├── Dockerfile # freeradius-server:3.2 + curl + envsubst
└── entrypoint.sh # envsubst sur tous les fichiers .conf (LL#44)mods-enabled/rest (extrait)
# LL#44: FreeRADIUS NE supporte PAS ${VAR} → entrypoint.sh génère les fichiers
rest {
tls {
# TLS désactivé en interne (docker rgz-net)
}
connect_uri = "http://rgz-api:8000"
connect_timeout = 2.000
timeout = 5.000
authorize {
uri = "$/api/v1/radius/authorize"
method = 'post'
body = 'json'
data = '{"username": "%{User-Name}", "nas_id": "%{NAS-Identifier}", "mac": "%{Calling-Station-Id}"}'
tls = $
}
accounting {
uri = "$/api/v1/radius/accounting"
method = 'post'
body = 'json'
tls = $
}
}clients.conf (extrait)
# Clients génériques (NAS CPE dans les VLAN 100-499)
# LL#44: envsubst remplace RADIUS_SECRET au démarrage
client vlan_100_499 {
ipaddr = 10.100.0.0/8
secret = RADIUS_SECRET_PLACEHOLDER
shortname = "rgz-nas"
nas_type = other
}
# Client localhost (tests internes)
client localhost {
ipaddr = 127.0.0.1
secret = RADIUS_SECRET_PLACEHOLDER
shortname = "localhost"
}Endpoints API RADIUS
Les endpoints RADIUS dans rgz-api sont appelés uniquement par FreeRADIUS.
POST /api/v1/radius/authorize
// Request (envoyé par FreeRADIUS)
{
"username": "RGZ-0197979964",
"nas_id": "access_kossou",
"mac": "AA:BB:CC:DD:EE:FF"
}
// Response — Access-Accept
{
"control:Auth-Type": "Accept",
"reply:Session-Timeout": 3600,
"reply:Simultaneous-Use": 2,
"reply:WISPr-Bandwidth-Max-Down": 5120,
"reply:WISPr-Bandwidth-Max-Up": 1024,
"reply:Acct-Interim-Interval": 60
}
// Response — Access-Reject
{
"control:Auth-Type": "Reject",
"reply:Reply-Message": "Session expirée ou aucun forfait actif"
}POST /api/v1/radius/accounting
// Request Accounting-Start
{
"username": "RGZ-0197979964",
"nas_id": "access_kossou",
"mac": "AA:BB:CC:DD:EE:FF",
"acct_status_type": "Start",
"acct_session_id": "uuid-session",
"framed_ip_address": "10.142.0.50"
}Attributs RADIUS Utilisés
| Attribut | Direction | Valeur type | Description |
|---|---|---|---|
User-Name | Request | RGZ-0197979964 | Identité abonné (subscriber_ref) |
NAS-Identifier | Request | access_kossou | ID du point d'accès revendeur |
Calling-Station-Id | Request | AA:BB:CC:DD:EE:FF | MAC de l'appareil abonné |
Session-Timeout | Reply | 3600 | Durée max de la session (secondes) |
Simultaneous-Use | Reply | 2 ou 4 | Appareils simultanés max |
WISPr-Bandwidth-Max-Down | Reply | 5120 | MIR descendant (Kbps) |
WISPr-Bandwidth-Max-Up | Reply | 1024 | MIR montant (Kbps) |
Acct-Interim-Interval | Reply | 60 | Fréquence accounting (secondes) |
Healthcheck
# Healthcheck Docker — test local
# (utilise radtest qui est inclus dans le Dockerfile)
echo "User-Name=RGZ-test" | radtest RGZ-test test 127.0.0.1 1812 $RADIUS_SECRET
# Vérifier que FreeRADIUS écoute bien
docker exec rgz-radius ss -ulnp | grep -E "1812|1813|3799"
# Logs FreeRADIUS (debug mode)
docker exec -it rgz-radius freeradius -X 2>&1 | head -50
# Tester depuis l'hôte
docker exec rgz-radius radtest healthcheck healthcheck localhost 1812 "$RADIUS_SECRET"Sécurité
| Règle | Implémentation |
|---|---|
| SEC-05 RADIUS_SECRET | Variable d'env uniquement, jamais en dur dans les fichiers config |
| SEC-05 NAS-Identifier | Validé contre la table reseller_sites.nas_id dans rgz-api avant Accept |
| LL#44 envsubst | entrypoint.sh remplace tous les placeholders avant démarrage FreeRADIUS |
| Simultaneous-Use | Géré par FreeRADIUS (pas par l'API) → plus robuste contre les race conditions |
# docker/radius/entrypoint.sh — LL#44 obligatoire
#!/bin/sh
# FreeRADIUS ne supporte pas ${VAR} → envsubst obligatoire
envsubst < /etc/freeradius/clients.conf.tmpl > /etc/freeradius/3.0/clients.conf
envsubst < /etc/freeradius/mods-available/rest.tmpl > /etc/freeradius/3.0/mods-enabled/rest
exec freeradius -fCommandes Utiles
# Démarrage avec rebuild
docker compose -f /home/claude-dev/RGZ/docker-compose.core.yml up -d --build rgz-radius
# Logs temps réel
docker logs rgz-radius -f --tail=100
# Status ports
docker exec rgz-radius ss -ulnp
# Tester une auth (simuler un CPE)
docker exec rgz-radius radtest "RGZ-0197979964" "dummy" 127.0.0.1 1812 "$RADIUS_SECRET"
# Debug mode interactif (verbose — arrête le service courant)
docker exec rgz-radius freeradius -X
# Vérifier la config (syntaxe)
docker exec rgz-radius freeradius -C
# Envoyer un CoA depuis l'API (DBA daemon #26)
docker exec rgz-radius radclient -x 127.0.0.1:3799 coa "$RADIUS_SECRET" << EOF
User-Name = "RGZ-0197979964"
WISPr-Bandwidth-Max-Down = 2048
EOFImplémentation TODO
- [x] Dockerfile créé (
docker/radius/Dockerfile) - [x] Service défini dans
docker-compose.core.yml - [x] Variables d'env dans
.env.example - [ ]
config/radius/clients.conf— NAS VLAN 100-499 + localhost - [ ]
config/radius/radiusd.conf— Config globale, tuning workers - [ ]
config/radius/mods-enabled/rest— Module REST → rgz-api - [ ]
config/radius/sites-enabled/default— Flux authorize → authenticate → post-auth - [ ]
config/radius/dictionary— Attributs WISPr custom - [ ]
docker/radius/entrypoint.sh— envsubst de tous les fichiers .tmpl (LL#44) - [ ]
app/api/v1/endpoints/radius.py— Endpoints authorize + accounting dans rgz-api - [ ] Validation NAS-Identifier vs reseller_sites en DB (SEC-05)
- [ ] Tests intégration RADIUS ↔ API (radtest)
Dernière mise à jour: 2026-02-21