Configuring WPA2 Enterprise with EAP-TLS in Mac OS X and Linux

Setting up the CA

Follow the steps on setting up a Certificate Authority (CA) using OpenSSL.

Issuing the client certificate and private key

Once the CA has been configured, we will generate a private key and an unsigned public key digital certificate.

# openssl req -new -days 365 -newkey rsa:1024
-keyout sslkey.pem -out unsigned.pem

The unsigned public key digital certificate, stored in a PEM-encoded file named unsigned.pem will be sent to the CA for signing:

# openssl ca -in unsigned.pem -out cert.pem

Installing the client certificate and private key

The next step consists in installing the private key, public key digital certificate and CA public key certificate.


The private key, public key digital certificate and CA certificate files should get installed into a location where only root and wpa_supplicant can access them, for example, /etc/wpa:

# mkdir /etc/wpa
# chown root.root /etc/wpa
# chmod 700 /etc/wpa

Mac OS X

Mac OS X can only import private keys in PKCS#12 so we need to export all the previous items to a suitable format:

# openssl pkcs12 -export -in cert.pem -inkey key.pem
-out client.p12 -name "host.domain"

Where "host.domain" denotes the FQDN of the host which this digital certificate and private key are intended for.

The output file client.p12 contains the private key and public key digital certificate. This bundle should get moved to the host using a secure distribution channel, like an SSH/SCP/SFTP session or a USB key. Also, the CA digital certificate, usually named cacert.pem, should also get copied to the host.

On Mac OS X, using the GUI, double click the cacert.pem file, and install the CA certificate into the X509Anchors keychain. This a system-wide keychain intended to store X.509 CA root digital certificates.

Next, using the GUI, double click on client.p12 file, supply the password that protects the private key stored in this file, and choose to install both the private key and public key into the login keychain. Next, make sure the private key has been installed:

Configuring the AirPort Express Wireless Access Point

Launch AirPort Admin Utility, select the desired base station and click the Configure icon from the toolbar:

Click the Change Wireless Security… button:

In this new window, fill in the information about the RADIUS server, like its IP address, shared secret and so on.

Configuring the Supplicant for WPA2 Enterprise


Create /etc/wpa_supplicant.conf using the following data:


The identity directive is required, or else the EAP-TLS negotiation will fail.

ap_scan=2 and scan_ssid=1 are needed when the Wireless Acccess Point is configured to not broadcast the ESSID.

Mac OS X

Launch Internet Connect from the Wireless menu:

If no 802.1X icon appears on the toolbar, choose File -> New 802.1X Conection…. Click the 802.1X icon. The window will look like this:

From the Configuration drop-down, select Edit Configurations…:

A window like this will open:

Fill in both the “Description” and “Wireless Network” fields with the ESSID of the Wireless network. Leave “User Name” and “Password” blanked, since we are not using password-based authentication.

From the “Authentication” listbox, clear the checkbox for all the protocols except for TLS. Select the TLS protocol and click the Configure button. A new window will open for you to select the private key that will be used for the EAP-TLS authentication mechanism:

From the drop-down listbox, select the name of the private key that matches the name of the private key installed in the previous section.

Click the Connect button. The Supplicant will authenticate against the Wireless Access Point. At this point, it is possible that Mac OS X asks confirmation for accessing the private key stored in your keychain. It is recommended to “Always Allow” the Supplicant access to the private key.

Launch System Preferences -> Network and Configure… the AirPort interface:

Click the “+” button to add a Preferred network:

Just enter the ESSID of the Wireless network and choose WPA2 Enterprise from the Wireless Security drop-down listbox. Also, make sure the Configuration field shows the name of the 802.1X configuration we created previously using Internet Connect.

Leave the rest of the fields blank, since we are not using password-based authentication.

FreeBSD firewall using PF

FreeBSD supports OpenBSD’s powerful firewall PF since version 5.3. The scenario I was pursuing was firewalling one of my FreeBSD machines:

