Phase 8b: 802.1X EAP-TLS
Phase 8b: 802.1X EAP-TLS Authentication
This section migrates the P16g from iPSK (DOMUS-IoT) to certificate-based 802.1X EAP-TLS (DOMUS-Secure) for both wired and wireless. After this, the P16g authenticates like the Razer — mutual TLS with Vault-issued certificates, no shared keys.
Prerequisites
-
Vault cluster running and unsealed (vault-01/vault-02)
-
Vault PKI intermediate (
pki_int) withdomus-clientrole configured -
ISE trusts DOMUS-ISSUING-CA in its certificate store
-
ISE policy set configured for EAP-TLS
-
WiFi backend switched from iwd to wpa_supplicant
Verify Razer’s Working Config (Reference)
Before configuring the P16g, confirm what the Razer uses — this is the target state.
# From the Razer — check the cert that ISE trusts
sudo openssl x509 -in /etc/ssl/certs/modestus-razer-eaptls.pem -noout -subject -issuer
Actual output (modestus-razer)
subject=O=Domus-Infrastructure, OU=Domus-Admins, CN=modestus-razer.inside.domusdigitalis.dev
issuer=CN=DOMUS-ISSUING-CA
# Verify the nmcli connections
nmcli c s | grep -E "Domus-Wired|Domus-WiFi"
Actual output (modestus-razer)
Domus-Wired-EAP-TLS 3f9fefb8-4da8-4a45-9c86-971b20afee42 ethernet enp130s0
Domus-WiFi-EAP-TLS 23672874-9cc0-48cf-9c72-d0e0dee53a7a wifi wlan0
Load Vault Credentials
ds d000 dev/vault
vault status
Expected output (Vault healthy)
Seal Type shamir
Initialized true
Sealed false
HA Enabled true
Active Node https://vault-02.inside.domusdigitalis.dev:8200
Issue Certificate from Vault PKI
Full Workflow
# 1. Load Vault credentials FIRST — without this, vault defaults to localhost:8200
ds d000 dev/vault
# 2. Set the TARGET hostname explicitly — do NOT use $(cat /etc/hostname)
# You may be issuing from the Razer for a different machine
HOSTNAME="modestus-p16g"
echo "Issuing cert for: ${HOSTNAME}.inside.domusdigitalis.dev"
# 3. Issue certificate — tee saves full JSON AND pipes summary to screen
# Full JSON → /tmp/${HOSTNAME}-vault-cert.json (cert + key for extraction)
# Summary → /tmp/${HOSTNAME}-vault-summary.json (safe to view later)
vault write -format=json pki_int/issue/domus-client \
common_name="${HOSTNAME}.inside.domusdigitalis.dev" \
ttl=8760h \
| tee /tmp/${HOSTNAME}-vault-cert.json \
| jq '{common_name: .data.common_name, serial: .data.serial_number, expiration: .data.expiration}' \
> /tmp/${HOSTNAME}-vault-summary.json
# 4. View the summary (doesn't expose private key)
cat /tmp/${HOSTNAME}-vault-summary.json | jq .
Extract Certificate Components with jq
$HOSTNAME should still be set from the issuance step above. Verify before extracting:
|
echo "Extracting for: $HOSTNAME"
# If empty, set it again: HOSTNAME="modestus-p16g"
# Client certificate
jq -r '.data.certificate' /tmp/${HOSTNAME}-vault-cert.json >| /tmp/${HOSTNAME}-eaptls.pem
# Private key
jq -r '.data.private_key' /tmp/${HOSTNAME}-vault-cert.json >| /tmp/${HOSTNAME}-eaptls.key
# CA chain (issuing CA + root CA)
jq -r '.data.ca_chain[]' /tmp/${HOSTNAME}-vault-cert.json >| /tmp/DOMUS-CA-CHAIN.pem
# Issuing CA only (optional — chain file is usually sufficient)
jq -r '.data.issuing_ca' /tmp/${HOSTNAME}-vault-cert.json >| /tmp/DOMUS-ISSUING-CA.pem
Verify Issued Certificate
# Subject, issuer, and validity dates
openssl x509 -in /tmp/${HOSTNAME}-eaptls.pem -noout -subject -issuer -dates
# CRITICAL: Must include "TLS Web Client Authentication" — without it, ISE silently rejects
openssl x509 -in /tmp/${HOSTNAME}-eaptls.pem -noout -text | grep -A1 "Extended Key Usage"
# Serial number (save this — needed for revocation)
jq -r '.data.serial_number' /tmp/${HOSTNAME}-vault-cert.json
# Expiration as human-readable date
jq -r '.data.expiration' /tmp/${HOSTNAME}-vault-cert.json | xargs -I {} date -d @{}
Verify Key Matches Certificate
# Both MD5 hashes MUST match — mismatched key/cert is a common cause of "TLS handshake failed"
CERT_MOD=$(openssl x509 -in /tmp/${HOSTNAME}-eaptls.pem -noout -modulus | md5sum | cut -d' ' -f1)
KEY_MOD=$(openssl rsa -in /tmp/${HOSTNAME}-eaptls.key -noout -modulus 2>/dev/null | md5sum | cut -d' ' -f1)
echo "Cert: $CERT_MOD"
echo "Key: $KEY_MOD"
[ "$CERT_MOD" == "$KEY_MOD" ] && echo "MATCH" || echo "MISMATCH — DO NOT PROCEED"
Install Certificates
# HOSTNAME should still be set from above — verify: echo $HOSTNAME
# If empty: HOSTNAME="modestus-p16g"
# Install client cert
sudo cp /tmp/${HOSTNAME}-eaptls.pem /etc/ssl/certs/
sudo chmod 644 /etc/ssl/certs/${HOSTNAME}-eaptls.pem
# Install private key (root-only read)
sudo cp /tmp/${HOSTNAME}-eaptls.key /etc/ssl/private/
sudo chmod 600 /etc/ssl/private/${HOSTNAME}-eaptls.key
sudo chown root:root /etc/ssl/private/${HOSTNAME}-eaptls.key
# Install CA chain
sudo cp /tmp/DOMUS-CA-CHAIN.pem /etc/ssl/certs/
sudo chmod 644 /etc/ssl/certs/DOMUS-CA-CHAIN.pem
# Verify chain validates
openssl verify -CAfile /etc/ssl/certs/DOMUS-CA-CHAIN.pem /etc/ssl/certs/${HOSTNAME}-eaptls.pem
# Cleanup temp files
rm -f /tmp/${HOSTNAME}-vault-cert.json /tmp/${HOSTNAME}-eaptls.* /tmp/DOMUS-*.pem
Configure WiFi Backend (wpa_supplicant)
Enterprise 802.1X requires wpa_supplicant, not iwd. Arch defaults to iwd — fix this before creating connections.
sudo mkdir -p /etc/NetworkManager/conf.d
echo -e "[device]\nwifi.backend=wpa_supplicant" | sudo tee /etc/NetworkManager/conf.d/wifi_backend.conf
# Disable iwd completely
sudo systemctl stop iwd 2>/dev/null
sudo systemctl disable iwd 2>/dev/null
sudo systemctl mask iwd
# Enable wpa_supplicant
sudo systemctl enable wpa_supplicant
sudo systemctl start wpa_supplicant
sudo systemctl restart NetworkManager
Create Wired 802.1X Connection
# HOSTNAME should still be set from above — verify: echo $HOSTNAME
# If empty: HOSTNAME="modestus-p16g"
# Find the wired interface name
WIRED_IF=$(ip -o link show | awk -F': ' '/state UP/ && !/lo|wlan/ {print $2; exit}')
echo "Wired interface: $WIRED_IF"
sudo nmcli connection add \
type ethernet \
con-name "Domus-Wired-EAP-TLS" \
ifname "$WIRED_IF" \
802-1x.eap tls \
802-1x.identity "${HOSTNAME}.inside.domusdigitalis.dev" \
802-1x.identity-flags 0 \
802-1x.ca-cert /etc/ssl/certs/DOMUS-CA-CHAIN.pem \
802-1x.client-cert /etc/ssl/certs/${HOSTNAME}-eaptls.pem \
802-1x.private-key /etc/ssl/private/${HOSTNAME}-eaptls.key \
802-1x.private-key-password-flags 4 \
connection.autoconnect yes
sudo nmcli connection up "Domus-Wired-EAP-TLS"
|
|
Create WiFi 802.1X Connection
# HOSTNAME should still be set from above — verify: echo $HOSTNAME
# If empty: HOSTNAME="modestus-p16g"
sudo nmcli connection add \
type wifi \
con-name "Domus-WiFi-EAP-TLS" \
ifname wlan0 \
ssid "Domus-Secure" \
wifi-sec.key-mgmt wpa-eap \
802-1x.eap tls \
802-1x.identity "${HOSTNAME}.inside.domusdigitalis.dev" \
802-1x.ca-cert /etc/ssl/certs/DOMUS-CA-CHAIN.pem \
802-1x.client-cert /etc/ssl/certs/${HOSTNAME}-eaptls.pem \
802-1x.private-key /etc/ssl/private/${HOSTNAME}-eaptls.key \
802-1x.private-key-password-flags 4 \
connection.autoconnect yes
|
Do NOT bounce the WiFi connection from an SSH session over that same WiFi.
Option A: Run locally — open Kitty on the P16g desktop:
Option B: From SSH — write a script and run it with nohup so it survives the disconnect:
Then wait 30 seconds and SSH back in with the new IP (DHCP may reassign). Option C: Tee the command to a file, run locally:
|
Do NOT use identity-flags for WiFi — causes "invalid property" error. WiFi stores identity in the connection file by default.
|
Verify Both Connections
nmcli connection show --active | grep -E "Domus-Wired|Domus-WiFi"
# Verify 802.1X settings
nmcli connection show "Domus-Wired-EAP-TLS" | grep -E "802-1x.eap|802-1x.identity|GENERAL.STATE"
nmcli connection show "Domus-WiFi-EAP-TLS" | grep -E "802-1x.eap|802-1x.identity|GENERAL.STATE"
# Check IP — should be on DOMUS-Secure VLAN, not IoT VLAN
ip -4 addr show | awk '/inet / && !/127.0.0.1/ {print $NF, $2}'
ISE Verification (from Razer)
ds d000 dev/network
WIRED_MAC="a8:2b:dd:8f:23:e6"
WIFI_MAC="e0:d5:5d:6c:e1:66"
# Wired — should show dot1x/EAP-TLS, not mab
netapi ise mnt session $WIRED_MAC
netapi ise dc auth-history $WIRED_MAC --hours 1
# WiFi — should show dot1x/EAP-TLS
netapi ise mnt session $WIFI_MAC
netapi ise dc auth-history $WIFI_MAC --hours 1
# Raw SQL — confirm method changed from mab to dot1x
netapi ise dc query "
SELECT mac_address, authentication_method, passed, timestamp
FROM radius_authentications
WHERE mac_address IN ('$WIRED_MAC', '$WIFI_MAC')
AND timestamp > SYSDATE - 1
ORDER BY timestamp DESC
"
Expected Results
| Field | Wired | WiFi |
|---|---|---|
Connection |
|
|
Auth Method |
|
|
Protocol |
|
|
Identity |
|
|
Cert Issuer |
|
|
NAD |
Switch (10.50.1.10) |
Home-9800-WLC (10.50.1.40) |
Policy Set |
|
|
Remove iPSK Connection
Once EAP-TLS is verified on WiFi:
sudo nmcli connection delete "DOMUS-IoT"
# Verify only EAP-TLS connections remain
nmcli connection show | grep -E "Domus|EAP"
Troubleshooting
# Live logs — NetworkManager + wpa_supplicant
journalctl -u NetworkManager -t wpa_supplicant -f | grep -iE "eap|tls|auth|fail"
# Certificate chain verification
openssl verify -CAfile /etc/ssl/certs/DOMUS-CA-CHAIN.pem /etc/ssl/certs/$(hostname)-eaptls.pem
# Key/cert match (both hashes must be identical)
openssl x509 -in /etc/ssl/certs/$(hostname)-eaptls.pem -noout -modulus | md5sum
sudo openssl rsa -in /etc/ssl/private/$(hostname)-eaptls.key -noout -modulus | md5sum
# Bounce connection
sudo nmcli connection down "Domus-WiFi-EAP-TLS" && sudo nmcli connection up "Domus-WiFi-EAP-TLS"
| Issue | Fix |
|---|---|
"Secrets required but not provided" |
Missing |
"TLS handshake failed" |
CA chain incomplete — ensure DOMUS-CA-CHAIN.pem contains both ISSUING-CA and ROOT-CA |
"Authentication rejected" |
Certificate CN doesn’t match ISE identity — verify with |
WiFi "invalid property identity-flags" |
Remove |
Interface disappeared after disabling iwd |
Reload WiFi driver: |
Wrong VLAN assigned |
Check ISE authorization policy order — |