Implementing Enterprise-Grade 802.1X EAP-TLS Authentication in My Home Lab

2025-06-1712 min read

Implementing Enterprise-Grade 802.1X EAP-TLS Authentication in My Home Lab

Introduction

802.1X with EAP-TLS is the gold standard for wired network access control -- and it is notoriously painful to get right, even in production. In this post, I walk through my full implementation using FreeRADIUS, Samba4 Active Directory, and a Cisco Catalyst 2960, covering the PKI design, every configuration file that matters, and the five troubleshooting issues I hit along the way.

This goes beyond simple password-based authentication by leveraging X.509 certificates for device authentication, providing the same level of security found in enterprise environments.

Architecture Overview

Network Infrastructure:

  • Management VLAN: 172.16.99.0/24
  • Domain: tillynet.lan
  • Samba4 Active Directory Domain Controller: 172.30.30.30
  • pfSense Firewall with FreeRADIUS: 172.16.99.1
  • Cisco Catalyst 2960 Switch (Network Access Server)
  • Windows 10/11 domain-joined workstation: TILLYPC

Security Infrastructure:

  • Two-tier PKI with offline Root CA
  • Samba4-hosted Intermediate Certificate Authority
  • EAP-TLS authentication protocol
  • Certificate-based device authentication

The architecture separates the authentication server (FreeRADIUS), directory services (Samba4 AD), and network enforcement (Cisco switch) into distinct components communicating via standardized protocols.

Certificate Infrastructure Design

EAP-TLS lives and dies by its PKI. I designed a two-tier hierarchy that mirrors enterprise deployments.

Root Certificate Authority

The Root CA operates offline for maximum security, issuing only intermediate certificates. The root private key stays protected while the intermediate CA handles day-to-day operations.

Intermediate Certificate Authority

I integrated the Intermediate CA directly into my Samba4 AD environment, giving me centralized certificate management, integration with AD security groups, and a realistic enterprise-like setup.

Server Certificate Generation

For the FreeRADIUS server certificate, I wrote a script that automates generation while ensuring proper extensions and SANs:

hljs bash
#!/bin/bash
SERVICE_FQDN=$1
BASE_DIR="/usr/local/samba/private/tls"
KEY_SIZE=2048
DAYS_VALID=825

INTERMEDIATE_KEY="${BASE_DIR}/intermediate.key"
INTERMEDIATE_CRT="${BASE_DIR}/intermediate.crt"
CA_CHAIN="${BASE_DIR}/ca-chain.crt"

if [[ -z "$SERVICE_FQDN" ]]; then
  echo "Usage: $0 <service.fqdn>"
  exit 1
fi

OUTPUT_DIR="${BASE_DIR}/${SERVICE_FQDN}"
mkdir -p "${OUTPUT_DIR}"

# Generate private key and certificate signing request
openssl req -new -newkey rsa:${KEY_SIZE} -nodes \
  -keyout "${OUTPUT_DIR}/${SERVICE_FQDN}.key" \
  -out "${OUTPUT_DIR}/${SERVICE_FQDN}.csr" \
  -subj "/CN=${SERVICE_FQDN}" \
  -addext "subjectAltName = DNS:${SERVICE_FQDN}"

# Create certificate extensions
EXT_FILE="${OUTPUT_DIR}/v3_ext.cnf"
cat > "${EXT_FILE}" <<EOF
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = ${SERVICE_FQDN}
EOF

# Sign certificate with intermediate CA
openssl x509 -req \
  -in "${OUTPUT_DIR}/${SERVICE_FQDN}.csr" \
  -CA "${INTERMEDIATE_CRT}" \
  -CAkey "${INTERMEDIATE_KEY}" \
  -CAcreateserial \
  -out "${OUTPUT_DIR}/${SERVICE_FQDN}.crt" \
  -days ${DAYS_VALID} -sha256 \
  -extfile "${EXT_FILE}" \
  -extensions v3_req

# Build complete certificate chain
cat "${OUTPUT_DIR}/${SERVICE_FQDN}.crt" "${CA_CHAIN}" > "${OUTPUT_DIR}/${SERVICE_FQDN}-fullchain.crt"

rm -f "${EXT_FILE}"

This script ensures the server certificate includes the proper Extended Key Usage (EKU) for server authentication and maintains the complete chain required for client validation.

Client Certificate Generation

Client certificates require different extensions -- specifically the Client Authentication EKU. I created a separate script:

hljs bash
#!/bin/bash
FQDN="$1"
CERT_DIR="/usr/local/samba/private/tls/$FQDN"
mkdir -p "$CERT_DIR"

