Linux内核密钥保留服务(Key Retention Service)深入工程实战
一、为什么内核需要自己管密钥
用户态密钥管理长期面临一个结构性困境:密钥的生命周期与进程绑定,进程退出即销毁。这导致三个难以回避的工程难题。
密钥用途的跨进程需求。 kernel-centic 认证的典型场景包括:NFS 客户端持有 user-space 提供的认证密钥、DNS 解析器使用预配置的信任锚验证 DNSSEC 记录、内核模块加载器验证模块签名。这些场景要求密钥在内核遭遇用户态重启后依然可访问。
进程隔离与挂载隔离。 systemd 时代一个会话进程持有 TLS 私钥是常见配置,但密钥被挡在进程边界之外。容器场景下,一个 init 进程需要向后续 fork 出的子进程传递共享密钥材料。
密钥不应触及磁盘明文。 文件系统密钥是一个悖论:如果密钥存在磁盘上,谁保护它?TPM/TEE 是硬解,但纯软件方案需要内核在内存中维护密钥,永不换出到交换区。
Linux Key Retention Service(LKRYPTS)正是为解决这些问题而生,自 2.6 内核引入,是一套运行在内核空间、面向内核子系统消费的密钥缓存基础设施。
二、核心架构
2.1 数据结构全景
include/linux/key.h 中定义的 struct key 是核心对象:
struct key {
refcount_t usage; // 引用计数
key_serial_t serial; // 全局唯一序列号
union {
struct list_head graveyard; // 消亡链表
struct rb_node serial_node; // 红黑树节点
};
struct rw_semaphore sem; // 内容读写信号量
KEYMASSOC *type; // 密钥类型指针
char *description; // 描述字符串
void *payload; // 实际密钥材料
union key_payload payload;
struct key_restriction *restrict_link;
struct key_preparsed_content *preparsed;
};
struct keyring 继承自 struct key,额外包含一个红黑树管理的名称→键指针映射表,构成层级化命名空间。
struct key_user 是每个 UID 的配额账户,跟踪该用户拥有的全部密钥和总负载大小。quota 控制保护系统免受单用户 DoS。
struct key_type 定义密钥类型的操作向量,包含 preparse(二进制→解析实例)、free_preparse、instantiate(创建键)、update、match(查找键)、revoke、destroy。
2.2 密钥命名空间模型
System Keyring (.builtin_trusted_keys)
└─ 内置 CA 证书(启动时加载)
└─ 二次签名数据库(secondary_trusted_keys)
Platform Keyring (.platform)
└─ UEFI PK/KEK/db/dbx(固件提供)
Machine Keyring (.machine)
└─ MOKList( Shim / Fedora / RHEL)
Keyring Keyring (.keyring)
└─ 命名 keyring 的容器
User Keyring (@us / @u) ← UID-bound, 登录时自动实例化
Session Keyring (@s / @) ← 绑定到会话(login/su/新开 shell)
Process Keyring ← 仅当前进程
Thread Keyring ← 仅当前线程
Request-Key Keyring ← 临时请求密钥时的暂存位置
这套命名空间让不同作用域的密钥天然隔离,又通过命名链接允许受控共享。
2.3 配额与生命周期
struct key_user 包含硬性配额 controls:
- `keys`:per-user 持有的 key 总数(默认200)
- `qnbytes`:payload 总字节数(默认200KB)
触发配额超限的 key 创建返回 EDQUOT。系统守护进程(kerrierqd GC 密钥回收过期条目)。单个 key 可设置 expiry 时间戳(keyctl settimeout),迫使消费者周期性续期,实现密钥的滚动淘汰。
三、权限模型
3.1 访问权限位
每个 key 拥有四类权限集,分别针对持有者(possessor)、所有者(owner)、所属组(group)、其他用户(other):每类含 8 个位:
| 位 | 含义 |
|---|---|
| VIEW | 读取 payload 与元数据 |
| READ | 列出 keyring 成员 / 读取 payload |
| WRITE | 更新/创建/链接/取消链接 |
| SEARCH | 在 keyring 中搜索 / 通过搜索获取 |
| LINK | 创建指向该 key 的链接 |
| ATTR | 设置权限/所有者/超时 |
3.2 Possessor 语义
Possessor 是一个逻辑概念:「持有 key」的进程不一定要拥有该 UID。当进程通过 search 操作找到一个 key 时,key 类型可以选择将该 key 登记到进程的 credential 上(key_fs),此后该进程作为 possessor 对该 key 拥有权限位 HEAD。
这个机制使得一个 root 无法直接读取 sudo -u alice keyctl show 的 session keyring 中的 key,因为 alice 的 key 不进入 root 的 possessor 集合。
3.3 权限决策路径
security/key_permission() 决策顺序:
- `KEY_AUTH` 授权密钥:用户预先通过 `request_key` 获取的授权链
- `KEY_PERM` 常规权限位判断
- 内核 LSM(SELinux/AppArmor)钩子 `key_permission(key, cred, perm)` 终审
- `request_key("dns_resolver", "dnssec.
.com")` 向密钥服务请求公钥 - 返回的 payload 包含 RRSIG 验证需要的数据
- 验证失败返回 EKEYREJECTED 而非静默跳过
- CephX 认证密钥
- NGINX TLS session ticket key(通过 starttls 注入)
- fscrypt 文件加密主密钥(经由 fscrypt-provisioning 类型转译)
- 不占用内核线性映射区域的不可换出内存
- 巨大密钥(如 PGP 密钥环)按 page 加载,减少内存峰值
- 注入 kerberos ccache 类型的缓存
- 清理 stale 的订阅 keyring
- 与 `pam_keyinit` PAM 模块配合处理登录 keyring 创建
- TPM 持有 SRK (Storage Root Key),平台唯一
- `trusted` 类型密钥由 TPM seal,解封(unseal)后才能读取
- `encrypted` 类型由 `user` 或 `trusted` 类型密钥加密,可更新明文而不改动父密钥
四、keyctl 与内核接口
4.1 系统调用
#include <keyutils.h>
key_serial_t add_key(const char *type, const char *description,
const void *payload, size_t plen,
key_serial_t keyring);
key_serial_t request_key(const char *type, const char *description,
const char *callout_info,
key_serial_t keyring);
key_serial_t keyctl(int cmd, ...);
4.2 keyctl 常用操作
| 操作 | 功能 |
|---|---|
| KEYCTL_GET_KEYRING_ID | 获取命名 keyring ID |
| KEYCTL_JOIN_SESSION_KEYRING | 加入/切换 session keyring |
| KEYCTL_UPDATE | 更新 key 内容 |
| KEYCTL_REVOKE | 立即吊销密钥 |
| KEYCTL_SET_TIMEOUT | 设置过期时间 |
| KEYCTL_SEARCH | 在 keyring 中搜索 |
| KEYCTL_READ | 读取 payload |
| KEYCTL_DESCRIBE | 获取描述字符串 |
| KEYCTL_SETPERM | 设置权限位 |
| KEYCTL_CLEAR | 清空 keyring |
| KEYCTL_INSTANTIATE | 实例化 pending 的 key |
| KEYCTL_NEGATE | 将 pending key 标记为 neg |
| KEYCTL_SET_REQKEY_KEYRING | 指定默认请求 keyring |
| KEYCTL_SET_TIMEOUT | 设置超时 |
| KEYCTL_ASSUME_AUTHORITY | 接管请求授权 |
五、密钥类型的工程实践
5.1 DNS Resolver 类型
dns_resolver 是内核级 DNSSEC 验证的核心承载者。当 glibc 解析器通过 /etc/resolv.conf 收到需要 DNSSEC 验证的查询时,实际上走的是内核级调用路线:
systemd-resolved 通过 "/run/systemd/resolve/io.systemd.Resolve" D-Bus 接口向 keyring 注入信任锚。
5.2 用户/登录密钥类型
user 类型是最通用的 sym-cipher/rand 密钥容器,/proc/keys 中占用最多条目。典型用途:
logon 类型的描述必须包含":"(type:description),不要求 UID 绑定,是跨用户凭证的理想选择。
5.3 大密钥(big_key)类型
big_key 类型专为超过 PAGE_SIZE(4KB) 的密钥内容设计。它不走 kernel buffer,而是创建 shmem(tmpfs)文件存储 payload,key 仅保留引用指针。
这种设计有两点好处:
5.4 自建密钥类型
内核模块可通过 register_key_type() 注册自定义类型。编写要点:
static struct key_type my_key_type = {
.name = "myapp",
.preparse = my_key_preparse,
.free_preparse = my_key_free_preparse,
.instantiate = my_key_instantiate,
.update = my_key_update,
.match = my_key_match,
.destroy = my_key_destroy,
.describe = my_key_describe,
.read = my_key_read,
};
破坏函数必须释放 payload 并减少引用计数,内核设有密钥审计(integrity)保障。
六、request-key 守护进程与密钥代理
6.1 /sbin/request-key 工作原理
request_key() 在内核侧返回一个 pending key(负序列号),然后在用户态调用 /sbin/request-key 。工具随后依据 /etc/request-key.conf 判断如何获取密钥:
# /etc/request-key.conf
create dns_resressor * * /bin/key.dns_resolver %k %d %c %u %g
create user debug:foo * /usr/local/bin/get-appkey %k %d
6.2 与 systemd 的集成
-keyrings.service 随登录会话启动,监听 keyring session 切换事件,负责:
RFC 提议的 kernel-keyrings sdbus API 让 systemd --user 的进程也能编程式管理密钥。
七、生产运维与调试
7.1 /proc/keys 与 /proc/key-users
cat /proc/keys 输出结构:
ser flg uid gid perms expd tgt description
01c8 I--Q--- 1000 1000 3f1f0000 - _ rfce:krb ccache 042
0a8b ----L--- 0 0 00030000 6 _ .dns_resolver
flags 字段是 8 位掩码:I=Instantiated, R=Revoked, D=In负Progress, Q=Contributing to user's quota, L=Request-key pending。
cat /proc/key-users:
usage nkeys nikeys qnkeys qnbytes
0: 142 142 0 0 12456
1000: 8 8 0 3 924
7.2 keyctl 日常运维
# 列出当前 session keyring 中所有 key
keyctl list @s
# 将某个 key 设置为 1 小时过期
keyctl timeout $KEYID 3600
# 吊销(读取即失败)
keyctl revoke $KEYID
# 通过指定 keyring 在后台异步创建 key
keyctl padd user testKey @s <<< "mysecret01234"
# 永久驻留 keyring(不受 session 切换影哬)
keyctl link $KEYID @u
# 从 session keyring 原子注销自身所有 key
keyctl clear @s
7.3 审计密钥访问
通过 keyctl session 命令建立 isolated session:
# 创建密钥并放入隔离 keyring
new=`keyctl newring mytmp @u`
keyctl padd user testKey $new <<< "s3cr3"
# 在隔离 session 中执行
keyctl session my_session bash
keyctl list @s # 仅能看到 my_session 中的 key
exit
keyctl unlink $new @u
八、安全加固配置
8.1 sysctl 控制
# 开启 key garbage collector(默认开启,修改回收阈值)
kernel.keys.gc_delay = 300 # 秒
kernel.keys.maxkeys = 200 # 系统密钥总数上限
kernel.keys.maxbytes = 200000 # 系统密钥总字节上限
# 是否允许用户继承父进程 keyring(阻止凭据泄露)
kernel.keys.disable_capabilities_boolean = 0
# 设置 1 则普通进程不自动获得 CAP_SYS_ADMIN 级别的 key 访问权
8.2 LSM 钩子与最小权限
SELinux 默认策略允许所有 key 操作。加固时可通过:
# 仅允许 httpd_t 域读取特定 key 类型
allow httpd_t cert_t:file { read getattr open };
type_transition httpd_t httpd_key_t:process httpd_t;
AppArmor 的 key 规则:
capability sys_admin,
deny key (view, read) keyring="user" description="ssh_private",
8.3 Kexec 场景
kexec 热重启时,加载新内核会触发密钥服务重建。关键配置是 -kexec-handover 钩子确保密钥材料与 kexec 同步,否则 CTRL-M: kexec handover failed。最小可行做法是将密钥持久化到 TPM NV 索引,新内核启动时重新 provision。
九、与 TPM/TEE 的协同
LKRYPTS 不取代 TPM,而是提供互补层级:
实际 seals 流程:
1. keyctl add trusted tkm "new 32" @s
└─生成 32 字节随机数交由 TPM seal 原树
2. keyctl print $ID
└─触发 TPM_Unseal → 明文进出 PCR 校验
PCR 绑定确保启动链一致性,度量不匹配时 TPM 拒绝解封。
十、典型生产故障
Q:NFS 挂载提示 "missing cached DNSSEC key"?
A:create dns_resolver * * /sbin/key.dns_resolver -t %t %k %d 配置缺失,导致 request_key() 无法在后台从 DNS 获取记录。
Q:sudo session keyring 隔离后失去对原 user keyring 的 view 权限?
A:session keyring 是初始化时关于 user keyring 的镜像,链接关系仅在创建时确定。需要 keyctl link @u @s 或在 /etc/sudoers 中配置 closefrom_override。
Q:容器内 keyctl 失败 返回 EPERM?
A:默认容器 CAP_SYS_ADMIN 受限,且 BPF LSM 可能拦截。需要 --cap-add SYS_ADMIN 或 security-opt=no-new-privileges=false。
十一、总结
Linux 密钥保留服务不是「用户态加密库的内核嵌入」,而是一套以引用计数为本体、以配额与权限位为约束、以序列号为入口的通用密钥缓存系统。理解 key、keyring 与 key_user 的关系,把握 possessor 和 LSM 钩子 的权限决策路径,是排查 DNSSEC 失败、NFS 认证中断、容器密钥隔离失败的关键。
在 TPM 2.0、fscrypt、DNSSEC、CephX、IMA 签名等场景下,密钥保留服务已是不可或缺的 Linux 内核基础设施。

发表评论 取消回复