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() 决策顺序:

  1. `KEY_AUTH` 授权密钥:用户预先通过 `request_key` 获取的授权链
  2. `KEY_PERM` 常规权限位判断
  3. 内核 LSM(SELinux/AppArmor)钩子 `key_permission(key, cred, perm)` 终审
  4. 四、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 验证的查询时,实际上走的是内核级调用路线:

    1. `request_key("dns_resolver", "dnssec..com")` 向密钥服务请求公钥
    2. 返回的 payload 包含 RRSIG 验证需要的数据
    3. 验证失败返回 EKEYREJECTED 而非静默跳过
    4. systemd-resolved 通过 "/run/systemd/resolve/io.systemd.Resolve" D-Bus 接口向 keyring 注入信任锚。

      5.2 用户/登录密钥类型

      user 类型是最通用的 sym-cipher/rand 密钥容器,/proc/keys 中占用最多条目。典型用途:

      • CephX 认证密钥
      • NGINX TLS session ticket key(通过 starttls 注入)
      • fscrypt 文件加密主密钥(经由 fscrypt-provisioning 类型转译)

      logon 类型的描述必须包含":"(type:description),不要求 UID 绑定,是跨用户凭证的理想选择。

      5.3 大密钥(big_key)类型

      big_key 类型专为超过 PAGE_SIZE(4KB) 的密钥内容设计。它不走 kernel buffer,而是创建 shmem(tmpfs)文件存储 payload,key 仅保留引用指针。

      这种设计有两点好处:

      1. 不占用内核线性映射区域的不可换出内存
      2. 巨大密钥(如 PGP 密钥环)按 page 加载,减少内存峰值
      3. 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 切换事件,负责:

        • 注入 kerberos ccache 类型的缓存
        • 清理 stale 的订阅 keyring
        • 与 `pam_keyinit` PAM 模块配合处理登录 keyring 创建

        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,而是提供互补层级:

        • TPM 持有 SRK (Storage Root Key),平台唯一
        • `trusted` 类型密钥由 TPM seal,解封(unseal)后才能读取
        • `encrypted` 类型由 `user` 或 `trusted` 类型密钥加密,可更新明文而不改动父密钥

        实际 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 内核基础设施。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部