Un brin de réseau


Tel un cahier de notes, ce blog propose des articles sur les technologies réseaux et leur utilisation pratique, le tout illustré avec des maquettes et des captures.

Python freeradius-api Python diffplus

(Free)RADIUS Draft

Le radius, c'est un os de l'avant-bras. Mais le RADIUS (Remote Authentication Dial In User Service), c'est un protocole ou service à mi-chemin entre les mondes réseau et système. En effet, dans le contexte opérateur par exemple, les clients RADIUS sont souvent des équipements réseaux (routeurs) et les serveurs RADIUS des machines Linux ou Windows (virtuelles ou physiques). En ceci, c'est une technologie souvent non maîtrisée—ou qui apparaît mystique—car l'équipe réseau considère que c'est à l'équipe système de s'en charger et vice-versa ! Par ailleurs, au-delà de l'aspect protocolaire, il y a, en particulier dans l'implémentation FreeRADIUS, un fort aspect algorithmique qui rejoint le monde du dév. Cela rend l'outil très puissant—car des traitements à valeur ajoutée peuvent être programmés—et, me concernant, j'ai beaucoup apprécié travailler avec et me perdre dans les détails techniques. En conclusion, le RADIUS est pluridisciplinaire et reflète bien la diversité inhérente à notre métier d'ingénieur réseaux.

Le RADIUS, ça Suze

Admettons-le, la définition de la RFC 2865 ne cache pas l'âge avancé du RADIUS (années 1990, c'est pas si vieux en fait) :


    Managing dispersed serial line and modem pools for large numbers of
    users can create the need for significant administrative support.
    Since modem pools are by definition a link to the outside world, they
    require careful attention to security, authorization and accounting.
    This can be best achieved by managing a single "database" of users,
    which allows for authentication (verifying user name and password) as
    well as configuration information detailing the type of service to
    deliver to the user (for example, SLIP, PPP, telnet, rlogin).
    

En effet, ci-dessus apparaissent les termes vieillissant de lignes série, modem et PPP. Mais soyez assurés, le RADIUS est une technologie bien actuelle, en lien avec la sécurité, qui fonctionne de pair avec par exemple : DHCP, LDAP, 802.1x, etc. Nous pouvons retenir que le RADIUS permet deux choses :

