Host VLAN Assignment 802.1x Authentication in RADIUS

Summary of the Issue:

In your Univention template, the LDAP attribute for the VLAN ID is written as univentionVlanId (with a lowercase “L”), but the FreeRADIUS server expects univentionVLanId (with an uppercase “L”).

This mismatch causes the LDAP query to fail to find the VLAN ID, even though the user (PC81$) is correctly a member of the vlan201 group. As a result, the Tunnel-Private-Group-ID is not included in the Access-Accept packet, and the switch does not know which VLAN to assign to the client.

Affected Code Lines:

In the post-auth section of your configuration (/etc/freeradius/3.0/sites-enabled/default), the following lines are affected:

Incorrect (with lowercase “L”):

if ("%{ldap:ldap:///dc=,my,dc=domain,dc=de?univentionVlanId?sub?(&(memberUid=%{User-Name})(univentionObjectType=groups/group)(univentionVlanId=*)(univentionNetworkAccess=1))}") {
    update reply {
        Reply-Message := "DEBUG: Assigning VLAN-ID from user / computer object"
        Tunnel-Type := VLAN
        Tunnel-Medium-Type := IEEE-802
        Tunnel-Private-Group-ID := "%{ldap:ldap:///dc=,my,dc=domain,dc=de?univentionVlanId?sub?(&(memberUid=%{User-Name})(univentionObjectType=groups/group)(univentionVlanId=*)(univentionNetworkAccess=1))}"
    }
}

Correct (with uppercase “L”):

if ("%{ldap:ldap:///dc=,my,dc=domain,dc=de?univentionVLanId?sub?(&(memberUid=%{User-Name})(univentionObjectType=groups/group)(univentionVLanId=*)(univentionNetworkAccess=1))}") {
    update reply {
        Reply-Message := "DEBUG: Assigning VLAN-ID from user / computer object"
        Tunnel-Type := VLAN
        Tunnel-Medium-Type := IEEE-802
        Tunnel-Private-Group-ID := "%{ldap:ldap:///dc=,my,dc=domain,dc=de?univentionVLanId?sub?(&(memberUid=%{User-Name})(univentionObjectType=groups/group)(univentionVLanId=*)(univentionNetworkAccess=1))}"
    }
}

Summary of the Issue:

In your Univention template, the LDAP attribute for the VLAN ID is written as univentionVlanId (with a lowercase “L”), but the FreeRADIUS server expects univentionVLanId (with an uppercase “L”).

This mismatch causes the LDAP query to fail to find the VLAN ID, even though the user (PC81$) is correctly a member of the vlan201 group. As a result, the Tunnel-Private-Group-ID is not included in the Access-Accept packet, and the switch does not know which VLAN to assign to the client.


Affected Code Lines:

In the post-auth section of your configuration (/etc/freeradius/3.0/sites-enabled/default), the following lines are affected:

Incorrect (with lowercase “L”):

if ("%{ldap:ldap:///dc=,my,dc=domain,dc=de?univentionVlanId?sub?(&(memberUid=%{User-Name})(univentionObjectType=groups/group)(univentionVlanId=*)(univentionNetworkAccess=1))}") {
    update reply {
        Reply-Message := "DEBUG: Assigning VLAN-ID from user / computer object"
        Tunnel-Type := VLAN
        Tunnel-Medium-Type := IEEE-802
        Tunnel-Private-Group-ID := "%{ldap:ldap:///dc=,my,dc=domain,dc=de?univentionVlanId?sub?(&(memberUid=%{User-Name})(univentionObjectType=groups/group)(univentionVlanId=*)(univentionNetworkAccess=1))}"
    }
}

Correct (with uppercase “L”):

