Ceptor RADIUS Server
Ceptor has support for a RADIUS Server and client that supports authentication and accounting requests.
Features
- Behavior can be controlled fully via scripts - can be used for deciding which MFA authentication factors to offer which users based upon any attributes in the incoming AccessRequest
- Full Challenge support for prompting users in multiple steps.
- No stickiness required - all instances can take part in a regular clustered Ceptor installation.
- Full access to request/response package content, allowing scripts to manipulate full packet content, including all possible attributes.
- Supports Multifactor (MFA) Authentication Methods, allowing user to choose between multiple methods, or allowing specific users / groups access to a subset based upon any attribute/user role etc.
- Combined with Ceptor Authentication Plugins, supports advanced types of authentication, such as Azure MFA.
- Built-in radius client supporting e.g. PAP, CHAP, MSCHAPv2 protocols for proxying requests to remote radius servers.
- Shared secret configurable per client
- RadSec support, enabling TLS encrypted TCP communication which is not dependent on keeping source IP intact, so it is both more secure (better encryption) and more loadbalancer friendly.
Testing
As of Ceptor v6.4, the Tools menu in Ceptor Console now contains a Radius Client which you can use for testing various authentication scenarios.
Launcher Configuration
In order to get the RADIUS Server started the radius service should be configured in the ceptor_launch.xml. The radius server does not require its own JVM to run, so if the existing capacity can handle it, it could as an example be a service defined in the session controller classloader/JVM – for example like this:
<classloader name="sessionctrl"> <service name="radiusserver1" launcherclass="dk.itp.pp.radius.RadiusServerLauncher" /> </classloader>
The RADIUSÂ Server will require a minimum of configuration defining listening ports and addresses as well as shared secrets and an authentication plugin to use for validating authorization requests. For detailed configuration see the configuration reference:
<server name="radiusserver1" type="radius" description="Radius server" extends="applications"> <group name="listening" description="Radius server listening configuration"> <property name="authentication.listenport" value="1812" description="Authentication port"/> <property name="authentication.listenaddress" value="192.168.52.1" description="Authentication listening address - empty is default adapter"/> <property name="accounting.listenport" value="1813" description="Accounting port"/> <property name="accounting.listenaddress" value="127.0.0.1" description="Accounting listening address - empty is default adapter"/> </group> <group name="authentication" description="Radius server security configuration"> <property name="authtype.pap" value="9" description="Login authtype to verify user passwords"/> </group> <group name="security" description="Radius server security configuration"> <property name="sharedsecret.1" value="*=secret1" description="shared secret for all other clients than listed below"/> <property name="sharedsecret.2" value="127.0.0.1,10.0.0.1=secret2" description="shared secret for IP 127.0.0.1 and 10.0.0.1"/> </group> </server>
There are several more configuration parameters that can be set.Â
To enable RadSec protocol support, you also need some additional configuration, like this:
<property name="radsec.keystore.name" value="/ptskeystore" description="Name of keystore containing SSL server cert"/> <property name="radsec.keystore.password" value="password" description="Password to keystore"/> <property name="radsec.keystore.type" value="JKS" description="Type of keystore containing SSL server cert"/> <property name="radsec.listenurl" value="nios://0.0.0.0:2023?tlsprotocol=TLS&enabledprotocols=TLSv1.2,TLSv1.3" description="Listen URL for RadSec protocol"/> <property name="radsec.needtlsclientauth" value="true" description="True if we should require TLS client certificate"/> <property name="radsec.wanttlsclientauth" value="false" description="True if we should want TLS client certificate, but not require it"/> <property name="ca.certificates" value="${ceptor.home}/config/x509/radiustest.cer" description="pkcs#7 files containing CA certificates"/> <property name="ca.provider.ceptortest.check.crl" value="false" description="Set to true to do CRL checks - be careful to turn on either CRL or OCSP checks"/> <property name="ca.provider.ceptortest.check.ocsp" value="false" description=""/> <property name="ca.provider.ceptortest.class" value="dk.itp.security.authentication.x509.GenericCA" description="implementation class"/> <property name="ca.provider.ceptortest.issuerdn" value="C=DK, O=Ceptor, CN=radiustest" description="Certificate DN"/> <property name="ca.provider.ceptortest.sharedsecret" value="radsec" description="Shared secret, default is radsec"/> <property name="ca.providers" value="ceptortest" description="list of certificate issuers (providers)"/>
The configuration above enables listening for connection on port 2023, on all network interfaces, with the protocol TLS, and enabled protocol versions are TLS version 1.2 and 1.3. Regular Java JCE protocol names are supported, availability depends on Java version used - Minimum Java 8 is required, but at least Java 11 is recommended.
In addition to setting up listener and keystore for TLS server certificate, you can set up a list of CA certificates that are used if client certificates are required.
Note that it is possible to configure Ceptor Radius RadSec support without requiring client certificates - in that case, the default shared secret; "radsec" is used, and any clients are allowed to connect without presenting SSL client certificate.
When configuring which CA certificates to use, configuration is similar to that for X509 Client certificates (see full details at X.509 Certificate Properties ) - with the exception that a nonstandard shared secret can be configured for each CA, if required (by setting ca.provider.<providername>.sharedsecret
  ).
