本文基于 Ceph 源码(
auth/cephx/、auth/KeyRing.h、auth/Auth.h、mon/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 权限字符串解析 |