(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 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 :
- Authentication : est-ce vraiment 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 :

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.
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 (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
}
}
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 dans tout projet de dév—trouve sa raison dans des choix d'implémentation et 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 dans le Access-Accept
si l'authentification réussit.
Ainsi, on fait d'une pierre deux coups !
Un billet intéressant à ce sujet :
Why Authorization before Authentication?
La section authorize
contient le plus d'intelligence. En bref :
filter_username
: s'assure que la valeur de l'attributUser-Name
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 traitement spécifique é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)
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.
request
, reply
, proxy-request
, proxy-reply
ou control
—ce
dernier concernant les attributs internes à FreeRADIUS.
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.
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.