Kernel Lockdown Mode

Kernel lockdown is a small Linux Security Module (LSM) — its entire policy lives in security/lockdown/lockdown.c, about 170 lines — whose job is to restrict even a fully privileged process (root, CAP_SYS_ADMIN) from modifying or reading the running kernel through the many “back doors” the kernel historically exposes to privileged userspace. The motivation is the secure-boot trust chain: if firmware verified the bootloader, the bootloader verified the kernel, and the kernel verifies modules, then a root user who can kexec an arbitrary unsigned image, load an unsigned module, or mmap /dev/mem and patch kernel text has broken the chain — turning a verified kernel back into an unverified one. Lockdown closes those holes. It defines exactly two enforcement levels: integrity (block everything that lets userspace write the kernel) and confidentiality (also block everything that lets userspace read kernel memory). Escalation is one-waynoneintegrityconfidentiality, never back — and is exposed through the lockdown= boot parameter and the /sys/kernel/security/lockdown file (per the v6.12 source, security/lockdown/lockdown.c).

This note pins to the 6.12 LTS kernel (released 2024-11-17; the 6.18 LTS line, 2025-11-30, is identical in the relevant code per the kernel.org release list, checked 2026-06-19). Lockdown was merged as a full LSM in Linux 5.4 (2019); earlier out-of-tree iterations existed for years before that.

Uncertain

Verify: that upstream mainline 6.12 automatically enables lockdown when UEFI Secure Boot is on. Reason: the v6.12 source for security/lockdown/lockdown.c contains no Secure-Boot hook — it enables lockdown only via the lockdown= boot param, the CONFIG_LOCK_DOWN_KERNEL_FORCE_* build option, or the securityfs write. The patch that wired “Secure Boot ⇒ automatic lockdown” was rejected by Linus Torvalds and never merged, per discussion that the man-page text descended from a Fedora patch (Arch BBS thread 284085; the widely-cited man-page line about EFI Secure Boot auto-enabling lockdown originates from Fedora’s downstream patch, not mainline). To resolve: the auto-enable is a distribution patch (Fedora/RHEL, Debian/Ubuntu carry it); on a vanilla upstream kernel you must pass lockdown= yourself. Treated as distro-specific throughout this note. #uncertain


Mental Model

The right way to think about lockdown is as a second perimeter drawn around root itself. Classic Linux security assumes UID 0 is trustworthy — root is the policy. But on a Secure Boot machine, the threat model flips: the administrator may be trusted, but a process running as root after compromise (a remote-code-execution bug in a setuid daemon, a malicious container that escaped, an evil-maid with a root shell) must not be able to subvert the kernel that anti-malware, attestation, and disk encryption all depend on. Lockdown is the kernel saying: “I will refuse certain operations no matter who asks, because performing them would let the asker rewrite or read me, and that would invalidate the cryptographic guarantee that booted me.”

Crucially, lockdown is not about preventing what userspace can do to itself — it is about the handful of privileged interfaces that reach into the kernel’s own address space or execution. Loading a kernel module injects code into ring 0. kexec replaces the running kernel. Writing /dev/mem patches physical RAM, including kernel text. Reading /proc/kcore dumps kernel memory (and any secrets in it). Each of these is an operation that a hardened, attested system cannot allow, and each gets a guard.

flowchart TB
  subgraph LVL["Lockdown level (one-way ratchet)"]
    NONE["none<br/>(no restrictions)"] --> INT["integrity<br/>(block kernel WRITES)"]
    INT --> CONF["confidentiality<br/>(also block kernel READS)"]
  end
  ROOT["Privileged process<br/>(root / CAP_SYS_ADMIN)"] -->|"load unsigned module"| H1{"security_locked_down<br/>(LOCKDOWN_MODULE_SIGNATURE)?"}
  ROOT -->|"kexec_load unsigned"| H2{"...(LOCKDOWN_KEXEC)?"}
  ROOT -->|"open /dev/mem, write"| H3{"...(LOCKDOWN_DEV_MEM)?"}
  ROOT -->|"read /proc/kcore"| H4{"...(LOCKDOWN_KCORE)?"}
  H1 & H2 & H3 -->|"current level >= INTEGRITY"| DENY["-EPERM"]
  H4 -->|"current level >= CONFIDENTIALITY"| DENY
  H1 & H2 & H3 & H4 -->|"below threshold"| ALLOW["operation proceeds"]

