Keyring Types and Key Management

Where Kernel Keyrings explains what the kernel key retention service is and the per-process keyrings it anchors, this note covers the key types the service understands, the system keyrings that anchor the kernel’s own trust chain, the possession-and-permissions model that decides who can do what to a key, expiry/revocation, and the /sbin/request-key upcall by which the kernel asks userspace to construct a missing key. A key’s type (user, logon, keyring, big_key, asymmetric, encrypted, trusted) decides how its payload is parsed, stored, read back, and protected (core.rst, v6.12; trusted-encrypted.rst, v6.12). A small family of kernel-owned keyrings whose names begin with a dot — .builtin_trusted_keys, .secondary_trusted_keys, .machine, .platform, .blacklist — hold the X.509 certificates against which kernel module and kexec signatures are verified (system_keyring.c, v6.12).

Read Kernel Keyrings first for the struct key, serial numbers, the special KEY_SPEC_* per-process keyrings, and the three syscalls; this note builds on all of it.

The Key Types

A struct key_type registered by a kernel service defines the methods that parse the payload (preparse/instantiate), update it, match it in a search, read it back, and tear it down (core.rst). The handful of types that matter in practice:

  • keyring — the recursive type whose payload is a list of links to other keys. Created with a NULL payload; modified with KEYCTL_LINK/UNLINK/MOVE/CLEAR. The special per-process keyrings and every system keyring below are of this type.
  • user — “a description and a payload that are arbitrary blobs of data… can be created, updated and read by userspace, and aren’t intended for use by kernel services” (core.rst). The general-purpose userspace secret. The docs recommend prefixing the description with a subtype ID and colon, e.g. krb5tgt:.
  • logon — like user, but the payload is readable only from kernel space, never from userspace (core.rst). The description must be prefixed with a non-empty “subclass” string and a : (e.g. ecryptfs: or fscrypt:). This is the type for “userspace loads it once, kernel uses it, nobody can read it back” — exactly what disk/file encryption wants.
  • big_key — for large payloads such as Kerberos ticket caches (CONFIG_BIG_KEYS). Because the payload can be big, it is encrypted with ChaCha20-Poly1305 and may be paged out to a tmpfs-backed swap file rather than held in pinned kernel memory (Kconfig, v6.12: depends on TMPFS and CRYPTO_LIB_CHACHA20POLY1305).
  • asymmetric — wraps a public/asymmetric key (typically an X.509 certificate). It supports KEYCTL_PKEY_{QUERY,ENCRYPT,DECRYPT,SIGN,VERIFY} and, critically, signature-verification-based keyring restrictions. This is the type used to populate the system trusted keyrings, since a certificate can be checked against an existing trusted key before being admitted.
  • encrypted — a variable-length symmetric key whose plaintext lives only in the kernel; userspace sees only an encrypted, integrity-protected blob (trusted-encrypted.rst). It is sealed under a master key, which may be either a trusted or a user key. If the master is only a user key, the encrypted key is “only as secure as the user key encrypting them.”
  • trusted — a variable-length symmetric key created inside a hardware/firmware trust source and sealed so userspace only ever sees an encrypted blob (trusted-encrypted.rst, CONFIG_TRUSTED_KEYS). Supported trust sources in v6.12: TPM (Trusted Platform Module — sealed under a storage key, optionally bound to PCR integrity-measurement values), TEE (Trusted Execution Environment, OP-TEE on Arm TrustZone, rooted to a Hardware Unique Key), CAAM (NXP SoC crypto IP), and DCP (i.MX Data Co-Processor). The TPM case is the canonical one: a key can be sealed to specified PCR values and only unsealed if those PCRs (and the blob integrity) match — binding the secret to a measured boot state, which is how EVM gets a high-quality HMAC key that resists offline attack.

Trusted vs Encrypted vs User — the security gradient

