Newnix's Editorial Note

On a personal level, I would never recommend this, I think Active Directory is more pain than it's worth. However, I know it's commonly used for centralized user management, and there's not many alternatives, and of those that exist, even fewer are understood even half as well. So since you're here, you must be wanting to get something like this set up for your network. I'd also recommend looking at using HardenedBSD as the host, due to the additional security hardening and mitigations available by default. While I have not had the time nor desire to verify this works on such a stack, there's nothing that stands out as a major blocking factor either.

If you're not familiar with the technology, but you've been tasked to set it up, both RADIUS and Active Directory are methods of centralizing authentication. In this configuration, we're just using FreeRADIUS 3.X to translate the login attempts seen by network devices into login rquests that a Windows server or convincing enough *NIX server running as a domain controller can understand. This allows network administrators to be defined and managed in a single location, the Active Directory database, and only require creating a generic user or user class (depending on the device firmware) that uses RADIUS authentication.

Authentication Flow

When completed, the authentication flow will look like the following

		|-------------|
		| Workstation |
		|-------------|                        /----\
		    | | |                              | AD |
		    | | |                              \____/
		  /-------\                             ^ |
		  |  SSH  |                             R A
		  \-------/                             Q N
		    | | |                               | S
		    | | |                               | v
		|------------|                     |-----------|
		|   Remote   |-----Request-------->|   Free    |
		|   Device   |<-----Answer---------|  RADIUS   |
		|------------|                     |-----------|
		
So you try to ssh into a remote device, it calls out to FreeRADIUS on the FreeBSD host, then FreeRADIUS passes off the credentials you provided to the Active Directory server, and gets a response on whether or not you're allowed to access that device. Typically this is done through a filter on the Domain membership and Group membership of the user account. This can, of course, be modified on the device to allow local log-in if the RADIUS authentication failed for some reason, as well as configuring FreeRADIUS to allow some RADIUS-only users should the Active Directory server ever go down. We'll go into more detail on the specifics of these configs a bit later in this article. All configuration files and setup commands require privileged access, so either prepend them all with sudo or doas, or use sudo -sE to get a root shell while preserving your usual environment.

Setting Up FreeRADIUS on FreeBSD

This part is remarkably simple, just pkg install -y freeradius3-server && printf "%s_enable=\"YES\"\n" radiusd samba_server >> /etc/rc.conf This is all that is strictly necessary to get FreeRADIUS running on your host, of course you have to then start the daemon, but in this state it's not terribly helpful. We'll need to make a few changes to the files installed to /usr/local/etc/raddb/

Basic FreeRADIUS Configuration

The following changes will simply enable FreeRADIUS to actually work as an authentication server, the Active Directory portion is handled later in this document.
The first step necessary is defining in /usr/local/etc/raddb/sites-enabled/default which IP address and port the RADIUS daemon will bind to, listening for incoming connections. Per the standard, you should have a "listen" block similar to the following:

		/usr/local/etc/raddb/sites-enabled/default:
		listen {
			... # some boilerplate stuff
			ipaddr = 192.160.0.5 
			port = 1812
			... # more boilerplate stuff
		}
		
This will set up the daemon to listen for messages on 192.168.0.5 on port 1812 UDP, this address needs to be reachable by all cliets using this server as their authentication server.

Next step is to define which networks are allowed to connect to the RADIUS daemon, this can also be done via firewall rules, but you'll have to define at least one client block to enable authentication. Ideally, you'd be able to use a mix of the client block definitions and firewall rules to lock down access to only those devices that actually need RADIUS access. To do so you'll need to add a configuration block in /usr/local/etc/raddb/clients.conf similar to the following:

		/usr/local/etc/raddb/clients.conf:
		client SalesNet {
			secret = supersecretpassword#1
			ipaddr = 192.168.0.0/24
			nastype = juniper
		}
		
The above example will allow Juniper devices on the subnet 192.168.0.0/24 that know the secret string "supersecretpassword#1" to connect for authentication requests. You'll need to check the documentation included in the clients.conf file to get a list of known nastype values. If you need to have several different devices, you can either create multiple client blocks, or use the catch-all nastype of "other".

		/etc/rc.conf:
		radiusd_enable="YES"
		