The lockdown decision flow. What it shows: lockdown is a single LSM hook, security_locked_down(reason), scattered across hundreds of privileged call sites; each call passes a reason enumerator, and the hook returns -EPERM if the kernel’s current lockdown level is at or above the reason’s threshold. The insight: the level is a monotonically rising integer — once raised it cannot be lowered without a reboot (the ratchet) — and each protected operation is tagged with whether it threatens kernel integrity (writes) or confidentiality (reads), which is what places it in the integrity or confidentiality band.


Mechanical Walk-through

The single hook and the level variable

At its core, lockdown is astonishingly simple. The module keeps one static variable, kernel_locked_down, holding the current level, and registers exactly one LSM hook, locked_down, backed by lockdown_is_locked_down() (lockdown.c):

static enum lockdown_reason kernel_locked_down;
 
static int lockdown_is_locked_down(enum lockdown_reason what)
{
	if (WARN(what >= LOCKDOWN_CONFIDENTIALITY_MAX, "Invalid lockdown reason"))
		return -EPERM;
 
	if (kernel_locked_down >= what) {
		if (lockdown_reasons[what])
			pr_notice_ratelimited("Lockdown: %s: %s is restricted; ...\n",
				  current->comm, lockdown_reasons[what]);
		return -EPERM;
	}
	return 0;
}

The comparison kernel_locked_down >= what is the whole policy. Every protected call site invokes security_locked_down(REASON) (the LSM dispatcher that ends up calling this function), and the answer is just an integer comparison against the current level. If the operation’s reason sits at or below the active level, it is denied with -EPERM and a rate-limited kernel log line naming the offending process (current->comm) and the human-readable label.

Why the enum ordering is the design

The genius of lockdown is that the ordering of the lockdown_reason enum is the threshold logic. From include/linux/security.h, the enum lists every integrity-threatening reason first, then a sentinel LOCKDOWN_INTEGRITY_MAX, then every confidentiality-threatening reason, then a sentinel LOCKDOWN_CONFIDENTIALITY_MAX:

enum lockdown_reason {
	LOCKDOWN_NONE,
	LOCKDOWN_MODULE_SIGNATURE,        /* unsigned module loading */
	LOCKDOWN_DEV_MEM,                 /* /dev/mem,kmem,port */
	LOCKDOWN_EFI_TEST,
	LOCKDOWN_KEXEC,                   /* kexec of unsigned images */
	LOCKDOWN_HIBERNATION,
	LOCKDOWN_PCI_ACCESS,              /* direct PCI access */
	LOCKDOWN_IOPORT,                  /* raw io port access */
	LOCKDOWN_MSR,                     /* raw MSR access */
	LOCKDOWN_ACPI_TABLES,
	LOCKDOWN_DEVICE_TREE,
	LOCKDOWN_PCMCIA_CIS,
	LOCKDOWN_TIOCSSERIAL,
	LOCKDOWN_MODULE_PARAMETERS,
	LOCKDOWN_MMIOTRACE,
	LOCKDOWN_DEBUGFS,
	LOCKDOWN_XMON_WR,
	LOCKDOWN_BPF_WRITE_USER,          /* use of bpf to write user RAM */
	LOCKDOWN_DBG_WRITE_KERNEL,
	LOCKDOWN_RTAS_ERROR_INJECTION,
	LOCKDOWN_INTEGRITY_MAX,           /* <-- divider: label "integrity" */
	LOCKDOWN_KCORE,                   /* /proc/kcore access */
	LOCKDOWN_KPROBES,
	LOCKDOWN_BPF_READ_KERNEL,         /* use of bpf to read kernel RAM */
	LOCKDOWN_DBG_READ_KERNEL,
	LOCKDOWN_PERF,
	LOCKDOWN_TRACEFS,
	LOCKDOWN_XMON_RW,
	LOCKDOWN_XFRM_SECRET,
	LOCKDOWN_CONFIDENTIALITY_MAX,     /* <-- divider: label "confidentiality" */
};

