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) with domus-client role 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"

private-key-password-flags 4 — CRITICAL for passwordless keys. Without it, NetworkManager prompts for a password that doesn’t exist: "Secrets were required, but not provided."

identity-flags 0 — stores identity in the connection file. Only valid for wired — do NOT use for WiFi.

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.

nmcli connection down kills the WiFi link — your SSH session dies instantly and connection up never runs. You’re locked out until you walk to the machine.

Option A: Run locally — open Kitty on the P16g desktop:

sudo nmcli connection down "Domus-WiFi-EAP-TLS" && sudo nmcli connection up "Domus-WiFi-EAP-TLS"

Option B: From SSH — write a script and run it with nohup so it survives the disconnect:

echo 'sleep 2 && sudo nmcli connection down "Domus-WiFi-EAP-TLS" && sudo nmcli connection up "Domus-WiFi-EAP-TLS"' > /tmp/bounce-wifi.sh
chmod +x /tmp/bounce-wifi.sh
nohup /tmp/bounce-wifi.sh &

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:

echo 'sudo nmcli connection down "Domus-WiFi-EAP-TLS" && sudo nmcli connection up "Domus-WiFi-EAP-TLS"' > /tmp/bounce-wifi.sh
chmod +x /tmp/bounce-wifi.sh
# Then run /tmp/bounce-wifi.sh from the P16g's local terminal
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

Domus-Wired-EAP-TLS

Domus-WiFi-EAP-TLS

Auth Method

dot1x

dot1x

Protocol

EAP-TLS

EAP-TLS

Identity

modestus-p16g.inside.domusdigitalis.dev

modestus-p16g.inside.domusdigitalis.dev

Cert Issuer

DOMUS-ISSUING-CA

DOMUS-ISSUING-CA

NAD

Switch (10.50.1.10)

Home-9800-WLC (10.50.1.40)

Policy Set

Wired Dot1X Closed

Corp WIFI

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 private-key-password-flags 4 — add with nmcli connection modify

"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 openssl x509 -noout -subject

WiFi "invalid property identity-flags"

Remove identity-flags — only valid for wired connections

Interface disappeared after disabling iwd

Reload WiFi driver: sudo modprobe -r iwlmvm mac80211 iwlwifi && sudo modprobe iwlwifi

Wrong VLAN assigned

Check ISE authorization policy order — netapi ise get-authz-rules "Corp WIFI"