VPN with StrongSwan Based on Group Membership

StrongSwan can use RADIUS as an authentication backend. In case the RADIUS service also returns group membership information it becomes possible for the IKE server to also choose the VPN configuration based on the membership information.

StrongSwan

EAP-RADIUS, as documented in the projects WIKI, describes how to use a RADIUS backend in the StrongSwan VPN server. I find enabling RADIUS quite simple. All it takes is adding the RADIUS server and its secret to the eap-radius configuration section of the IKE service charon.

A little further down at the section Group Selection the documentation says that charon will honor the Class attribute in a RADIUS reply, provided the class_group option is set to yes. So my complete config eap-radius.conf looks like:

eap-radius.conf
eap-radius {
    class_group = yes
    load = yes
    secret = testing123
    server = 192.2.0.12
}

The RADIUS Configuration

I use FreeRADIUS as server. For a first test, I just add two users to the users file:

users
user1 Cleartext-Password := "password1"
	Class = "sales"

user2 Cleartext-Password := "password2"
	Class = "admins"

Since StrongSwan will authenticate with EAP also the TLS stuff, i.e. CA, server certificate and key has to be set up. Please refer to the FreeRADIUS documentation how to configure that part. Also the StrongSwan server has to be added to the clients that are allowed to use the RADIUS service.

After starting the freeradius daemon, the RADIUS service is ready to authenticate incoming requests.

StrongSwan Configuration

The StrongSwan project offers example configurations for nearly every setup you can think of. The scenario I use is described in IKEv2-EAP-TTLS-RADIUS. Thus the configuration of my VPN gateway looks like this:

conn admins
        left=192.2.0.1
        leftsubnet=192.2.0.0/24
        leftcert=server-cert.pem
        leftauth=pubkey
        leftfirewall=yes
        leftsendcert=always
        #
        right=%any
        rightsourceip=198.51.100.0/25
        rightid=%any
        rightsendcert=never
        rightauth=eap-radius
        rightgroups="admins"
        #
        fragmentation=yes
        auto=add

conn sales
        left=192.2.0.1
        leftsubnet=192.2.0.0/24
        leftcert=server-cert.pem
        leftauth=pubkey
        leftfirewall=yes
        leftsendcert=always
        #
        right=%any
        rightsourceip=198.51.100.128/25
        rightid=%any
        rightsendcert=never
        rightauth=eap-radius
        rightgroups="sales"
        #
        fragmentation=yes
	auto=add

The configuration checks the Class attribute from the reply of the RADIUS server. The VPN server assigns IP addresses to the clients depending on their group (better Class) membership. With firewall rules in place separate policies could be applied for the both groups.

The important part of the VPN log of a connection of the user1 of the sales team looks like:

charon: 06[IKE] authentication of 'user1' with EAP successful
charon: 06[CFG] constraint check failed: group membership to 'admins' required
charon: 06[CFG] selected peer config 'admins' inacceptable: non-matching authentication done
charon: 06[CFG] switching to peer config 'sales'
charon: 06[IKE] authentication of <server id> (myself) with EAP
charon: 06[IKE] IKE_SA sales[4] established between 192.2.0.1[<server id>]...93.104.100.218[user1]
charon: 06[IKE] peer requested virtual IP %any
charon: 06[IKE] assigning virtual IP 198.51.100.129 to peer 'user1'
charon: 06[IKE] CHILD_SA sales{3} established with SPIs c63a699b_i 362e33a9_o and TS 192.2.0.0/24 === 198.51.100.129/32

The user was authenticated successfully from the RADIUS server, but the Class does not match. So charon looks for the next connection where the sales definition matches. So the user gets an IP address from that pool.

Group attributes from LDAP

The LDAP module of FreeRADIUS also provides a section for group lookup. Depending on the organization of your LDAP you have to adjust the options in the configuration of the group section, especially the membership_filter or membership_attribute.

Since the LDAP-Group is no ordinary RADIUS attribute, you have to enable caching. While FreeRADIUS searches for the user it also looks for the group memberships and adds it to the LDAP-Group attribute array. In my case I want to have the group name, so I set cacheable_name = 'yes' in the user section of the ldap module.

After authorization the LDAP-Group attribute array contains the list of groups the user is included. In the post-auth section of a server I can set the Class attribut.

switch &control:LDAP-Group[*] {
  case "admins" {
    update reply {
      Class = "admins"
    }
  }
  case "sales" {
    update reply {
      Class = "sales"
    }
  }
}

I have to use the switch / case structure since a user can be member of multiple groups. So the LDAP-Group might be an array, not a single value. If the VPN groups are of a specific form, like "vpn-*", you also could have such a construction:

if ( &control:LDAP-Group[*] =~ /^vpn-.*/ ) {
  update reply {
    Class = "%{0}"
  }
}

The =~ operator checks for a regular expression. Only the entries that fit remain in the list and the first element in that list is assigned to the Class attribute.

For the sake of clarity of the server configuration you could move this mapping in the post-auth section into a policy.

Please mail me if you have questions or improvements to ms@sys4.de.

Michael Schwartzkopff, 13 Dec 2020