#!/bin/bash # dot1x-macsec-up.sh — initqueue/settled hook. # Order of operations is load-bearing: # 1. pull client cert from TPM NV into /run # 2. start wpa_supplicant (argv[0]=@wpa_supplicant => survives switch-root) # 3. wait for EAP success + macsec0 # 4. DHCP on macsec0 (the pre-auth lease, if any, is invalid post-auth/VLAN) # 5. derive hostnqn/hostid from the cert so dracut's nvmf module can # connect the NVMe/TCP root namespace over macsec0 # Idempotent: guarded by a stamp file because initqueue may re-run hooks. type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh STAMP=/run/dot1x/.done [ -e "$STAMP" ] && exit 0 mkdir -p /run/dot1x /run/wpa_supplicant # Best-effort boot-splash status for the 'fleet' Plymouth theme. NEVER fatal and # NEVER reorders anything — a splash call must not perturb the load-bearing # auth -> macsec -> DHCP -> NVMe sequence below. No-op if plymouth isn't running. ply() { plymouth update --status="$1" 2>/dev/null || true; } # --- knobs (override on the kernel cmdline) --------------------------------- NIC=$(getarg rd.dot1x.if=) if [ -z "$NIC" ]; then # First non-loopback, non-macsec interface. Pure shell — ls/grep/head are # not guaranteed to exist in the initramfs. for _dev in /sys/class/net/*; do _name=${_dev##*/} case "$_name" in lo|macsec*) continue ;; esac NIC=$_name; break done fi NVINDEX=$(getarg rd.dot1x.nvindex=); [ -n "$NVINDEX" ] || NVINDEX=0x01800001 TIMEOUT=$(getarg rd.dot1x.timeout=); [ -n "$TIMEOUT" ] || TIMEOUT=60 info "dot1x-macsec: using interface $NIC, NV index $NVINDEX" # --- 0. TEMPORARY test bypass (rd.dot1x=0) ---------------------------------- # Disables 802.1X + MACsec so the UKI / rootfs / NVMe-oF boot path can be # validated on an OPEN switch port before the auth layer is brought online. # The physical NIC is brought up with DHCP directly and dracut's cmdline-driven # nvmf module connects the root namespace over it — there is no macsec0, no # supplicant, no TPM cert read. The rest of the boot (rd.nvmf.discover, verity, # systemd.volatile=overlay) is unchanged because it is driven by the kernel # cmdline, not by this hook. SECURITY: no link auth or encryption in this mode — lab/test # networks only. Remove rd.dot1x=0 from the cmdline to restore full security. if [ "$(getarg rd.dot1x=)" = "0" ]; then warn "dot1x-macsec: 802.1X/MACsec DISABLED (rd.dot1x=0) — TEST MODE, no link security" ply mode-test # splash: mark TPM/802.1X/MACsec rows as skipped (not failed) # Networking in test mode is owned ENTIRELY by `ip=dhcp` on the cmdline: # dracut's network module brings the NIC up, gets a lease and writes # resolv.conf. Do NOT `ifup`/`ip link set up` the NIC here — a second # configuration races dracut's own ("Error: ipv4: Address already # assigned"). (The secure path below is different: there the hook owns # macsec0, which dracut knows nothing about, so it must address it itself.) # # Static test NVMe identity (the secure path derives this from the TPM # cert). SPDK serves "allow any host", so this is cosmetic here, but keep it # so the target can host-filter if desired. if [ ! -s /etc/nvme/hostnqn ]; then mkdir -p /etc/nvme echo "nqn.2026-06.org.example.fleet:test-host" > /etc/nvme/hostnqn echo "11111111-1111-1111-1111-111111111111" > /etc/nvme/hostid info "dot1x-macsec: test hostnqn $(cat /etc/nvme/hostnqn)" fi # The NVMe-oF connect is driven by the initqueue/online hook # (nvmf-connect.sh), which fires when the ip=dhcp link comes online — the # correct event-driven place. (A job queued on the MAIN initqueue from here # is registered but never re-executed once the loop blocks on events, so it # never connected; and this distro's nvmf module registers no online # autoconnect of its own.) : > "$STAMP" exit 0 fi # --- 1. client certificate out of TPM NV ------------------------------------ ply tpm-active if [ ! -s /run/dot1x/client.pem ]; then if ! tpm2_nvread -T device:/dev/tpmrm0 "$NVINDEX" -o /run/dot1x/client.der; then warn "dot1x-macsec: tpm2_nvread failed — cannot authenticate" exit 1 fi openssl x509 -inform DER -in /run/dot1x/client.der -out /run/dot1x/client.pem fi ply tpm-done # --- 2. supplicant as a root storage daemon --------------------------------- ip link set "$NIC" up if [ ! -e /run/dot1x/wpa.pid ]; then # bash exec -a sets argv[0]; '@' prefix tells systemd's switch-root # killing spree to spare this process (root storage daemon convention). setsid bash -c "exec -a @wpa_supplicant /usr/sbin/wpa_supplicant \ -Dmacsec_linux -i $NIC \ -c /etc/dot1x/wpa_supplicant-dot1x.conf \ -P /run/dot1x/wpa.pid" & fi ply dot1x-active # --- 3. wait for authentication + macsec0 ----------------------------------- i=0 while [ "$i" -lt "$TIMEOUT" ]; do if wpa_cli -p /run/wpa_supplicant -i "$NIC" status 2>/dev/null \ | grep -q '^suppPortStatus=Authorized' \ && ip link show macsec0 >/dev/null 2>&1; then break fi sleep 1; i=$((i + 1)) done if [ "$i" -ge "$TIMEOUT" ]; then warn "dot1x-macsec: authentication / MACsec timeout after ${TIMEOUT}s" exit 1 fi info "dot1x-macsec: port authorized, macsec0 up after ${i}s" ply dot1x-done ply macsec-active # --- 4. address the secure interface; netroot rides on macsec0 -------------- ip link set macsec0 up ply macsec-done ply net-active # DHCP on macsec0 via NetworkManager (NOT the old network-legacy `ifup`): NM owns # macsec0, so it RENEWS the lease in the running system after switch-root — the # whole point of the network-manager port. macsec0 was created externally by # wpa_supplicant; ask NM to manage+activate it with a default DHCP profile. The # physical NIC stays unmanaged (nm-unmanaged.sh cmdline drop-in), so # wpa_supplicant keeps EAPOL/MKA on it. -w 0 returns immediately; we poll below. nmcli device set macsec0 managed yes 2>/dev/null || true nmcli -w 0 device connect macsec0 2>/dev/null || true # Wait for the DHCP lease (IPv4 on macsec0) before netroot/NVMe connect. i=0 while [ "$i" -lt "$TIMEOUT" ]; do ip -4 addr show macsec0 2>/dev/null | grep -q 'inet ' && break sleep 1; i=$((i + 1)) done if [ "$i" -ge "$TIMEOUT" ]; then warn "dot1x-macsec: no DHCP lease on macsec0 after ${TIMEOUT}s" exit 1 fi info "dot1x-macsec: macsec0 addressed via NetworkManager (DHCP)" ply net-done # --- 5. derive per-host NVMe identity from the TPM certificate -------------- # Keeps the image fleet-generic: the host NQN is a function of the machine # identity already proven via 802.1X, not a baked-in value. if [ ! -s /etc/nvme/hostnqn ]; then mkdir -p /etc/nvme SERIAL=$(openssl x509 -in /run/dot1x/client.pem -serial -noout \ | cut -d= -f2 | tr 'A-F' 'a-f') # Adjust the NQN domain/date to your org's convention (must match the # allow-list on the nvmet target if host filtering is enabled). echo "nqn.2026-06.org.example.fleet:host-${SERIAL}" > /etc/nvme/hostnqn # Stable hostid derived from the same serial (UUID-shaped) printf '%s' "$SERIAL" | sha256sum | cut -c1-32 \ | sed 's/\(.\{8\}\)\(.\{4\}\)\(.\{4\}\)\(.\{4\}\)\(.\{12\}\).*/\1-\2-\3-\4-\5/' \ > /etc/nvme/hostid info "dot1x-macsec: hostnqn $(cat /etc/nvme/hostnqn)" fi : > "$STAMP" exit 0