Skip to content

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

Configuration

Variables d'environnement

VariableExempleDescription
RADIUS_SECRETchangeme-radius-secret-strongSecret partagé NAS ↔ FreeRADIUS (SEC-05)
REST_URLhttp://rgz-api:8000/api/v1/radius/URL de base module REST
RADIUS_DB_URLpostgresql://rgz:pass@rgz-db:5432/rgzDB pour les sessions simultanées
COA_SECRETchangeme-coa-secretSecret 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

json
// 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

json
// 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

AttributDirectionValeur typeDescription
User-NameRequestRGZ-0197979964Identité abonné (subscriber_ref)
NAS-IdentifierRequestaccess_kossouID du point d'accès revendeur
Calling-Station-IdRequestAA:BB:CC:DD:EE:FFMAC de l'appareil abonné
Session-TimeoutReply3600Durée max de la session (secondes)
Simultaneous-UseReply2 ou 4Appareils simultanés max
WISPr-Bandwidth-Max-DownReply5120MIR descendant (Kbps)
WISPr-Bandwidth-Max-UpReply1024MIR montant (Kbps)
Acct-Interim-IntervalReply60Fréquence accounting (secondes)

Healthcheck

bash
# 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ègleImplémentation
SEC-05 RADIUS_SECRETVariable d'env uniquement, jamais en dur dans les fichiers config
SEC-05 NAS-IdentifierValidé contre la table reseller_sites.nas_id dans rgz-api avant Accept
LL#44 envsubstentrypoint.sh remplace tous les placeholders avant démarrage FreeRADIUS
Simultaneous-UseGéré par FreeRADIUS (pas par l'API) → plus robuste contre les race conditions
bash
# 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 -f

Commandes Utiles

bash
# 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
EOF

Implé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

PROJET MOSAÏQUE — 81 outils, 22 conteneurs, 500+ revendeurs WiFi Zone