if ("%{ldap:ldap:///dc=,my,dc=domain,dc=de?univentionVLanId?sub?(&(memberUid=%{User-Name})(univentionObjectType=groups/group)(univentionVLanId=*)(univentionNetworkAccess=1))}") {
    update reply {
        Reply-Message := "DEBUG: Assigning VLAN-ID from user / computer object"
        Tunnel-Type := VLAN
        Tunnel-Medium-Type := IEEE-802
        Tunnel-Private-Group-ID := "%{ldap:ldap:///dc=,my,dc=domain,dc=de?univentionVLanId?sub?(&(memberUid=%{User-Name})(univentionObjectType=groups/group)(univentionVLanId=*)(univentionNetworkAccess=1))}"
    }
}

Why Is the VLAN Not Assigned?

  1. LDAP Query Fails:
  • The query for univentionVlanId (lowercase “L”) returns no results, even though the vlan201 group exists and the user PC81$ is a member.
  • In the logs, you can see that the VLAN ID is not included in the Access-Accept packet:
(11) Sent Access-Accept Id 182 from 10.10.36.236:1812 to 10.10.37.30:1812 length 208
(11)   User-Name = "host/pc81.my.domain.dc"
(11)   Tunnel-Type := VLAN

The line for the VLAN ID is missing:

Tunnel-Private-Group-ID := 201
  1. Switch Does Not Receive VLAN ID:
  • Without the Tunnel-Private-Group-ID, the switch does not know which VLAN to assign to the client.
  • As a result, the client is authenticated (Access-Accept) but not assigned to the correct VLAN.
  1. Some Requests Are Rejected (Access-Reject):
(9) Sent Access-Reject Id 180 from IP:1812 to SWITCH_IP:1812 length 20

This can happen if authentication fails (e.g., due to incorrect credentials or missing LDAP attributes).

Summary:

  • Problem: Incorrect spelling of the LDAP attribute (univentionVlanId instead of univentionVLanId).
  • Result: The VLAN ID is not found and not included in the Access-Accept packet.

This in one of the Problems.
I cant resolve the Problem now.
We need Help!
And Thank You!

My mistake…

Here ist the solution:

@%@UCRWARNING=# @%@
######################################################################
#
#	As of 2.0.0, FreeRADIUS supports virtual hosts using the
#	"server" section, and configuration directives.
#
#	Virtual hosts should be put into the "sites-available"
#	directory.  Soft links should be created in the "sites-enabled"
#	directory to these files.  This is done in a normal installation.
#
#	If you are using 802.1X (EAP) authentication, please see also
#	the "inner-tunnel" virtual server.  You will likely have to edit
#	that, too, for authentication to work.
#
#	$Id: dbad8e4fedee7e22a4da8f28f23fc9ea5cb726c7 $
#
######################################################################

