Example eduroam setup
This is an example basic eduroam configuration for the debops.freeradius Ansible role.
Note
The files are available in the DebOps monorepo, as separate YAML
files in the docs/ansible/roles/debops.freeradius/examples/eduroam/
directory.
You can put these files in the Ansible inventory, in
ansible/inventory/host_vars/<hostname>/
directory. After doing this and
tweaking the configuration you should run the debops.freeradius and
debops.resources Ansible roles against the host.
This configuration is based on the example eduroam configuration guide on the FreeRADIUS Wiki. You should check this page for detailed guide about this setup.
FreeRADIUS configuration
---
# Configuration based on: https://wiki.freeradius.org/guide/eduroam
# DNS domain user by RADIUS
radius_domain: '{{ ansible_domain }}'
# Separate VLAN for external eduroam users, from other organizations
radius_guest_vlan: '101'
# Separate VLAN for internal eduroam users, from our organization
radius_local_vlan: '102'
# X.509 certificate configuration, based on DebOps PKI
radius_pki_realm: 'domain'
radius_cert_file: '/etc/pki/realms/{{ radius_pki_realm }}/default.crt'
radius_key_file: '/etc/pki/realms/{{ radius_pki_realm }}/default.key'
radius_ca_file: '/etc/pki/realms/{{ radius_pki_realm }}/CA.crt'
# Subnet on which WiFi Access Points can talk with RADIUS server
radius_access_point_subnet: '192.168.2.0/24'
# Shared passphrase for the Access Points to authenticate with the RADIUS
# Server
radius_access_point_password: '{{ lookup("password", secret
+ "/radius/known-secret-password") }}'
# Password of the client endpoint to authorized access to the network
radius_test_user_password: '{{ lookup("password", secret
+ "/radius/default-test-password") }}'
# Allow access to FreeRADIUS service by WiFi Access Points
freeradius__host_allow: [ '{{ radius_access_point_subnet }}' ]
# Configuration for the 'debops.freeradius' Ansible role
freeradius__host_configuration:
- name: 'sites-available/default'
raw: |
# The domain users will add to their username to have their credentials
# routed to your institution. You will also need to register this
# and your RADIUS server addresses with your NRO.
operator_name = "{{ radius_domain }}"
# The VLAN to assign eduroam visitors
eduroam_guest_vlan = "{{ radius_guest_vlan }}"
# The VLAN to assign your students/staff
eduroam_local_vlan = "{{ radius_local_vlan }}"
server eduroam {
listen {
type = auth
ipaddr = *
port = 1812
}
authorize {
# Log requests before we change them
linelog_recv_request
# split_username_nai is a policy in the default distribution to
# split a username into username and domain. We reject user-name
# strings without domains, as they're not routable.
split_username_nai
if (noop || !&Stripped-User-Domain) {
reject
}
# Send the request to the NRO for your region.
# The details of the FLRs (Federation Level RADIUS servers)
# are in proxy.conf.
# You can make this condition as complex as you like, to
# include additional subdomains just concatenate the conditions
# with &&.
if (&Stripped-User-Domain != "${operator_name}") {
update {
control:Load-Balance-Key := &Calling-Station-ID
control:Proxy-To-Realm := 'eduroam_flr'
# Operator name (RFC 5580) identifies the network the
# request originated from. It's not absolutely necessary
# but it helps with debugging.
request:Operator-Name := "1${operator_name}"
}
return
}
# If the EAP module returns 'ok' or 'updated', it means it has handled
# the request and we don't need to call any other modules in this
# section.
eap {
ok = return
updated = return
}
}
pre-proxy {
attr_filter.pre-proxy
linelog_send_proxy_request
}
post-proxy {
attr_filter.post-proxy
linelog_recv_proxy_response
}
authenticate {
eap
}
post-auth {
# To implement eduroam you must:
# - Use wireless access points or a controller which supports
# dynamic VLAN assignments.
# - Have that feature enabled.
# - Have the guest_vlan/local_vlan available to the controller,
# or to all your access points.
# eduroam user traffic *MUST* be segregated, this is *NOT* optional.
update reply {
Tunnel-Type := VLAN
Tunnel-Medium-Type := IEEE-802
}
if (&control:Proxy-To-Realm) {
update reply {
Tunnel-Private-Group-ID := ${eduroam_guest_vlan}
}
}
else {
update reply {
Tunnel-Private-Group-ID := ${eduroam_local_vlan}
}
}
# We're sending a response to one of OUR network devices for one of
# OUR users so provide it with the real user-identity.
if (&session-state:Stripped-User-Name) {
update reply {
User-Name := "%{session-state:Stripped-User-Name}@%{Stripped-User-Domain}"
}
}
linelog_send_accept
Post-Auth-Type REJECT {
attr_filter.access_reject
linelog_send_reject
}
}
}
state: 'present'
- name: 'mods-available/eap'
raw: |
eap {
# The initial EAP type requested. Change this to peap if you're
# using peap, or tls if you're using EAP-TLS.
default_eap_type = ttls
# The maximum time an EAP-Session can continue for
timer_expire = 60
# The maximum number of ongoing EAP sessions
max_sessions = ${max_requests}
tls-config tls-common {
# The public certificate that your server will present
certificate_file = {{ radius_cert_file }}
# The private key for the public certificate
private_key_file = {{ radius_key_file }}
# The password to decrypt 'private_key_file'
#private_key_password = whatever
private_key_password = ''
# The certificate of the authority that issued 'certificate_file'
#ca_file = ${cadir}/ca.pem
ca_file = {{ radius_ca_file }}
# If your AP drops packets towards the client, try reducing this.
fragment_size = 1024
# When issuing client certificates embed the OCSP URL in the
# certificate if you want to be able to revoke them later.
ocsp {
enable = yes
override_cert_url = no
use_nonce = yes
}
}
tls {
tls = tls-common
}
ttls {
tls = tls-common
default_eap_type = mschapv2
virtual_server = "eduroam-inner"
}
peap {
tls = tls-common
default_eap_type = mschapv2
virtual_server = "eduroam-inner"
}
}
state: 'present'
- name: 'mods-available/linelog'
raw: |
linelog linelog_recv_request {
filename = syslog
syslog_facility = local0
syslog_severity = debug
format = "action = Recv-Request, %{pairs:request:}"
}
linelog linelog_send_accept {
filename = syslog
syslog_facility = local0
syslog_severity = debug
format = "action = Send-Accept, %{pairs:request:}"
}
linelog linelog_send_reject {
filename = syslog
syslog_facility = local0
syslog_severity = debug
format = "action = Send-Reject, %{pairs:request:}"
}
linelog linelog_send_proxy_request {
filename = syslog
syslog_facility = local0
syslog_severity = debug
format = "action = Send-Proxy-Request, %{pairs:proxy-request:}"
}
linelog linelog_recv_proxy_response {
filename = syslog
syslog_facility = local0
syslog_severity = debug
format = "action = Recv-Proxy-Response, %{pairs:proxy-reply:}"
}
state: 'present'
- name: 'proxy.conf'
raw: |
home_server eduroam_flr_server_1 {
ipaddr = 127.0.0.1
secret = secret
status_check = status-server
}
# Only uncomment if there are two FLRS
#home_server eduroam_flr_server_2 {
# ipaddr = <ip-address>
# secret = <secret>
# status_check = status-server
#}
home_server_pool eduroam_flr_pool {
type = keyed-balance
home_server = eduroam_flr_server_1
# Only uncomment if there are two FLRS
# home_server = eduroam_flr_server_2
}
realm eduroam_flr {
auth_pool = eduroam_flr_pool
nostrip
}
state: 'absent'
no_log: '{{ debops__no_log | d(True) }}'
- name: 'clients.conf'
raw: |
client localhost {
ipaddr = 127.0.0.1
secret = testing123
}
#client eduroam_flr_server_1 {
# ipaddr = <ip-address>
# secret = <secret>
# nastype = 'eduroam_flr'
#}
# As above, only uncomment if there are two federation level servers
#client eduroam_flr_server_2 {
# ipaddr = <ip-address>
# secret = <secret>
# nastype = 'eduroam_flr'
#}
client wireless_access_points_mgmt {
ipaddr = {{ radius_access_point_subnet }}
# This should be long and random
secret = {{ radius_access_point_password }}
}
state: 'present'
no_log: '{{ debops__no_log | d(True) }}'
- name: 'sites-available/inner-tunnel'
raw: |
server eduroam-inner {
listen {
type = auth
ipaddr = *
port = 18120 # Used for testing only. Requests proxied internally.
}
authorize {
# The outer username is considered garabage for autz purposes, but
# the domain portion of the outer and inner identities must match.
split_username_nai
if (noop || (&Stripped-User-Domain && \
(&outer.Stripped-User-Domain != &Stripped-User-Domain))) {
reject
}
# Make the user's real identity available to anything that needs
# it in the outer server.
update {
&outer.session-state:Stripped-User-Name := &Stripped-User-Name
}
# EAP for PEAPv0 (EAP-MSCHAPv2)
inner-eap {
ok = return
}
# THIS IS SITE SPECIFIC
#
# The files module is *ONLY* used for testing. It lets you define
# credentials in a flat file, IT WILL NOT SCALE.
#
# - If you use OpenLDAP with salted password hashes you should
# call the 'ldap' module here and use EAP-TTLS-PAP as your EAP method.
# - If you use OpenLDAP with cleartext passwords you should
# call the 'ldap' module here and use EAP-TTLS or PEAPv0.
# - If you use an SQL DB with salted password hashes you should call
# the 'sql' module here and use EAP-TTLS-PAP as your EAP method.
# - If you use an SQL DB with cleartext passwords you should call
# the 'sql' module here and use EAP-TTLS or PEAPv0.
# - If you use Novell you should call the 'ldap' module here and
# set ``edir = yes`` in ``mods-available/ldap`` and use EAP-TTLS or
# PEAPv0.
# - If you use Active Directory, you don't need anything here (remove
# the call to files) but you'll need to follow this
# [guide](freeradius-active-directory-integration-howto) and use
# EAP-TTLS-PAP or PEAPv0.
# - If you're using EAP-TLS (i'm impressed!) remove the call to files.
#
# EAP-TTLS-PAP and PEAPv0 are equally secure/insecure depending on how the
# supplicant is configured. PEAPv0 has a slight edge in that you need to
# crack MSCHAPv2 to get the user's password (but this is not hard).
files
pap
mschap
}
authenticate {
inner-eap
mschap
pap
# Comment pap, and uncomment the stanza below if you're using
# Active Directory this will allow it to work with EAP-TTLS-PAP.
#pap {
# ntlm_auth
#}
}
}
state: 'present'
- name: 'mods-enabled/inner-eap'
link_src: '../mods-available/inner-eap'
- name: 'mods-available/inner-eap'
raw: |
eap inner-eap {
default_eap_type = mschapv2
timer_expire = 60
max_sessions = ${max_requests}
mschapv2 {
send_error = yes
}
}
state: 'present'
- name: 'mods-config/files/authorize'
comment: |
This sets the same password for any user that tries to authenticate, do
not use in production environment
raw: |
DEFAULT Cleartext-Password := '{{ radius_test_user_password }}'
state: 'present'
no_log: '{{ debops__no_log | d(True) }}'
Additional resources
The install-eapol_test
script created by this configuration can be used
to install the eapol_test command on either the same host as the
FreeRADIUS server, or on a different, remote host, to test the connectivity
over the network.
---
# Configuration based on: https://wiki.freeradius.org/guide/eduroam
# X.509 certificate configuration, based on DebOps PKI
radius_pki_realm: 'domain'
radius_cert_file: '/etc/pki/realms/{{ radius_pki_realm }}/default.crt'
radius_key_file: '/etc/pki/realms/{{ radius_pki_realm }}/default.key'
radius_ca_file: '/etc/pki/realms/{{ radius_pki_realm }}/CA.crt'
# Directory where test configuration files are stored
config_dir: '/srv/eapol-test'
# Secret passphrase for the Access Points to authenticate with the RADIUS
# Server
radius_access_point_password: '{{ lookup("password", secret
+ "/radius/known-secret-password") }}'
# Example user to test authentication to RADIUS
radius_test_user_identity: 'a_user@{{ ansible_domain }}'
# Password of the client endpoint to authorized access to the network
radius_test_user_password: '{{ lookup("password", secret
+ "/radius/default-test-password") }}'
# Configuration for the 'debops.resources' Ansible role
resources__host_files:
- content: |
#!/bin/bash
# Install eapol_test for testing RADIUS EAP connections
sudo apt-get update
sudo apt-get -yq install git build-essential \
libssl-dev devscripts \
pkg-config libnl-3-dev \
libnl-genl-3-dev
git clone --depth 1 --no-single-branch https://github.com/FreeRADIUS/freeradius-server.git
cd freeradius-server/scripts/ci/
./eapol_test-build.sh
sudo cp ./eapol_test/eapol_test /usr/local/bin/
dest: '/usr/local/bin/install-eapol_test'
mode: '0755'
- content: |
#
# eapol_test -c eap-tls.conf -s "{{ radius_access_point_password }}" \
# -a <radius-ip-server>
#
network={
key_mgmt=WPA-EAP
eap=TTLS
identity="{{ radius_test_user_identity }}"
anonymous_identity="anonymous@{{ ansible_domain }}"
# Uncomment to validate the server's certificate by checking
# it was signed by this CA.
ca_cert="{{ radius_ca_file }}"
password="{{ radius_test_user_password }}"
phase2="auth=PAP"
}
dest: '{{ config_dir }}/eap-tls.conf'
mode: '0644'
- content: |
#
# eapol_test -c peap-mschapv2.conf -s "{{ radius_access_point_password }}" \
# -a <radius-ip-address>
#
network={
key_mgmt=WPA-EAP
eap=PEAP
identity="{{ radius_test_user_identity }}"
anonymous_identity="anonymous@{{ ansible_domain }}"
# Uncomment to validate the server's certificate by checking
# it was signed by this CA.
ca_cert="{{ radius_ca_file }}"
password="{{ radius_test_user_password }}"
phase2="auth=MSCHAPV2 mschapv2_retry=0"
phase1="peapver=0"
}
dest: '{{ config_dir }}/peap-mschapv2.conf'
mode: '0644'
- content: |
#
# eapol_test -c tls.conf -s "{{ radius_access_point_password }}" \
# -a <radius-ip-address>
#
network={
key_mgmt=WPA-EAP
eap=TLS
anonymous_identity="anonymous@{{ ansible_domain }}"
# Uncomment to validate the server's certificate by checking
# it was signed by this CA.
ca_cert="{{ radius_ca_file }}"
# supplicant's public cert
client_cert="{{ radius_cert_file }}"
# supplicant's private key
private_key="{{ radius_key_file }}"
# password to decrypt private key
private_key_passwd=""
}
dest: '{{ config_dir }}/tls.conf'
mode: '0644'