FreeRadius Wifi PEAP/MSCHAPv2
FreeRadius server set up on FreeBSD
Join domain with Samba, Authentication use mschapv2
Assigned VLAN by AD group via mod_perl
Request Certificate
openssl.conf
[ req ]
prompt = no
encrypt_key = no
default_bits = 2048
req_extensions = v3_req
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
commonName = wifi-bkk.example.com
emailAddress = ne@example.com
countryName = TH
stateOrProvinceName = Bangkok
localityName = BKK
0.organizationName = Example
organizationalUnitName = NE
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
IP.1 = 192.168.214.250
DNS.1 = wifi-bkk.example.com
[ xpclient_ext ]
extendedKeyUsage = 1.3.6.1.5.5.7.3.2
[ xpserver_ext ]
extendedKeyUsage = 1.3.6.1.5.5.7.3.1
Generate server certificate
openssl req -new -nodes -keyout key.pem -out req.pem -days 1825 -config openssl.cnf
Generate dh file
openssl dhparam -check -text -5 -out dh 4096
dd if=/dev/urandom of=random count=2
Secure cert file
chown -R freeradius:freeradius cert
chmod -R 0700 cert
chmod 0400 cert/*
openssl x509 -text -noout -in /root/cert/cert.crt
Install software
cd /usr/ports/net/freeradius3
make install clean
cd /usr/ports/net/samba413
make install clean
Config freeradius
Enable mod_perl
ln -s /usr/local/etc/raddb/mods-available/perl /usr/local/etc/raddb/mods-enabled/perl
vi /usr/local/etc/raddb/mods-available/perl
perl {
filename = ${modconfdir}/perl/radius.pl
}
vi /usr/local/etc/raddb/mods-available/eap
eap {
default_eap_type = peap
timer_expire = 60
ignore_unknown_eap_types = no
cisco_accounting_username_bug = no
max_sessions = ${max_requests}
tls-config tls-common {
private_key_file = /root/cert/key.pem
certificate_file = /root/cert/cert.crt
ca_file = /root/cert/cacert.crt
dh_file = /root/cert/dh
# random_file = /root/cert/random
check_crl = no
#ca_path = ${cadir}
cipher_list = "DEFAULT"
ecdh_curve = "prime256v1"
cache {
enable = yes
lifetime = 24 # hours
max_entries = 255
}
}
peap {
tls = tls-common
default_eap_type = mschapv2
copy_request_to_tunnel = no
use_tunneled_reply = no
virtual_server = "inner-tunnel"
}
mschapv2 {
# send_error = no
}
}
vi /usr/local/etc/raddb/mods-available/mschap
mschap {
with_ntdomain_hack = yes
ntlm_auth = "/usr/local/bin/ntlm_auth --use-cached-creds --request-nt-key --username=%{mschap:User-Name:-%{Stripped-User-Name}:-%{%{User-Name}:-None}} --challenge=%{%{mschap:Challenge}:-00} --nt-response=%{%{mschap:NT-Response}:-00}"
}
vi /usr/local/etc/raddb/clients.conf
client localhost {
ipaddr = 127.0.0.1
proto = *
secret = test
require_message_authenticator = no
nas_type = other # localhost isn't usually a NAS...
}
# IPv6 Client
client localhost_ipv6 {
ipv6addr = ::1
secret = testing123
}
#AP.BKK2
client AP-BKK2 {
ipaddr = 192.168.214.250
proto = *
secret = xxxxxxxx
require_message_authenticator = no
nas_type = other # localhost isn't usually a NAS...
}
vi /usr/local/etc/raddb/sites-available/default
authorize {
chap
mschap
eap {
ok = return
}
files
}
authenticate {
mschap
eap
}
accounting {
perl
}
post-auth {
perl
}
Config Samba
vi /usr/local/etc/smb4.conf
[global]
netbios name = BKK2-RADIUS
workgroup = EXAMPLE
server string = BKK2 RADIUS
security = ads
invalid users = root
socket options = TCP_NODELAY
idmap uid = 16777216-33554431
idmap gid = 16777216-33554431
winbind use default domain = yes
winbind max domain connections = 5
winbind max clients = 1000
# allow enumeration of winbind users and groups
winbind enum users = yes
winbind enum groups = yes
password server = *
realm = example.com
Join domain
vi /etc/hosts
192.168.213.240 bkk2-radius.example.com bkk2-radius
net ads join -U Administrator
net ads testjoin
ntlm_auth --username=user --domain=domain
winbindpriv permission
chown root:freeradius /var/db/samba4/winbindd_priviliged/pipe
chown root:freeradius /var/db/samba4/winbindd_priviliged
vi /etc/nsswitch.conf
group: files winbind
passwd: files winbind
vi /etc/rc.conf
samba_server_enable="YES"
radiusd_enable="YES"
Conifg perl radius
For assigne VLAN and accounting on Fortigate
sub post_auth for assigne VLAN and add group
sub accounting for accounting, query MAC/IP tuple from Fortigate(Main router)
Enable/disable function on use
vi /usr/local/etc/raddb/mods-config/perl/radius.pl
#!/usr/local/bin/perl
use utf8;
use strict;
use warnings;
use Data::Dumper;
# Filter AD group name
my $ad_filter = 'AD\-(?:Executives|Managers|Engineering|Web|Support|Billing)';
# Use for Query ARP/IP
my $ssh_arg = '-p 55 192.168.254.66';
my $radius_host = '192.168.254.66';
my $radius_secret = 'fortigate';
my $radius_called = 'BKK2';
# find vlan for user
sub find_vlan {
my %class = map { $_ => 1 } @_;
return 215 if ( $class{'AD-Engineering'} );
return 216 if ( $class{'AD-Billing'} );
return 215 if ( $class{'AD-Web'} );
return 215 if ( $class{'AD-Managers'} );
return 215 if ( $class{'AD-Executives'} );
return 214 if ( $class{'AD-Support'} );
return 217; # Other/Unknow = Guest VLAN
}
sub send_rad_ip {
my $host = shift;
my $secret = shift;
my $user = shift;
my $ip = shift;
my $action = shift;
my @acct_class = @_;
open( RAD, "| /usr/local/bin/radclient -q $host acct $secret" )
|| return 1;
print RAD "Acct-Status-Type=$action\n";
print RAD "Acct-Authentic=RADIUS\n";
print RAD "Acct-Session-Id=$ip-$user\n";
print RAD "Called-Station-Id=00-50-56-BD-04-71:$radius_called\n";
print RAD "User-Name=$user\n";
for (@acct_class) {
print RAD "Class=$_\n";
}
print RAD "Service-Type=Framed\n";
print RAD "Framed-IP-Address=$ip\n" if ($ip);
close RAD;
return 0;
}
my $regex_ip = '(?:\d{1,3}\.){3}\d{1,3}';
my $regex_mac =
'(?:(?:[0-9A-Fa-f]{2}[:\-]?){2}[\.:\-]){2}(?:[0-9A-Fa-f]{2}[:\-]?){2}';
# Normalized mac address
sub extract_mac {
shift =~ /($regex_mac)/;
return 0 unless ($1);
my $mac = uc $1;
$mac =~ s/[^0-9A-F]//g;
$mac = lc($mac);
$mac =~ s/..\K(?=.)/\:/sg;
return $mac;
}
# Normalized username
sub strip_username {
my $user = shift;
my $index;
if ( ( $index = index( $user, '/' ) ) != -1
|| ( $index = index( $user, '\\' ) ) != -1 )
{
return substr( $user, $index + 1 );
}
if ( ( $index = index( $user, '@' ) ) != -1 ) {
return substr( $user, 0, $index );
}
return $user;
}
# Get IP from Fortigate
sub find_arp {
my $mac = shift;
my $out = qx{/usr/bin/ssh $ssh_arg "get system arp"};
while ( $out =~ /($regex_ip)\s+\d+\s+($regex_mac)/g ) {
my $qmac = extract_mac($2);
return $1 if ( $mac eq $qmac );
}
return "";
}
# Get IP from Fortigate
sub find_dhcp {
my $mac = shift;
my $out = qx{/usr/bin/ssh $ssh_arg "execute dhcp lease-list"};
while ( $out =~ /($regex_ip)\s+($regex_mac)/g ) {
my $qmac = extract_mac($2);
return $1 if ( $mac eq $qmac );
}
return "";
}
# function to find AD group from SAMBA
sub find_group($;$) {
my ( $user, $prefix ) = @_;
my ($sid) = split( /\s/, qx{/usr/local/bin/wbinfo --name-to-sid $user} )
or return ();
$prefix = "" unless ($prefix);
my @groups = ();
my @domgroups = qx{/usr/local/bin/wbinfo --user-domgroups $sid};
for (@domgroups) {
chomp;
push @groups, $1
if qx{/usr/local/bin/wbinfo --sid-to-fullname $_} =~
/.*\\($prefix.*)\s\d+/;
}
return @groups;
}
#startwith
sub begins_with {
#return !rindex( $_[0], $_[1], 0 );
return substr( $_[0], 0, length( $_[1] ) ) eq $_[1];
}
# RADIUS function
our ( %RAD_REQUEST, %RAD_REPLY, %RAD_CHECK );
use constant {
RLM_MODULE_REJECT => 0, # immediately reject the request
RLM_MODULE_OK => 2, # the module is OK, continue
RLM_MODULE_HANDLED => 3, # the module handled the request, so stop
RLM_MODULE_INVALID => 4, # the module considers the request invalid
RLM_MODULE_USERLOCK => 5, # reject the request (user is locked out)
RLM_MODULE_NOTFOUND => 6, # user not found
RLM_MODULE_NOOP => 7, # module succeeded without doing anything
RLM_MODULE_UPDATED => 8, # OK (pairs modified)
RLM_MODULE_NUMCODES => 9 # How many return codes there are
};
use constant {
L_DBG => 1,
L_AUTH => 2,
L_INFO => 3,
L_ERR => 4,
L_PROXY => 5,
L_ACCT => 6
};
# Function to handle accounting
sub accounting {
# For debugging purposes only
#&log_request_attributes;
if ( $RAD_REQUEST{'Calling-Station-Id'} ) {
my $user = strip_username( $RAD_REQUEST{'User-Name'} );
my $mac = extract_mac $RAD_REQUEST{'Calling-Station-Id'};
&radiusd::radlog( L_ACCT,
"Accounting $RAD_REQUEST{'Acct-Status-Type'} for $user" );
if ( $RAD_REQUEST{'Acct-Status-Type'} eq 'Interim-Update' ) {
my $ip = $RAD_REQUEST{'Framed-IP-Address'} || find_arp($mac);
unless ($ip) {
return RLM_MODULE_NOOP;
}
my @group = find_group( $user, $ad_filter );
if ( $RAD_REQUEST{'NAS-Port-Type'} eq 'Ethernet' ) {
push( @group, 'Ethernet' );
}
elsif ( $RAD_REQUEST{'Fortinet-Client-IP-Address'} ) {
# Fortinet-Vdom-Name = _internal
# Fortinet-Client-IP-Address = 223.24.156.237
push( @group, 'VPN' );
}
elsif ( begins_with( $RAD_REQUEST{'NAS-Port-Type'}, 'Wireless' ) ) {
push( @group, 'Wireless' );
}
&radiusd::radlog( L_ACCT,
"Interim for $user, IP: $ip, MAC: $mac, Class: "
. join( ', ', @group ) );
send_rad_ip( $radius_host, $radius_secret, $user, $ip,
'Interim-Update', @group );
}
elsif ( $RAD_REQUEST{'Acct-Status-Type'} eq 'Start' ) {
my $ip = $RAD_REQUEST{'Framed-IP-Address'};
my @group = find_group( $user, $ad_filter );
if ( $RAD_REQUEST{'NAS-Port-Type'} eq 'Ethernet' ) {
push( @group, 'Ethernet' );
}
elsif ( $RAD_REQUEST{'Fortinet-Client-IP-Address'} ) {
# Fortinet-Vdom-Name = _internal
# Fortinet-Client-IP-Address = 223.24.156.237
push( @group, 'VPN' );
}
elsif ( begins_with( $RAD_REQUEST{'NAS-Port-Type'}, 'Wireless' ) ) {
push( @group, 'Wireless' );
}
if ($mac) {
unless ($ip) {
$ip = find_dhcp($mac);
}
unless ($ip) {
sleep(3);
$ip = find_arp($mac);
}
}
unless ($ip) {
&radiusd::radlog( L_ACCT,
"Login for $user, IP: $ip, MAC: $mac, NOIP" );
return RLM_MODULE_NOOP;
}
&radiusd::radlog( L_ACCT,
"Login for $user, IP: $ip, MAC: $mac, Class: "
. join( ', ', @group ) );
send_rad_ip( $radius_host, $radius_secret, $user, $ip, 'Start',
@group );
}
elsif ( $RAD_REQUEST{'Acct-Status-Type'} eq 'Stop' ) {
my $ip = $RAD_REQUEST{'Framed-IP-Address'} || find_arp($mac);
&radiusd::radlog( L_ACCT, "Logout for $user, IP: $ip, MAC: $mac" );
send_rad_ip( $radius_host, $radius_secret, $user, $ip, 'Stop' );
}
}
return RLM_MODULE_OK;
}
sub authorize {
my $user = $RAD_REQUEST{'User-Name'};
my $mac = $RAD_REQUEST{'Calling-Station-Id'};
&radiusd::radlog( L_AUTH, "AUTH received for $user, MAC: $mac" );
return RLM_MODULE_NOOP;
}
# Function to handle authenticate
sub authenticate {
return RLM_MODULE_NOOP;
}
# Function to handle preacct
sub preacct {
return RLM_MODULE_NOOP;
}
# Function to handle checksimul
sub checksimul {
return RLM_MODULE_NOOP;
}
# Function to handle pre_proxy
sub pre_proxy {
return RLM_MODULE_NOOP;
}
# Function to handle post_proxy
sub post_proxy {
return RLM_MODULE_NOOP;
}
# Function to handle post_auth
sub post_auth {
# For debugging purposes only
# &log_request_attributes;
return RLM_MODULE_REJECT if ( $RAD_REQUEST{'User-Name'} =~ /^host\// );
return RLM_MODULE_OK unless ( $RAD_REQUEST{'User-Name'} );
my $user = strip_username $RAD_REQUEST{'User-Name'};
my @group = find_group( $user, $ad_filter );
if ( $RAD_REQUEST{'NAS-Port-Type'} eq 'Ethernet' ) {
push( @group, 'Ethernet' );
}
elsif ( $RAD_REQUEST{'Fortinet-Client-IP-Address'} ) {
# Fortinet-Vdom-Name = _internal
# Fortinet-Client-IP-Address = 223.24.156.237
push( @group, 'VPN' );
}
elsif ( begins_with( $RAD_REQUEST{'NAS-Port-Type'}, 'Wireless' ) ) {
push( @group, 'Wireless' );
}
$RAD_REPLY{'Class'} = \@group;
my $vlan = find_vlan(@group);
$RAD_REPLY{'Tunnel-Type'} = 13;
$RAD_REPLY{'Tunnel-Medium-Type'} = 6;
$RAD_REPLY{'Tunnel-Private-Group-Id'} = "$vlan";
my $mac = extract_mac $RAD_REQUEST{'Calling-Station-Id'};
&radiusd::radlog( L_AUTH,
"AUTH for $user, MAC: $mac, VLAN: $vlan, Class: "
. join( ', ', @group ) );
if ( $RAD_REQUEST{'NAS-Port-Type'} eq 'Ethernet' ) {
# from switch, send to acct
&radiusd::radlog( L_INFO,
"PORT-TYPE for user $user is ethernet, send rad acct" );
my $ip =
$RAD_REQUEST{'Framed-IP-Address'}
|| find_dhcp($mac)
|| find_arp($mac);
if ($ip) {
&radiusd::radlog( L_INFO,
"Interim for $user, IP: $ip, MAC: $mac, Class: "
. join( ', ', @group ) );
send_rad_ip( $radius_host, $radius_secret, $user, $ip,
'Interim-Update', @group );
}
}
return RLM_MODULE_UPDATED;
}
# Function to handle detach
sub detach {
&radiusd::radlog( L_DBG, "rlm_perl::Detaching. Reloading. Done." );
}
sub accounting_start {
}
sub accounting_stop {
}
sub log_request_attributes {
# This shouldn't be done in production environments!
# This is only meant for debugging!
for ( keys %RAD_REQUEST ) {
&radiusd::radlog( L_DBG, "RAD_REQUEST: $_ = $RAD_REQUEST{$_}" );
}
}
#END
Extra
Local user
vi /usr/local/etc/raddb/users
username1 Cleartext-Password := "user-password1", MS-CHAP-Use-NTLM-Auth := 0
username2 Cleartext-Password := "user-password2", MS-CHAP-Use-NTLM-Auth := 0
Class = "AD-Support"
username3 Cleartext-Password := "user-password3", MS-CHAP-Use-NTLM-Auth := 0