server default {
listen {
	type = auth
	ipaddr = *
@!@
print('\tport = %s' % (configRegistry.get('freeradius/conf/port', '0'), ))
@!@

	limit {
	      max_connections = 16
	      lifetime = 0
	      idle_timeout = 30
	}
}

#
#  This second "listen" section is for listening on the accounting
#  port, too.
#
listen {
	ipaddr = *
@!@
print('\tport = %s' % (configRegistry.get('freeradius/conf/accountingport', '0'), ))
@!@
	type = acct

	limit {
	}
}

# IPv6 versions of the above - read their full config to understand options
listen {
	type = auth
	ipv6addr = ::	# any.  ::1 == localhost
@!@
print('\tport = %s' % (configRegistry.get('freeradius/conf/port', '0'), ))
@!@
	limit {
	      max_connections = 16
	      lifetime = 0
	      idle_timeout = 30
	}
}

listen {
	ipv6addr = ::
@!@
print('\tport = %s' % (configRegistry.get('freeradius/conf/accountingport', '0'), ))
@!@
	type = acct

	limit {
	}
}

#  Authorization. First preprocess (hints and huntgroups files),
#  then realms, and finally look in the "users" file.
authorize {
	filter_username

@!@
if configRegistry.is_true('freeradius/conf/allow-mac-address-authentication'):
    print("\trewrite_calling_station_id_univention")
else:
    print("#\trewrite_calling_station_id_univention")
@!@

	preprocess

@!@
auth_type = configRegistry.is_true('freeradius/conf/auth-type/mschap')

if auth_type:
    print('\t\tchap')
else:
    print('#\tchap')
@!@

@!@
auth_type = configRegistry.is_true('freeradius/conf/auth-type/mschap')

if auth_type:
    print('\tmschap')
else:
    print('#\tmschap')
@!@

@!@
print('%s' % configRegistry.get('freeradius/conf/realm', 'suffix'))
@!@

@!@
if configRegistry.is_true('freeradius/conf/allow-mac-address-authentication'):
    print("\tuser_name_is_mac")
    ldap_base = configRegistry['ldap/base']
    print('''
    if (control:Auth-Type == "CSID" && EAP-Message ) {
        if ("%{ldap:ldap:///''' + ldap_base + '''?uid?sub?(macAddress=%{Calling-Station-Id})}") {
            update request {
                Tmp-String-0 := "%{ldap:ldap:///''' + ldap_base + '''?uid?sub?(macAddress=%{Calling-Station-Id})}"    # The uid attribute in the ldap object is filled with the host name and a trailing dollar sign.
            }
            if ("%{ldap:ldap:///''' + ldap_base + '''?univentionNetworkAccess?sub?(|(&(|(memberUid=%{Tmp-String-0})(macAddress=%{Calling-Station-Id}))(univentionObjectType=groups/group)(univentionNetworkAccess=1))(&(uid=%{Tmp-String-0})(univentionNetworkAccess=1)))}") {
                update control {
                    Cleartext-Password := "%{User-Name}"
                }
            }
        }
    }''')
else:
    print("#\tuser_name_is_mac")
@!@
	eap {
	        ok = return
#	        updated = return
	}

@!@
print('%s' % configRegistry.get('freeradius/conf/users', 'files'))
@!@

	#
	#  The ldap module reads passwords from the LDAP database.
	ldap

	expiration
	logintime

	pap

	Autz-Type New-TLS-Connection {
	          ok
	}
}


#  Authentication.
authenticate {
    @!@
if configRegistry.is_true('freeradius/conf/allow-mac-address-authentication'):
    ldap_base = configRegistry['ldap/base']
    text = '''
    Auth-Type CSID {
        if ("%{ldap:ldap:///''' + ldap_base + '''?uid?sub?(macAddress=%{Calling-Station-Id})}") {
            update request {
                Tmp-String-0 := "%{ldap:ldap:///''' + ldap_base + '''?uid?sub?(macAddress=%{Calling-Station-Id})}"    # The uid attribute in the ldap object is filled with the host name and a trailing dollar sign.
            }
            if ("%{ldap:ldap:///''' + ldap_base + '''?univentionNetworkAccess?sub?(|(&(|(memberUid=%{Tmp-String-0})(macAddress=%{Calling-Station-Id}))(univentionObjectType=groups/group)(univentionNetworkAccess=1))(&(uid=%{Tmp-String-0})(univentionNetworkAccess=1)))}") {
                ok
            }
            else {
                reject
            }
        }
    }
    '''
    print(text)
@!@

	#
	#  PAP authentication
	Auth-Type PAP {
	        pap
	}

@!@
auth_type = configRegistry.is_true('freeradius/conf/auth-type/mschap')

if auth_type:
    print('\tAuth-Type CHAP {')
    print('\t\tchap')
    print('\t}')
else:
    print('#      Auth-Type CHAP {')
    print('#\tchap')
    print('#      }')
@!@

@!@
auth_type = configRegistry.is_true('freeradius/conf/auth-type/mschap')

if auth_type:
    print('\tAuth-Type MS-CHAP {')
    print('\t\tmschap')
    print('\t}')
else:
    print('#      Auth-Type MS-CHAP {')
    print('#\tmschap')
    print('#      }')
@!@

	Auth-Type LDAP {
	        ldap
	}

	Auth-Type eap {
	    eap
	}
}


#
#  Pre-accounting.
#
preacct {
	preprocess

	acct_unique

@!@
print('%s' % configRegistry.get('freeradius/conf/realm', 'suffix'))
@!@

@!@
print('%s' % configRegistry.get('freeradius/conf/users', 'files'))
@!@
}

#
#  Accounting.
#
accounting {
	detail

	unix

	-sql

	exec

	attr_filter.accounting_response
}


#  Session database.
session {
}


#  Post-Authentication
post-auth {

	if (session-state:User-Name && reply:User-Name && request:User-Name && (reply:User-Name == request:User-Name)) {
	        update reply {
		                &User-Name !* ANY
	        }
	}
	update {
	        &reply: += &session-state:
	}

	-sql

	exec

	# Load module: ldap
	ldap

	#####################################################################
	# VLAN-Zuweisung fuer 802.1X (User- und Computer-Authentifizierung)
	#
	# Wir muessen drei Faelle abdecken:
	#  1) Computer-Auth (EAP-TLS):  User-Name = "host/<hostname>.<domain>"
	#                               -> umschreiben zu "<HOSTNAME>$"
	#  2) User mit Realm:           User-Name = "DOMAIN\username"
	#                               -> Stripped-User-Name = "username" nutzen
	#  3) User ohne Realm:          User-Name = "username"
	#                               -> User-Name direkt nutzen
	#####################################################################

	# Fall 1: Computer-Authentifizierung
@!@
print('\tif ("%%{tolower:%%{request:User-Name}}" =~ /^host\\/([^\\.]+)\\.%s/) {' % configRegistry.get('domainname', '').lower())
@!@
	        update request {
	                User-Name := "%{toupper:%{1}}$"
	        }
	}
	# Fall 2: User mit NT-Domain-Realm (z.B. "KL\username")
	#         ntdomain hat bereits Stripped-User-Name gesetzt.
	elsif (&Stripped-User-Name) {
	        update request {
	                User-Name := "%{Stripped-User-Name}"
	        }
	}
	# Fall 3: User ohne Realm -> User-Name bleibt wie er ist.

@!@
if configRegistry.is_true('freeradius/conf/allow-mac-address-authentication'):
    ldap_base = configRegistry['ldap/base']
    default_vlanid = configRegistry.get('freeradius/vlan-id', '')
    text = '''
    if (control:Auth-Type == "CSID" || (Calling-Station-Id && EAP-Message && control:Cleartext-Password)) {
        if ( Calling-Station-Id && "%{ldap:ldap:///''' + ldap_base + '''?uid?sub?(macAddress=%{Calling-Station-Id})}") {
            update request {
                User-Name := "%{ldap:ldap:///''' + ldap_base + '''?uid?sub?(macAddress=%{Calling-Station-Id})}"    # The uid attribute in the ldap object is filled with the host name and a trailing dollar sign.
            }
        }
        # For known users as well for known machines we take the vlan-id from the group the user/machine is member of.
        # In case there are assignments for several groups the first vlan-id is automatically taken.
        if ("%{ldap:ldap:///''' + ldap_base + '''?univentionVLanId?sub?(&(|(memberUid=%{User-Name})(macAddress=%{Calling-Station-Id}))(univentionObjectType=groups/group)(univentionVLanId=*)(univentionNetworkAccess=1))}") {
            update reply {
                Reply-Message := "DEBUG: Assigning VLAN-ID from user / computer object"
                Tunnel-Type := VLAN
                Tunnel-Medium-Type := IEEE-802
                Tunnel-Private-Group-Id := "%{ldap:ldap:///''' + ldap_base + '''?univentionVLanId?sub?(&(|(memberUid=%{User-Name})(macAddress=%{Calling-Station-Id}))(univentionObjectType=groups/group)(univentionVLanId=*)(univentionNetworkAccess=1))}"

        }
        # If we can't find a matching VLAN ID for the user or machine client in LDAP, we return the default VLAN ID, if configured.
        # If no default vlan-id is configured in ucr we do not return any vlan information
        elsif ("''' + default_vlanid + '''") {
            update reply {
                Reply-Message := "DEBUG: Not found, assigning default VLAN-ID"
                Tunnel-Type := VLAN
                Tunnel-Medium-Type := IEEE-802
                Tunnel-Private-Group-Id := "''' + default_vlanid + '''"
            }
        }
    }
'''
    print(text)
@!@
    # Check if the user or machine exists and do post-auth actions
    # else do nothing in post-auth
    # This way we also make sure that we do not change the VLAN ID again if the non-EAP-auth (MAC address auth) succeeded before (see above)
@!@
ldap_base = configRegistry['ldap/base']
if configRegistry.is_true('freeradius/conf/allow-mac-address-authentication'):
    print('    if ("%{ldap:ldap:///' + ldap_base + '?uid?sub?(|(uid=%{User-Name})(macAddress=%{Calling-Station-Id}))}") {')
else:
    print('    if ("%{ldap:ldap:///' + ldap_base + '?uid?sub?(uid=%{User-Name})}") {')
@!@
        # For known users as well for known machines we take the vlan-id from the group the user/machine is member of.
        # In case there are assignments for several groups the first vlan-id is automatically taken.
@!@
ldap_base = configRegistry['ldap/base']
if configRegistry.is_true('freeradius/conf/allow-mac-address-authentication'):
    print('        if ("%{ldap:ldap:///' + ldap_base + '?univentionVLanId?sub?(&(|(memberUid=%{User-Name})(macAddress=%{Calling-Station-Id}))(univentionObjectType=groups/group)(univentionVLanId=*)(univentionNetworkAccess=1))}") {')
else:
    print('        if ("%{ldap:ldap:///' + ldap_base + '?univentionVLanId?sub?(&(memberUid=%{User-Name})(univentionObjectType=groups/group)(univentionVLanId=*)(univentionNetworkAccess=1))}") {')
@!@
            update reply {
                Reply-Message := "DEBUG: Assigning VLAN-ID from user / computer object"
                Tunnel-Type := VLAN
                Tunnel-Medium-Type := IEEE-802
@!@
ldap_base = configRegistry['ldap/base']
if configRegistry.is_true('freeradius/conf/allow-mac-address-authentication'):
    print('                Tunnel-Private-Group-Id := "%{ldap:ldap:///' + ldap_base + '?univentionVLanId?sub?(&(|(memberUid=%{User-Name})(memberUid=%{User-Name}))(univentionObjectType=groups/group)(univentionVLanId=*)(univentionNetworkAccess=1))}"')
else:
    print('                Tunnel-Private-Group-Id := "%{ldap:ldap:///' + ldap_base + '?univentionVLanId?sub?(&(memberUid=%{User-Name})(univentionObjectType=groups/group)(univentionVLanId=*)(univentionNetworkAccess=1))}"')
@!@

            }
        }
        # If we can't find a matching VLAN ID for the user or machine client in LDAP, we return the default VLAN ID, if configured.
        # If no default vlan-id is configured in ucr we do not return any vlan information
        elsif ("@%@freeradius/vlan-id@%@") {
            update reply {
                Reply-Message := "DEBUG: Not found, assigning default VLAN-ID"
                Tunnel-Type := VLAN
                Tunnel-Medium-Type := IEEE-802
                Tunnel-Private-Group-Id := "@%@freeradius/vlan-id@%@"
            }
        }
    }

	#  Remove reply message if the response contains an EAP-Message
	remove_reply_message_if_eap

	Post-Auth-Type REJECT {
	        # log failed authentications in SQL, too.
	        -sql
	        attr_filter.access_reject

	        # Insert EAP-Failure message if the request was
	        # rejected by policy instead of because of an
	        # authentication failure
	        eap

	        #  Remove reply message if the response contains an EAP-Message
	        remove_reply_message_if_eap
	}

	Post-Auth-Type Challenge {
	}

	Post-Auth-Type Client-Lost {
	}

	if (EAP-Key-Name && &reply:EAP-Session-Id) {
	        update reply {
		                &EAP-Key-Name := &reply:EAP-Session-Id
	        }
	}
}

pre-proxy {
}

post-proxy {
	eap
}
}

I´ve set the Stripped-Username an a second auth to check the Users and PC´s.