The Dell 820 kept "finding a cert" and bypassing enrolment despite repeated BIOS TPM clears. tpm2_nvreadpublic revealed 0x01800001 on that hardware is a FIRMWARE platform index (platformcreate|policydelete|writelocked, 70 bytes — not our cert; siblings at 0x01800003/4 + 0x01810000). platformcreate survives TPM2_Clear and policydelete needs the platform's secret policy, so it's permanent and unusable by us — the blind check false-positived on it, and enrolment could never write there anyway. Same class of bug as the EK-handle collision. Move the NV cert index default 0x01800001 -> 0x01800100 (free on the 820, owner range, overridable via rd.dot1x.nvindex): cmdline (extract-artifacts.sh), fleet-enrol -nvindex, the dracut hooks, bench scripts, CLAUDE.md invariant #1, and design/SSP docs. The signing-key handle (0x81000100) is unchanged. Harden WriteNVCert: refuse a platformcreate index with a clear "pick a free owner index via rd.dot1x.nvindex" error instead of a confusing undefine failure. Regression simtest/TestWriteNVCertRefusesPlatformIndex; NV roundtrip + ReadNVCert tests moved to 0x01800100. All go tests + packer validate pass.
164 lines
7.3 KiB
Bash
Executable File
164 lines
7.3 KiB
Bash
Executable File
#!/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=0x01800100
|
|
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
|