CephX 认证机制深度解析

本文基于 Ceph 源码(auth/cephx/auth/KeyRing.hauth/Auth.hmon/AuthMonitor.cc)分析 ceph-fuse 挂载时的鉴权流程、CephX 双向验证原理、keyring 数据结构及两处持久化的设计意图。


一、鉴权的整体分工

Ceph FUSE 挂载的鉴权涉及三个模块,职责严格分离

模块 职责 关键问题
Monitor 身份认证(Authentication) "你是谁?"
MDS 访问控制(Authorization) "你能做什么?"
OSD 数据访问控制 "你能读写哪个 pool?"

Monitor 只负责颁发凭证,不负责业务权限检查;MDS 收到凭证后,从中读取 caps 字符串,对每次 metadata 操作做细粒度鉴权。


二、CephX 协议:双向挑战-应答验证

2.1 核心思想

CephX 借鉴 Kerberos 的设计,密钥本身从不在网络上传输,通过"挑战-应答"的方式证明双方都持有相同的共享密钥。

核心函数位于 auth/cephx/CephxProtocol.cc

cpp 复制代码
void cephx_calc_client_server_challenge(
    CephContext *cct,
    CryptoKey& secret,          // 共享密钥(AES-128)
    uint64_t server_challenge,  // 服务端随机数
    uint64_t client_challenge,  // 客户端随机数
    uint64_t *key,              // 输出:验证值
    std::string &error)
{
    CephXChallengeBlob b;
    b.server_challenge = server_challenge;
    b.client_challenge = client_challenge;

    bufferlist enc;
    encode_encrypt(cct, b, secret, enc, error);  // 用 secret 加密 {nonce1, nonce2}

    // 将加密结果 XOR 折叠为 uint64 验证值
    uint64_t k = 0;
    const ceph_le64 *p = (const ceph_le64 *)enc.c_str();
    for (int pos = 0; pos + sizeof(k) <= enc.length(); pos += sizeof(k), p++)
        k ^= *p;
    *key = k;
}

2.2 完整握手流程

复制代码
客户端 (ceph-fuse)                        Monitor
       │                                      │
       │  ① AUTH_REQUEST: entity_name         │
       │ ───────────────────────────────────► │
       │                                      │
       │  ② SERVER_CHALLENGE (随机 uint64)    │
       │ ◄─────────────────────────────────── │
       │                                      │
       │ 生成 client_challenge(随机 uint64)   │
       │ 从本地 keyring 读取 secret            │
       │ req.key = calc(secret,               │
       │               server_challenge,      │
       │               client_challenge)      │
       │                                      │
       │  ③ CLIENT_AUTH: {client_challenge,   │
       │                   req.key}           │
       │ ───────────────────────────────────► │
       │                                      │  从 key_server 取该 entity 的 secret
       │                                      │  expected_key = calc(secret,
       │                                      │                      server_challenge,
       │                                      │                      client_challenge)
       │                                      │  比对 req.key == expected_key ?
       │                                      │  ✅ 相等 → 身份认证通过
       │  ④ SESSION_KEY + TICKETS             │
       │ ◄─────────────────────────────────── │
       │  (用 client secret 加密,           │
       │    只有 client 能解开)              │

Monitor 端的验证逻辑来自 auth/cephx/CephxServiceHandler.cc

cpp 复制代码
uint64_t expected_key;
cephx_calc_client_server_challenge(cct, secret, server_challenge,
                                   req.client_challenge, &expected_key, error);
if (req.key != expected_key) {
    ret = -EPERM;  // 验证失败
    break;
}

2.3 为什么叫"双向验证"?

验证方向 机制
Client → Monitor Monitor 独立计算 expected_key,与 client 上报值比对,相等则证明 client 持有正确 secret
Monitor → Client Monitor 返回的 ticket 用 client secret 加密 (见 cephx_build_service_ticket_reply),client 能成功解密则证明 Monitor 同样持有这个 secret

关键安全性:secret 本身永远不在网络上传输,只传加密后的计算结果,防止网络监听攻击。


三、Ticket 机制:一次认证,多服务复用