Incoming firewalling

  • Only incoming SSH connections from known SSH clients should be accepted.
  • Only incoming Syslog traffic from known Syslog clients should be accepted.
  • Only incoming ICMP Echo-Reply, ICMP Echo-Request and ICMP Destination-Unreachable datagrams should be accepted. Any other ICMP datagram is potentially dangerous.
  • No other incoming traffic should be allowed, but should get logged.

Outgoing firewalling

  • Only outgoing DNS queries to known DNS servers should be accepted.
  • Only outgoing NTP traffic to know NTP servers should be accepted.
  • Only outgoing ICMP Echo-Reply, ICMP Echo-Request and ICMP Destination-Unreachable datagrams should be accepted. Any other ICMP datagram is potentially dangerous.
  • No other outgoing traffic should be allowed, but should get logged.

The contents of /etc/pf.conf should look lite this:

scrub in all pass quick on lo0 all icmp_types = "{ echorep, unreach, echoreq }" syslog_sources = "{,, }" ssh_sources = "{, }" ntp_servers = "{ }" dns_servers = "{ }" block in log all pass in on rl0 inet proto icmp icmp-type $icmp_types keep state pass in on rl0 proto tcp from $ssh_sources to self port { 22 } flags S/SA keep state pass in on rl0 proto udp from $syslog_sources to self port { 514 } block out log all pass out on rl0 inet proto icmp icmp-type $icmp_types keep state pass out on rl0 proto udp from self to $dns_servers port { 53 } keep state pass out on rl0 proto udp from self to $ntp_servers port { 123 } keep state

To enable PF and PF logging support to start automatically during boot, the following lines should be added to /etc/rc.conf:


syslog-ng replacement for FreeBSD

FreeBSD uses syslog by default. However, syslog is very old and inflexible, so I decided to replace it with syslog-ng. syslog-ng syntax is far more easier to read and understand than syslog’s one.

syslog-ng uses the following elements to determine what to log and where to log it:

  • Source: Defines where log entries do come from. For example, syslog-ng can read log entries from the /var/run/log local socket, from the network via UDP port 514, via TCP, and so on.

    I just decided to split the sources in two:

    1. A local source, called src
    2. A network source, called net

    This allows easily to distinguish between locally generated log entries and log entries generated elsewhere by a network device or host machine.

  • Destination: Defines where do log entries will get logged into. For example, log entries can be written to a file, can be sent to another syslog-compatible server, sent to a socket, and so on.

    I have kept the default destinations, and added a new one called airport, pointing to file /var/log/airport.log. All log events generated by my AirPort Express Wireless Access Point will get logged into this destination.

  • Filter: Defines a matching criteria for log entries. Allows to distinguish log entries by some common attributes, like the source host, facility, logging level, a regular expression matching the entry description, and so on.

    I have kept the default filters, but added a new one called f_airport, which matches all log entries whose source is my Wireless Access Point.

Finally, log entries combine sources, filters and destinations. When a log entry is received, it is matched against every log rules until a source and a filter matches. Then, the log entry is sent to the destination or destinations for that matching rule.

Since I wanted to centralize some logs into my FreeBSD server across the network, concretely my AirPort Express logs, this is the /usr/local/etc/syslog-ng/syslog-ng.conf file I used to achieve it:

# options
options { long_hostnames(off); sync(0); };

# sources
source src { unix-dgram("/var/run/log");
             unix-dgram("/var/run/logpriv" perm(0600));
             internal(); file("/dev/klog"); };

source net { udp(); };