La database peut correspondre à un simple fichier texte ou, plus idéalement, à une base SQL (facilitant ainsi l'exploitation des données).

Aux AAA

Attention au terme « reconnus » utilisé plus haut qui rejoint la notion d'AAA (Authentication, Authorization and Accounting). Le concept est bien expliqué dans la documentation NetworkRADIUS :

On dit aussi que le RADIUS est un protocole AAA, à l'instar des protocoles DIAMETER et TACACS.

Vous n'avez pas les bases

Point de vue protocolaire, le RADIUS se suffit généralement de cinq types de paquet—attention, le terme « paquet » désigne plusieurs couches du modèle OSI et son usage est contextuel : ici, nous parlons bien sûr de la PDU RADIUS généralement encapsulée sur UDP (même si TCP est possible).


    1       Access-Request            (RFC 2865)
    2       Access-Accept             (RFC 2865)
    3       Access-Reject             (RFC 2865)
    4       Accounting-Request        (RFC 2866)
    5       Accounting-Response       (RFC 2866)
    

La séquence est plutôt intuitive et suit le modèle classique client-serveur :

radius-sequence
Le parallèle avec la notion d'AAA a été fait ci-dessus. Noter qu'il y a trois types d'accounting : début de session (Start), milieu pour du keepalive (Interim-Update) et fin (Stop).

Chacun ses attributs

Tout paquet RADIUS—exception faite pour l'Accounting-Response—contient les fameux attributs. Il s'agit d'informations (sous la forme clé-valeur) nécessaires ou utiles à l'établissement et au maintien de la session. Le format d'un paquet RADIUS est le suivant :


    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |     Code      |  Identifier   |            Length             |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                                                               |
    |                         Authenticator                         |
    |                                                               |
    |                                                               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |  Attributes ...
    +-+-+-+-+-+-+-+-+-+-+-+-+-
    

Le champ Code correspond aux types listés plus haut, c'est-à-dire 1 pour Access-Request, 2 pour Access-Accept, etc. Les attributs sont—s'il y en a—présents dans le champ Attributes de longueur variable. Le champ Identifier permet d'identifier un couple requête-réponse : par exemple, si un Access-Request porte l'ID 0x31, l'Access-Accept correspondant portera ce même ID. Cela aide le serveur à détecter les duplicatas reçus mais aussi nous, humains, à repérer dans le flot d'une capture ou d'un log un couple requête-réponse particulier.

Enfin, le champ Authenticator permet (notamment) au client de s'assurer de l'authenticité des réponses du serveur, afin de se prémunir contre une attaque man-in-the-middle—ce champ ne sera pas détaillé davantage ici.

Par l'exemple

Des extraits issus d'une capture Wireshark seront plus parlants :


    Code: Access-Request (1)
    Packet identifier: 0x31 (49)
    Attribute Value Pairs
        AVP: t=User-Name(1) val=my-super-box@the-operator
        AVP: t=CHAP-Password(3) val=c64cb06d8c466dcd8434b4c31b60b05169
        AVP: t=NAS-Identifier(32) val=my-super-nas
        AVP: t=Calling-Station-Id(31) val=aa:bb:cc:dd:ee:ff
    

Le message Access-Request est envoyé du NAS au serveur RADIUS afin d'authentifier—et d'autoriser comme nous le verrons ci-après—l'utilisateur. Il contient surtout le couple username-password mais beaucoup d'autres informations peuvent s'y trouver comme le nom du NAS ou encore l'adresse MAC de la box. Le serveur RADIUS s'en servira si besoin pour effectuer divers traitements algorithmiques (exemples : faire un mapping entre l'adresse MAC et l'ID du client dans le SI de l'opérateur, enlever ou ajouter certains attributs lors de la réponse—l'étape suivante—en fonction du nom du NAS).


    Code: Access-Accept (2)
    Packet identifier: 0x31 (49)
    Attribute Value Pairs
        AVP: t=Framed-IP-Address(8) val=1.2.3.4
        AVP: t=Framed-IP-Netmask(9) val=255.255.255.255
        AVP: t=Vendor-Specific(26) vnd=ciscoSystems(9)
            VSA: t=Cisco-AVPair(1) val=qos-policy-out=add-class(sub,(class-default),shape(100000000)
    

Le message Access-Accept envoyé du serveur RADIUS au NAS a un double rôle—comme illustré sur le schéma précédent. D'une part, il valide l'authentification (auth) de l'utilisateur auprès du NAS ; d'autre part, il retourne les autorisations (autz) via des attributs. Bien comprendre que autorisations ne signifient pas forcément niveau de droits ou liste de commandes tolérées—d'ailleurs, je ne l'ai vu que rarement. Dans l'exemple ci-dessus, il y a l'adresse IP publique que le NAS assignera à la box et le débit descendant souscrit par le client. Il s'agit bien d'autorisations : ainsi, le client sera autorisé à accéder à Internet grâce à son IP avec un débit descendant de 100 Mbps. Pour un autre client, ce débit pourra différer selon les options souscrites.


    Code: Accounting-Request (4)
    Packet identifier: 0x32 (50)
    Attribute Value Pairs
        AVP: t=Acct-Status-Type(40) val=Start(1)
        AVP: t=Acct-Session-Id(44) val=000000bf
        AVP: t=User-Name(1) val=my-super-box@the-operator
        AVP: t=NAS-Identifier(32) val=my-super-nas
        AVP: t=Calling-Station-Id(31) val=aa:bb:cc:dd:ee:ff
    

Une fois les étapes auth et autz réalisées, l'étape optionnelle acct peut alors débuter avec un message Accounting-Request de type Start envoyé du NAS au serveur RADIUS. L'intérêt de cette étape d'accounting est le suivi de l'utilisateur (est-il en ligne ? hors ligne ? quelle consommation de données ?). On imagine tout à fait une interface Web de suivi des utilisateurs indiquant en vert ceux en ligne et en rouge ceux hors ligne, par exemple. Ici aussi, en plus d'un ID de session propre à chaque utilisateur, beaucoup d'autres informations utiles ou non au serveur RADIUS peuvent être contenues dans le message.


    Code: Accounting-Response (5)
    Packet identifier: 0x32 (50)
    

Le message Accounting-Response, envoyé du serveur RADIUS au NAS en réponse aux trois types d'accounting, ne contient pas d'attributs (RFC 2866 §4.2). Il joue alors le rôle d'un simple ack.


    Code: Accounting-Request (4)
    Packet identifier: 0x33 (51)
    Attribute Value Pairs
        AVP: t=Acct-Status-Type(40) val=Interim-Update(3)
        AVP: t=Acct-Session-Id(44) val=000000bf
        AVP: t=User-Name(1) val=my-super-box@the-operator
        AVP: t=Acct-Input-Octets(42) val=2035
        AVP: t=Acct-Output-Octets(43) val=1246
        AVP: t=NAS-Identifier(32) val=my-super-nas
        AVP: t=Calling-Station-Id(31) val=aa:bb:cc:dd:ee:ff
    

Le message Accounting-Request de type Interim-Update envoyé du NAS au serveur RADIUS est tel un keepalive. Il indique périodiquement que l'utilisateur est toujours en ligne. Il peut notamment préciser la consommation de données de ce dernier (attributs Acct-Input-Octets et Acct-Output-Octets). On remarquera que l'ID de session 000000bf est évidemment le même que dans le message précédent.


    Code: Accounting-Request (4)
    Attribute Value Pairs
        AVP: t=Acct-Status-Type(40) val=Stop(2)
        AVP: t=Acct-Session-Id(44) val=000000bf
        AVP: t=User-Name(1) val=my-super-box@the-operator
        AVP: t=Acct-Input-Octets(42) val=2521
        AVP: t=Acct-Output-Octets(43) val=1674
        AVP: t=Acct-Terminate-Cause(49) val=Admin-Reset(6)
        AVP: t=NAS-Identifier(32) val=my-super-nas
        AVP: t=Calling-Station-Id(31) val=aa:bb:cc:dd:ee:ff
    

Enfin, le message Accounting-Request de type Stop envoyé du NAS au serveur RADIUS indique que l'utilisateur est passé hors ligne. Plusieurs causes sont possibles (RFC 2866 §5.10) : coupure volontaire (comme c'est le cas ici avec Admin-Reset) ou involontaire, etc.

Du contexte

Sauf tests particuliers, la séquence RADIUS présentée plus haut rentre toujours dans un contexte d'accès des utilisateurs au réseau. C'est souvent en DHCP ou en PPPoE. Ci-dessous des schémas intégrant cette séquence à ces deux cas.

Du FreeRADIUS

FreeRADIUS est un projet open source proposant une implémentation d'un serveur RADIUS sous Unix. Nous rentrons un peu plus dans le monde système.


      Welcome to the FreeRADIUS project, the open source implementation of RADIUS,
      an IETF protocol for AAA (Authorisation, Authentication, and Accounting).

      The FreeRADIUS project maintains the following components:
      a multi protocol policy server (radiusd) that implements RADIUS, DHCP, BFD, and ARP; (…)
    

En réalité, aujourd'hui FreeRADIUS va plus loin et embarque notamment un serveur DHCP.

Soyons technique

Le but n'est pas de décrire bêtement l'installation d'un serveur FreeRADIUS pour se satisfaire à la fin d'un « ça marche, m'voyez ». Le but est de survoler la séquence RADIUS, comprendre sa logique et son algorithmie en lien avec la notion d'AAA introduite en début d'article. Et ce, dans un contexte opérateur où des CPE s'enregistrent en IPoE ou PPPoE auprès d'un BRAS—le NAS (client RADIUS)—qui requête le serveur RADIUS :

freeradius-arch

Dans la suite, nous nous concentrons évidemment sur la partie RADIUS.

J'utilise dans la maquette un container Docker pour le serveur FreeRADIUS.

Commençons par un extrait de la séquence par défaut :


    #
    # /etc/freeradius/sites-enabled/default
    #

    server default {
        # L3-L4 configuration for Auth (and Autz)
        listen {
            type = auth
            ipaddr = *
            port = 1812
        }

        # L3-L4 configuration for Acct
        listen {
            type = acct
            ipaddr = *
            port = 1813
        }
    }
    

Ci-dessus, rien de bien compliqué. Ces lignes indiquent sur quelles IP et quels ports UDP le serveur RADIUS écoute pour l'auth (autz inclus) et l'acct. De nombreux autres paramètres peuvent être configurés. Le mieux reste alors de consulter le fichier suscité généreusement commenté.

On s'ennuit là

Bon, d'accord. Rentrons dans le vif du sujet. Je vais proposer une séquence FreeRADIUS qui n'a pas la prétention d'être exhaustive. Elle constitue un canevas générique et simplifié que j'ai toutefois configuré avec succès chez plusieurs opérateurs.

Authentication, Authorization

Tout d'abord, forgeons un paquet RADIUS de type Access-Request via la commande radclient proposée par FreeRADIUS :


    $ cat access-request.txt
    User-Name = my-super-box@operator1.com
    User-Password = my-super-pass

    $ cat access-request.txt | radclient localhost auth testing123
    

Un tel paquet est émis en local et reçu en local par le serveur FreeRADIUS. C'est très pratique en phase de test ! À la réception de l'Access-Request, le serveur exécute les sections suivantes dans leur ordre d'apparition :


    #
    # /etc/freeradius/sites-enabled/default
    #

    1
    authorize {
        filter_username
        suffix # is proxy needed? if so, sets Proxy-To-Realm attribute

        if ( ! &control:Proxy-To-Realm ) {
            # Proxy is NOT needed, pursue local processing
            sql
            my_super_module
            my_other_module
            chap
            pap
        }
    }

    2 Executed only if proxy is NOT needed
    authenticate {
        Auth-Type PAP {
            pap
        }
        Auth-Type CHAP {
            chap
        }
    }

    3 Executed in any case
    post-auth {
        sql
        Post-Auth-Type REJECT {
            sql
            attr_filter.access_reject
        }
    }
    

(1) La section authorize. Elle contient, de notre point de vue administrateur, le plus d'intelligence. En bref :

(2) La section authenticate. Elle n'est exécutée que si la requête n'a pas à être proxyfiée (encore, on en reparle plus bas). Les deux derniers appels de la section précédente sont en fait requis pour celle-ci qui exécute la logique d'authentification proprement dite selon le Auth-Type positionné. Si elle réussit, un paquet de type Access-Accept est généré avec tous les attributs de contexte reply positionnés dans la section précédente (cela inclut les attributs en base). Nous avons déjà illustré la différence entre PAP et CHAP du point de vue PPP avec des captures Wireshark. Je conseille en outre la lecture de l'excellent article PAP vs CHAP. Is PAP less secure? pour comprendre que, finalement, du point de vue serveur RADIUS, PAP est davantage secure. D'autres protocoles d'authentification, comme MS-CHAP et EAP, sont également supportés.

(3) La section post-auth. Elle a pour rôle premier de logger—dans un fichier ou en base comme c'est le cas ici—le résultat de l'authentification de la section précédente. Si elle a échoué, un paquet de type Access-Reject est généré dans celle-ci et l'appel au module attr_filter.access_reject retire tout attribut de cette réponse, excepté ceux qui sont permis par la RFC 2865.

Game of Realms

Arrêtons-nous un instant sur suffix, en réalité défini dans le module realm, soit royaume ou domaine. Son rôle—à l'instar d'une fonction split—est de découper l'attribut User-Name de la requête reçue en deux parties selon un délimiteur particulier. Dans notre exemple :


    request:User-Name = my-super-box@operator1.com (standard attribute from request, hence the "request" context)
    control:Stripped-User-Name = my-super-box      (internal attribute from parsing, hence the "control" context)
    control:Realm = operator1.com                  (internal attribute from parsing, hence the "control" context)
    

D'autres syntaxes sont par ailleurs prédéfinies, le realm pouvant se situer en préfixe ou en suffixe :


      #  'realm/username'
      realm IPASS {
      	format = prefix
      	delimiter = "/"
      }

      #  'username@realm'
      realm suffix {
      	format = suffix
      	delimiter = "@"
      }

      #  'realm!username'
      realm bangpath {
      	format = prefix
      	delimiter = "!"
      }

      #  'username%realm'
      realm realmpercent {
      	format = suffix
      	delimiter = "%"
      }

      #  'domain\user'
      realm ntdomain {
      	format = prefix
      	delimiter = "\\"
      }
    

Mais quelle est la finalité de ce découpage ? L'idée est que le serveur puisse effectuer des traitements différenciés selon les realms (et donc, pour tous les utilisateurs qui y sont associés). Plus précisément, le realm est un discriminant qui, en lien avec le fichier proxy.conf, indique au serveur que le traitement de la requête reçue sera par la suite local ou bien délégué à un autre serveur—dernier cas dit de proxy RADIUS (RFC 2865 §2.3).


      #
      # /etc/freeradius/proxy.conf
      #

      # Local realms
      realm operator1.com { }

      # Realms to proxy
      realm operator2.com {
        authhost = radius.operator2.com
        accthost = radius.operator2.com
        secret   = the-super-s3cr3t!
      }
      realm operator3.com {
        authhost = radius.operator3.com
        accthost = radius.operator3.com
        secret   = the-0th3r-secret?
      }
    

À titre d'exemple, le proxy RADIUS peut s'avérer utile lors de fusions de plusieurs structures. Imaginons que Operator1 rachète Operator2 et Operator3. Une unification de leurs serveurs RADIUS respectifs est envisagée. C'est probablement complexe, chaque opérateur possédant sa propre gestion RADIUS (liée à son modèle de services, son SI, ses choix architecturaux, etc.) et FreeRADIUS n'étant pas le seul outil sur le marché. En attendant, les serveurs peuvent se requêter entre eux grâce à ce mécanisme de proxy. Si l'on admet que le serveur FreeRADIUS de Operator1 est désigné comme principal, on pourrait aboutir à la configuration prise en exemple ci-dessus : il reçoit toutes les requêtes, traite en local celles de ses utilisateurs historiques (en @operator1.com) et délègue celles des autres utilisateurs (en @operator2.com et @operator3.com) aux opérateurs rachetés.

La trace d'exécution du serveur FreeRADIUS pour notre première Access-Request prise en exemple donnerait :


    (1) Received Access-Request Id 54 from 127.0.0.1:53978 to 127.0.0.1:1812 length 86
    (1)   User-Name = "my-super-box@operator1.com"
    (1)   User-Password = "my-super-pass"
    (1)   NAS-IP-Address = 127.0.1.1
    (1)   NAS-Port = 0
    (1)   Message-Authenticator = 0x8d5957bbb7a41460cdbfa6795179309c
    (1) # Executing section authorize from file /etc/freeradius/sites-enabled/radius
    (1)   authorize {
    (1)     policy filter_username {
    (…)
    (1) suffix: Checking for suffix after "@"
    (1) suffix: Looking up realm "operator1.com" for User-Name = "my-super-box@operator1.com"
    (1) suffix: Found realm "operator1.com"
    (1) suffix: Adding Stripped-User-Name = "my-super-box"
    (1) suffix: Adding Realm = "operator1.com"
    (1) suffix: Authentication realm is LOCAL
    

La trace d'exécution pour une deuxième Access-Request donnerait :


    (2) Received Access-Request Id 96 from 127.0.0.1:34579 to 127.0.0.1:1812 length 86
    (2)   User-Name = "my-other-box@operator2.com"
    (2)   User-Password = "my-other-pass"
    (2)   NAS-IP-Address = 127.0.1.1
    (2)   NAS-Port = 0
    (2)   Message-Authenticator = 0xd65411aa600c8ea252d37992fe759eb4
    (2) # Executing section authorize from file /etc/freeradius/sites-enabled/radius
    (2)   authorize {
    (2)     policy filter_username {
    (…)
    (2) suffix: Checking for suffix after "@"
    (2) suffix: Looking up realm "operator2.com" for User-Name = "my-other-box@operator2.com"
    (2) suffix: Found realm "operator2.com"
    (2) suffix: Adding Stripped-User-Name = "my-other-box"
    (2) suffix: Adding Realm = "operator2.com"
    (2) suffix: Proxying request from user box to realm operator2.com
    (2) suffix: Preparing to proxy authentication request to realm "operator2.com"
    

Le proxy RADIUS est aussi très fréquent dans L2TP où le RADIUS de l'opérateur de collecte (qui serait Operator1 ici) proxyfie à ceux des opérateurs de services (qui seraient Operator2 et Operator3 ici). C'est en effet ces derniers—ceux qui fournissent les services finaux (typiquement, accès Internet)—qui recensent les utilisateurs reconnus sur leur réseau ; dupliquer leurs bases de données RADIUS dans celle de l'opérateur de collecte serait à l'évidence stupide : sujet aux erreurs, peu évolutif, etc. Le realm, tel un discriminant donc, permet à l'opérateur de collecte de savoir vers qui aiguiller les requêtes.

Finalement, le rôle du module realm est double : il découpe le username reçu et indique au serveur via l'attribut Proxy-To-Realm que le traitement de la requête sera par la suite local ou délégué, comme l'explique sa documentation :


    The realm module splits a User-Name attribute into "user" and "realm" portions.
    If the realm is found, the modules sets the control:Proxy-To-Realm attribute to
    the realm name. The server will then proxy the packet to the given realm.
    

Et nous comprenons alors la condition :


    if ( ! &control:Proxy-To-Realm ) {
        # Proxy is NOT needed, pursue local processing
        sql
        my_super_module
        my_other_module
        (…)
    }
    

Car si besoin de proxy il y a, nul intérêt dans la plupart des cas d'exécuter la suite de la section authorize.

Accounting


      #
      # Handle an Accounting-Request packet.
      #
      preacct {
        preprocess
        acct_unique
        suffix
      }
      accounting {
        detail
        sql
        (…)
        attr_filter.accounting_response
      }
      session {
      }
    

      #
      # If proxying is needed (the "suffix" module tells if it is the case).
      # This applies for both Access-Request and Accounting-Request packets.
      #
      pre-proxy { }
      post-proxy { }
    }
  

Suite en cours de rédaction.