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.

#about #contact

(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 formatté d'une certaine façon (exemple) 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 dans le schéma. 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) indispensables 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 (GitHub) proposant une implémentation d'un serveur RADIUS sous Linux. Nous rentrons donc 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 ici n'est pas de décrire bêtement l'installation d'un serveur FreeRADIUS et sa configuration initiale. Tout le monde sait lire un tutoriel, et ce n'est pas ce qui manque sur la toile. 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. Pour ce faire, un extrait de la séquence par défaut constitue une bonne base :


      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—non pertinents ici nous concernant—peuvent être configurés. Le mieux reste alors de consulter le fichier suscité qui est généreusement commenté.

On s'ennuit là.

Bon ok. Rentrons dans le vif du sujet. Je vais prendre en exemple une séquence FreeRADIUS qui n'a pas la prétention d'être exhaustive. Elle est un canevas simplifié que j'ai toutefois rencontré et configuré avec succès chez plusieurs opérateurs. Considérons l'Access-Request suivante émise par une exécution de la commande radclient en local sur le serveur lui-même :


      (…)
      Ready to process requests
      (1) Received Access-Request Id 54 from 127.0.0.1:53978 to 127.0.0.1:1812 length 86
      (1)   User-Name = "box@operator1.fr"
      (1)   User-Password = "my-super-pass"
      (1)   NAS-IP-Address = 127.0.1.1
      (1)   NAS-Port = 0
      (1)   Message-Authenticator = 0x8d5957bbb7a41460cdbfa6795179309c
      

À la réception de cette requête, le serveur FreeRADIUS exécute les trois sections suivantes dans leur ordre d'apparition :


      #
      # Handle an Access-Request packet
      #

      authorize {
          filter_username
          suffix

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

      authenticate {
          Auth-Type PAP {
              pap
          }
          Auth-Type CHAP {
              chap
          }
      }

      post-auth {
          sql
          Post-Auth-Type REJECT {
              sql
              attr_filter.access_reject
          }
      }
      

La section authorize contient le plus d'intelligence. En bref :

Les deux derniers appels sont en fait nécessaires pour la section qui suit, authenticate, qui exécutera la logique d'authentification proprement dite selon le Auth-Type positionné. 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 des excellents articles PAP vs CHAP. Is PAP less secure? et How authentication protocols work? pour comprendre que, finalement, du point de vue serveur RADIUS, PAP est davantage secure. Enfin, noter que d'autres protocoles d'authentification, comme MS-CHAP et EAP, sont également supportés.

Game of Realms

Arrêtons-nous un instant sur suffix, qui correspond à une syntaxe définie dans le module realm, soit royaume ou domaine en français. Son rôle est de découper—à l'instar d'une fonction split—le nom d'utilisateur reçu dans la requête en deux parties selon un délimiteur particulier. Par exemple, si ce dernier est box@operator1.fr, nous obtiendrons :


      request:User-Name = box@operator1.fr (attribut standard issu de la requête)
      control:Stripped-User-Name = box     (attribut interne issu du découpage)
      control:Realm = operator1.fr         (attribut interne issu du découpage)
      

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 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).


        #
        # proxy.conf (sample)
        #

        # Local realms
        realm operator1.fr { }

        # Realms to proxy
        realm operator2.fr {
          authhost = radius.operator2.fr
          accthost = radius.operator2.fr
          secret   = the-super-s3cr3t!
        }

        realm operator3.fr {
          authhost = radius.operator3.fr
          accthost = radius.operator3.fr
          secret   = the-0th3r-secret?
        }
      

À titre d'exemple, le proxy RADIUS peut devenir utile lors de fusions de plusieurs structures. Imaginons que Operator1 rachète Operator2 et Operator3. Dans le cadre de cette fusion, une unification des serveurs RADIUS respectifs est envisagée. Cette tâche est probablement complexe : chaque opérateur possède sa propre gestion RADIUS (liée à son modèle de services, son SI et à ses choix architecturaux) et FreeRADIUS n'est 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 (minimale) prise en exemple ci-dessus : il reçoit toutes les requêtes, traite en local celles de ses utilisateurs historiques (en @operator1.fr) et délègue celles des autres utilisateurs (en @operator2.fr et @operator3.fr) 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 = "box@operator1.fr"
      (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.fr" for User-Name = "box@operator1.fr"
      (1) suffix: Found realm "operator1.fr"
      (1) suffix: Adding Stripped-User-Name = "box"
      (1) suffix: Adding Realm = "operator1.fr"
      (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 = "box@operator2.fr"
      (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.fr" for User-Name = "box@operator2.fr"
      (2) suffix: Found realm "operator2.fr"
      (2) suffix: Adding Stripped-User-Name = "box"
      (2) suffix: Adding Realm = "operator2.fr"
      (2) suffix: Proxying request from user box to realm operator2.fr
      (2) suffix: Preparing to proxy authentication request to realm "operator2.fr"
      

Le proxy RADIUS est aussi très fréquent dans L2TP où le RADIUS de l'opérateur de collecte (qui serait Operator1 ici) proxifie à 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 : d'une part, il découpe le nom d'utilisateur reçu et d'autre part, il indique au serveur—techniquement, via un flag Proxy-To-Realm—que le traitement de la requête sera local ou délégué, comme l'indique un extrait de sa documentation complète :


      (…) the control:Proxy-To-Realm attribute is set to the realm name.
      The server will then find this attribute and proxy the request
      instead of performing local authentication.
      

Et nous comprenons alors la condition :


        authorize {
            (…)
            suffix
            
            if (!&control:Proxy-To-Realm) {
                # Proxy not needed, local processing
                (…)
            }
        }
        

Car si besoin de proxy il y a, nul besoin en général 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.