Network Manager refactor

This commit is contained in:
Audiobuzz
2026-06-17 13:40:28 +00:00
parent 4b0e0135b3
commit 55aa517976
10 changed files with 198 additions and 62 deletions
+26 -7
View File
@@ -24,13 +24,19 @@
# #
name: Build Diskless UKI name: Build Diskless UKI
run-name: ${{ gitea.actor }} builds the diskless fleet UKI run-name: ${{ gitea.actor }} builds the diskless fleet UKI (${{ gitea.ref_name }})
on: on:
push: push:
branches: branches:
- "main" - "**" # every branch — feature branches get a branch-name+sha tag
tags: tags:
- "v*" - "v*" # tag push -> clean semver tag (v1.2.3 -> 1.2.3)
# One build per ref at a time; a newer push to the same branch cancels the
# in-flight build (each build is a ~17-min VM run, so don't stack them).
concurrency:
group: uki-build-${{ gitea.ref }}
cancel-in-progress: true
env: env:
PRODUCT_VERSION: "1.15.4" # Packer version PRODUCT_VERSION: "1.15.4" # Packer version
@@ -68,13 +74,26 @@ jobs:
- -
name: Derive image version name: Derive image version
id: vars id: vars
# Tag push (v1.2.3 -> 1.2.3); otherwise 0.0.0-<short-sha>. # This becomes the OCI tag (build-oci.sh tags ${oci_image}:${image_version})
# and the artifact dir name. Two cases:
# - tag push (refs/tags/v1.2.3) -> "1.2.3" (release)
# - branch push (refs/heads/<name>) -> "<name>-<sha7>" (dev image)
# The branch name is sanitized to the OCI tag charset
# ([A-Za-z0-9._-], <=128, no leading . or -): e.g. feature/foo -> feature-foo,
# giving feature-foo-ab12cd3. So every branch yields a uniquely tagged,
# pushed image you can boot/test without clobbering main or releases.
run: | run: |
ref="${GITHUB_REF#refs/*/}" short_sha="$(echo "${GITHUB_SHA}" | cut -c1-7)"
if [ "${GITHUB_REF#refs/tags/}" != "$GITHUB_REF" ]; then if [ "${GITHUB_REF#refs/tags/}" != "$GITHUB_REF" ]; then
version="${ref#v}" version="${GITHUB_REF#refs/tags/}"
version="${version#v}"
else else
version="0.0.0-$(echo "${GITHUB_SHA}" | cut -c1-7)" branch="${GITHUB_REF#refs/heads/}"
# invalid chars -> '-'; trim leading '.'/'-'; cap length headroom for -sha7
safe_branch="$(printf '%s' "$branch" \
| sed -e 's#[^A-Za-z0-9._-]#-#g' -e 's#^[.-]*##' | cut -c1-100)"
[ -n "$safe_branch" ] || safe_branch="branch"
version="${safe_branch}-${short_sha}"
fi fi
echo "image_version=${version}" >> "$GITHUB_OUTPUT" echo "image_version=${version}" >> "$GITHUB_OUTPUT"
echo "Building image_version=${version}" echo "Building image_version=${version}"
+33 -10
View File
@@ -26,7 +26,8 @@ http/ks.cfg Kickstart for the throwaway build VM
files/dracut/95dot1x-macsec/ Custom dracut module (the heart of the system) files/dracut/95dot1x-macsec/ Custom dracut module (the heart of the system)
files/etc/dot1x/ Supplicant config + RADIUS CA (placeholder) files/etc/dot1x/ Supplicant config + RADIUS CA (placeholder)
files/etc/dracut.conf.d/90-diskless.conf no-hostonly + drivers + modules files/etc/dracut.conf.d/90-diskless.conf no-hostonly + drivers + modules
files/etc/NetworkManager/ NM unmanaged-device config (NM unmanaged split is now runtime:
95dot1x-macsec/nm-unmanaged.sh, secure-mode only)
scripts/extract-artifacts.sh qcow2 -> kernel/initramfs/erofs+verity/cmdline scripts/extract-artifacts.sh qcow2 -> kernel/initramfs/erofs+verity/cmdline
scripts/build-oci.sh ukify + sbsign + roothash gate + OCI image scripts/build-oci.sh ukify + sbsign + roothash gate + OCI image
serving/ Lab serving stack: dnsmasq + nginx + SPDK serving/ Lab serving stack: dnsmasq + nginx + SPDK
@@ -75,10 +76,27 @@ instead of working around it.
1. **The supplicant must survive switch-root.** It is launched with 1. **The supplicant must survive switch-root.** It is launched with
`exec -a @wpa_supplicant` (systemd root-storage-daemon convention) and `exec -a @wpa_supplicant` (systemd root-storage-daemon convention) and
its state lives under `/run`. Do not “clean up” the `@`, move state off its state lives under `/run`. Do not “clean up” the `@`, move state off
`/run`, or hand the interface to NetworkManager — MKA times out in ~6 s `/run`, or hand the **physical NIC** to NetworkManager — MKA times out in
without keepalives and the root filesystem link dies. The NM ~6 s without keepalives and the root filesystem link dies. The NM
unmanaged-devices config exists for this reason; keep it in sync with unmanaged split exists for this reason (see below); keep it in sync with
interface naming. interface naming.
1. **NM owns IP/DHCP; wpa_supplicant owns the physical NIC.** The initramfs
uses the `network-manager` dracut module (not `network-legacy`) so NM
**renews the DHCP lease** in the running system after switch-root — without
this the one-shot lease expired, the address was dropped, and the NVMe/TCP
root died (“keep alive timeout” / “no route to host”). The managed/unmanaged
split is therefore load-bearing and **mode-aware**:
- **Secure path:** IP rides on **`macsec0`**, which must stay **MANAGED** by
NM. The **physical NIC (`en*`)** must stay **UNMANAGED** (wpa_supplicant
owns EAPOL+MKA on it). This is enforced at runtime by the cmdline-stage
hook `nm-unmanaged.sh`, which writes
`/run/NetworkManager/conf.d/10-dot1x-unmanaged.conf` **before NM starts**.
- **Test path (`rd.dot1x=0`):** no macsec0; the physical NIC carries IP and
must be **MANAGED** so NM renews it — so the hook writes nothing.
There is intentionally **no static `99-dot1x-unmanaged.conf`** (it couldn't
be mode-aware — it wrongly unmanaged `en*` in test mode, blocking renewal).
Never add macsec0 back to an unmanaged list, and never statically unmanage
`en*` for all modes.
1. **roothash coupling is a hard gate.** `build-oci.sh` refuses to build if 1. **roothash coupling is a hard gate.** `build-oci.sh` refuses to build if
the `roothash=` in the UKI cmdline ≠ `verity.json`. Never weaken, skip, the `roothash=` in the UKI cmdline ≠ `verity.json`. Never weaken, skip,
or warn-instead-of-fail this check: a mismatched UKI/rootfs pair is an or warn-instead-of-fail this check: a mismatched UKI/rootfs pair is an
@@ -137,10 +155,15 @@ instead of working around it.
**two separate NVMe namespaces** (`rootfs.erofs` + `rootfs.hash`, distinct **two separate NVMe namespaces** (`rootfs.erofs` + `rootfs.hash`, distinct
pinned UUIDs), so the cmdline uses plain `systemd.verity_root_data=` / pinned UUIDs), so the cmdline uses plain `systemd.verity_root_data=` /
`systemd.verity_root_hash=` by-id devices with no offset. `systemd.verity_root_hash=` by-id devices with no offset.
- `network-legacy` dracut module needs `dhclient`; RHEL 9 omits it from the - **Network module: `network-manager` (ported 2026-06-17 from `network-legacy`).**
minimal install, so the provisioner installs `dhcp-client` (without it dracut network-legacy's one-shot DHCP lease was never renewed after switch-root, so
fails "module network-legacy can't be installed"). dhclient is deprecated — at lease expiry the address was dropped and the NVMe/TCP root died. The
the durable fix is porting the hook to the `network-manager` dracut module. `network-manager` module hands its initramfs connections to the real-root NM
(state under `/run/NetworkManager`), which renews the lease. No more
`dhcp-client`/`dhclient` (deprecated). The hook drives macsec0 DHCP via `nmcli`
(not `ifup`). See invariant on the managed/unmanaged split. BENCH-VERIFY: lease
actually renews past expiry (test path); macsec0 managed + `en*` unmanaged with
MKA surviving switch-root (secure path).
- Writable root overlay is `systemd.volatile=overlay`, **not** `rootovl`: - Writable root overlay is `systemd.volatile=overlay`, **not** `rootovl`:
RHEL/Rocky dracut ships no `overlayfs`/`rootovl` module. Needs RHEL/Rocky dracut ships no `overlayfs`/`rootovl` module. Needs
`systemd-volatile-root.service` in the initramfs (bundled by recent dracut; `systemd-volatile-root.service` in the initramfs (bundled by recent dracut;
@@ -164,8 +187,8 @@ These bit us bringing the build up on Rocky 9.8; they are easy to regress.
the dest to exist or the tree lands elsewhere and later `install` steps fail. the dest to exist or the tree lands elsewhere and later `install` steps fail.
- **dracut module name** in `add_dracutmodules` omits the `NN` dir-prefix: the - **dracut module name** in `add_dracutmodules` omits the `NN` dir-prefix: the
directory is `95dot1x-macsec` but the module is `dot1x-macsec` (cf. `nvmf`). directory is `95dot1x-macsec` but the module is `dot1x-macsec` (cf. `nvmf`).
- **`network-legacy` → `dhcp-client`** and **overlay → `systemd.volatile=overlay`**: - **`network-manager` dracut module** (was `network-legacy`+`dhcp-client`) and
see the bench-verify items above. **overlay → `systemd.volatile=overlay`**: see the bench-verify items above.
- **Test toggle**: `disable_dot1x=true` bakes `rd.dot1x=0`; the hook then skips - **Test toggle**: `disable_dot1x=true` bakes `rd.dot1x=0`; the hook then skips
802.1X/MACsec and DHCPs the NIC directly for NVMe-oF on an open port. The auth 802.1X/MACsec and DHCPs the NIC directly for NVMe-oF on an open port. The auth
stack stays in the image, dormant. The matching lab DHCP/HTTP/NVMe-oF serving stack stays in the image, dormant. The matching lab DHCP/HTTP/NVMe-oF serving
+20 -10
View File
@@ -211,22 +211,26 @@ build {
inline = [ inline = [
"set -euxo pipefail", "set -euxo pipefail",
# dhcp-client provides dhclient, which the dracut 'network-legacy' module # No dhcp-client: the initramfs now uses the 'network-manager' dracut
# (and therefore the 95dot1x-macsec hook's ifup step) requires. RHEL9 # module (90-diskless.conf), not 'network-legacy'/dhclient. NM renews the
# deprecated dhclient in favour of NetworkManager, so the minimal ISO # DHCP lease in the running system after switch-root, fixing the lease-
# omits it; without it network-legacy fails dracut's check() and the # expiry outage (legacy's one-shot lease was never renewed -> address
# diskless module 'cannot be installed'. (Strategic path: port the hook to # dropped -> NVMe/TCP root died). NetworkManager is already pulled in by
# the 'network-manager' dracut module — see CLAUDE.md bench-verify notes.) # @^minimal-environment, so nothing extra is needed for it here.
# zstd: dracut compresses the initramfs with compress="zstd" # zstd: dracut compresses the initramfs with compress="zstd"
# (90-diskless.conf); the minimal ISO lacks the zstd binary, so without it # (90-diskless.conf); the minimal ISO lacks the zstd binary, so without it
# dracut warns and silently falls back to gzip (a larger initramfs). # dracut warns and silently falls back to gzip (a larger initramfs).
"dnf -y install wpa_supplicant tpm2-tools tpm2-tss nvme-cli cryptsetup dracut-network dhcp-client zstd openssl", "dnf -y install wpa_supplicant tpm2-tools tpm2-tss nvme-cli cryptsetup dracut-network NetworkManager zstd openssl",
# Stage etc files into place # Stage etc files into place
"install -D -m0644 /tmp/etc-stage/dot1x/radius-ca.pem /etc/dot1x/radius-ca.pem", "install -D -m0644 /tmp/etc-stage/dot1x/radius-ca.pem /etc/dot1x/radius-ca.pem",
"install -D -m0644 /tmp/etc-stage/dot1x/wpa_supplicant-dot1x.conf /etc/dot1x/wpa_supplicant-dot1x.conf", "install -D -m0644 /tmp/etc-stage/dot1x/wpa_supplicant-dot1x.conf /etc/dot1x/wpa_supplicant-dot1x.conf",
"install -D -m0644 /tmp/etc-stage/dracut.conf.d/90-diskless.conf /etc/dracut.conf.d/90-diskless.conf", "install -D -m0644 /tmp/etc-stage/dracut.conf.d/90-diskless.conf /etc/dracut.conf.d/90-diskless.conf",
"install -D -m0644 /tmp/etc-stage/NetworkManager/99-dot1x-unmanaged.conf /etc/NetworkManager/conf.d/99-dot1x-unmanaged.conf", # NB: the old static /etc/NetworkManager/conf.d/99-dot1x-unmanaged.conf is
# GONE. It can't be mode-aware (it wrongly unmanaged en* in test mode too,
# blocking renewal). The 95dot1x-macsec cmdline hook nm-unmanaged.sh now
# writes /run/NetworkManager/conf.d/10-dot1x-unmanaged.conf at boot, only in
# secure mode, reserving the physical NIC for wpa_supplicant.
"chmod 0755 /usr/lib/dracut/modules.d/95dot1x-macsec/*.sh", "chmod 0755 /usr/lib/dracut/modules.d/95dot1x-macsec/*.sh",
# Inject the fleet NIC drivers into the dracut config # Inject the fleet NIC drivers into the dracut config
@@ -253,8 +257,14 @@ build {
# present (a false negative that wrongly failed the build for many runs). # present (a false negative that wrongly failed the build for many runs).
"lsinitrd /boot/initramfs-generic.img > /tmp/lsinitrd.txt", "lsinitrd /boot/initramfs-generic.img > /tmp/lsinitrd.txt",
"grep -E 'macsec|wpa_supplicant|tpm2_nvread|nvme.tcp|erofs|veritysetup' /tmp/lsinitrd.txt", "grep -E 'macsec|wpa_supplicant|tpm2_nvread|nvme.tcp|erofs|veritysetup' /tmp/lsinitrd.txt",
# Assert the test-mode NVMe-oF connect unit made it in (systemd-driven # Assert NetworkManager is in the initramfs — the network-manager dracut
# initramfs → the connect is a systemd unit, not a dracut hook). # module is what renews the DHCP lease post-switch-root. A missing/renamed
# network module would silently regress to no-renewal, so fail loudly.
"grep -q 'NetworkManager' /tmp/lsinitrd.txt || { echo 'ERROR: NetworkManager missing from initramfs (network-manager dracut module not installed)' >&2; exit 1; }",
# Assert the secure-mode NM unmanaged hook + test-mode NVMe-oF connect unit
# made it in (systemd-driven initramfs → the connect is a systemd unit, not
# a dracut hook).
"grep -q 'nm-unmanaged.sh' /tmp/lsinitrd.txt || { echo 'ERROR: nm-unmanaged.sh hook missing from initramfs' >&2; exit 1; }",
"grep -q 'nvmf-connect.service' /tmp/lsinitrd.txt || { echo 'ERROR: nvmf-connect.service missing from initramfs' >&2; exit 1; }", "grep -q 'nvmf-connect.service' /tmp/lsinitrd.txt || { echo 'ERROR: nvmf-connect.service missing from initramfs' >&2; exit 1; }",
# ---- Build hygiene --------------------------------------------------- # ---- Build hygiene ---------------------------------------------------
+32 -19
View File
@@ -5,27 +5,40 @@ Persistent backlog, kept in sync with the working session task tracker. Status:
--- ---
## [ ] 1. Port the dracut module off `network-legacy` → `network-manager` ## [~] 1. Port the dracut module off `network-legacy` → `network-manager`
Durable replacement for the current `dhcp-client` stopgap. `dhclient` is **IMPLEMENTED 2026-06-17 — awaiting boot-bench verification.** Fixes the
deprecated in RHEL 9 and will eventually leave the repos; the build only works confirmed lease-expiry outage: network-legacy's one-shot DHCP lease was never
today because the provisioner installs `dhcp-client` so `network-legacy` passes renewed post-switch-root, so the address dropped at lease expiry → NVMe/TCP root
dracut's `check()`. died ("keep alive timeout" / "no route to host"). NM hands its initramfs
connections to the real-root NM (`/run/NetworkManager` state), which renews.
- Rewrite both network bring-up paths in Done:
[../files/dracut/95dot1x-macsec/dot1x-macsec-up.sh](../files/dracut/95dot1x-macsec/dot1x-macsec-up.sh): - `90-diskless.conf` `add_dracutmodules`: `network-legacy` → `network-manager`.
the secure path's `ifup macsec0` and the `rd.dot1x=0` bypass path's - `module-setup.sh` `depends()` → `network-manager`; added `nmcli`; installs the
`ifup $NIC` — use `nmcli`/NetworkManager-in-initramfs (or `ip=` cmdline + the new cmdline hook.
nm module) instead. - NEW `nm-unmanaged.sh` (cmdline hook): secure mode writes
- Change `depends()` `network-legacy` → `network-manager` in `/run/NetworkManager/conf.d/10-dot1x-unmanaged.conf`
[../files/dracut/95dot1x-macsec/module-setup.sh](../files/dracut/95dot1x-macsec/module-setup.sh). (`unmanaged-devices=interface-name:en*`) **before NM starts**; no-op in test
- Update `add_dracutmodules` in mode. Replaces the deleted static `99-dot1x-unmanaged.conf` (couldn't be
[../files/etc/dracut.conf.d/90-diskless.conf](../files/etc/dracut.conf.d/90-diskless.conf). mode-aware — it wrongly unmanaged `en*` in test mode, blocking renewal).
- Drop `dhcp-client` from the provisioner `dnf install` in - `dot1x-macsec-up.sh` secure path: `ifup macsec0` → `nmcli` (managed + DHCP,
[../diskless.pkr.hcl](../diskless.pkr.hcl). poll for IPv4). macsec0 MANAGED so NM renews; physical NIC UNMANAGED.
- Re-verify the `lsinitrd` assertion. - `diskless.pkr.hcl`: dropped `dhcp-client`, added explicit `NetworkManager`;
- **Gate:** full boot-chain smoke test (swtpm + hostapd + FreeRADIUS) before removed the static-config install; lsinitrd now asserts `NetworkManager` +
merge, per CLAUDE.md. `nm-unmanaged.sh` present.
- Static lint clean (`bash -n`, `shellcheck`, `packer validate`).
**BENCH-VERIFY before trusting (the gate):**
- Test path (`disable_dot1x=true`): boot, `nmcli device` shows boot NIC MANAGED;
run past the DHCP lease time (drop the lab dnsmasq lease low) — address
persists, lease renews (`journalctl -u NetworkManager`), NVMe + SSH stay up.
- Secure path (swtpm + hostapd + FreeRADIUS, `disable_dot1x=false`): `en*`
UNMANAGED, `macsec0` MANAGED + renewing; MKA survives switch-root; verity root
mapped; NVMe stays connected past lease time; no "Address already assigned".
- **Risk:** ordering of the unmanaged drop-in vs NM start in secure mode. If NM
transiently grabs `en*`, fall back to also baking a static `en*`-unmanaged file
for secure-only builds.
## [ ] 2. Apply RHEL9 STIG hardening to the built image ## [ ] 2. Apply RHEL9 STIG hardening to the built image
+10 -5
View File
@@ -61,11 +61,16 @@ CLAUDE.md invariant #9.
These don't contradict the PDF (it doesn't specify them) but are worth recording. These don't contradict the PDF (it doesn't specify them) but are worth recording.
- **Network backend.** The dracut hook uses `network-legacy`/`ifup`, which needs - **Network backend.** The dracut hook uses the `network-manager` dracut module
`dhclient`; RHEL 9 deprecated it and the minimal ISO omits it, so the (ported 2026-06-17 from `network-legacy`/`ifup`/`dhclient`). NM hands its
provisioner installs `dhcp-client` explicitly. dhclient is on its way out — the initramfs connections to the real-root NetworkManager, which **renews the DHCP
durable fix is porting the hook to the `network-manager` dracut module lease** in the running system — network-legacy's one-shot lease was never
(tracked; see project todo). renewed, so the address dropped at lease expiry and the NVMe/TCP root died.
Managed/unmanaged is mode-aware: secure path → `macsec0` managed (NM DHCPs +
renews it), physical `en*` unmanaged (wpa_supplicant owns EAPOL/MKA), enforced
by the `nm-unmanaged.sh` cmdline hook writing a `/run/NetworkManager/conf.d`
drop-in before NM starts; test path → boot NIC managed, hook is a no-op. No
static unmanaged file (it couldn't be mode-aware). Awaiting boot-bench verify.
- **Build ISO.** Defaults to a version-pinned Rocky 9.x **minimal** ISO on the - **Build ISO.** Defaults to a version-pinned Rocky 9.x **minimal** ISO on the
internal mirror (`iso_url` + `sha256` `iso_checksum`). `http/ks.cfg` installs internal mirror (`iso_url` + `sha256` `iso_checksum`). `http/ks.cfg` installs
only core+sshd+chrony from the ISO; the diskless tooling is pulled online by only core+sshd+chrony from the ISO; the diskless tooling is pulled online by
+20 -2
View File
@@ -107,8 +107,26 @@ info "dot1x-macsec: port authorized, macsec0 up after ${i}s"
# --- 4. address the secure interface; netroot rides on macsec0 -------------- # --- 4. address the secure interface; netroot rides on macsec0 --------------
ip link set macsec0 up ip link set macsec0 up
# Hand macsec0 to dracut's network stack for DHCP + netroot resolution # DHCP on macsec0 via NetworkManager (NOT the old network-legacy `ifup`): NM owns
ifup macsec0 # 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)"
# --- 5. derive per-host NVMe identity from the TPM certificate -------------- # --- 5. derive per-host NVMe identity from the TPM certificate --------------
# Keeps the image fleet-generic: the host NQN is a function of the machine # Keeps the image fleet-generic: the host NQN is a function of the machine
+14 -2
View File
@@ -11,7 +11,10 @@ check() {
} }
depends() { depends() {
echo "network-legacy bash" # network-manager (not network-legacy): NM renews the DHCP lease in the real
# root after switch-root (the legacy one-shot lease was never renewed). The
# secure path drives DHCP on macsec0 via nmcli instead of the old `ifup`.
echo "network-manager bash"
return 0 return 0
} }
@@ -29,7 +32,9 @@ install() {
# distro's dracut nvmf module populates discovery.conf but never registers # distro's dracut nvmf module populates discovery.conf but never registers
# an initqueue/online autoconnect hook). The nvmf module installs nvme too, # an initqueue/online autoconnect hook). The nvmf module installs nvme too,
# but list it here so the hook's dependency is explicit and load-order-safe. # but list it here so the hook's dependency is explicit and load-order-safe.
inst_multiple wpa_supplicant wpa_cli tpm2_nvread openssl ip setsid \ # nmcli: the secure path brings macsec0 up via NetworkManager (DHCP+renewal)
# instead of the old network-legacy `ifup`.
inst_multiple wpa_supplicant wpa_cli tpm2_nvread openssl ip setsid nmcli \
grep sed cut tr sha256sum sleep mkdir nvme grep sed cut tr sha256sum sleep mkdir nvme
# TSS libraries that tpm2_nvread dlopens # TSS libraries that tpm2_nvread dlopens
@@ -39,6 +44,13 @@ install() {
inst_simple /etc/dot1x/wpa_supplicant-dot1x.conf /etc/dot1x/wpa_supplicant-dot1x.conf inst_simple /etc/dot1x/wpa_supplicant-dot1x.conf /etc/dot1x/wpa_supplicant-dot1x.conf
inst_simple /etc/dot1x/radius-ca.pem /etc/dot1x/radius-ca.pem inst_simple /etc/dot1x/radius-ca.pem /etc/dot1x/radius-ca.pem
# cmdline-stage hook (runs BEFORE NetworkManager starts): in secure mode it
# drops /run/NetworkManager/conf.d/10-dot1x-unmanaged.conf so NM never grabs
# the physical EAPOL NIC (wpa_supplicant owns it). No-op in rd.dot1x=0 test
# mode, so NM manages+renews the boot NIC. Replaces the old static
# 99-dot1x-unmanaged.conf, which couldn't be mode-aware.
inst_hook cmdline 10 "$moddir/nm-unmanaged.sh"
# Hook: runs from the initqueue once the NIC exists, before netroot # Hook: runs from the initqueue once the NIC exists, before netroot
inst_hook initqueue/settled 30 "$moddir/dot1x-macsec-up.sh" inst_hook initqueue/settled 30 "$moddir/dot1x-macsec-up.sh"
@@ -0,0 +1,37 @@
#!/bin/bash
# nm-unmanaged.sh — dracut cmdline-stage hook (runs BEFORE NetworkManager starts).
#
# In the SECURE path (802.1X/MACsec), wpa_supplicant owns the physical NIC: it
# runs EAPOL and the MACsec key agreement (MKA) on it, and that link carries the
# root filesystem. NetworkManager must NOT touch that NIC — a competing NM
# supplicant would drop MKA and kill netroot. So drop an NM config that marks the
# physical NIC unmanaged. IP is carried by macsec0, which is left MANAGED so NM
# does DHCP *and renews the lease* on it.
#
# In the TEST path (rd.dot1x=0) there is no macsec0; IP rides on the physical NIC
# directly and we WANT NM to manage+renew it — so write nothing.
#
# Why /run (not /etc) and why the cmdline stage: /run/NetworkManager/conf.d is
# read by NM and the tmpfs survives switch-root, so the same rule applies in both
# the initramfs and the real root. Writing here at the cmdline stage guarantees
# the file exists before NM is started, so NM never transiently manages the EAPOL
# NIC. This replaces the old static /etc/.../99-dot1x-unmanaged.conf, which could
# not be mode-aware (it wrongly unmanaged en* in test mode too, blocking renewal).
type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
# Test mode: leave the boot NIC managed so NM renews its lease.
[ "$(getarg rd.dot1x=)" = "0" ] && return 0
_dir=/run/NetworkManager/conf.d
_conf="$_dir/10-dot1x-unmanaged.conf"
[ -e "$_conf" ] && return 0 # idempotent (cmdline hooks may re-run)
mkdir -p "$_dir"
# interface-name:en* matches the predictable physical NIC names (eno/ens/enp);
# macsec0 is deliberately NOT listed, so NM manages it for DHCP/renewal.
{
echo "[keyfile]"
echo "unmanaged-devices=interface-name:en*"
} > "$_conf"
info "dot1x-macsec: NM unmanaged drop-in written ($_conf) — physical NIC reserved for wpa_supplicant"
@@ -1,6 +0,0 @@
# The physical NIC and macsec0 are owned by the initramfs-launched
# @wpa_supplicant (root storage daemon). NetworkManager must not touch
# them, or it will spawn a competing supplicant and drop the MKA session
# carrying the root filesystem.
[keyfile]
unmanaged-devices=interface-name:macsec0;interface-name:en*
+6 -1
View File
@@ -9,7 +9,12 @@ hostonly="no"
# by systemd.volatile=overlay on the kernel cmdline (RHEL/Rocky dracut has no # by systemd.volatile=overlay on the kernel cmdline (RHEL/Rocky dracut has no
# 'overlayfs'/'rootovl' module — that's a Debian/third-party feature); we only # 'overlayfs'/'rootovl' module — that's a Debian/third-party feature); we only
# need the overlay kernel filesystem, included via 'filesystems' below. # need the overlay kernel filesystem, included via 'filesystems' below.
add_dracutmodules+=" dot1x-macsec network-legacy nvmf systemd-veritysetup " # network-manager (NOT network-legacy): NM in the initramfs hands its
# connections off to the real-root NetworkManager via /run/NetworkManager state,
# so the DHCP lease is RENEWED in the running system. network-legacy's one-shot
# lease was never renewed post-switch-root -> the address was dropped at lease
# expiry -> NVMe/TCP root died ("keep alive timeout" / "no route to host").
add_dracutmodules+=" dot1x-macsec network-manager nvmf systemd-veritysetup "
filesystems+=" erofs overlay " filesystems+=" erofs overlay "
add_drivers+=" macsec nvme-tcp nvme-fabrics dm-verity @NIC_DRIVERS@ " add_drivers+=" macsec nvme-tcp nvme-fabrics dm-verity @NIC_DRIVERS@ "
install_items+=" /etc/dot1x/radius-ca.pem /etc/dot1x/wpa_supplicant-dot1x.conf " install_items+=" /etc/dot1x/radius-ca.pem /etc/dot1x/wpa_supplicant-dot1x.conf "