flowchart LR
  USER["user / logon key<br/>plaintext in kernel RAM"]
  ENC["encrypted key<br/>sealed by a master key"]
  TRUST["trusted key<br/>sealed by TPM/TEE/CAAM/DCP"]
  USER -->|"master = user key:<br/>only as strong as that key"| ENC
  TRUST -->|"master = trusted key:<br/>root of trust in hardware"| ENC
  TPM["TPM PCRs<br/>(measured boot)"] -. "optional seal to PCRs" .-> TRUST

The protection gradient among symmetric secret types. What it shows: a user/logon key is plaintext in kernel memory; an encrypted key is a blob unsealed by a master key; a trusted key roots that protection in hardware and can additionally be bound to boot-time integrity measurements. The insight to take: an encrypted key’s real strength is the strength of its master — rooting it in a trusted key (not a user key) is what upgrades it from “obfuscated” to “hardware-protected,” and PCR sealing is what makes the secret refuse to unseal on a tampered boot.

A concrete chain from the documentation (trusted-encrypted.rst):

$ keyctl add trusted kmk "new 32" @u           # 32-byte key generated & sealed by the TPM
440502848
$ keyctl add encrypted evm "new trusted:kmk 32" @u   # EVM key sealed under the trusted "kmk"
159771175
$ keyctl pipe 159771175 > evm.blob             # save the (encrypted) blob; plaintext never leaves kernel

Line 1 creates a 32-byte trusted key under the TPM (on TPM 2.0 a persistent storage key handle must be supplied, e.g. keyhandle=0x81000001). Line 2 creates an encrypted EVM key whose master is kmk; only the encrypted form is ever exported. keyctl print of either returns hex-ASCII of the sealed blob, never the plaintext. The trusted-key payload for TPM 2.0 is stored in a recognizable ASN.1 format using the TCG “TPM Sealed Data” OID 2.23.133.10.1.5.

The System Keyrings — Anchoring the Kernel’s Trust Chain

The kernel verifies its own loadable code and data against X.509 public keys held on dedicated, kernel-owned keyrings. These are created during boot by device_initcalls and their names all begin with . (system_keyring.c, digsig.c):

  • .builtin_trusted_keys — public keys compiled into the kernel image (CONFIG_SYSTEM_TRUSTED_KEYRING, CONFIG_SYSTEM_TRUSTED_KEYS). At minimum it holds the module-signing key the build embedded. It is allocated read/search-only to userspace and has no link restriction that admits new keys — you cannot add to it at runtime, only at build time. This is the ultimate built-in root for Kernel Module Signing verification.
  • .secondary_trusted_keys — a runtime-extensible ring (CONFIG_SECONDARY_TRUSTED_KEYRING). It is created with a link restriction so that a new certificate is admitted only if it is validly signed by a key already on the secondary ring, the builtin ring, or (if present) the machine ring — implemented by restrict_link_by_builtin_and_secondary_trusted() / restrict_link_by_builtin_secondary_and_machine() (system_keyring.c). At init it is linked to .builtin_trusted_keys, so a search of the secondary ring transitively reaches the builtin keys.
  • .machine — Machine Owner Keys (CONFIG_INTEGRITY_MACHINE_KEYRING). On a UEFI system, MOK entries the machine owner enrolled via mokutil/shim are loaded here. Unlike platform keys, “keys contained in the .machine keyring will” be linked into .secondary_trusted_keys and so gain imputed trust for module/kexec verification (integrity/Kconfig). Whether MOK keys are trusted this way is gated by the UEFI variable MokListTrustedRT: uefi_check_trust_mok_keys() looks for that mokvar entry, and only if present does the kernel trust the MOK list on .machine (machine_keyring.c). A CA-restriction option (CONFIG_INTEGRITY_CA_MACHINE_KEYRING) can require that only CA keys land on .machine, with the rest spilling to .platform.
  • .platform — platform/firmware-provided keys (CONFIG_INTEGRITY_PLATFORM_KEYRING). Populated from the UEFI Secure Boot databases (db, and MOK that isn’t trusted onto .machine). These keys are trusted only for verifying a kexec’d kernel image — they are deliberately not linked into the module-verification chain, so a firmware-vendor key cannot be used to sign a kernel module (integrity/Kconfig: “verifying the kexec’ed kernel image”). Keys are added “without validation” because they come from the trusted firmware databases (platform_keyring.c).
  • .blacklist — explicitly untrusted keys and hashes (CONFIG_SYSTEM_BLACKLIST_KEYRING). It is allocated with KEY_ALLOC_SET_KEEP so it cannot be cleared, and it holds revoked certificate hashes and blacklisted binary hashes; a signature whose key or hash is on this ring is rejected outright (blacklist.c). It is the kernel’s analogue of the UEFI dbx revocation database, and can be augmented from the firmware revocation list (CONFIG_SYSTEM_REVOCATION_LIST).

