Kernel Module Signing

Kernel module signing lets the kernel refuse to load any loadable module (.ko) whose cryptographic signature it cannot verify against a public key it already trusts. A loadable module runs with full kernel privilege — it can read and write any memory, hook any syscall, hide any process — so an unsigned-module load is, functionally, arbitrary code execution in ring 0. Module signing closes that hole: the build process appends a PKCS#7 detached signature plus a fixed magic marker (~Module signature appended~\n) to the end of each .ko, and at load time the kernel strips that trailer and verifies it against the .builtin_trusted_keys keyring (and, if configured, the .secondary_trusted_keys/.machine keyrings). Whether an unsigned or badly-signed module is rejected or merely tainted is governed by CONFIG_MODULE_SIG_FORCE and the module.sig_enforce boot parameter — and enforcement is forced on automatically whenever kernel lockdown is active. This note pins all code and config details to the 6.12 LTS kernel (released 2024-11-17).

This is the runtime continuation of the Secure Boot and the Kernel Trust Chain: Secure Boot decides which kernel runs; module signing decides which modules that kernel will load. The mechanism lives in kernel/module/signing.c and rests on the keyring infrastructure of Kernel Keyrings.

Mental Model

A signed module is just an ordinary .ko ELF file with three extra things glued onto the end (outside the ELF structure entirely): a PKCS#7 message (the signature), a small fixed-layout descriptor, and a human-readable marker string. At load time the kernel works backwards from the end of the file: it checks for the marker, peels off the descriptor to learn the signature length, hands the module body and the signature to the generic PKCS#7 verifier, and only proceeds if the verifier finds a trusted key that signed exactly those bytes. The trust decision is therefore inseparable from the kernel’s keyrings — the same keyrings that Secure Boot populates from firmware.

flowchart TB
  subgraph KO["Signed module file (.ko) — bytes in order"]
    BODY["ELF module body<br/>(code + data)"]
    SIG["PKCS#7 signature<br/>(detached, over the body)"]
    DESC["struct module_signature<br/>(algo, hash, id_type, sig_len)"]
    MARK["~Module signature appended~\\n<br/>(magic marker)"]
  end
  BODY --> SIG --> DESC --> MARK
  LOAD["finit_module() / init_module()"] -->|"module_sig_check()"| CHK{"marker present<br/>at end of file?"}
  CHK -->|"yes"| STRIP["strip marker + descriptor,<br/>read sig_len"]
  STRIP --> VER["verify_pkcs7_signature(...,<br/>VERIFY_USE_SECONDARY_KEYRING)"]
  VER -->|"trusted key found"| OK["load (sig_ok = true)"]
  VER -->|"no/ bad key"| ENF{"sig_enforce<br/>or lockdown?"}
  CHK -->|"no"| ENF
  ENF -->|"yes"| REJ["-EKEYREJECTED / -EPERM:<br/>refuse to load"]
  ENF -->|"no"| TAINT["load anyway,<br/>taint kernel"]

The on-disk layout of a signed module and the load-time verification path. What it shows: the signature material is appended after the ELF container (not embedded in it), so the kernel verifies by reading the trailer backwards, and the allow/reject decision splits on whether enforcement is active. The insight to take: because the signature is outside the ELF, any tool that rewrites the .ko — most notably strip — silently destroys the signature; and because verification ends in a keyring search, “is this module trusted?” reduces entirely to “is its signing key on a trusted keyring?”

What Gets Signed, and How (the Build Side)

Module signing is enabled by CONFIG_MODULE_SIG in “Enable Loadable Module Support.” With it on, the build can sign modules automatically during make modules_install if CONFIG_MODULE_SIG_ALL is set (module-signing.rst, v6.12: modules “will be automatically signed during the modules_install phase of a build”). The hash algorithm is chosen by a mutually-exclusive Kconfig group — CONFIG_MODULE_SIG_SHA256, …SHA384, …SHA512, plus the SHA-3 variants …SHA3_256/384/512.

The signing key is named by CONFIG_MODULE_SIG_KEY, documented as the “File name or PKCS#11 URI of module signing key,” defaulting to certs/signing_key.pem. If that default is left in place and the file does not exist, the kernel build generates a keypair itself — controlled by certs/x509.genkey and the key-type choice (MODULE_SIG_KEY_TYPE_RSA, an RSA-4096 key, or MODULE_SIG_KEY_TYPE_ECDSA, a NIST P-384 key). The certificate from that keypair is compiled directly into the kernel image and ends up on the .builtin_trusted_keys keyring at boot — which is exactly how a kernel can trust modules built alongside it without any external key management. The docs strongly recommend supplying your own x509.genkey rather than relying on the auto-generated defaults, and warn that “the private key must be either destroyed or moved to a secure location and not kept in the root node of the kernel source tree.”

