(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 :
- Établir une database centralisée des utilisateurs reconnus sur le réseau—c'est l'aspect service.
- Parler avec cette database via un langage particulier—c'est l'aspect protocolaire.
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 :
- Authentication : est-ce bien toi ? Typiquement, cela correspond à la vérification username-password.
- Authorization : quelles sont tes permissions ? Par exemple, le niveau de l'utilisateur sur un équipement (
admin
,tech
, etc.). Mais pas que. C'est dans cette étape que l'adresse IP d'une box peut lui être assignée, comme le débit souscrit par le client. Cela lui permet alors d'accéder au service Internet souscrit. - Accounting : pour suivre les utilisateurs (quand sont-ils en ligne ? hors ligne ? quelle consommation de données ? etc.).
On dit aussi que le RADIUS est un protocole AAA, à l'instar des protocoles DIAMETER et TACACS.
auth
, autz
et acct
.
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 :
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.
A
et B
sont indépendants—et c'est d'ailleurs rare qu'ils correspondent aussi proprement.
Par exemple, les renewal DHCP et keepalive PPP peuvent être fixés à 60s
quand l'Interim-Update
est fixé à 300s
.
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 :
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
}
}
Remarquez déjà : la section authorize
est exécutée en premier, étonnamment !
Étonnamment, car cela signifie que le serveur procède à l'autorisation avant l'authentification.
Dans un monde idéal, en toute logique, on authentifie un utilisateur puis on lui accorde ses autorisations ;
en effet, si l'authentification échoue, nul besoin d'aller plus loin.
Cette imperfection—comme souvent dans un projet de dév—s'explique par :
- Des choix d'implémentation : certains traitements doivent être effectués en amont de la section
authenticate
commesuffix
,chap
etpap
. - Des choix d'optimisation : le couple username-password est récupéré via le module
sql
, qui récupère par la même en base tous les attributs à retourner si l'authentification réussit. Ainsi, on fait d'une pierre deux coups !
Le billet Why Authorization before Authentication? est intéressant à ce sujet. Par ailleurs, la v4 de FreeRADIUS renommera certains éléments pour rendre l'algorithmie plus claire : « For example, recv Access-Request is clearer than authorize. »
(1) La section authorize
. Elle contient, de notre point de vue administrateur, le plus d'intelligence. En bref :
filter_username
: s'assure que la valeur de l'attributUser-Name
de la requête reçue est correcte (pas d'espace, etc.)suffix
: parse la valeur de l'attributUser-Name
et positionne l'attribut interneProxy-To-Realm
(on en reparle plus bas avec la condition associée)sql
: récupère les attributs en base (cela pourrait aussi être fait « à l'ancienne » depuis un fichier texte avec le modulefiles
)my_super_module
: un exemple de module spécifique dont le contenu est écrit en Unlang (un langage créé par et pour FreeRADIUS)my_other_module
: idemchap
: positionne l'attribut interneAuth-Type
, si non déjà positionné plus haut, à la valeurCHAP
si la requête contient l'attributCHAP-Password
pap
: positionne l'attribut interneAuth-Type
, si non déjà positionné plus haut, à la valeurPAP
(authentification par défaut)
request
, reply
, proxy-request
, proxy-reply
ou control
—ce
dernier concernant les attributs internes à FreeRADIUS,
d'où l'utilisation de control:Proxy-To-Realm
dans notre séquence.
Cela sert à désigner sans ambiguïté un attribut qui est parfois valide dans plusieurs contextes et donc de réaliser des manipulations plus fines.
(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
.
authorize
est exécutée inutilement,
ce qui est sous-optimal mais qui, plus grave, peut engendrer des effets de bord.
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.