The integrity subsystem additionally creates .ima and .evm keyrings (named _ima/_evm if CONFIG_INTEGRITY_TRUSTED_KEYRING is off) for IMA and EVM appraisal certificates (digsig.c keyring_name[]).

flowchart TB
  BUILTIN[".builtin_trusted_keys<br/>(compiled-in, immutable)"]
  SECONDARY[".secondary_trusted_keys<br/>(runtime, sig-restricted)"]
  MACHINE[".machine<br/>(MOK, if MokListTrustedRT)"]
  PLATFORM[".platform<br/>(UEFI db / firmware)"]
  BLACKLIST[".blacklist<br/>(dbx-like revocations)"]
  SECONDARY -->|"linked to"| BUILTIN
  MACHINE -->|"linked into"| SECONDARY
  MODSIG["module / kexec signature check"] -->|"trusts"| SECONDARY
  KEXEC["kexec image check"] -->|"also trusts"| PLATFORM
  MODSIG -.->|"reject if on"| BLACKLIST

The system-keyring trust topology in v6.12. What it shows: .secondary_trusted_keys chains down to the immutable .builtin_trusted_keys; .machine (MOK) feeds the secondary ring only when firmware says to trust it; .platform is a separate ring trusted for kexec but not modules; .blacklist overrides everything. The insight to take: the asymmetry is the security property — platform/firmware keys must not be allowed to sign modules, so they live on a ring the module path never consults, and any key/hash on .blacklist is rejected no matter which trusted ring would otherwise vouch for it.

Possession and the Permissions Mask

Every key carries an owner UID, a group GID, and a 32-bit permissions mask with up to eight bits each for four categories — possessor, user, group, and other — of which six are defined (core.rst, key.h). The six permissions are View (see attributes), Read (see the payload / keyring contents), Write (instantiate/update payload or add/remove links), Search (find a key / search a keyring), Link (link the key into another keyring), and Set Attribute (change UID/GID/perm). The bit groups in key.h are:

#define KEY_POS_VIEW  0x01000000   /* possessor bits (0x3f000000) */
#define KEY_USR_VIEW  0x00010000   /* user bits      (0x003f0000) */
#define KEY_GRP_VIEW  0x00000100   /* group bits     (0x00003f00) */
#define KEY_OTH_VIEW  0x00000001   /* other bits     (0x0000003f) */

This is the same owner/group/other idea as UNIX file modes (see Discretionary Access Control) but with a fourth, kernel-specific category — the possessor — and a richer six-verb vocabulary instead of read/write/execute. The mask is what you read in /proc/keys (e.g. 1f3f0000) and is decoded by keyctl as --alswrv (the trailing letters are View, Read, Write, Search, Link, set-Attr for the possessor).

Possession is the subtle and important part. Per keyrings(7), “possession is not a fundamental property of a key, but must rather be calculated.” A thread possesses a key if it can reach that key by a chain of search-permitted links starting from one of its own thread/process/session keyrings: if a possessed keyring links a key, that key is possessed too, recursively. The kernel encodes this in the key_ref_t handle by abusing the least-significant bit of the pointer (key.h): make_key_ref(key, possession) ORs the bool into bit 0, is_key_possessed() tests it, and key_ref_to_ptr() masks it off. The possessor permission bits then apply to a thread that possesses the key, on top of whatever the user/group/other bits grant. This is how a process can be given strong access to a key it has linked without that access leaking to every other process owned by the same UID — the canonical use is the request_key upcall, where the handler is granted possession of the requester’s keyrings so it can fetch the auxiliary credentials it needs.