认证通过后,Monitor 为每个目标服务(MDS、OSD)分别生成 Service Ticket

复制代码
Monitor 颁发的 Ticket 结构:

  {session_key, validity}  → 用 client secret 加密  → client 能解开,得到 session_key
  {ticket_info}            → 用 service secret 加密 → 只有目标 MDS/OSD 能解开
  • ticket_info 包含:entity_name、global_id、caps(权限字符串)、有效期
  • MDS 收到后,用自己的 rotating secret 解密,验证 ticket 合法性,再检查 caps

MDS 的 caps 细粒度控制

bash 复制代码
# 允许读写所有文件系统
allow rw

# 限定文件系统名
allow rw fsname=myfs

# 限定路径(根路径限制)
allow rw fsname=myfs path=/home/user1

# 限定客户端网络来源
allow rw network 10.0.0.0/8

caps 解析由 mds/MDSAuthCaps.cc 实现。


四、Keyring 数据结构

Monitor 的 keyring 在内存中分三层组织:

4.1 EntityAuth(单个 entity 的完整信息)

源码位于 auth/Auth.h

cpp 复制代码
struct EntityAuth {
    CryptoKey key;                 // AES-128 密钥(16字节)
    map<string, bufferlist> caps;  // 权限字符串
    // 示例:
    // "mds"  -> "allow rw fsname=myfs"
    // "osd"  -> "allow rw pool=myfs_data"
    // "mon"  -> "allow r"
};

4.2 KeyRing(本地 keyring 文件的内存表示)

源码位于 auth/KeyRing.h

cpp 复制代码
class KeyRing : public KeyStore {
    map<EntityName, EntityAuth> keys;
    // EntityName 示例:
    //   client.admin
    //   client.cephfs_user
    //   osd.0 / mds.myfs-0
};

4.3 KeyServerData(Monitor 内部的权威存储)

源码位于 auth/cephx/CephxKeyServer.h

cpp 复制代码
struct KeyServerData {
    map<EntityName, EntityAuth> secrets;         // 所有 entity 的 key+caps(权威副本)
    KeyRing *extra_secrets;                      // 启动时从文件导入的额外 key

    map<uint32_t, RotatingSecrets> rotating_secrets;
    // rotating_secrets 用于 MDS/OSD/Auth 服务解密 ticket
    // key: CEPH_ENTITY_TYPE_MDS / CEPH_ENTITY_TYPE_OSD / CEPH_ENTITY_TYPE_AUTH
};

五、两处持久化的设计原因

5.1 问题根源:Bootstrap 引导悖论

Monitor 的 keyring 存在两处持久化:本地文件和 RocksDB。这并不是冗余备份,而是解决两个完全不同的问题。

核心悖论:

复制代码
Monitor 启动时需要认证才能加入集群
              ↓
认证需要从 RocksDB 里取 keyring
              ↓
读 RocksDB 需要先加入集群(Paxos 同步)
              ↓
加入集群需要认证...
              ↓
              💥 死锁

本地文件 keyring 就是打破这个死锁的引导密钥。

5.2 两处持久化的各自职责