The minimum configuration for using a self-signed certificate is:
<property name="ca.certificates" value="${ceptor.home}/config/x509/selfsigned.cer" description="Add Certificate to list here"/> <property name="ca.provider.selfsigned.issuerdn" value="C=DK, O=Ceptor, CN=radiustest" description="Certificate DN"/> <property name="ca.providers" value="selfsigned" description="Add provider name to selfsigned"/>
Accounting
Radius accounting is handled through logging account requests. The requests are logged through an appender "RadiusAccounting". Add configuration for this appender to log in a specific file if requred. If not the accounting entries will end up in the standard log file for the Radius server. The configuration for this is part of the default distribution and can be found in logback.xml in CEPTOR_HOME/pp/classes
Two factor (challenge) handling
The Radius server support two factor logins. It is supported with authentication plugins that can either validate the userid/password on the first authentication request and then prompt for a challenge after login and also with authentication plugins using the newToken method on first request and not validating the userid/password untill the challenge is validated.
In the first scenario the login method must throw a PTException with the error code "AuthErrorCodes.ERROR_NEED_OTP" for the radius server to send a challenge back to the radius client.
In both scenarios the authentication plugin called (typically the PAP authentication plugin) can set a session state variable "radius.challenge" which will be used as the challenge text on clients that supports this. This could be user friendly is for example showing the last 2 or 3 digits of the telephone number to help the user. If this session state variable is not set, the configuration parameter "authentication.challenge" is used instead if defined. See configuration reference for more information.Â
Setting up properties for two factor login using the user administration database and SMS/OTP challenges could look like this (other properties are not shown) if using the dk.itp.portalprotect.useradmin.server.SMSUAAuthenticationPlugin (configuration described here):
<server name="radiusserver1" type="radius" description="Radius server" extends="applications"> .... <group name="authentication" description="Radius server security configuration"> <property name="authtype.pap" value="43" description="Login authtype to verify user passwords in user admin database"/> <property name="authtype.challenge" value="43" description="Login authtype used to verify the challenge"/> <property name="authentication.challenge" value="Please enter the code received as SMS: " description="The challenge text to show to the user"/> </group> .... </server>
Scripting
Ceptor Radius module supports two different types of scripts;
- Accounting script
- Authentication script
For accounting, specify the script in the accounting.script
 configuration option.