For changing ownership, group, or the mask, being the owner or holding CAP_SYS_ADMIN suffices (core.rst). On top of all this, an active LSM (SELinux has a key security class) is invoked after the basic checks — it can only further restrict, never grant (core.rst); see The Linux Security Module Framework.

Expiry, Revocation, and Restriction

A key may be given a lifetime via KEYCTL_SET_TIMEOUT (or by the type at instantiation). When it elapses the key becomes expired and accesses fail with EKEYEXPIRED; an expired key can still be updated back to positive. KEYCTL_REVOKE makes a key permanently unavailable (EKEYREVOKED, no longer findable); KEYCTL_INVALIDATE is the immediate version — it marks the key and wakes the garbage collector, which removes invalidated keys from all keyrings at once (core.rst). Revoked and expired keys are reaped only after gc_delay (300s default); dead keys (type unregistered) are reaped promptly.

Keyring link restrictions (KEYCTL_RESTRICT_KEYRING, or keyring_alloc() with a struct key_restriction in-kernel) are how the trusted-key rings stay tamper-resistant: the restriction’s check function is called on every attempted link and may reject the new key (core.rst, key.h). The standard application is verifying that a candidate X.509 certificate’s signature chains to a key already on a trusted ring — restrict_link_by_signature for the asymmetric type. KEY_ALLOC_BYPASS_RESTRICTION lets in-kernel code add a key the restriction would otherwise reject (used when wiring .builtin into .secondary at boot).

The request-key Upcall

When a kernel request_key() (or the userspace syscall) finds no matching key and callout_info is supplied, the kernel constructs the key by calling out to userspace (request-key.rst, v6.12). The dance:

  1. The requester A calls request_key(); the kernel searches A’s thread, process, then session keyrings (plus a per-task one-key cache if CONFIG_KEYS_REQUEST_CACHE=y). If found, done.
  2. Not found: the kernel creates an uninstantiated key U of the requested type/description, and an authorisation key V that records “A is the context in which U should be instantiated.”
  3. The kernel forks and execves /sbin/request-key with a fresh session keyring containing a link to auth key V. The command line is /sbin/request-key create <key> <uid> <gid> <threadring> <processring> <sessionring> <callout_info> — the three keyrings are A’s, passed so the handler can find auxiliary tokens and cache the result.
  4. /sbin/request-key assumes the authority of V (KEYCTL_ASSUME_AUTHORITY). With that authority, its key searches transparently search A’s keyrings using A’s UID/GID/groups/security label — so it can fetch, say, A’s Kerberos TGT (key W) even though it runs as a different process.
  5. The handler does whatever it must (contact a KDC, read a file) and finishes U with KEYCTL_INSTANTIATE/INSTANTIATE_IOV, or marks it KEYCTL_NEGATE/KEYCTL_REJECT.
  6. On instantiation, auth key V is automatically revoked so it cannot be reused; the kernel deletes V and returns U to A.