# destinations
destination messages { file("/var/log/messages"); };
destination security { file("/var/log/security"); };
destination authlog { file("/var/log/auth.log"); };
destination maillog { file("/var/log/maillog"); };
destination lpd-errs { file("/var/log/lpd-errs"); };
destination xferlog { file("/var/log/xferlog"); };
destination cron { file("/var/log/cron"); };
destination debuglog { file("/var/log/debug.log"); };
destination consolelog { file("/var/log/console.log"); };
destination all { file("/var/log/all.log"); };
destination newscrit { file("/var/log/news/news.crit"); };
destination newserr { file("/var/log/news/news.err"); };
destination newsnotice { file("/var/log/news/news.notice"); };
destination console { file("/dev/console"); };
destination allusers { usertty("*"); };
#destination loghost { udp("loghost" port(514)); };
destination airport { file("/var/log/airport.log"); };
destination linksys { file("/var/log/linksys.log"); };

# log facility filters
filter f_auth { facility(auth); };
filter f_authpriv { facility(authpriv); };
filter f_not_authpriv { not facility(authpriv); };
filter f_console { facility(console); };
filter f_cron { facility(cron); };
filter f_daemon { facility(daemon); };
filter f_ftp { facility(ftp); };
filter f_kern { facility(kern); };
filter f_lpr { facility(lpr); };
filter f_mail { facility(mail); };
filter f_news { facility(news); };
filter f_security { facility(security); };
filter f_user { facility(user); };
filter f_uucp { facility(uucp); };
filter f_local0 { facility(local0); };
filter f_local1 { facility(local1); };
filter f_local2 { facility(local2); };
filter f_local3 { facility(local3); };
filter f_local4 { facility(local4); };
filter f_local5 { facility(local5); };
filter f_local6 { facility(local6); };
filter f_local7 { facility(local7); };

# log level filters
filter f_emerg { level(emerg); };
filter f_alert { level(alert..emerg); };
filter f_crit { level(crit..emerg); };
filter f_err { level(err..emerg); };
filter f_warning { level(warning..emerg); };
filter f_notice { level(notice..emerg); };
filter f_info { level(info..emerg); };
filter f_debug { level(debug..emerg); };
filter f_is_debug { level(debug); };

# airport filter
filter f_airport { host("airport"); };

# linksys filter
filter f_linksys { host("linksys"); };

# *.err;kern.warning;auth.notice;mail.crit              /dev/console
log { source(src); filter(f_err); destination(console); };
log { source(src); filter(f_kern); filter(f_warning); destination(console); };
log { source(src); filter(f_auth); filter(f_notice); destination(console); };
log { source(src); filter(f_mail); filter(f_crit); destination(console); };

# *.notice;authpriv.none;kern.debug;;mail.crit;news.err /var/log/messages
log { source(src); filter(f_notice); filter(f_not_authpriv); destination(messages); };
log { source(src); filter(f_kern); filter(f_debug); destination(messages); };
log { source(src); filter(f_lpr); filter(f_info); destination(messages); };
log { source(src); filter(f_mail); filter(f_crit); destination(messages); };
log { source(src); filter(f_news); filter(f_err); destination(messages); };

# security.*                                            /var/log/security
log { source(src); filter(f_security); destination(security); };

#;                               /var/log/auth.log
log { source(src); filter(f_auth); filter(f_info); destination(authlog); };
log { source(src); filter(f_authpriv); filter(f_info); destination(authlog); };

#                                             /var/log/maillog
log { source(src); filter(f_mail); filter(f_info); destination(maillog); };

#                                              /var/log/lpd-errs
log { source(src); filter(f_lpr); filter(f_info); destination(lpd-errs); };

#                                              /var/log/xferlog
log { source(src); filter(f_ftp); filter(f_info); destination(xferlog); };

# cron.*                                                /var/log/cron
log { source(src); filter(f_cron); destination(cron); };

# *.=debug                                              /var/log/debug.log
log { source(src); filter(f_is_debug); destination(debuglog); };

# *.emerg                                               *
log { source(src); filter(f_emerg); destination(allusers); };

# airport logging
log { source(net); filter(f_airport); destination(airport); };

# linksys logging
log { source(net); filter(f_linksys); destination(linksys); };