The low-level signing is done by scripts/sign-file (built from scripts/sign-file.c). It takes four arguments — hash algorithm, private key, public-key X.509 cert, and the module:

scripts/sign-file sha512 kernel-signkey.priv \
        kernel-signkey.x509 module.ko

A passphrase or PKCS#11 PIN is passed via the $KBUILD_SIGN_PIN environment variable. Looking at what sign-file.c actually does (v6.12): it digests the module body, builds a detached PKCS#7 message over that digest (PKCS7_sign(..., PKCS7_NOCERTS | PKCS7_BINARY | PKCS7_DETACHED ...), or the CMS equivalent when built against newer OpenSSL), then appends to the destination file, in order: the module body, the PKCS#7 signature, a fixed struct module_signature descriptor, and the marker string. The relevant tail of main():

sig_size = BIO_number_written(bd) - module_size;
sig_info.sig_len = htonl(sig_size);
ERR(BIO_write(bd, &sig_info, sizeof(sig_info)) < 0, "%s", dest_name);
ERR(BIO_write(bd, magic_number, sizeof(magic_number) - 1) < 0, "%s", dest_name);

Line-by-line: sig_size is the byte count of the PKCS#7 blob just written; it is stored big-endian (htonl) into the sig_len field of the descriptor; the descriptor (sig_info) is written next; finally magic_number — the marker string, written without its trailing NUL (sizeof - 1) — is appended. The result on disk is exactly the four-part layout in the diagram.

The descriptor itself is struct module_signature, from include/linux/module_signature.h (v6.12):

struct module_signature {
    u8  algo;        /* Public-key crypto algorithm [0] */
    u8  hash;        /* Digest algorithm [0] */
    u8  id_type;     /* Key identifier type [PKEY_ID_PKCS7] */
    u8  signer_len;  /* Length of signer's name [0] */
    u8  key_id_len;  /* Length of key identifier [0] */
    u8  __pad[3];
    __be32  sig_len; /* Length of signature data */
};

For PKCS#7 signatures (id_type = PKEY_ID_PKCS7) the algo/hash/name fields are zero because all of that information is inside the PKCS#7 message; only sig_len matters to the kernel, telling it how many trailing bytes are signature. The marker is defined in the same header — and the comment is worth keeping because it explains the odd choice of string:

/* In stripped ARM and x86-64 modules, ~ is surprisingly rare. */
#define MODULE_SIG_STRING "~Module signature appended~\n"

Note the trailing \n: the marker is 28 bytes, and the load-time check compares against sizeof(MODULE_SIG_STRING) - 1 (the string minus its NUL), so the newline is part of what must match. A frequent mistake when reconstructing the layout by hand is omitting that newline.

Verification at Load Time (the Kernel Side)

Every module load path — insmod, modprobe, and the init_module(2) / finit_module(2) syscalls — funnels into the kernel’s loader, and “the signature checking is all done within the kernel” with no userspace involvement (module-signing.rst). The entry point is module_sig_check() in kernel/module/signing.c (v6.12). Here is its core:

int module_sig_check(struct load_info *info, int flags)
{
    int err = -ENODATA;
    const unsigned long markerlen = sizeof(MODULE_SIG_STRING) - 1;
    const char *reason;
    const void *mod = info->hdr;
    bool mangled_module = flags & (MODULE_INIT_IGNORE_MODVERSIONS |
                                   MODULE_INIT_IGNORE_VERMAGIC);
    if (!mangled_module &&
        info->len > markerlen &&
        memcmp(mod + info->len - markerlen, MODULE_SIG_STRING, markerlen) == 0) {
        /* We truncate the module to discard the signature */
        info->len -= markerlen;
        err = mod_verify_sig(mod, info);
        if (!err) {
            info->sig_ok = true;
            return 0;
        }
    }
    ...

Walking it: markerlen is the marker length (28). The mangled_module check matters — if the caller passed MODULE_INIT_IGNORE_MODVERSIONS or MODULE_INIT_IGNORE_VERMAGIC (the --force flags), the module has been altered and “a module with version information removed is no longer the module that was signed,” so the kernel refuses to even attempt verification on a mangled module. Otherwise it does a memcmp of the file’s last 28 bytes against the marker; on a match it truncates the marker, calls mod_verify_sig(), and on success sets info->sig_ok = true and returns. The MODULE_INIT_* flags themselves come from include/uapi/linux/module.h.

mod_verify_sig() then reads the trailing struct module_signature, validates it, peels off sig_len bytes as the signature, and calls the generic verifier:

return verify_pkcs7_signature(mod, modlen, mod + modlen, sig_len,
                              VERIFY_USE_SECONDARY_KEYRING,
                              VERIFYING_MODULE_SIGNATURE,
                              NULL, NULL);

The first two arguments are the module body and its length (the data the signature is over); the next two are the detached PKCS#7 signature and its length; VERIFY_USE_SECONDARY_KEYRING is the critical one — it tells verify_pkcs7_message_sig() (in certs/system_keyring.c) to validate trust against the .secondary_trusted_keys keyring. Because the secondary keyring is linked to .builtin_trusted_keys (and, if present, to .machine), the search transparently follows those links, so a module signed by the built-in key, by a key in the secondary keyring, or by an enrolled MOK on the .machine keyring all verify. The verifier also runs is_key_on_revocation_list() first, so a signature whose key is on the kernel’s blacklist (populated from UEFI dbx/MokListXRT) is rejected even if the key would otherwise be trusted.

Enforce or Taint? sig_enforce, MODULE_SIG_FORCE, and Lockdown

What happens when verification fails — or when the module simply has no signature — is the whole policy question, and it is answered by the tail of module_sig_check():

switch (err) {
case -ENODATA:  reason = "unsigned module"; break;
case -ENOPKG:   reason = "module with unsupported crypto"; break;
case -ENOKEY:   reason = "module with unavailable key"; break;
default:
    /* All other errors are fatal ... even if signatures aren't required. */
    return err;
}
if (is_module_sig_enforced()) {
    pr_notice("Loading of %s is rejected\n", reason);
    return -EKEYREJECTED;
}
return security_locked_down(LOCKDOWN_MODULE_SIGNATURE);

There are three non-fatal error classes — unsigned (-ENODATA), unsupported crypto (-ENOPKG), and unavailable key (-ENOKEY) — and everything else (a present-but-invalid signature, a parse failure, out of memory) is always fatal, returned immediately even when enforcement is off. That asymmetry is deliberate: a missing signature might be tolerable in a permissive configuration, but a forged or corrupt signature is always a hard error.

For the three soft errors, the decision is two-pronged:

  1. If is_module_sig_enforced() is true, the load is rejected with -EKEYREJECTED and you get Loading of unsigned module is rejected in the kernel log.
  2. Otherwise the kernel defers to security_locked_down(LOCKDOWN_MODULE_SIGNATURE) — so even with sig_enforce off, an active lockdown (which includes LOCKDOWN_MODULE_SIGNATURE in its integrity set) still blocks the load. If neither gate fires, the module loads and the kernel is tainted (flagged as having loaded an unverified module).

sig_enforce itself is a module parameter (kernel/module/signing.c):

static bool sig_enforce = IS_ENABLED(CONFIG_MODULE_SIG_FORCE);
module_param(sig_enforce, bool_enable_only, 0644);

So its initial value is CONFIG_MODULE_SIG_FORCE: build the kernel with that Kconfig and enforcement is on from the start. Independently, the module.sig_enforce=1 boot parameter flips it on at boot — and the documented subtlety is that “if CONFIG_MODULE_SIG_FORCE is set, that is always true, so this option does nothing” (kernel-parameters.txt, v6.12). The bool_enable_only type means the parameter can only be flipped on at runtime via sysfs, never back off — a one-way ratchet. And there is an exported one-way setter, set_module_sig_enforced(), which the lockdown machinery calls so that entering lockdown forces module-signature enforcement on regardless of how the kernel was configured. That is the concrete link: turning on lockdown (whether by lockdown=integrity or by a distro’s Secure-Boot patch) makes sig_enforce true, which is why “Secure Boot machine ⇒ unsigned modules rejected” holds in practice.

The Keyrings, Briefly

Module verification ends in a keyring search, so it is worth naming the keyrings (full detail in Kernel Keyrings and Secure Boot and the Kernel Trust Chain). .builtin_trusted_keys holds certificates compiled into the kernel — the module-signing key plus anything from CONFIG_SYSTEM_TRUSTED_KEYS; it is allocated and populated in certs/system_keyring.c. .secondary_trusted_keys (present when CONFIG_SECONDARY_TRUSTED_KEYRING is set) can have keys added at runtime, but only if the new key’s X.509 wrapper is itself signed by a key already on the builtin or secondary keyring (restrict_link_by_builtin_and_secondary_trusted) — a CA-chained trust extension, not an open door. The .machine keyring (with CONFIG_INTEGRITY_MACHINE_KEYRING) receives the owner’s enrolled MOK keys and is linked into the secondary keyring, which is the precise mechanism that lets a self-signed, MOK-enrolled out-of-tree driver load even under enforcement. The .platform keyring (firmware db keys) is deliberately excluded from this chain — it is for kexec/IMA, not module loading — so a giant vendor CA in db does not implicitly become trusted to sign your kernel modules.

Failure Modes

  • Loading of unsigned module is rejected / modprobe: ERROR: could not insert 'foo': Key was rejected by service. -EKEYREJECTED from an enforced kernel loading an unsigned/untrusted module. The classic case is an out-of-tree driver (NVIDIA, ZFS, VirtualBox, DKMS) on a Secure-Boot/lockdown system. Fix: sign it with a key the kernel trusts — typically an enrolled MOK (see Secure Boot and the Kernel Trust Chain).
  • strip-ping a signed module breaks it. Because the signature is appended outside the ELF container, any tool that rewrites or strips the .ko discards or invalidates the trailer. The docs are blunt: “Signed modules are BRITTLE as the signature is outside of the defined ELF container … they MAY NOT be stripped once the signature is computed and attached.” Symptom: a module that loaded yesterday now reports “unsigned module” after a packaging step ran strip.
  • --force-loading a signed module fails verification. Passing insmod --force (or finit_module with MODULE_INIT_IGNORE_VERMAGIC/IGNORE_MODVERSIONS) sets the mangled_module flag, and module_sig_check() refuses to even attempt verification — the altered module is no longer the signed one.
  • Key was rejected even with a valid signature. The signing key’s certificate is on the kernel revocation list (dbx/MokListXRT), so is_key_on_revocation_list() rejects it before the trust check.
  • Compression confusion. With CONFIG_MODULE_COMPRESS the on-disk .ko is compressed (.ko.xz, .ko.zst); the signature must be applied to the uncompressed module, and the kernel decompresses (via MODULE_INIT_COMPRESSED_FILE) before checking the marker. Signing the compressed blob, or compressing after signing in the wrong order, yields “unsigned module.”
  • Forgetting the trailing newline when hand-building the layout. The marker is ~Module signature appended~\n; omitting the \n makes the memcmp miss and the module reads as unsigned.

Alternatives and When to Choose Them

  • IMA appraisal of modules (finit_module + IMA) instead of appended signatures. With CONFIG_IMA_APPRAISE_MODSIG, a module can carry an IMA-style signature (or be vouched for by an IMA xattr/policy) and lockdown’s module-loading restriction is “waived if the module file being loaded is vouched for by IMA appraisal” (kernel_lockdown(7)). Choose this when you already run Integrity Measurement Architecture and want one integrity model for all files, not a module-specific trailer.
  • CONFIG_MODULE_SIG_FORCE vs module.sig_enforce boot param. Compile-time force gives a kernel that can never be talked out of enforcement; the boot param defers the choice to boot time (and is itself overridden — to “always on” — by lockdown). Pick compile-time force for appliance/immutable images; pick the boot param plus lockdown for general distros.
  • Don’t sign at all; rely on filesystem and Secure Boot only. If /lib/modules is on read-only, dm-verity-protected, Secure-Boot-verified storage, the modules’ integrity is already covered by the storage layer; module signing is then belt-and-suspenders. Most distros still enable it because the storage assumption rarely holds in practice.

Production Notes

The dominant real-world friction is the out-of-tree-driver problem on Secure-Boot fleets: a stock Fedora/RHEL/Ubuntu kernel boots into lockdown integrity mode, sig_enforce is therefore on, and an unsigned NVIDIA/ZFS/DKMS module is rejected with -EKEYREJECTED. The standard remediation pattern is to generate a per-machine signing key, enroll its certificate as a MOK with mokutil (a one-time, console-confirmed step), and wire DKMS to run sign-file automatically on every rebuild — after which the .machine keyring trusts those modules and they load under enforcement. Distribution kernels themselves are signed at build with the distro’s vendor key (whose certificate is built into .builtin_trusted_keys), so all in-tree modules verify out of the box. Two recurring packaging bugs to watch for: a build step that strips the .ko after signing (silently invalidating it), and applying the signature to a compressed module rather than the raw one. Both manifest as “unsigned module” at load despite a signature having been generated.

See Also