现代操作系统需要应对多样化的加密需求——磁盘加密、文件系统完整性校验、模块签名验证、数字证书链——这些场景要求内核态能够安全地生成、存储、调度以及销毁密钥材料。Linux Key Retention Service(KRS)正是为解决这一系列需求而设计的内建子系统,自 2.6.x 内核引入以来逐步扩展到几乎每一个涉及密码学操作的领域。

但 KRS 的设计哲学与用户态密钥管理器有本质区别。内核中的密钥不只是数据结构,它们与进程权限、namespace 边界、生命周期引用计数以及内存回收策略深度耦合。理解这套机制是构建可信计算栈、dm-crypt 部署乃至 Secure Boot 强制策略的前置条件。

核心设计:密钥作为一等对象

KRS 将密钥建模为内核对象,每个密钥拥有唯一的序列号(serial number)、类型标识(key type)、有效属主(UID/GID)、权限掩码(perm)以及可变的载荷长度。

核心数据结构 struct key 的关键字段:

struct key {
    refcount_t          kref;           /* 引用计数 */
    unsigned int        flags;          /* KEY_FLAG_* 状态位 */
    key_serial_t        serial;         /* 全局唯一序列号 */
    struct rw_semaphore sem;            /* 内容修改保护 */
    struct key_user     *user;          /* 属于哪个 key_user */
    void                *security;      /* LSM 安全钩子数据 */
    time64_t            expiry;         /* 过期时间 */
    time64_t            revoked_at;     /* 吊销时刻 */
    struct key_type     *type;          /* 类型操作表 */
    union key_payload   payload;        /* 密钥载荷 */
    struct key_restriction *restrict_link;
};

KRS 最显著的特征是不强制密钥驻留内存。当内存压力来临时,key_gc_timer 会遍历过期或最近未使用的密钥,将载荷清零后释放回内存池。这种设计使得嵌入式或长期运行的服务器应用场景下,系统不会无限制持有全部密钥明文。

Keyring 层级与密钥查找

每种密钥类型可以挂载到不同的 keyring 上。内核维护了四种进程级 keyring:

Keyring 生命周期 用途
thread-keyring 线程级 线程私有
process-keyring 进程级 setpgid 场景
session-keyring 会话级 shell session,keyctl_session 隔离
user-keyring 用户级 用户所有凭证的锚点

查找路径由 key_lookup() 完成,其时间复杂度取决于 keyring 是否启用红黑树优化。当 keyring 内部密钥数量超过阈值(KEYRING_SEARCH_LOST_THRESHOLD,通常 16),结构体会从链表迁移为 struct rb_tree,查找复杂度从 O(n) 恒定至 O(log n)。

用户态通过 request_key() 系统调用可以在进程上下文内触发动态密钥加载。典型的调用链:

/* 请求 "user" 类型的 "mykey",在 .requestkey 找不到时回落到用户态助手 */
key_serial_t id = request_key("user", "mykey", NULL, KEY_SPEC_THREAD_KEYRING);
if (id < 0) {
    perror("request_key: 密钥不存在或助手失败");
    return -1;
}

若密钥未缓存,KRS 会启动 request-key 用户态助手(/sbin/request-key),根据 /etc/request-key.conf 中匹配的描述符规则执行脚本,该助手通过临时 keyring 将获取的密钥注入返回。

密钥类型体系(key_type)