/etc/request-key.conf and /etc/request-key.d/*.conf map (type, description) patterns to the actual handler program (e.g. cifs.upcall, key.dns_resolver, request-key-spawned keyctl scripts). If the handler exits non-zero or dies, the kernel negatively instantiates U for a short window so repeated requests for an unobtainable key are throttled (ENOKEY or the rejected errno) rather than re-spawning the handler in a loop (request-key.rst). The search error priority is EKEYREVOKED > EKEYEXPIRED > ENOKEY.

sequenceDiagram
  participant A as Process A
  participant K as Kernel
  participant H as /sbin/request-key
  A->>K: request_key(type, desc, callout_info)
  K->>K: search thread→process→session (miss)
  K->>K: create uninstantiated key U + auth key V
  K->>H: fork+exec with session ring linking V
  H->>K: KEYCTL_ASSUME_AUTHORITY (V)
  H->>K: request_key(...) → searches A's rings as A → key W
  H->>K: KEYCTL_INSTANTIATE(U, payload)
  K->>K: revoke V, delete it
  K-->>A: return key U

The /sbin/request-key upcall for constructing a missing key. What it shows: the kernel does not block on userspace directly — it forks a handler, hands it an authorisation key, and lets the handler act as if it were the requester for the duration. The insight to take: the authorisation key V is the security linchpin — it scopes the handler’s borrowed authority to exactly one key construction and is revoked the instant U is instantiated, so a compromised handler cannot keep impersonating the requester.

Failure Modes and Common Misunderstandings

  • “My module won’t load: Loading of unsigned module is rejected.” With CONFIG_MODULE_SIG_FORCE (or lockdown in integrity mode), only modules whose signature verifies against a key on .builtin_trusted_keys/.secondary_trusted_keys/.machine load. A self-signed key must be enrolled as a MOK and trusted onto .machine (which requires MokListTrustedRT) — merely being in .platform is not enough for modules. See Kernel Module Signing.
  • Confusing .platform with .machine. Both are populated from firmware/MOK, but .platform is trusted only for kexec, while .machine (when firmware-trusted) feeds the module chain. Enrolling a key in the wrong place is a common cause of “kexec works but module load fails,” or vice versa.
  • encrypted key that isn’t actually secure. An encrypted key sealed under a user-type master is only as strong as that master, which sits in kernel RAM as plaintext. For real protection the master must be a trusted key rooted in a TPM/TEE (trusted-encrypted.rst).
  • Trusted key won’t unseal after a kernel update. If the key was sealed to PCRs that change when the boot components change, the TPM refuses to unseal. The fix is to reseal under the new PCR values (keyctl update ... pcrinfo=...) before updating, or to keep multiple blobs sealed under each expected PCR set.
  • request-key upcall silently fails. If /etc/request-key.conf has no matching rule, or the handler isn’t installed, the key is negatively instantiated and subsequent requests fail with ENOKEY for the negative-key window — easy to misread as “the kernel has no key support.”

Alternatives and When to Choose Them

  • user vs logon: choose logon whenever the kernel is the only legitimate consumer (encryption keys) — it removes “read it back from userspace” as an attack. Use user for tokens userspace genuinely needs to re-read.
  • encrypted vs trusted: trusted when a TPM/TEE/CAAM/DCP is present and you want a hardware root and optional measured-boot binding; encrypted when no trust source exists and you accept software-only protection rooted in another key (trusted-encrypted.rst).
  • System keyrings vs an in-userspace allow-list: the kernel-side rings are mandatory for kernel-code trust (module/kexec) because the check happens in ring 0 where userspace cannot be trusted to gate it. Userspace allow-lists (e.g. an init-time policy check) cannot substitute.

Production Notes

On a typical Secure-Boot Fedora/RHEL or Ubuntu system, cat /proc/keys shows the .builtin_trusted_keys, .secondary_trusted_keys, .platform, and (on recent kernels with MOK trust) .machine rings, each holding the distro’s CA and, for .platform, the Microsoft and OEM UEFI certificates. The most common real-world workflow is DKMS-built out-of-tree modules under Secure Boot: the admin generates a key, enrolls it with mokutil --import, reboots to confirm in shim’s MokManager, and thereafter sign-file (whose four arguments are the hash algorithm, the private key, the public key, and the module — appended as a signature plus the ~Module signature appended~ magic at the module’s end, per module-signing.rst) produces a module the kernel accepts via the .machine ring. For credentials, MIT Kerberos’s KEYRING:persistent: ccache, cifs-utils’ cifs.upcall, and the in-kernel DNS resolver (key.dns_resolver) are the canonical request-key upcall consumers — keyctl show @s and journalctl -t request-key are the diagnostic stops when an upcall misbehaves.

See Also