The Authentication script allows to override the exact behavior when receiving an access request packet from a Radius client, and the Accounting script allows handling an incoming accounting packet, parsing the contents and saving the values where you need to, in case the default behavior of writing it to a SLF4J logging appender is insufficient.
If you specify the configuration authentication.script
then you can specify a script (Javascript / Python or Groovy) which will be executed - this allows you to customize handling of the incoming request.
If a script is specified, it overrides the other options for authtype.pap, authtype.challenge etc.
When the script is called, it has a variable called context
which contains the following information:
public class RadiusContext { /** Session ID - also used as state */ public String session; /** Ceptor Agent */ public RadiusService agent; /** Incoming access request */ public AccessRequest accessRequest; /** Reply that will be sent */ public RadiusPacket reply; /** Incoming accounting request, if this script is called when an accounting packet arrives */ public AccountingRequest accountingRequest; /** Client making the call */ public InetSocketAddress client; /** Authenticator where you can obtain shared secret for a particular client if needed */ public RadiusServerAuthenticator authenticator; }
import dk.itp.security.passticket.*; import dk.itp.security.passticket.server.AuthErrorCodes; import dk.itp.pp.radius.packet.*; import dk.itp.pp.radius.attribute.*; // Change these to match required data int authenticationType = AuthTypes.AUTHTYPE_GOOGLEAUTH; String otpPrompt = "Enter OTP displayed in Authenticator: "; if (context.session) { // We have a session, so it must be 2nd call with an OTP String userPassword = context.accessRequest.getUserPassword(); def challenge = new Object[1]; challenge[0] = userPassword; context.agent.logon(context.session, authenticationType, context.agent.getUser(context.session), challenge); context.reply.addAttribute( new StringAttribute( TypeConstants.STATE, context.session) ); context.reply.setPacketType(TypeConstants.PACKAGE_TYPE_ACCESS_ACCEPT); } else { // Starting from scratch, create a new session and authenticate into it def sourceIP = context.accessRequest.getAttributeValue("NAS-IP-Address"); if (!sourceIP) sourceIP = context.accessRequest.getAttributeValue("Calling-Station-Id"); context.session = context.agent.newSession(sourceIP); String userName = context.accessRequest.getUserName(); String userPassword = context.accessRequest.getUserPassword(); try { context.agent.logon(context.session, authenticationType, userName, userPassword); } catch(PTException e) { if( e.getErrorCode() == AuthErrorCodes.ERROR_NEED_OTP ) { context.reply.addAttribute( new StringAttribute( TypeConstants.STATE, context.session) ); context.reply.addAttribute( "Reply-Message", otpPrompt ); context.reply.setPacketType(TypeConstants.ACCESS_CHALLENGE); } else { throw e; } } }
Full configuration example
<server name="radiusserver1" type="radius" description="Radius server" extends="applications"> <group name="listening" description="Radius server listening configuration"> <property name="accounting.listenaddress" value="127.0.0.1" description="Accounting listening address - empty is default adapter"/> <property name="accounting.listenport" value="1813" description="Accounting port"/> <property name="authentication.listenaddress" value="0.0.0.0" description="Authentication listening address - empty is default adapter"/> <property name="authentication.listenport" value="1812" description="Authentication port"/> <property name="radsec.keystore.name" value="/ptskeystore" description="Name of keystore containing SSL server cert"/> <property name="radsec.keystore.password" value="password" description="Password to keystore"/> <property name="radsec.keystore.type" value="JKS" description="Type of keystore containing SSL server cert"/> <property name="radsec.listenurl" value="nios://0.0.0.0:2023?tlsprotocol=TLS&enabledprotocols=TLSv1.2,TLSv1.3" description="Listen URL for RadSec protocol"/> <property name="radsec.needtlsclientauth" value="true" description="True if we should require TLS client certificate"/> <property name="radsec.wanttlsclientauth" value="false" description="True if we should want TLS client certificate, but not require it"/> <property name="sockettimeout" value="3000" description="Listening timeout for authentiation and accounting"/> </group> <group name="other" description="Radius server configuration"> <property name="authentication.challenge" value="43" description="Challenge authentication plugin if PAP authentication plugin supports two factor logins"/> <property name="authentication.pap" value="9" description="PAP authentication plugin (for example UAAuthenticationPlugin)"/> <property name="authentication.script" value="%{script:groovy}import dk.itp.security.passticket.*; import dk.itp.security.passticket.server.AuthErrorCodes; import dk.itp.pp.radius.packet.*; import dk.itp.pp.radius.attribute.*; // Change these to match required data int authenticationType = AuthTypes.AUTHTYPE_GOOGLEAUTH; String otpPrompt = "Enter OTP displayed in Authenticator: "; if (context.session) { // We have a session, so it must be 2nd call with an OTP String userPassword = context.accessRequest.getUserPassword(); def challenge = new Object[1]; challenge[0] = userPassword; context.agent.logon(context.session, authenticationType, context.agent.getUser(context.session), challenge); context.reply.addAttribute( new StringAttribute( TypeConstants.STATE, context.session) ); context.reply.setPacketType(TypeConstants.PACKAGE_TYPE_ACCESS_ACCEPT); } else { // Starting from scratch, create a new session and authenticate into it def sourceIP = context.accessRequest.getAttributeValue("NAS-IP-Address"); if (!sourceIP) sourceIP = context.accessRequest.getAttributeValue("Calling-Station-Id"); context.session = context.agent.newSession(sourceIP); String userName = context.accessRequest.getUserName(); String userPassword = context.accessRequest.getUserPassword(); try { context.agent.logon(context.session, authenticationType, userName, userPassword); } catch(PTException e) { if( e.getErrorCode() == AuthErrorCodes.ERROR_NEED_OTP ) { context.reply.addAttribute( new StringAttribute( TypeConstants.STATE, context.session) ); context.reply.addAttribute( "Reply-Message", otpPrompt ); context.reply.setPacketType(TypeConstants.ACCESS_CHALLENGE); } else { throw e; } } } " description="Script executed to perform authentication - when set, authentication.pap, authentication.twofactor and authentication.chap are ignored"/> <property name="authentication.twofactor" value="false" description="Allows the radius server to do a twofactor login. For two factor logins using the newToken method (google authenticator for example)"/> <property name="clientsessions.forcetimeout" value="30" description="Force timeout in minutes for client sessions"/> <property name="clientsessions.maxcount" value="100000" description="Number of client sessions to store in the server"/> <property name="clientsessions.timetolive" value="5" description="Time to live for client sessions if not seen"/> <property name="packet.debug" value="true" description="Set to true to log contents of radius packets (as INFO - will be logged as DEBUG by otherwise)"/> <property name="ppsessions.forcetimeout" value="30" description="Force timeout in minutes for pp sessions"/> <property name="ppsessions.maxcount" value="100000" description="Number of pp sessions to store in the server"/> <property name="ppsessions.timetolive" value="5" description="Time to live for pp sessions if not seen"/> <property name="username.sessionid" value="true" description="Set the User-Name attribute on accept packages to PP session ID. Some clients might send it back. Some might not support it!"/> </group> <group name="security" description="Radius server security configuration"> <property name="sharedsecret.1" value="*=secret1" description="shared secret for all other clients than listed below"/> <property name="sharedsecret.2" value="127.0.0.1,10.0.0.1=secret2" description="shared secret for IP 127.0.0.1 and 10.0.0.1"/> </group> <group name="radsec_ssl" description="Radius security SSL client certificate configuration"> <property name="ca.certificates" value="${ceptor.home}/config/x509/radiustest.cer" description="pkcs#7 files containing CA certificates"/> <property name="ca.provider.ceptortest.check.crl" value="false" description="Set to true to do CRL checks - be careful to turn on either CRL or OCSP checks"/> <property name="ca.provider.ceptortest.check.ocsp" value="false" description=""/> <property name="ca.provider.ceptortest.class" value="dk.itp.security.authentication.x509.GenericCA" description="implementation class"/> <property name="ca.provider.ceptortest.issuerdn" value="C=DK, O=Ceptor, CN=radiustest" description="Certificate DN"/> <property name="ca.provider.ceptortest.sharedsecret" value="radsec" description="Shared secret, default is radsec"/> <property name="ca.providers" value="ceptortest" description="list of certificate issuers (providers)"/> <property name="http.proxyHost" value="localhost" description="proxy host"/> <property name="http.proxyPassword" value="" description="proxy password for proxy authentication"/> <property name="http.proxyPort" value="8888" description="proxy port"/> <property name="http.proxyUser" value="" description="proxy userid for proxy authentication"/> <property name="proxy.enable" value="false" description="use proxy server"/> </group> </server>
© Ceptor ApS. All Rights Reserved.