crates/nornir-build-thing/src/kconfig/k8s-hardened.config
# nordisk K8S-HARDENED overlay — merged ON TOP of `hardened.config` for the
# `k8s-hardened` profile of the `[[build.kernel]]` kind. It is `hardened` with
# exactly ONE decision reversed, and it is a separate file rather than an edited
# copy so that the reversal is the whole diff: every other line of the KSPP-style
# overlay — its vetoes, its `MODULES=n`, its usermode-helper door — still comes
# from `hardened.config`, and a change there reaches this profile unasked.
#
# The decision reversed is `# CONFIG_BPF_SYSCALL is not set`. `hardened.config`
# priced it as "nothing in this constellation loads a BPF program", which was
# true until a Kubernetes node was declared (tunnr `rusternetes-node`, 2026-10-29)
# and the requirement closure refused it BY NAME:
#
# CONFIG_BPF_SYSCALL must be `y` for tunnr/rusternetes-node:k8s (on cgroup v2
# the device controller IS a BPF program (BPF_PROG_TYPE_CGROUP_DEVICE) …)
#
# So the price is not avoidable on a node: cgroup v2 has no other device
# controller, and an OCI runtime that cannot attach one cannot enforce a pod's
# device allow-list. Cilium (`cni-cilium`) needs bpf(2) for its whole datapath
# on top. The `hardened` profile itself does NOT change — an appliance that runs
# no pods keeps the veto, and this profile is chosen by the row that pays.
#
# Later wins in `merge_kconfig`, so the `=y` below REPLACES hardened's veto line
# in place; the canary then checks the `=y` survived `olddefconfig` like every
# other line. None of the four is EXPERT-gated on 6.12 (kernel/bpf/Kconfig), and
# `hardened.config` already carries EXPERT=y for the vetoes that are.
#
# MEASURED 2026-10-29 on linux-6.12.104, kconfig stage only (`make <base>` ->
# `merge_config.sh -m` -> `olddefconfig` -> the build script's canary loop; no
# compile, no boot), in the nordisk kernel toolchain image:
#
# | merge | base | canary refusals |
# |----------------------------------------------------|------------|-----------------|
# | base + hardened + this file (`normal` preset) | defconfig | 0 |
# | tunnr-kernel-k8s, rusternetes-node default plan | tinyconfig | 0 from this file |
# | tunnr-kernel-k8s, node plan with cni-cilium + csi | tinyconfig | 0 from this file |
#
# All four lines (and the def_bool BPF_JIT_DEFAULT_ON) are `=y` in each built
# `.config`. The node's own rows refused at first (NF_TABLES_INET needs IPV6,
# BPF_EVENTS needs KPROBE/UPROBE_EVENTS, FIB_RULES is a hidden bool); tunnr
# 1a637a9 respelled them, and both node plans now pass the canary with 0
# refusals (built and booted 2026-10-29, tunnr 86620d2).
# ── bpf(2): taken, and boxed in as far as the kernel allows ────────────────
# The syscall. `select`s BPF, IRQ_WORK, TASKS_TRACE_RCU, BINARY_PRINTF and, with
# NET, NET_SOCK_MSG / NET_XGRESS / PAGE_POOL (kernel/bpf/Kconfig, 6.12) — none
# of them selects DEBUG_FS or anything else `hardened.config` vetoes.
# Cost: the attack surface `hardened.config` removed is back — a verifier and a
# JIT that compile attacker-influenced bytecode in ring 0, a recurring CVE
# source. The three lines after this one are what keeps that surface to
# privileged loaders and to JIT-ed code only.
CONFIG_BPF_SYSCALL=y
# The JIT. `depends on BPF && (HAVE_CBPF_JIT || HAVE_EBPF_JIT)`; x86_64 selects
# HAVE_EBPF_JIT. Cilium's base requirements name it, and BPF_JIT_ALWAYS_ON
# below cannot exist without it.
# Cost: native code generated at runtime in kernel text — the JIT-spray class.
# Mitigated, not removed: `net.core.bpf_jit_harden=2` blinds constants for
# every loader and is a sysctl, so a node that wants it sets it at boot.
CONFIG_BPF_JIT=y
# No interpreter: every program is JIT-ed, and the in-kernel BPF interpreter —
# the gadget Spectre-v2 BPF attacks speculatively execute — is not compiled.
# `depends on BPF_SYSCALL && HAVE_EBPF_JIT && BPF_JIT`; it also forces
# BPF_JIT_DEFAULT_ON (def_bool), so `bpf_jit_enable` is pinned to 1.
# Cost: a program the JIT cannot compile is refused at load instead of
# interpreted; on x86_64 the JIT covers the whole instruction set, so nothing
# a node loads is turned away for it.
CONFIG_BPF_JIT_ALWAYS_ON=y
# Unprivileged bpf(2) is off at boot: `kernel.unprivileged_bpf_disabled=2`, so
# only CAP_BPF/CAP_SYS_ADMIN loaders (the runtime, the kubelet, the Cilium
# agent) can load a program. The knob stays writable: root may set 0 (re-enable
# unprivileged loads) or 1 (off until reboot, no transition back) — a node that
# wants the latter sets it at boot.
# `default y` upstream and `depends on BPF_SYSCALL`; named so the claim does not
# rest on a default.
# Cost: no unprivileged socket filters through bpf(2) — classic `SO_ATTACH_FILTER`
# and seccomp filters go through `setsockopt`/`prctl` and are unaffected.
CONFIG_BPF_UNPRIV_DEFAULT_OFF=y