The accompanying comment in the header states the rule explicitly: “Lockdown reasons that protect kernel integrity (ie, the ability for userland to modify kernel code) are placed before LOCKDOWN_INTEGRITY_MAX. Lockdown reasons that protect kernel confidentiality (ie, the ability for userland to extract information from the running kernel that would otherwise be restricted) are placed before LOCKDOWN_CONFIDENTIALITY_MAX.” (security.h).

When the level is set to integrity, kernel_locked_down becomes LOCKDOWN_INTEGRITY_MAX. Now any reason with a numeric value <= LOCKDOWN_INTEGRITY_MAX — i.e., every write-the-kernel operation, plus the sentinel itself — fails the >= test and is denied; but a confidentiality reason like LOCKDOWN_KCORE, which is numerically larger, still passes. Setting the level to confidentiality (LOCKDOWN_CONFIDENTIALITY_MAX) raises the bar so even the read operations are denied. The enum is sorted so that a single integer comparison implements the two-band policy — no tables, no per-reason flags.

The level setter and the one-way ratchet

The only way the level rises is through lock_kernel_down():

static int lock_kernel_down(const char *where, enum lockdown_reason level)
{
	if (kernel_locked_down >= level)
		return -EPERM;          /* refuse to lower or no-op */
	kernel_locked_down = level;
	pr_notice("Kernel is locked down from %s; ...\n", where);
	return 0;
}

The guard if (kernel_locked_down >= level) return -EPERM; is what makes escalation strictly one-way: you can never set a level lower than or equal to the current one. Once the kernel is in confidentiality, nothing — not root, not a securityfs write, not anything short of a reboot — can return it to integrity or none. This is deliberate: a relaxation path would be an obvious bypass.

How the level gets raised in practice

There are three entry points, all visible in lockdown.c:

  1. Boot parameter (early_param("lockdown", lockdown_param)): lockdown=integrity or lockdown=confidentiality on the kernel command line calls lock_kernel_down("command line", ...) very early. The kernel-parameters doc describes it tersely: lockdown= [SECURITY,EARLY] { integrity | confidentiality } … If set to integrity, kernel features that allow userland to modify the running kernel are disabled. If set to confidentiality, kernel features that allow userland to extract confidential information from the kernel are also disabled.”

  2. Build-time default via the CONFIG_LOCK_DOWN_KERNEL_FORCE_{INTEGRITY,CONFIDENTIALITY} Kconfig choice. lockdown_lsm_init() reads these and calls lock_kernel_down("Kernel configuration", ...) at LSM init.

  3. securityfs write to /sys/kernel/security/lockdown (covered next).

The securityfs interface

lockdown_secfs_init() creates the file /sys/kernel/security/lockdown (mode 0644) via securityfs_create_file(). Reading it lists the available levels with the active one bracketed; writing a level name escalates. The read handler walks a tiny three-element array, lockdown_levels[] = {LOCKDOWN_NONE, LOCKDOWN_INTEGRITY_MAX, LOCKDOWN_CONFIDENTIALITY_MAX}, and prints each level’s label — bracketing the current one:

if (kernel_locked_down == level)
	offset += sprintf(temp+offset, "[%s] ", label);
else
	offset += sprintf(temp+offset, "%s ", label);