# Generate client certificate with appropriate extensions
openssl req -new -nodes -newkey rsa:2048 \
  -keyout "$CERT_DIR/$FQDN.key" \
  -out "$CERT_DIR/$FQDN.csr" \
  -subj "/CN=$FQDN" \
  -config <(cat <<EOF
[ req ]
default_bits       = 2048
prompt             = no
default_md         = sha256
req_extensions     = v3_req
distinguished_name = dn

[ dn ]
CN = $FQDN

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = $FQDN
EOF
)

# Sign with intermediate CA
openssl x509 -req \
  -in "$CERT_DIR/$FQDN.csr" \
  -CA /usr/local/samba/private/tls/intermediate.crt \
  -CAkey /usr/local/samba/private/tls/intermediate.key \
  -CAcreateserial \
  -out "$CERT_DIR/$FQDN.crt" \
  -days 825 -sha256 \
  -extfile <(cat <<EOF
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = $FQDN
EOF
)

cat "$CERT_DIR/$FQDN.crt" /usr/local/samba/private/tls/ca-chain.crt > "$CERT_DIR/$FQDN-fullchain.crt"

PKCS#12 Export for Windows

Windows workstations require certificates in PKCS#12 format. I automated the conversion:

hljs bash
#!/bin/bash
FQDN="$1"
PFX_PASSWORD="${2:-""}"
CERT_BASE_DIR="/usr/local/samba/private/tls"
CERT_DIR="$CERT_BASE_DIR/$FQDN"

KEY_FILE="$CERT_DIR/$FQDN.key"
CERT_FILE="$CERT_DIR/$FQDN.crt"
CHAIN_FILE="$CERT_BASE_DIR/ca-chain.crt"
PFX_OUTPUT="$CERT_DIR/$FQDN.pfx"

# Validate all required files exist
for file in "$KEY_FILE" "$CERT_FILE" "$CHAIN_FILE"; do
  if [[ ! -f "$file" ]]; then
    echo "Error: Required file not found: $file"
    exit 1
  fi
done

# Generate PKCS#12 bundle
openssl pkcs12 -export \
  -inkey "$KEY_FILE" \
  -in "$CERT_FILE" \
  -certfile "$CHAIN_FILE" \
  -out "$PFX_OUTPUT" \
  -passout pass:"$PFX_PASSWORD"

This ensures Windows clients receive certificates with the complete chain, enabling proper validation against the root CA.

FreeRADIUS Configuration

Configuring FreeRADIUS for EAP-TLS with Active Directory integration required careful attention to several components. The pfSense FreeRADIUS package provides a web interface, but understanding the underlying configuration is essential for troubleshooting.

RADIUS Client Configuration

I configured the Cisco switch as a RADIUS client (NAS):

  • Client Name: TL-ASW1-2960C
  • Client IP Address: The management IP of the switch
  • Shared Secret: A 32-character randomly generated pre-shared key matching the switch's AAA configuration
  • NAS Type: Cisco

LDAP Integration Challenges

This was the most complex part of the implementation. Windows computer accounts authenticate differently from user accounts, and getting FreeRADIUS to handle that correctly took several attempts.

Initial (broken) LDAP configuration:

  • Base DN: DC=tillynet,DC=lan
  • Filter: (sAMAccountName=%{Stripped-User-Name:-%{User-Name}})
  • Base Filter: (objectClass=person)

This failed because Windows computer authentication uses the host/computername.domain format, computer accounts live in CN=Computers by default, and their sAMAccountName includes a trailing dollar sign.

Corrected LDAP configuration:

  • Base DN: CN=Computers,DC=tillynet,DC=lan
  • Filter: (&(objectClass=computer)(|(sAMAccountName=%{mschap:User-Name}$)(servicePrincipalName=%{User-Name})))
  • Base Filter: (objectClass=computer)
  • LDAP Authorization: Enabled (critical -- without this, FreeRADIUS cannot process LDAP responses for authorization decisions)

The filter handles two authentication formats: the sAMAccountName with trailing $ for the computer account name, and the servicePrincipalName for the host/computername.domain format used during authentication.

EAP-TLS Configuration

Critical EAP settings:

  • Default EAP Type: TLS
  • TLS Configuration:
    • Server Certificate: radius.tillynet.lan
    • Private Key: Corresponding private key
    • Certificate Chain: Complete CA chain
  • PEAP Configuration:
    • Default EAP Type: MSCHAPV2 (for inner tunnel, though not used in pure EAP-TLS)
    • Copy Request to Tunnel: Yes
    • Use Tunneled Reply: Yes

