Hi,
I was hoping the following may provide a more complete start to finish integration guide for others that may have similar requirements. I would very much like to invite dialogue regarding any recommendations anyone may have. Our requirements were: * RADIUS servers domain joined to handle PAP and MS-CHAP v2 authentication using information only from Active Directory * We do not want to store credentials in AD with reversible encryption * AD Group membership validation based on source * Yubico OTP Multi-Factor Authentication (MFA) System are domain joined as member servers using Samba. Samba winbind subsequently presents users and groups as native system accounts, which allows references to just the group name and unlike LDAP does not make the link sensitive to the placement of the security group within the AD folders or organisational units (distinguished name or DN). Password Authentication Protocol (PAP) transmits unencrypted passwords (i.e. plain-text) over the network so connectivity should be encrypted using either IPSec or TLS (RadSec or EAP-TTLS). MS-CHAP v2 provides mutual authentication and uses a hash of the password. This means that the request only includes a hash representing the password, so there is nothing to cut the password and OTP out of. One can however transmit the OTP as the username and then lookup the associated owner's AD account name for FreeRADIUS to retrieve the hash used during authentication via Samba winbind. Yubico devices generate a unique 32 character modhex passcode each time you press the button on a YubiKey 5 or scan it using NFC. This passcode comes after a 12 character public id, which is unique to each YubiKey. We therefore have two method of transmitting the OTP during login. The first is the classic RSA token method (although reversed) where we use our username, type our password and then press the YubiKey button, herewith an example: Username: davidh Password: secretcccccct00001krbhnvrjrdlujuujdcjvltdcrdkhhtit This method requires PAP, as we require the client to transmit the entered credentials, so that we can cut the information in to a password and Yubico OTP. The alternative is to make use of the YubiKey's public ID to lookup the AD username. This method works for PAP and allows MS-CHAP v2 authentication, herewith an example: Username: cccccct00001krbhnvrjrdlujuujdcjvltdcrdkhhtit Password: secret In the second case we type out password out and then press the YubiKey when prompted for the username. Devices we were authenticating to are generally switches and routers where we can transmit attributes as part of the access accept message to grant eg access permissions. Some platforms however (eg Check Point Management and VyOS) require user accounts to be defined, in those cases you are stuck with PAP being the only possible integration. PS: All is not lost, if you implement a jump host that you enforce MFA access to, which in turn simply uses single factor. Additional detail on the Yubico OTP is available directly from Yubico: https://developers.yubico.com/OTP/OTPs_Explained.html The following notes pertain to a Debian buster (10) install, they would require path changes but should in essence work on any Linux distribution. Join system to Active Directory: apt-get install libnss-winbind samba winbind; pico /etc/krb5.conf [libdefaults] default_realm = AD.ACME.COM dns_lookup_realm = false dns_lookup_kdc = true pico /etc/samba/smb.conf [---------------------------------------- /etc/samba/smb.conf ----------------------------------------] [global] server role = member server workgroup = ACME realm = ad.acme.com netbios name = FreeRADIUS1 server string = FreeRADIUS and Yubico OTP bind interfaces only = yes interfaces = 127.0.0.1/8 192.168.1.48/24 ntlm auth = mschapv2-and-ntlmv2-only guest account = nobody idmap cache time = 300 kerberos method = system keytab idmap config * : backend = tdb idmap config * : range = 100001-120000 idmap config Domain-01 : backend = rid idmap config Domain-01 : range = 1000-100000 template homedir = /home/users/%U template shell = /sbin/nologin log level = 2 log file = /var/log/samba/%m.log enable core files = no max log size = 50 dont descend = /dev, /mirror, /proc time server = no wins support = no map acl inherit = yes store dos attributes = yes printing = cups cups options = raw winbind use default domain = yes winbind enum users = yes winbind enum groups = yes winbind expand groups = 2 [nobody] path = /dev/null comment = Access denied - Guest guest ok = no printable = no browseable = no [---------------------------------------- /etc/samba/smb.conf ----------------------------------------] net ads join -U adm-domain-davidh; systemctl restart smbd; pico /etc/nsswitch.conf; # Append 'winbind' to 'passwd' and 'group' Check winbind and ntlm_auth authentication are working: wbinfo -t ntlm_auth --allow-mschapv2 --request-nt-key --domain=ACME --username=davidh pico /etc/group /etc/gshadow # Add 'freerad' to 'winbindd_priv:' Finally check that you are able to retrieve users and groups as if they were local users: getent passwd davidh getent group client_cpe_admin Install FreeRADIUS with the YubiKey module: apt-get install freeradius-yubikey If necessary set shortname on defined clients to apply different AD group membership or altering what reply attributes are sent back /etc/freeradius/3.0/clients.conf # Hash out existing active lines, then append: client fw1.acme.com { ipaddr = 192.168.10.1 secret = **************** require_message_authenticator = yes nastype = other shortname = checkpoint } client switch6.acme.com { ipaddr = 192.168.20.7 secret = **************** require_message_authenticator = yes nastype = cisco shortname = client_cpe } Define custom attributes: /etc/freeradius/3.0/dictionary.custom ATTRIBUTE sAMAccountName 3000 string ATTRIBUTE Original-User-Name 3001 string Sample Livingston style users file, in this case for use with MikroTik RouterOS, make as many as you need with whatever reply attributes you require: /etc/freeradius/3.0/users DEFAULT FreeRADIUS-Client-Shortname == "client_cpe", Group == "client_cpe_view" Mikrotik-Group = "view" DEFAULT FreeRADIUS-Client-Shortname == "client_cpe", Group == "client_cpe_admin" Mikrotik-Group = "full" DEFAULT FreeRADIUS-Client-Shortname == "client_cpe", Auth-Type := Reject Reply-Message = "Access Denied - Not a member of any client_cpe security groups" DEFAULT Auth-Type := Reject Reply-Message = "Access Denied" Edit MS-CHAP v2 module to use winbind and alter the username: /etc/freeradius/3.0/mods-available/mschap winbind_username = "%{%{sAMAccountName}:-%{mschap:User-Name}}" winbind_domain = "%{mschap:NT-Domain}" Add 'allow-mschapv2' and again change the username expansion variable: /etc/freeradius/3.0/mods-available/ntlm_auth program = "/usr/bin/ntlm_auth --allow-mschapv2 --request-nt-key --domain=DOMAIN-01 --username=%{%{sAMAccountName}:-%{mschap:User-Name}} --password=%{User-Password}" Register for a Yubico API integration account and then enable and configure the module: cd /etc/freeradius/3.0/mods-enabled; ln -s ../mods-available/yubikey yubikey; /etc/freeradius/3.0/mods-enabled/yubikey validate = yes client_id = ***** api_key = '****************************' The following changes are all in /etc/freeradius/3.0/sites-available/default Hash out 'pap', we'll force NTLMv2 by obtaining a hash of the password hash from AD with each authentication request and perform necessary operations on the plain text password we receive as part of the request After 'preprocess' in 'authorize {' if (&User-Name =~ /^([cbdefghijklnrtuv]{44})$/) { update request {Yubikey-OTP = "%{1}"} } elsif (&User-Password =~ /^(.+?)([cbdefghijklnrtuv]{44})$/) { update request {User-Password := "%{1}", Yubikey-OTP = "%{2}"} } if (&Yubikey-OTP =~ /^([cbdefghijklnrtuv]{12})/) { switch "%{1}" { case cccccct00001 {update request {sAMAccountName = "davidh"}} case cccccct00002 {update request {sAMAccountName = "philipo"}} } if (!&sAMAccountName || (&User-Name != &Yubikey-OTP && &User-Name != &sAMAccountName)) { update reply {Reply-Message := "Unregistered or user mismatch"} reject } } Before and after 'files' in 'authorize {' update request {FreeRADIUS-Client-Shortname = "%{Client-Shortname}"} if (&sAMAccountName && &User-Name != &sAMAccountName) { update request { Original-User-Name := "%{User-Name}", User-Name := "%{sAMAccountName}"}} files if (&Original-User-Name) {update request {User-Name := "%{Original-User-Name}"}} End of 'authorize {' if (&control:Auth-Type == "mschap") { if (&Yubikey-OTP) { update control {Auth-Type := "YubiCHAP"} } else {update control {Auth-Type := "MS-CHAP"}} if (!control:Auth-Type && User-Password) { if (&Yubikey-OTP) { update control {Auth-Type := "YubiNTLM"} } else {update control {Auth-Type := "ntlm_auth"}} } Exclusively have the following in 'authenticate {' Auth-Type MS-CHAP { mschap } Auth-Type YubiCHAP { mschap yubikey } Auth-Type ntlm_auth { ntlm_auth } Auth-Type YubiNTLM { ntlm_auth yubikey } Beginning of 'post-auth {' if (&sAMAccountName && &User-Name != &sAMAccountName) {update request {User-Name := "%{sAMAccountName}"}} Beginning of 'Post-Auth-Type REJECT {' if (&sAMAccountName && &User-Name == &Yubikey-OTP) {update request {User-Name := "%{sAMAccountName}"}} Herewith all active lines, as a comparative to where you've ended up: [---------------------------------------- grep -v '^\s*#\|^\s*$' /etc/freeradius/3.0/sites-enabled/default ----------------------------------------] server default { listen { type = auth ipaddr = * port = 0 limit { max_connections = 16 lifetime = 0 idle_timeout = 30 } } listen { ipaddr = * port = 0 type = acct limit { } } listen { type = auth ipv6addr = :: # any. ::1 == localhost port = 0 limit { max_connections = 16 lifetime = 0 idle_timeout = 30 } } listen { ipv6addr = :: port = 0 type = acct limit { } } authorize { filter_username preprocess if (&User-Name =~ /^([cbdefghijklnrtuv]{44})$/) { update request {Yubikey-OTP = "%{1}"} } elsif (&User-Password =~ /^(.+?)([cbdefghijklnrtuv]{44})$/) { update request {User-Password := "%{1}", Yubikey-OTP = "%{2}"} } if (&Yubikey-OTP =~ /^([cbdefghijklnrtuv]{12})/) { switch "%{1}" { case cccccct0001 {update request {sAMAccountName = "davidh"}} case cccccct0002 {update request {sAMAccountName = "philipo"}} } if (!&sAMAccountName || (&User-Name != &Yubikey-OTP && &User-Name != &sAMAccountName)) { update reply {Reply-Message := "Unregistered or user missmatch"} reject } } chap mschap digest suffix eap { ok = return } update request {FreeRADIUS-Client-Shortname = "%{Client-Shortname}"} if (&sAMAccountName && &User-Name != &sAMAccountName) { update request { Original-User-Name := "%{User-Name}", User-Name := "%{sAMAccountName}"}} files if (&Original-User-Name) {update request {User-Name := "%{Original-User-Name}"}} -sql -ldap expiration logintime if (&control:Auth-Type == "mschap") { if (&Yubikey-OTP) { update control {Auth-Type := "YubiCHAP"} } else {update control {Auth-Type := "MS-CHAP"}} } if (!control:Auth-Type && User-Password) { if (&Yubikey-OTP) { update control {Auth-Type := "YubiNTLM"} } else {update control {Auth-Type := "ntlm_auth"}} } } authenticate { Auth-Type MS-CHAP { mschap } Auth-Type YubiCHAP { mschap yubikey } Auth-Type ntlm_auth { ntlm_auth } Auth-Type YubiNTLM { ntlm_auth yubikey } } preacct { preprocess acct_unique suffix files } accounting { detail unix -sql exec attr_filter.accounting_response } session { } post-auth { if (&sAMAccountName && &User-Name != &sAMAccountName) {update request {User-Name := "%{sAMAccountName}"}} update { &reply: += &session-state: } -sql exec remove_reply_message_if_eap Post-Auth-Type REJECT { if (&sAMAccountName && &User-Name == &Yubikey-OTP) {update request {User-Name := "%{sAMAccountName}"}} -sql attr_filter.access_reject eap remove_reply_message_if_eap } Post-Auth-Type Challenge { } } pre-proxy { } post-proxy { eap } } [---------------------------------------- grep -v '^\s*#\|^\s*$' /etc/freeradius/3.0/sites-enabled/default ----------------------------------------] NB: Once everyone has a YubiKey hash out the single factor authenticate options (Auth-Type MS-CHAP and Auth-Type ntlm_auth) methods or update unlang to exempt 2FA from jump hosts or NMS. Unvalidated idea for users file: DEFAULT Yubikey-OTP !* "", Packet-Src-IP-Address !~ /192\.168\.14\./, Auth-Type := Reject Reply-Message = "Access Denied - 2FA required" Regards David Herselman - List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html |
On Feb 22, 2021, at 4:45 PM, David Herselman via Freeradius-Users <[hidden email]> wrote:
> I was hoping the following may provide a more complete start to finish integration guide for others that may have similar requirements. I would very much like to invite dialogue regarding any recommendations anyone may have. Long detailed instructions are good. If possible, updating the Wiki might help. > Our requirements were: > > * RADIUS servers domain joined to handle PAP and MS-CHAP v2 authentication using information only from Active Directory > * We do not want to store credentials in AD with reversible encryption "passwords" maybe? > * AD Group membership validation based on source > * Yubico OTP Multi-Factor Authentication (MFA) > > System are domain joined as member servers using Samba. Samba winbind subsequently presents users and groups as native system accounts, which allows references to just the group name and unlike LDAP does not make the link sensitive to the placement of the security group within the AD folders or organisational units (distinguished name or DN). Password Authentication Protocol (PAP) transmits unencrypted passwords (i.e. plain-text) over the network To be clear: passwords are encrypted using some crappy RADIUS-specific scheme. It's been done since 1993, and no one has been able to break it using methods other than brute force. > so connectivity should be encrypted using either IPSec or TLS (RadSec or EAP-TTLS). That's still a good recommendation. TLS and IPSec are *much* better than whatever weird RADIUS encryption is being used for the password. > MS-CHAP v2 provides mutual authentication and uses a hash of the password. Anyone who can monitor the MS-CHAP data can pretty trivially break it. So MS-CHAP should *also* be secured using IPSec or TLS. > This means that the request only includes a hash representing the password, so there is nothing to cut the password and OTP out of. One can however transmit the OTP as the username and then lookup the associated owner's AD account name for FreeRADIUS to retrieve the hash used during authentication via Samba winbind. > > Yubico devices generate a unique 32 character modhex passcode each time you press the button on a YubiKey 5 or scan it using NFC. This passcode comes after a 12 character public id, which is unique to each YubiKey. We therefore have two method of transmitting the OTP during login. The first is the classic RSA token method (although reversed) where we use our username, type our password and then press the YubiKey button, herewith an example: > Username: davidh > Password: secretcccccct00001krbhnvrjrdlujuujdcjvltdcrdkhhtit > > This method requires PAP, as we require the client to transmit the entered credentials, so that we can cut the information in to a password and Yubico OTP. > > The alternative is to make use of the YubiKey's public ID to lookup the AD username. This method works for PAP and allows MS-CHAP v2 authentication, herewith an example: > Username: cccccct00001krbhnvrjrdlujuujdcjvltdcrdkhhtit > Password: secret > > In the second case we type out password out and then press the YubiKey when prompted for the username. Devices we were authenticating to are generally switches and routers where we can transmit attributes as part of the access accept message to grant eg access permissions. Some platforms however (eg Check Point Management and VyOS) require user accounts to be defined, in those cases you are stuck with PAP being the only possible integration. Ugh. They should just use RADIUS. That's what it's designed for. This solution is a little weird. But if it works, ship it! And yes, there's nothing really wrong here. You're playing games with names / passwords. You're *not* making anything insecure. Alan DeKok. - List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html |
In reply to this post by Users mailing list
Hi,
Just a small update with regards to getting the desired behaviour. One can not perform regex operations on IP attributes in the users file. The following will subsequently *not* work: DEFAULT Yubikey-OTP !* "", Packet-Src-IP-Address =~ "^196\.10\.10", Auth-Type := Reject Reply-Message = "Access Denied - 2FA required" Herewith the same thing in unlang, place it just after the '!control:Auth-Type && User-Password' check in the 'authorize {' section: if (!&Yubikey-OTP) { if (&Packet-Src-IP-Address =~ /^196\.10\.10/) { update reply {Reply-Message := "Access Denied - 2FA required"} reject } } Regards David Herselman - List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html |
> On Feb 23, 2021, at 12:37 PM, David Herselman via Freeradius-Users <[hidden email]> wrote: > > Hi, > > Just a small update with regards to getting the desired behaviour. One can not perform regex operations on IP attributes in the users file. The following will subsequently *not* work: > DEFAULT Yubikey-OTP !* "", Packet-Src-IP-Address =~ "^196\.10\.10", Auth-Type := Reject > Reply-Message = "Access Denied - 2FA required" > > Herewith the same thing in unlang, place it just after the '!control:Auth-Type && User-Password' check in the 'authorize {' section: > > if (!&Yubikey-OTP) { > if (&Packet-Src-IP-Address =~ /^196\.10\.10/) { Or even better: if (<ipv4prefix>&Packet-Src-IP-Address < 192.168.10/24) { Simple and faster. :) See "man unlang" for details. Alan DeKok. - List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html |
Free forum by Nabble | Edit this page |