key_type 定义了每种密钥的操作函数表,内置类型覆盖主要场景:

  • .user — 透明用户密钥,用于任意短字符串/数据承载
  • logon — 无类别描述符(用于 dm-crypt、fscrypt,类型前缀带 :name)
  • keyring — keyring 自身也是密钥类型,允许嵌套
  • .asymmetric — 非对称密钥(X.509、PKCS#8),用于模块签名验证
  • .blacklist — 吊销证书哈希黑名单

自定义类型需调用 register_key_type(),并实现 instantiate、update、match_preparse 和 revoke 回调。

关键细节在于 instantiate 函数不会直接持有明文 payload。出于安全考虑,KRS 要求类型在 instantiate 后立即注册到内存,而密钥内容的修改通过 key_update()(需要写权限)完成。这一分阶段设计防止了同一不可信上下文同时修改盐和载荷的竞争。

实战一:dm-crypt 与 LUKS 的密钥交付

dm-crypt 是 KRS 在磁盘加密场景下最典型的消费者。通过 libdevmapper 可选择将加密主密钥注入 .logon 类型 key,然后在 dmsetup 创建阶段从内核 keyring 抽取:

# 步骤一:用用户密码生成 enckey,注入 keyring
pktool -i /path/to/raw_key.bin my_logon_key <<EOF
type=logon
description=cryptroot:luks-abc123
EOF

# 步骤二:从 keyring 中取回 serial,构造 dm-crypt table
KEY_SERIAL=$(keyctl search @s user cryptroot:luks-abc123)
dmsetup create cryptroot --table "0 $(blockdev --getsz /dev/sda2) crypt aes-cbc-essiv:sha256 $(keyctl pipe $KEY_SERIAL) 0 /dev/sda2 4096"

要点:cryptroot 前缀按 LUKS UUID 归属于 .logon 密钥,便于 systemd-cryptsetup 在启动时基于 UUID 自动匹配。一旦设备解锁成功并挂载,run 时 .requestkey 配置默认不再触发说明——密钥已通过 cryptsetup 注入名为 .root_crypto 的全局系统 keyring。

实战二:fscrypt 文件加密

fscrypt(ext4/f2fs 的文件级加密)采用 .logon 类型 key,key descriptor 由 fscrypt_formatted_key_prefix() 生成,格式为 fscrypt:N(N 为 policy version)。

/* 使用 keyctl 注入 fscrypt 密钥 */
struct fscrypt_key raw_key = {
    .mode = FSCRYPT_MODE_AES_256_XTS,
    .size = 64,  /* XTS mode 需要 twice the AES key size */
    .raw = {...},
};

/* add_key("logon", "fscrypt:5176e0f8e7ab4c3d:7d202020...", &raw_key, sizeof(raw_key), KEY_SPEC_USER_KEYRING) */

内核在 fscrypt_provisioning_key 结构中维护派生链:保护密钥(wrapped key)从不直接写入磁盘,只有 master key 在缓存驻留。当触发 FSC_REMOVE_ENCRYPTION_WATERMARK 时,内核强制验证 refcount = 0,以防活跃 inode 引用被泄露。

内核加密 API(crypto API)与 KRS 的衔接

许多key_type使用者并不直接调用 KRS API,而是通过更高层的 wrapper。dm-crypt 实际流程:

• KRS 持有 .logon 密钥

• crypt_config 通过 crypto_alloc_skcipher() 确定底层算法

• 使用 crypto_skcipher_setkey() 将 KRS 内容注册为会话密钥

• skcipher_request_set_crypt() 提交 bio 加密

值得注意的是,crypto_skcipher_setkey() 成功后即使调用 key_revoke(),会话密钥依旧有效直到 crypto tfm 对象被销毁。这在 dm-crypt 场景下可能导致固化解密能力窗口,需要在设备移除流程显式同步。

/* 安全移除顺序必须正确 */
dm_table_destroy(table);              /* 先销毁 tfm */
key_put(master_key);                  /* 释放 KRS 引用 */
kmemzero_explicit(raw_buf, size);      /* 擦除本地副本 */

权限模型与 LSM 集成

KRS 的权限掩码按位拆分,四位一组对应查看/读取/写入/搜索操作,分别作用于属主、所属组、其他用户和系统/POSIX capabilities。

perm = 0x3f3f0000
       ││││  ││││
       ││││  └──┴─── Posix caps (CAP_SYS_ADMIN 等)
       └──┴─────── 其他用户权限

当进程调用 keyctl_read() 时执行如下决策链:

keyctl_read(key, buffer, buflen)
  → key->type->read(key, buffer, buflen)
    → key_permission(key, KEY_NEED_READ)
      → key_task_permission(key, current_cred(), KEY_NEED_READ)
        → LSM 钩子 security_key_permission()
          → SELinux/AppArmor 判定
          → 回退传统权限矩阵

SELinux 上通过 key security class 定义策略规则:

allow cryptsetup_t user_home_t:file { open read };
allow cryptsetup_t self:key { write search };

内存回收与 GC 策略

key_gc 工作队列负责修剪过期 key。关键参数位于 /proc/sys/kernel/keys/:

参数 默认值 含义
maxbytes 20000 × payload 所有 key 中 payload 的总字节上限
maxkeys 200 哈希桶中最大 key 数
gc_delay 300 (秒) key 过期后保留时间

当超限时,key_garbage_collector() 按 LRU 顺序选择牺牲者,调用 type->destroy() → payload.data 释放。注意:如果 KSM(Kernel Samepage Merging)已合并密页,KRS 的 kvfree() 不会触发合并页的拆分,这会隐性泄漏部分明文明文图像到 /sys/kernel/mm/ksm/。生产环境建议对 dm-crypt 场景设置 the KSM exclude flag。

实战三:TPM 密封存储集成

将 KRS 密钥密封至 PCR 值是 Secure Boot 的标配。典型步骤:

/* 使用 tpm2_key_protector kernel module (v6.5+) */
static int seal_key_to_tpm(struct key *key)
{
    /* 1. 生成 32 字节随机 blocal auth */
    get_random_bytes(auth, 32);

    /* 2. 调用 tpm2_seal_trusted() */
    struct trusted_key_payload *p;
    p = trusted_key_payload_alloc();
    p->key_len = AES_KEY_SIZE;
    get_random_bytes(p->key, p->key_len);

    /* 3. 创建 sealed blob,绑定至 PCR 7 (secure boot policy) */
    tpm2_seal_trusted(p, TPM_TRANSPORT_TIS, TPM_ALG_SHA256,
                      pcr_mask = BIT(7));

    /* 4. 生成 trusted 类型 key,key·options = trap_to_drbg_key */
    key_instantiate_and_link(key, p, sizeof(*p), NULL, NULL);
}

内核在调入 trusted 类型后会将 payload 标记为 KEY_NOT_TRUSTED,直到 UNSEAL 流程在 TPM 内完成 PCR 校验。此期间进程看到的只是第三方 blob,明文密钥无法直接 read。

对比 encrypted 类型(encrypted_key 与 master key 两阶段),其 master key 本身驻留 KRS 中。这一两阶段加密的取舍在于:可信场景用 trusted(依赖硬件强根),多租户场景用 encrypted(master key 可跨机器迁移)。

生产环境常见坑

1. 硬链接攻击

早期 KRS 查找逻辑对 key description 处理不够严格。在 request_key() 时若注入相对路径描述符,可能使得助手在意外 cwd 下执行。修补方法:统一使用绝对 key 命名、开启 SELinux key 策略、在容器内限制 CAP_SYS_ADMIN。

2. 密钥注入与 namespace 边界

key_user 计数器绑定到 user_namespaces,容器内创建的 key 在其 init_user_ns 中不可见。但若容器共享同一 uid 且未启用 key namespace 隔离,内核中 key 的 ownership 判定依赖 current_cred()->user(即当前 creds),进而跨命名空间泄露。生产环境建议开启 CONFIG_KEYS_ICACHE_HASH_BITS 调大 hash table 减少碰撞。

3. 内存剩余与 cold boot

KRS 的 kvfree() 对应的 SLUB 缓存可能在 KEY_REVOKE 后未即时归零。在极端场景(冷启动攻击)下,部分攻击会将内核转储或者利用重启前未清零的物理页。修改:启用 CONFIG_INIT_ON_FREE_DEFAULT_ON 并在 define_class 调用 kmem_cache_create_usercopy() 时传入 __GFP_ZERO。

4. 模块签名强制

CONFIG_MODULE_SIG_FORCE 要求模块必须由 .asymmetric 类型信任密钥签名。这些公钥链驻留在 .builtin_revoked 与 .module_trusted keyring 中:

# 查看模块验签使用的公钥
keyctl list %:.asymmetric
# 输出示例:
#   1: --alswrv   0     0 asymmetri Build time autogenerated kernel key: x509.signer

闭源驱动厂商需要将其公钥通过 CONFIG_MODULE_SIG_KEY 指定路径注入 MOK(Machine Owner Key),这是 KRS 在 Secure Boot 流程中的第二层锚点。

监控与调试

KEYCTL_DEBUGGING(keyctl show /proc/keys /proc/key-users)提供全面状态。生产环境下这些 proc 文件默认只对 root 可见,建议通过可访问审计机制代理。

# /proc/keys 示例
009a8280 I--Q---   190perm 3f010000  1000  1000 user      krbcc: krb5 ccache
082bf255 I--Q---   120perm 3f010000  1000  1000 logon     fscrypt:571e8d5c:12

# /proc/key-users 示例
     0:   105          55/1001          0/20000       186/20000
       ↑               ↑                 ↑              ↑
    UID           key count        byte count    max-byte

调试新增密钥类型时 ftrace 钩子 trace_key_* 系列可提供事件级调用链:

echo 1 > /sys/kernel/debug/tracing/events/key/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 输出:
#          <...>-1234 [002] ...1  568.492038: key_instantiate: 0x8800c000 user "logon.key1" (attr 3)
#          <...>-1234 [002] ...1  568.492538: key_request: 122 -> 258 (helper_pid)

总结

KRS 并非单纯「在内核里存密码那么简单」。它融合了类型安全、内存生命周期管理、LSM 访问控制和引用所有权模型,形成完整的密钥材料管理栈。从 cryptsetup 启动到 verity root hash,从模块验签到 EVM 文件保护,每个安全边界背后都有 KRS 提供密钥调度基础设施。

扩展层面,硬件可信根(TPM/TEE)、容器隔离、跨命名区密钥租约等仍然属于持续演进的子领域。对于希望在生产密钥管理层面深入的内核开发者与 SRE 工程师,KRS 是必须翻过的一道坎——翻过去之后整个可信计算栈的图景就清晰了。


本文聚焦 Linux 5.10 - 6.8 内核间 KRS 的架构演进与实践实现,涉及代码参考均来自 mainline 内核源码与官方文档。生产部署建议优先参考发行版 keyutils 包配套版本。
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部