The "Copy Request to Tunnel" and "Use Tunneled Reply" settings are crucial -- they pass authentication attributes between the outer and inner authentication contexts.

Interface Binding

I bound FreeRADIUS specifically to the management interface (172.16.99.1) rather than all interfaces, for better security and clearer troubleshooting.

Group Policy Configuration for 802.1X

Certificate Deployment Strategy

I chose manual certificate deployment for this lab to better understand the process. In production, I would recommend auto-enrollment via certificate templates and Group Policy.

Wired Network Policy Configuration

I created a GPO at: Computer Configuration > Policies > Windows Settings > Security Settings > Wired Network (IEEE 802.3) Policies

Policy Settings:

  • Policy Name: "Enterprise 802.1X EAP-TLS Authentication"
  • Use Windows Wired Auto Config: Enabled
  • Network Authentication Method: Smart Card or other Certificate (EAP-TLS)
  • Authentication Mode: Computer authentication
  • Validate server certificate: Enabled
  • Connect to these servers: radius.tillynet.lan
  • Trusted Root Certification Authorities: My root CA
  • Use simple certificate selection: Enabled

GPO Deployment and Verification

I linked the GPO to the Workstations OU where my test computer resides. To verify:

hljs cmd
gpresult /r /scope computer
gpupdate /force

The gpresult output confirmed the wired network policy was being applied correctly.

Cisco Catalyst 2960 Configuration

The switch serves as the 802.1X authenticator, blocking network access until authentication succeeds.

Global AAA Configuration

hljs cisco
# Enable new AAA model
aaa new-model

# Configure RADIUS server
radius-server host 172.16.99.1 auth-port 1812 acct-port 1813 key YourSharedSecret
radius-server timeout 10
radius-server retransmit 3

# Configure 802.1X authentication
aaa authentication dot1x default group radius

The 10-second timeout balances responsiveness with reliability, allowing for network latency without excessive delays during authentication failures.

802.1X Global Configuration

hljs cisco
# Enable 802.1X system-wide
dot1x system-auth-control

This enables 802.1X globally on the switch. Individual ports still need per-port configuration.

Port-Level Configuration

I configured a test port (FastEthernet0/2) for 802.1X:

hljs cisco
interface FastEthernet0/2
 switchport mode access
 authentication port-control auto
 dot1x pae authenticator
 spanning-tree portfast
  • switchport mode access: Configures the port as an access port
  • authentication port-control auto: Blocks the port until successful 802.1X authentication
  • dot1x pae authenticator: Sets the port as an 802.1X authenticator
  • spanning-tree portfast: Reduces convergence time for end-device connections

Troubleshooting Journey

The implementation involved extensive troubleshooting that taught me more than the configuration itself. Here are the five major issues I hit.

Issue 1: RADIUS Port Mismatch

Symptoms: No communication between the switch and FreeRADIUS.

I enabled debugging on the switch:

hljs cisco
terminal monitor
debug radius
debug dot1x all

The debug output revealed:

*Feb 10 23:25:41.810: RADIUS(00000000): Send Access-Request to 172.16.99.1:1645

Root Cause: The switch was using legacy RADIUS ports (1645/1646) instead of the standard ports (1812/1813).

Fix: Explicitly specified the correct ports:

hljs cisco
radius-server host 172.16.99.1 auth-port 1812 acct-port 1813 key YourSharedSecret

After the change, debug output confirmed RADIUS communication was working (though authentication still failed -- see Issue 2).

Issue 2: LDAP Filter Misconfigured for Computer Accounts

Symptoms: RADIUS packets reached the server, but authentication consistently returned Access-Reject.

Checking /var/log/radius.log showed:

[ldap] ERROR: Unable to create filter
[ldap] ERROR: Failed to create LDAP filter

Root Cause: The LDAP filter was configured for user authentication, not computer authentication. Windows computer accounts use the host/computername.domain format, live in CN=Computers, and have a trailing $ on their sAMAccountName.

Fix: I changed the Base DN to CN=Computers,DC=tillynet,DC=lan, updated the filter to (&(objectClass=computer)(|(sAMAccountName=%{mschap:User-Name}$)(servicePrincipalName=%{User-Name}))), changed the Base Filter to (objectClass=computer), and enabled LDAP Authorization.

Issue 3: EAP-TLS Tunnel Attribute Passing

Symptoms: After resolving the LDAP issues, authentication intermittently failed with TLS-related errors.