So cat /sys/kernel/security/lockdown shows something like [none] integrity confidentiality on an unlocked kernel, or none integrity [confidentiality] once fully locked. The labels come from a string table, lockdown_reasons[], in security/security.c, which maps each enum to text — [LOCKDOWN_INTEGRITY_MAX] = "integrity", [LOCKDOWN_CONFIDENTIALITY_MAX] = "confidentiality", and [LOCKDOWN_NONE] = "none". The write handler matches the supplied string against those labels and calls lock_kernel_down("securityfs", level) — which, thanks to the ratchet, can only ever raise the level.

Early-LSM ordering

Lockdown must be live before the kernel parses other boot parameters that could themselves be lockdown-relevant. The CONFIG_SECURITY_LOCKDOWN_LSM_EARLY Kconfig, when set, registers lockdown with DEFINE_EARLY_LSM(lockdown) instead of DEFINE_LSM, and the help text explains: “Enable the lockdown LSM early in boot … to ensure that lockdown enforcement can be carried out on kernel boot parameters that are otherwise parsed before the security subsystem is fully initialised. If enabled, lockdown will unconditionally be called before any other LSMs” (Kconfig). Lockdown is a minor, stackable LSM (it carries no per-object security blob and only registers the one hook), so it runs alongside SELinux/AppArmor rather than competing with them — see Major vs Minor LSMs and LSM Stacking and Module Ordering.


What Each Level Blocks

This is the practical heart of the topic. The label lockdown_reasons[] strings (verbatim from security.c) are the canonical enumeration of what is blocked.

integrity — block userspace from modifying the kernel