At this point, you can run service radiusd start to have the RADIUS daemon begin listening for connections, though it's unlikely to be terribly useful at this point.

Connecting FreeBSD to Active Directory

First, you need to install the most recent SAMBA utilities, at the time of this writing this is SAMBA 4.8. So you'll need to run pkg install -y samba48, to get the necessary utilities, and if you've been following this writing you'll already have the service "enabled", and will start on boot in the future.

		/etc/rc.conf:
		samba_server_enable="YES"
		
So next, we just need to create a basic config file for SAMBA, like so:
		/usr/local/etc/smb4.conf:
		[global]
		## Replace any variables in ${} with the appropriate data for your setup
		## E.G. if your domain is called "exile", you'd use
		## workgroup = EXILE
		workgroup = ${DOMAIN}
		## Necessay security settings
		security = ads
		winbind use default domain = no
		## In keeping with our previous example, we'll 
		## replace ${DOMAIN_FQDN} with a plausible example, lets say
		## password server = exile.exile.digital
		password server = ${DOMAIN_FQDN}
		realm = ${DOMAIN_FQDN}

		## How to treat home directories
		[homes]
		comment = Home Directories
		browseable = no
		realm = ${DOMAIN_FQDN}
		
After saving this file, a command needs to be run as root with the help of a domain administrator. We'll be using their account to authenticate to the domain controller and then enroll the server in the domain. Once you have your domain admin, or at least their credentials, run:
		net ads join -U ${ADMIN} -S ${DOMAIN_CONTROLLER}
		
So an example would be something like:
		net ads join -U newnix_admin -S ex01
		
This command, if it succeeds, will allow you to start the samba daemons with service samba_server start. It will only need to be repeated in the event that a new server is installed that also needs to communicate as a domain member.
To be on the safe side, ensure that winbindd is running, simply run pgrep -lf winbind, if you get a list of processes, then you're good! If you don't get anything back, you'll need to run winbindd. The ntlm_auth process needs to communicate with it to verify group membership. If this isn't important to your deployment, you can omit it.

Using Active Directory Authentication with FreeRADIUS

So before going into the necessary changes, I'd like to mention that this is being written out on 2019.02.26, using FreeRADIUS 3.0 and SAMBA 4.8. Please check to see if there's more recent documentation available before you follow this blindly, as I'm unsure that I'll ever actually have to work on such a setup again. That said, this is a surprisingly simple setup, just with a lot of very poorly organized documentation.

Forcing AD Authentication

To ensure that we only use AD for authentication, we'll force all login attempts to go through ntlm_auth(1), in a larger deployment, you may want to look at using winbindd(8) tools directly, but this is currently unsupported to my knowledge. To accomplish this, we'll be modifying /usr/local/etc/raddb/users

		/usr/local/etc/raddb/users:
		DEFAULT Auth-Type = ntlm_auth
		

Provided everything else is either commented out or deleted, this forces all auth attempts to be handled by the ntlm_auth module, which doesn't exist by default. We'll need to create it, and in doing so define how these authentication attempts are to be handled, this is done via /usr/local/etc/raddb/mods-enabled/ntlm_auth.

		/usr/local/etc/raddb/mods-enabled/ntlm_auth:
		exec ntlm_auth {
			wait = yes
			program = "/usr/local/bin/ntlm_auth --requent-nt-key --domain=${DOMAIN} --username=%{mschap:User-Name} --password=%{User-Password} --require-membership-of='${DOMAIN}\${GROUP}'"
		}
		

This is the command used to verify a user can actually log in, if ntlm_auth(1) returns NTSTATUS_OK or similar "OK" message, then the user is granted access, otherwise, FreeRADIUS sends a reject message, and the device has to decide if that means rejecting the login attempt entirely or falling back to a different authentication method.

Ensuring AD Auth is Available

If you've been following this article, you'll now have all the parts in place to use AD authentication as the backend for your RADIUS install, barring the final change, making sure that FreeRADIUS knows how to actually authenticate against AD. This requires a single line change in /usr/local/etc/raddb/sites-enabled/default, which would be similar to the following:

		/usr/local/etc/raddb/sites-enabled/default:
		authenticate {
			... # boilerplate, not important for this article
			auth_log # techincally not necessary, but helpful for tracking login attempts
			ntlm_auth # enable use of the ntlm_auth module we wrote earlier
			... # more boilerplate
		}
		