维度 本地文件(/etc/ceph/ceph.mon.keyring RocksDB(Monitor Store)
解决的问题 Bootstrap 引导 运行时权威存储
存的是什么 Monitor 自身 identity key + 初始 client.admin 所有 entity 的完整 keyring
读取时机 进程启动最早期,Paxos 未建立时 加入 quorum 之后
写入时机 集群初始化时生成,之后不再更新 每次 ceph auth add/del/caps,通过 Paxos 提交
谁写 管理员 / 部署工具 Paxos(所有 Monitor 节点一致)
格式 INI 文本文件,人可读 二进制 bufferlist 编码
跨节点 ❌ 仅本地 ✅ 所有 Monitor 强一致同步

5.3 Monitor 启动时序

复制代码
Monitor 进程启动
│
├── Phase 1: 本地独立启动(无需集群)
│   ├── 读 /etc/ceph/ceph.mon.keyring
│   │     → 获得 mon. 的 secret key
│   └── 用这个 key 向其他 Monitor 证明身份
│
├── Phase 2: 加入 Paxos quorum(需要认证)
│   └── Paxos 选举、数据同步
│
└── Phase 3: 集群已就绪
    ├── AuthMonitor::get_initial_keyring()
    │     → mon->store->get("mkfs", "keyring", bl)  // 从 RocksDB 读
    └── 合并进 key_server.data.secrets(内存权威副本)
          → 此后响应所有 client/osd/mds 的认证请求

源码印证(mon/AuthMonitor.cc):

cpp 复制代码
void AuthMonitor::get_initial_keyring(KeyRing *keyring)
{
    bufferlist bl;
    int ret = mon->store->get("mkfs", "keyring", bl);
    if (ret == -ENOENT) {
        return;
    }
    ceph_assert(ret == 0);
    auto p = bl.cbegin();
    decode(*keyring, p);
}

5.4 一个具体的对比例子

假设集群运行了 6 个月,通过 ceph auth add 添加了 1000 个 client:

复制代码
/etc/ceph/ceph.mon.keyring(本地文件)
├── [mon.]          ← 只有这 1 个,6 个月前写入,从未变过
└── [client.admin]  ← 只有这 1 个

RocksDB (auth store)(运行时权威)
├── [mon.]
├── [client.admin]
├── [client.user001]   ← 后来通过 ceph auth add 添加
├── [client.user002]
├── ...
└── [client.user1000]  ← 共 1002 个 entity

Monitor 重启时:只靠本地文件能完成引导(2 个 key 足够认证),加入 quorum 后从 RocksDB 同步,才获得完整的 1002 个 key。

5.5 如果只保留一处会怎样?

假设 后果
只保留 RocksDB,删掉本地文件 Monitor 启动时无法通过认证加入集群(引导死锁)
只保留本地文件,删掉 RocksDB 多 Monitor 节点无法保持 key 一致;运行时新增的 client key 无法持久化;本地文件丢失无法恢复

六、完整流程总结

复制代码
认证(Authentication)  →  Monitor 负责  →  "你是谁"(CephX 双向挑战-应答)
授权(Authorization)   →  MDS 负责      →  "你能做什么"(caps 字符串检查)
数据访问控制            →  OSD 负责      →  "你能读写哪个 pool"

持久化设计:
  本地文件 keyring  →  解决 Bootstrap 引导问题(静态,只写一次)
  RocksDB store     →  解决运行时一致性问题(动态,Paxos 保证多节点同步)
  两者是互补关系,不是冗余关系。

参考源码

文件 内容
auth/cephx/CephxProtocol.cc CephX 握手核心算法(cephx_calc_client_server_challenge
auth/cephx/CephxServiceHandler.cc Monitor 端认证处理(handle_request CEPHX_GET_AUTH_SESSION_KEY)
auth/cephx/CephxClientHandler.cc 客户端握手(build_request
auth/KeyRing.h KeyRing 数据结构
auth/Auth.h EntityAuth 数据结构
auth/cephx/CephxKeyServer.h KeyServerData 数据结构(Monitor 权威存储)
mon/AuthMonitor.cc get_initial_keyring()import_keyring()
mds/MDSAuthCaps.cc MDS caps 权限字符串解析
相关推荐
马立杰4 小时前
Ceph 集群手动部署
ceph·分布式存储
bukeyiwanshui4 小时前
20260528 Ceph 分布式存储 集群配置
分布式·ceph
qq_356408664 小时前
Kubernetes Rook-Ceph 高可用存储部署文档
ceph·容器·kubernetes
潮起鲸落入海5 小时前
ceph集群mon 以及池管理
ceph
一个行走的民5 小时前
Ceph OSD CPU 占用高排查:BlueFS Buffered IO 与 NUMA 亲和性深度分析
ceph
一个行走的民5 小时前
Ceph Monitor 管理职责全景解析
ceph
一个行走的民5 小时前
Ceph Monitor 如何管理文件系统(FS)元数据
ceph
运维栈记5 小时前
Ceph 入门:一文读懂分布式存储的“瑞士军刀”
分布式·ceph
一个行走的民6 小时前
Ceph Monitor 订阅推送机制深度解析
ceph