diff --git a/salt/common/init.sls b/salt/common/init.sls index 120e73e3e..9618d2c67 100644 --- a/salt/common/init.sls +++ b/salt/common/init.sls @@ -130,6 +130,17 @@ common_sbin: - so-pcap-import {% endif %} +# Pin physical NIC names by MAC (run-once) so a kernel upgrade can't renumber the +# interfaces SO binds by name. The marker keeps it a one-time setup; an admin can +# pre-create the marker to opt out. +pin_nic_names: + cmd.run: + - name: /usr/sbin/so-nic-pin + - unless: 'test -e /opt/so/state/nic_names_pinned' + - require: + - file: common_sbin + - file: statedir + common_sbin_jinja: file.recurse: - name: /usr/sbin diff --git a/salt/common/tools/sbin/so-nic-pin b/salt/common/tools/sbin/so-nic-pin new file mode 100644 index 000000000..9e2b4e13a --- /dev/null +++ b/salt/common/tools/sbin/so-nic-pin @@ -0,0 +1,76 @@ +#!/bin/bash +# +# so-nic-pin — pin physical NIC names by permanent MAC via classic by-MAC udev +# rules, so a kernel upgrade can't renumber them. +# +# Security Onion binds its management and monitor interfaces BY NAME in pillar +# (host:mainint, sensor:mainint, and bond0 is built on a specific physical NIC). +# A kernel upgrade can change the kernel/systemd-udevd predictable-naming output +# and renumber those NICs (e.g. enp1s0 -> enp2s0), which breaks the grid: the +# pillar references a name that no longer exists and bond/bridge bring-up fails. +# +# This writes /etc/udev/rules.d/70-persistent-net.rules pinning each PHYSICAL NIC +# to its CURRENT name by its PERMANENT MAC, freezing the names across future kernel +# changes. It only writes the rules file; it does NOT live-trigger a rename (the +# rules apply on the next boot/kernel, and a live rename would be disruptive). +# +# Run-once: gated by the drop file /opt/so/state/nic_names_pinned. If the marker is +# present the script does nothing, so an admin can pre-create it to opt out. Invoked +# from the common state on every highstate; the marker keeps it a one-time setup. + +NET_RULES_FILE="/etc/udev/rules.d/70-persistent-net.rules" +MARKER="/opt/so/state/nic_names_pinned" + +log() { echo -e "[so-nic-pin] $*"; } + +# Echo " " for every PHYSICAL NIC. A physical NIC is backed by a +# real device (has device/driver), which excludes bond0/sobridge/docker0/veth*/lo whose +# MACs are dynamic and must never be pinned. The PERMANENT MAC is used (ethtool -P, with +# fallbacks), not the current one: an enslaved bond member's current MAC is rewritten to +# the bond's, so matching on it would be wrong/ambiguous. +physical_nics() { + local path n mac + for path in /sys/class/net/*; do + n="${path##*/}" + [ "$n" = "lo" ] && continue + [ -e "${path}/device/driver" ] || continue # real device only + mac="$(ethtool -P "$n" 2>/dev/null | awk '/Permanent address/{print $NF}')" + case "$mac" in ""|00:00:00:00:00:00) mac="$(cat "${path}/bonding_slave/perm_hwaddr" 2>/dev/null)" ;; esac + case "$mac" in ""|00:00:00:00:00:00) mac="$(cat "${path}/address" 2>/dev/null)" ;; esac + case "$mac" in ""|00:00:00:00:00:00) continue ;; esac + echo "$n $mac" + done +} + +# Turn " " lines on stdin into classic by-MAC persistent-net udev rules. +render_net_rules() { + echo "# Generated by so-nic-pin: pin NIC names by MAC so kernel upgrades can't renumber them." + echo "# Security Onion binds its management/monitor interfaces by name; do not hand-edit." + local n mac + while read -r n mac; do + [ -n "$n" ] || continue + printf 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="%s", NAME="%s"\n' \ + "$mac" "$n" + done +} + +[ "$(id -u)" -eq 0 ] || exit 0 # salt runs us as root; bail quietly otherwise +[ -e "${MARKER}" ] && exit 0 # run-once guard (mirrors the state's unless) + +nics="$(physical_nics)" +if [ -z "${nics}" ]; then + log "no physical NICs detected — nothing to pin (will retry on next highstate)" + exit 0 # do NOT drop the marker; let it retry later +fi + +log "pinning physical NICs by permanent MAC:" +echo "${nics}" | sed 's/^/ /' + +[ -f "${NET_RULES_FILE}" ] && cp -f "${NET_RULES_FILE}" "${NET_RULES_FILE}.bak" +echo "${nics}" | render_net_rules > "${NET_RULES_FILE}" || { + log "ERROR: failed to write ${NET_RULES_FILE}" + exit 1 +} + +mkdir -p "$(dirname "${MARKER}")" && touch "${MARKER}" +log "wrote ${NET_RULES_FILE} ($(grep -c '^SUBSYSTEM' "${NET_RULES_FILE}") NIC(s) pinned); dropped ${MARKER}"