Everything below LOCKDOWN_INTEGRITY_MAX:

  • unsigned module loading (LOCKDOWN_MODULE_SIGNATURE) — init_module/finit_module reject modules without a valid signature, because a module is arbitrary ring-0 code. (Note CONFIG_SECURITY_LOCKDOWN_LSM selects MODULE_SIG.) See Kernel Module Signing.
  • kexec of unsigned images (LOCKDOWN_KEXEC) — the legacy kexec_load(2) is blocked entirely under lockdown (it provides no signature); only kexec_file_load(2) with a signed image is allowed. The actual call site in kernel/kexec.c is result = security_locked_down(LOCKDOWN_KEXEC); if (result) return result;.
  • hibernation (LOCKDOWN_HIBERNATION) — suspend-to-disk writes (and on resume, executes) a kernel image to swap; without signed/encrypted hibernation an attacker could tamper with the image, so it is blocked.
  • /dev/mem,kmem,port (LOCKDOWN_DEV_MEM) — direct access to physical memory and I/O ports via /dev/mem, /dev/kmem, /dev/port would let userspace overwrite kernel text/data.
  • raw MSR access (LOCKDOWN_MSR) — writes to model-specific registers (via /dev/cpu/*/msr) can redirect interrupt handlers or SYSCALL entry points into attacker code.
  • direct PCI access / raw io port access (LOCKDOWN_PCI_ACCESS, LOCKDOWN_IOPORT) — writing PCI config space / BARs or using ioperm/iopl lets a device be reprogrammed to DMA over kernel memory.
  • use of bpf to write user RAM (LOCKDOWN_BPF_WRITE_USER) — restricts BPF helpers that can write memory (e.g. bpf_probe_write_user), which could be coaxed into corrupting privileged state.
  • Plus: modifying ACPI tables, modifying device tree contents, unsafe module parameters, unsafe mmio (mmiotrace), debugfs access (debugfs exposes many unfiltered write knobs), reconfiguration of serial port IO (TIOCSSERIAL), /dev/efi_test access, direct PCMCIA CIS storage, xmon write access and use of kgdb/kdb to write kernel RAM (PowerPC/debugger paths), and RTAS error injection.

confidentiality — additionally block userspace from reading the kernel

Everything between LOCKDOWN_INTEGRITY_MAX and LOCKDOWN_CONFIDENTIALITY_MAX:

  • /proc/kcore access (LOCKDOWN_KCORE) — /proc/kcore is an ELF view of all kernel memory; reading it dumps secrets (keys, KASLR offsets, other processes’ data).
  • use of bpf to read kernel RAM (LOCKDOWN_BPF_READ_KERNEL) — restricts BPF helpers like bpf_probe_read_kernel that read arbitrary kernel addresses, which is exactly how a confidentiality breach via eBPF would occur (cross-link Major vs Minor LSMs for how BPF-LSM is itself an LSM).
  • use of kprobes (LOCKDOWN_KPROBES) and unsafe use of perf (LOCKDOWN_PERF) — kprobes and certain perf modes can read register/memory state across the kernel.
  • use of tracefs (LOCKDOWN_TRACEFS) — the tracing filesystem can expose kernel memory and addresses.
  • use of kgdb/kdb to read kernel RAM (LOCKDOWN_DBG_READ_KERNEL), xmon read and write access (LOCKDOWN_XMON_RW), and xfrm SA secret (LOCKDOWN_XFRM_SECRET, IPsec key dumping).

A reader confused about why confidentiality blocks more than integrity should note the asymmetry: a kernel-write primitive is also implicitly a read primitive (you can read before you overwrite), so confidentiality is a strict superset. There is no level that blocks reads but not writes.


Configuration and Verification

# 1. Check current level (read the securityfs file).
cat /sys/kernel/security/lockdown
# [none] integrity confidentiality     <- unlocked
# none integrity [confidentiality]     <- fully locked
 
# 2. Escalate at runtime (one-way!). Requires the securityfs to be writable
#    and the writer to be root; cannot be undone without reboot.
echo integrity | sudo tee /sys/kernel/security/lockdown
echo confidentiality | sudo tee /sys/kernel/security/lockdown
# A subsequent "echo integrity" will FAIL with EPERM (cannot lower).
 
# 3. Set at boot via the kernel command line (the supported way to start locked).
#    /etc/default/grub:  GRUB_CMDLINE_LINUX="... lockdown=confidentiality"
#    then: sudo grub-mkconfig -o /boot/grub/grub.cfg   (Debian: update-grub)
 
# 4. Observe a denial in the kernel log.
sudo modprobe some_unsigned_module
dmesg | tail
# Lockdown: modprobe: unsigned module loading is restricted; see man kernel_lockdown.7

Line-by-line: step 1 reads the file — bracketing marks the active level. Step 2 writes a level name; the kernel matches it against the lockdown_reasons[] label and ratchets up. The failure on a lower level is the lock_kernel_down() guard returning -EPERM. Step 3 is how production systems start in lockdown — the boot param runs before userspace, so there is no window where the kernel is unlocked. Step 4 shows the diagnostic: the log line is the pr_notice_ratelimited() from lockdown_is_locked_down(), naming the process (modprobe) and the human label (unsigned module loading).

The Kconfig surface:

CONFIG_SECURITY_LOCKDOWN_LSM=y          # build the LSM at all
CONFIG_SECURITY_LOCKDOWN_LSM_EARLY=y    # register before other LSMs / early params
CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE=y    # default level: none (override w/ INTEGRITY or CONFIDENTIALITY)

CONFIG_SECURITY_LOCKDOWN_LSM must also appear in the CONFIG_LSM= ordered list (or be added via the lsm= boot parameter) for the hooks to register — see LSM Stacking and Module Ordering and the lsm= param: “Choose order of LSM initialization. This overrides CONFIG_LSM, and the ‘security=’ parameter.” (kernel-parameters).


Failure Modes and Common Misunderstandings

“Lockdown stops root from doing anything.” No. Lockdown blocks only the ~30 enumerated operations that reach into the kernel’s own integrity/confidentiality. root can still kill processes, read user files, change network config — lockdown is surgical, not a general privilege drop.

“I enabled Secure Boot, so my upstream kernel is locked down.” Not on a vanilla mainline kernel — see the uncertainty callout at the top. The v6.12 lockdown source has no Secure-Boot trigger; that wiring is a distribution patch (Fedora/RHEL, Debian/Ubuntu). On those distros, Secure Boot → automatic integrity (or confidentiality) lockdown is real and expected; on a hand-rolled kernel you must pass lockdown= yourself.

“I’ll just echo none > /sys/kernel/security/lockdown to debug.” Impossible. The ratchet (if (kernel_locked_down >= level) return -EPERM;) forbids lowering. The only way out is a reboot with a different (or no) lockdown= param — which is exactly the property an attacker cannot exploit without already controlling the boot path. This is also why developers find lockdown surprising: tools that worked as root (loading an unsigned out-of-tree module, attaching kprobes, reading /proc/kcore with crash/drgn) suddenly return -EPERM, and the only fix is a kernel built without forced lockdown or booted without the param.

Lockdown bypass bugs. Because lockdown’s protection is a manually placed security_locked_down() call at each dangerous site, a missing call is a bypass. A historical example: CVE-class fixes have patched paths where a lockdown check was absent or could be sidestepped (e.g. kexec/IMA interactions in the 5.10 stable series, where a kexec lockdown check could be bypassed via IMA policy — LKML stable backport). The lesson: lockdown is only as complete as its call-site coverage, which is why new kernel-write/read interfaces must remember to add a security_locked_down() guard.


Alternatives and How It Relates to Other Mechanisms

Lockdown is one band in the integrity stack, not a standalone product. The chain is: Secure Boot and the Kernel Trust Chain (firmware verifies bootloader and kernel) → Kernel Module Signing (kernel verifies modules) → lockdown (kernel refuses to be modified or read by privileged userspace) → Integrity Measurement Architecture / Extended Verification Module (runtime file-integrity appraisal). Lockdown is the piece that protects the running kernel after boot; module signing protects what gets added to it; Secure Boot protects what booted it.

Compared with the MAC modules (SELinux, AppArmor): those confine processes against a rich, configurable policy of who-may-touch-what. Lockdown is the opposite — a tiny, non-configurable policy with exactly two levels, aimed not at process-to-resource access but at the kernel-modification surface. You generally run lockdown and SELinux together (both are LSMs and stack).

Compared with module.sig_enforce: that boot param (module.sig_enforce / CONFIG_MODULE_SIG_FORCE) forces module-signature enforcement specifically. Lockdown’s integrity level includes unsigned-module blocking (and a lot more), so on a locked-down kernel you get sig-enforcement for free, but sig_enforce alone does not give you kexec//dev/mem/MSR protection.


Production Notes

On Fedora and RHEL, a Secure-Boot machine boots with lockdown automatically engaged (their downstream patch); administrators who need to load out-of-tree modules (e.g. NVIDIA, ZFS, DKMS drivers) must either enroll their own Machine Owner Key (MOK) and sign the module, or disable Secure Boot — a frequent real-world friction point. Ubuntu behaves similarly. This is why “my proprietary GPU driver won’t load on a Secure Boot laptop” is one of the most common lockdown-adjacent support tickets.

Security tooling that introspects the kernel — crash, drgn, bpftrace reading kernel memory, perf in certain modes — is degraded or blocked under confidentiality, so observability stacks on locked-down production hosts must account for it (typically by staying at integrity, which leaves the read paths open). Confidentiality lockdown is most appropriate where the threat is exfiltration of kernel secrets (multi-tenant hosts, machines holding disk-encryption keys), whereas integrity lockdown suffices where the threat is kernel tampering and you still want to debug.

A final point worth internalizing: lockdown’s value is entirely conditional on the trust chain below it. A locked-down kernel booted by an unverified bootloader gives an attacker who controls boot a trivial bypass — boot a kernel with lockdown=none. That is why lockdown is paired with Secure Boot in practice and why the (distro) auto-enable ties the two together: lockdown without Secure Boot protects against post-boot privilege abuse but not against an attacker who can choose the boot parameters.


See Also