Running radiusd -X in debug mode showed the EAP-TLS handshake completing, but inner authentication failing.

Root Cause: The EAP tunnel was not passing authentication attributes between the outer and inner contexts.

Fix: I changed "Copy Request to Tunnel" and "Use Tunneled Reply" from "No" to "Yes" in the EAP-PEAP configuration.

Issue 4: Certificate Validation Failures on the Client

Symptoms: Some authentication attempts failed with certificate validation errors on the client side.

Windows Event Viewer (System log) showed certificate chain validation errors.

Root Cause: The certificate chain was incomplete, and the server certificate's CN did not match the configured server name in the client's 802.1X profile.

Fix:

  1. Ensured FreeRADIUS was configured with the complete chain (server cert + intermediate + root)
  2. Verified the server certificate's CN matched the FQDN in the client's 802.1X profile
  3. Confirmed the root CA was properly installed in the client's Trusted Root Certification Authorities store

Issue 5: Authentication Timeouts

Symptoms: Authentication worked but took excessively long, sometimes timing out.

Root Cause: LDAP query timeouts, certificate validation delays, and network latency between components.

Fix: I adjusted the RADIUS server timeout to 10 seconds on the switch, enabled persistent LDAP connections (connection pooling), and configured client-side certificate caching.

Advanced Troubleshooting Techniques

Packet Capture Analysis

I used tcpdump on the pfSense firewall to capture RADIUS traffic:

hljs bash
tcpdump -i em0 -s0 -w radius_capture.pcap udp port 1812

This let me verify RADIUS packets were reaching the server and analyze authentication flow timing.

LDAP Testing

I validated LDAP connectivity and search filters directly from the FreeRADIUS host:

hljs bash
ldapsearch -x -H ldaps://172.30.30.30:636 \
  -D "CN=ldapbind,CN=Users,DC=tillynet,DC=lan" \
  -W -b "CN=Computers,DC=tillynet,DC=lan" \
  "(&(objectClass=computer)(sAMAccountName=TILLYPC$))"

Certificate Validation Testing

hljs bash
openssl verify -CAfile ca-chain.crt radius.tillynet.lan.crt

Windows Event Log Analysis

On the Windows client, the most useful logs were:

  • System Log: 802.1X authentication events
  • Security Log: Authentication and authorization events
  • Applications and Services Logs > Microsoft > Windows > Wired-AutoConfig: Detailed 802.1X debugging

Lessons Learned

  • Computer auth is not user auth. Windows machines authenticate as host/computername.domain with a trailing $ on the sAMAccountName. If your LDAP filter is built for user accounts, it will silently fail. Target CN=Computers and use the compound filter shown above.
  • "Copy Request to Tunnel" is not optional. Without it, the inner EAP context cannot see the authentication attributes from the outer context. Turn it on from the start.
  • Legacy RADIUS ports will bite you. Cisco switches default to 1645/1646 unless you explicitly specify 1812/1813. Always check with debug radius output.
  • Troubleshoot layer by layer. Start with debug radius and debug dot1x all on the switch. Verify RADIUS reachability first, then LDAP connectivity, then certificate chain validation. Trying to debug the whole stack at once is a losing game.
  • Automate certificate lifecycle in production. Manual cert deployment worked for this lab, but any real deployment needs auto-enrollment via certificate templates and Group Policy.

Future Enhancements

  • Dynamic VLAN Assignment: Assign VLANs based on computer group membership for automatic network segmentation.
  • Certificate Lifecycle Management: Automated enrollment and renewal using AD Certificate Services templates and Group Policy.
  • Monitoring and Analytics: Track authentication patterns, identify potential security issues, and build detailed reporting on network access.
  • Extended NAC Integration: More granular access control based on device health, compliance status, and user context.

Conclusion

Implementing 802.1X EAP-TLS in my home lab was one of the most challenging projects I have taken on, and one of the most rewarding. The key takeaways:

  1. PKI is the foundation. A well-designed certificate hierarchy, proper EKUs, and chain validation are non-negotiable for EAP-TLS.
  2. LDAP integration is where complexity hides. Computer authentication against AD requires understanding Windows auth mechanisms, LDAP schema quirks, and the differences between user and machine accounts.
  3. Systematic troubleshooting wins. Debug logs, packet captures, and component-by-component testing saved me hours of guessing.
  4. Security and performance are always a tradeoff. Connection pooling, caching, and timeout tuning are just as important as getting the auth flow working in the first place.

This setup now serves as a robust platform for exploring advanced NAC concepts and testing enterprise security scenarios.