And that's it, it's a surprisingly simple configuration, it just apparently isn't common enough for it to be well documented.

Troubleshooting

In the event you start running into trouble with authentication, there's several options for troubleshooting the services. You can do everything from using the radtest utility to tcpdump to get the raw packets being sent to and from the RADIUS daemon. I'll touch on the most useful options briefly, as there's no way for me to feasibly cover all scenarios and how they'd be presented.

Debug Mode
This is perhaps the simplest means of discovering why things are behaving differently than anticipated, you should be aware though that this will display all authentication requests in cleartext! Repeat: THIS WILL DISPLAY ALL AUTH ATTEMPTS IN CLEARTEXT!! So be careful who has access to these commands, you can't have everyone able to view login details. As root, or by using a privilege escalation tool, run the following command: service radiusd stop && radiusd -X, this is best run inside a tmux session or similar, due to the large amount of output for every request recieved. In this scenario, the most important messages will have the substring "ntlm_auth", since we're forcing all authentication to be handled by Active Directory. You'll see either NTSTATUS_OK for successful authentication, and the error messages otherwise. The result of the ntlm_auth command will determine the result sent to the device.
Packet Capture
This is less immediately helpful, but does not expose the passwords for authentication requests, and knowing that a successful login using ntlm_auth will result in the RADIUS daemon responding with an accept message, we can get enough information to know where to look next. The command we'll use, assuming that our daemon is liseting on the interface "em0", would resemble the following: tcpdump -vvttttlAi em0 udp and port 1812, this will print out packets used for RADIUS authentication both to and from the server. You may need to add on additional filtering, such as specifying a particular host or network as the source of the packets, especially in larger deployments where you can get overwhelmed quite quickly. This will show the contents of the packets, so you'll see which host is making the query and which user is trying to log in, as well as whether the attempt was successful or not in the response message. With this information, you'll be able to start manually testing with the ntlm_auth tool, or possibly the net tool to ensure the user meets all specified criteria.
AD Auth Failures
This is the one that can get a bit tricky, if login is getting refused, ensure you're using the most recent password for that account. If AD mandates new passwords every 90 days, then you'll have to make sure you use the updated password after that timeframe. After that, try manually running the command that FreeRADIUS uses, and remember that all conditions must be met! If you have the --require-membership-of flag set, that user needs to be a member of that group for authentication to work properly. It can be dropped to help narrow down the cause of the authentication rejection, so you'd instead run: ntlm_auth --request-nt-key --domain=${DOMAIN} --username=${USER}, which does not require root to work, and it'll prompt for that user's password. If that works, you should be reasonbly sure that the problem is group membership. To verify group membership, you can use the following, as root: net rpc group members '${GROUP_NAME}' -U ${USER}%${PASSWORD} -S ${DOMAIN_CONTROLLER}, which will print out all the members of the specified group. If you're unsure the proper group is even being used, you'll need to check the output of the command wbinfo -g to get the groups available. So be sure that AD and /usr/local/etc/raddb/mods-enabled/ntlm_auth are all in agreement on the domain, group, user, and password.
Radtest
radtest(1) is basically a wrapper around the radclient(1) command, that enables local testing of the FreeRADIUS configuration, in this scenario, the most useful invocation would be radtest -P udp -x ${USER} ${PASSWORD} ${SERVER_IP}:${SERVER_PORT} ${CLIENT_PORT} ${RADIUS_SECRET}, the "${CLIENT_PORT}" value doesn't really matter here, but the command uses positional variables rather than flags to specify all these options, so put in any valid port number. This command will emulate the process of requesting authentication based on the username and password provided, there's an additional -t flag to specify the authentication method, but since we're forcing everything through Active Directory, it's not of any use here. The -x flag will print out debugging info, so you'll get a better idea of where things are going wrong, but it's going to be most useful in conjuction with the debug mode of radiusd(8), where you can see the server-side auth process as well.