FreeRadius PEAPv0+MSCHAPv2 howto

I've recently been asked to set up a wifi network using user authentication against Active Directory via RADIUS, specifically using the PEAPv0/EAP-MSCHAPv2 protocol combination. This kinda stuff has potential for frustration, but I finally got it to work. Here's how.

  1. First of all, you need an Active Directory domain to authenticate against. MSCHAPv2 seems to actually require this, it looks like you can't use MSCHAPv2 against a passwd file or something. So, get one of those if you haven't already. I set one up using Samba 4.

  2. Join your RADIUS server into the aforementioned domain.

  3. FreeRadius has a sites mechanism much like the Apache2 web server does, and it comes with a few sites preconfigured. Disable the default site:

    rm /etc/freeradius/sites-enabled/default
    

    (This only removes a symlink, it does not delete the config altogether.)

  4. Create a new file named /etc/freeradius/sites-available/svedrindefault:

    authorize {
        preprocess
        mschap
        suffix
        eap
        files
    }
    authenticate {
        Auth-Type MS-CHAP {
            mschap
        }
        eap
    }
    
  5. Enable the site:

    ln -s /etc/freeradius/sites-available/svedrindefault /etc/freeradius/sites-enabled/
    
  6. The svedrindefault tunnel is now used for Phase 1. Phase 2 will use the inner-tunnel configuration, which you should be able to just leave alone and have it work. But just in case, here's my /etc/freeradius/sites-available/inner-tunnel:

    server inner-tunnel {
        listen {
           ipaddr = 127.0.0.1
           port = 18120
           type = auth
        }
        authorize {
            chap
            mschap
            suffix
            update control {
                   Proxy-To-Realm := LOCAL
            }
            eap {
                    ok = return
            }
            files
            expiration
            logintime
            pap
        }
        authenticate {
            Auth-Type PAP {
                    pap
            }
            Auth-Type CHAP {
                    chap
            }
            Auth-Type MS-CHAP {
                    mschap
            }
            unix
            eap
        }
        session {
            radutmp
        }
        post-auth {
            Post-Auth-Type REJECT {
                    attr_filter.access_reject
            }
        }
        pre-proxy {
        }
        post-proxy {
            eap
        }
    }
    
  7. Edit /etc/freeradius/modules/mschap. Here's mine:

    mschap {
            use_mppe = yes
            with_ntdomain_hack = yes
            ntlm_auth = "/usr/bin/ntlm_auth --request-nt-key --username=%{%{Stripped-User-Name}:-%{%{User-Name}:-None}} --challenge=%{%{mschap:Challenge}:-00} --nt-response=%{%{mschap:NT-Response}:-00}"
    }
    

    The ntlm_auth line is commented by default and I didn't edit it other than uncommenting it and setting the path. Not sure if it makes any difference, but it works, so I won't complain.

  8. Test ntlm_auth:

    # ntlm_auth  --username=svedrin --password="this is not svedrin's password"
    NT_STATUS_OK: Success (0x0)
    
  9. Allow freeradius to talk to winbind:

    # usermod -a -G winbindd_priv freerad
    # chown root:winbindd_priv /var/lib/samba/winbindd_privileged/
    
  10. Don't forget to restart FreeRadius when you're done.

  11. Now you should already be able to use the radtest client to authenticate using any domain account:

    # radtest -t mschap svedrin@local.lan "this is not svedrin's password" localhost:18120 0 testing123
    Sending Access-Request of id 20 to 127.0.0.1 port 18120
            User-Name = "svedrin@local.lan"
            NAS-IP-Address = 127.0.1.1
            NAS-Port = 0
            Message-Authenticator = 0x00000000000000000000000000000000
            MS-CHAP-Challenge = 0xfe62488abc132e2c
            MS-CHAP-Response = 0x00010000000000000000000000000000000000000000000000000a2220a1a0730c99485ee9963bf41cdac0389546968ee612
    rad_recv: Access-Accept packet from host 127.0.0.1 port 18120, id=20, length=84
            MS-CHAP-MPPE-Keys = 0x0000000000000000c6c90e3150570e5a59b4c1b7957121b90000000000000000
            MS-MPPE-Encryption-Policy = 0x00000001
            MS-MPPE-Encryption-Types = 0x00000006
    

    Access-Accept is the part that tells you it worked. If this works, your Phase 2 is ready to roll. If it doesn't work, well then, sucks to be you. Welcome to the first circle of hell or so.

    I can definitely recommend running the freeradius daemon in the foreground while debugging. I used tmux to split the console window in half, had freeradius -Xxx running in the top pane, and issued my debug commands in the bottom pane.

  12. Create the certificates your server is going to use to authenticate itself to the clients. (FreeRadius uses Debian's snakeoil certs by default, but those lack a coupl'a features needed for MSCHAPv2.) FreeRadius comes with a set of scripts that should make this pretty easy:

    cp -r /usr/share/doc/freeradius/examples/certs /etc/freeradius
    cd /etc/freeradius/certs
    vi ca.cnf # some of the defaults might suck, go take a look
    make
    

    This step should create a bunch'a files in the certs directory, most notably server.key, server.pem and ca.pem. Restart FreeRadius to make sure it knows about them.

  13. If you want to test both Phase 1 and Phase 2, you can do so using the eapol_test utility from WPA supplicant. Unfortunately, this thing does not come as part of any Debian package, so you'll have to build it yourself. Here's how:

    mkdir wpa
    cd wpa
    apt-get build-dep wpa
    apt-get source wpa
    cd wpa-2.1
    # build the packages to generate .config needed by the eapol_test build
    dpkg-buildpackage -us -uc
    cd wpa_supplicant
    make eapol_test
    

    If this worked, there should be a binary named eapol_test. Now you can create a configuration file for it, e.g. ~/eapol_test.conf:

    network={
        eap=PEAP
        eapol_flags=0
        key_mgmt=IEEE8021X
        identity="svedrin@local.lan"
        password="this is not svedrin's password"
        ca_cert="/etc/freeradius/certs/ca.pem"
        phase2="auth=MSCHAPV2"
        anonymous_identity="anon@local.lan"
    }
    

    And if you now run eapol_test against this config and your radius server, like so:

    ./eapol_test -c ~/eapol_test.conf -a127.0.0.1 -p1812 -stesting123 -r1
    

    ...it should dump a somewhat huge amount of logging lines and finally conclude with:

    EAP: deinitialize previously used EAP method (25, PEAP) at EAP deinit
    ENGINE: engine deinit
    MPPE keys OK: 2  mismatch: 0
    SUCCESS
    

    SUCCESS is good. We want SUCCESS. If it says FAILURE instead, well then again, sucks to be you.

    The good news is that eapol_test is really spammy about what it's doing, and it definitely tells you what's wrong — you just have to be able to identify the meaningful lines, which involves a measurable amount of educated guesses.

  14. Add an account for your WiFi access point to /etc/freeradius/clients.conf:

    client localhost {
            ipaddr = 127.0.0.1
            secret          = testing123
            require_message_authenticator = no
    }
    client 192.168.1.191 {  # WiFi AP's IP address
            secret          = admin
            shortname       = private-network-1
    }
    

    Without this account, FreeRadius won't allow your access point to talk to it. Configure those credentials in your Access Point.

  15. Tell your RADIUS server for which domain it is supposed to be authoritative. To do so, amend proxy.conf with a realm stanza such as this one:

    realm local.lan {
        type = radius
        secret = testing123
        authhost = LOCAL
        accthost = LOCAL
    }
    
  16. Now go ahead and try to authenticate a real WiFi client. If you got eapol_test to spit out SUCCESS, this should Just Work™.

Note that even if your client offers a dedicated "Domain Name" field, do not enter the domain name in the "domain name" field. Instead, login using <user>@<domain> in the "user name" field, and leave the domain field empty.

I must say, this hasn't been the most pleasant episode of my life as an IT guy, but I did get it to work eventually. Hopefully I won't have to touch this stuff again anytime soon.