runsisi's

technical notes

cephx

2019-02-14 runsisi#ceph

我们一直说 cephx 认证,实际上 cephx 是同时支持认证与授权的一整套用户和权限管理体系。

ceph 集群原生支持(不包括 rgw 等网关支持的外部机制)的认证和授权有两种类型:nonecephx,none 顾名思义就是没有相应的认证和授权控制,而 cephx 是 ceph 自定义的一套认证与授权机制。通常来说 none 更简单易用,特别是在客户端侧(如 cinder、nova 等)可以免去所有的一切繁琐配置,不仅可以避免出错而且在高性能环境相比 cephx 性能会有一定的提升。

当然由于 ceph 默认总是启用 cephx,以及避免某天用户以安全为由需要启用 cephx,而 cephfs 的目录授权也与 cephx 关联,同时考虑到运维人员已经熟悉了 cephx 的 用户、key,libvirt 的 secret 等各种概念,推动从 cephx 到 none 的实施存在风险。

这里主要讨论 cephx 的配置、工具使用以及代码实现。

在一个完整的 ceph 网络中,大概存在 mon/mgr/osd/mds/librados 五种类型的进程,如下图所示。需要注意的是 rgw、tcmu 等网关都属于 librados,他们都是 ceph 集群的客户端。

1

名词解释

entity

即 cephx 用户,由 TYPE 和 ID 两个字段组成,如 mon.a, osd.1, client.admin 等,以及与该用户关联的认证与授权信息,TYPE 字段类型包括 mon, mgr, osd, mds, client 一总五种类型,其中 osd 的 ID 是正整数字符串,而其他类型 entity 的 ID 可以是任意字符串;

key

一个 base64 编码的文本字符串,可以理解为用户的密码,用于认证,每个 cephx 用户都有一个关联的 key,一旦指定不能再改变;

caps

即 cephx 用户关联的权限,对于各种类型的服务,可以定义不同的访问权限;

rotating key

本质上也是一种 key,只是带超时属性,这种 key 只用于 ticket 加密,并不关联具体的 cephx 用户,每个 key 有一定的有效时间(ttl),因此有对应的版本号(secret id),每种类型的服务会维护最近生成的三个 key;

keyring

cephx 用户信息的文本表示,包括 entity 名字、key 以及 caps,如:

[client.admin]
        key = AQDPlMlbkWKJFRAAMRiH6wl20K9PK1KpVBSukA==
        auid = 0
        caps mds = "allow *"
        caps mgr = "allow *"
        caps mon = "allow *"
        caps osd = "allow *"

ticket

访问服务(AUTH/MON/OSD/MDS/MGR)的授权凭证,其自身经过加密包含在另一个称作 authorizer 的 bufferlist 中;

配置选项

cephx 的配置包括两块,一是 librados 客户端的配置,一是服务端 daemon 的配置,当然有个别选项对客户端和服务端都生效。

librados 客户端 与 服务端 daemon 同时生效的选项

auth_supported(""),客户端 或 服务端支持的认证类型,具有比 auth_client_required、auth_cluster_requiredauth_service_required 更高的优先级;

cephx_sign_messages(true),是否对消息进行签名和校验,如果 socket 连接不具有 CEPH_FEATURE_MSG_AUTH 特性,可以通过修改该参数要求消息的接收端不进行消息签名的校验;

librados 客户端(包括 librbd,rbd-mirror, rgw 等)选项

auth_client_required(“cephx, none”),客户端支持的认证类型;

服务端 daemon(即 ceph-mon, ceph-mgr, ceph-osd, ceph-mds)选项

cephx_require_signatures(false),CEPH_FEATURE_MSG_AUTH 兼容性参数,当 socket 连接的对端进程类型为 librados 客户端 或 服务端 daemon 生效;

cephx_require_version(1),CEPH_FEATUREMASK_CEPHX_V2 兼容性参数,当 socket 连接的对端进程类型为 librados 客户端 或 服务端 daemon 生效;

cephx_cluster_require_signatures(false),CEPH_FEATURE_MSG_AUTH 兼容性参数,当 socket 连接的对端进程类型为 服务端 daemon 生效;

cephx_cluster_require_version(1),CEPH_FEATUREMASK_CEPHX_V2 兼容性参数,当 socket 连接的对端进程类型为 服务端 daemon 生效;

cephx_service_requre_signatures(false),CEPH_FEATURE_MSG_AUTH 兼容性参数,当 socket 连接的对端进程类型为 librados 客户端 生效;

cephx_service_require_version(1),CEPH_FEATUREMASK_CEPHX_V2 兼容性参数,当 socket 连接的对端进程类型为 librados 客户端 生效;

auth_cluster_required(“cephx”),服务端支持的认证类型,当 socket 连接的对端进程类型为 服务端 daemon 生效;

auth_service_required(“cephx”),服务端支持的认证类型,当 socket 连接的对端进程类型为 librados 客户端 生效;

auth_mon_ticket_ttl(12_hr),AUTH 认证服务的 ticket 有效期,即对应前面提到的 mon 服务的 rotating key 有效期;

auth_service_ticket_ttl(1_hr),MGR、OSD、MDS 授权服务的 ticket 有效期,即对应前面提到的 mgr/osd/mds 服务的 rotating key 有效期;

命令行选项

每个访问 ceph 集群的 librados 客户端或者集群内部的服务 daemon 在运行时都会以指定的 cephx 用户进行认证与授权,其中:

librados 的 api 用户在连接集群时(rados_create2)支持传递类似 client.x 的用户名参数;

服务 daemon 在运行时由命令行参数 --id / -i 或者 --name / -n 指定(服务端的 id / name 在创建时确定);

ceph / rbd / rados 等 cli 程序在运行时由命令行参数 --id / --user 或者 --name / -n

工具

针对 cephx,ceph 提供了两个工具,一是独立的 keyring 操作工具 ceph-authtool,一是 ceph 命令行提供的 auth 模块。需要注意的是 ceph-authtool 只是一个本地的 keyring 操作工具,除了能够生成 base64 编码的 key 字符串,其他的功能所有的文本编辑器都能胜任。而 ceph auth 是对运行的 ceph 集群的操作,在这里我们只讨论 ceph auth 的使用。

ceph auth -h 显示 auth 模块支持的所有命令如下:

auth add <entity> {<caps> [<caps>...]}                 add auth info for <entity> from input file, or random key if no input is given, and/or any
                                                        caps specified in the command
auth caps <entity> <caps> [<caps>...]                  update caps for <name> from caps specified in the command
auth del <entity>                                      delete all caps for <name>
auth export {<entity>}                                 write keyring for requested entity, or master keyring if none given
auth get <entity>                                      write keyring file with requested key
auth get-key <entity>                                  display requested key
auth get-or-create <entity> {<caps> [<caps>...]}       add auth info for <entity> from input file, or random key if no input given, and/or any caps
                                                        specified in the command
auth get-or-create-key <entity> {<caps> [<caps>...]}   get, or add, key for <name> from system/caps pairs specified in the command.  If key already
                                                        exists, any given caps must match the existing caps for that key.
auth import                                            auth import: read keyring file from -i <file>
auth ls                                                list authentication state
auth print-key <entity>                                display requested key
auth print_key <entity>                                display requested key
auth rm <entity>                                       remove all caps for <name>

上述的命令实际上有一些是重复的,而且有些命令的解释很混乱,下面分类总结如下:

增加

auth add,增加一个用户,可以通过命令参数指定权限并随机生成一个 key,也可以从一个 keyring 文件(该文件定义了 key 和权限)导入用户(支持重复导入,但不支持权限的修改,需要注意与下面 auth import 的区别);

~# ceph auth add client.x mon 'allow *' osd 'allow *'
~# ceph auth add client.x -i keyring

auth import,导入用户 keyring 文件中定义的所有用户(支持重复导入),也可以用于更新权限,即在 keyring 文件修改用户权限然后导入,可以将用户原有的权限修改为 keyring 文件中定义的一致;

~# ceph auth import -i keyring

auth get-or-create,和 auth add 类似,但存在一个不同点:如果用户已存在,该命令会返回已存在的用户 key 信息(内容为 keyring 格式,包括用户名 + key,但不包含权限信息);

auth get-or-create-key,和 auth get-or-create 唯一的区别是返回的信息仅包含 key(不包含任何其它信息),而 auth get-or-create 返回的信息中包含用户名;

删除

auth del,删除用户,包括所有的权限;

auth rm,和 auth del 一模一样;

查询

auth ls,列出所有的用户及其关联的权限;

auth export,获取所有用户或指定用户的 key 和权限信息(标准输出流的内容为 keyring 格式,包括用户名 + key + 权限,标准错误流输出一句提示性的话);

~# ceph auth export -o keyring
~# ceph auth export client.x -o keyring

auth get,与 auth get 的区别为仅用于获取指定用户的 key 和权限信息(标准输出流的内容为 keyring 格式,包括用户名 + key + 权限,标准错误流输出一句提示性的话);

auth get-key,与 auth get 的区别为仅用于获取指定用户的 key(不包含任何其它信息);

auth print-key,与 get-key 一模一样;

auth print_key,与 get-key 一模一样;

更新

auth caps,设置用户的权限,不管用户之前设置的权限如何,使用命令行中提供的权限替换已有的权限;

~# ceph auth caps client.x mon 'allow *' osd 'allow *' mds 'allow *'

用户信息存储

集群中的用户信息(包括用户名、 key 和 权限)都固化在 mon 的 kv 数据库中(由于 paxos 的存在,所有的 monitor 拥有相同的数据)。

mon 在上电时会从自己的底层 kv 数据中将所有用户的数据都加载进内存,同时加载 mon 数据目录中自身的 keyring 文件中的 key;

其他的服务进程(osd/mds/mgr)在上电时会加载各自数据目录 keyring 文件中的 key;

librados 客户端在上电时都会读取命令行指定的 key 字符串或 keyring 文件;

而前面提到的 rotating key 通过 monclient 从 mon 获取并保存在内存中(librados 客户端并不直接拿到明文的 key,所有的授权都以 ticket 的形式从 mon 获取,而 osd/mds/mgr 既从 mon 获取它需要的其他服务的 ticket,也以明文的形式获取属于自身服务类型的 rotating key)。

认证及授权流程

在 cephx 中,认证 是指除 mon 之外的所有进程(包括 librados/osd/mds/mgr)在上电过程中,首先要通过 monclient 与 mon 交互并验证自身身份(即 用户名 + key 匹配),一旦验证通过,monclient 将得到一个 AUTH 服务的 ticket,作为后续获取其它服务(mon/osd/mds/mgr)ticket 的授权凭证。而 授权 是指 ceph 环境中的所有进程在访问其他目标进程时,在 socket 连接建立会话时会验证 ticket 的合法性并保存权限 在会话信息中,在后续的请求(如 op、command)处理时会校验是否具有请求所需的权限。

需要强调的是 ticket 只会在会话建立时进行校验,一旦会话建立成功,权限信息(caps)就已经保存在会话信息中了,因此虽然 ticket 有 ttl,但是只要会话不重建,会话中保存的权限信息不会随 ticket 的更新而得到更新。

在 cephx 认证与授权体系中,总共有 AUTH/MON/MGR/OSD/MDS 五种服务的授权 ticket,其中具体 mon/mgr/osd/mds/librados 各种进程需要哪些服务的 ticket 是由进程自身的特性决定的,实际上就是由进程需要请求哪些类型的服务决定的,总结如下:

mon: MON/MGR
mgr: AUTH/MON/MGR/OSD/MDS
osd: AUTH/MON/MGR/OSD
mds: AUTH/MON/MGR/OSD/MDS
librados: AUTH/MON/MGR/OSD

在 ceph 集群中,mon 是绝对的核心,如下图所示。

2

mon 与所有的进程都有交互,大部分情况下它都是被动接受其它进程的连接请求,其中 mgr/osd/mds/librados 进程中都存在一个 monclient 模块,与 mon 的交互都是通过这个 monclient 模块进行。但 mon 自身也会存在主动连接其它 mon 或 mgr 的情况,即图中从 mon 到 mgr 的虚线,需要注意的是 mgr 自身首先是 mon 的客户端,其认证、授权流程与其它服务进程并无两样,只是说 mon 可能存在主动发往 mgr 的请求。由于 mon 存储了所有的用户数据,因此 mon 的流程与 mgr/osd/mds/librados 的行为稍有不同,但所使用的 cephx 背后的算法原理是一致的。

mgr/osd/mds/librados

这些进程上电之后与 ceph 集群建立联系的第一步是调用 monclient.authenticate() 接口进行认证,认证完成之后,monclient 将从 mon 得到多个服务的授权 ticket,其大致的交互流程如下图所示。

3

AUTH 服务的 ticket 实际上是 monclient 向 mon 认证之后得到的第一个 ticket,即代码中的 CEPHX_GET_AUTH_SESSION_KEY 请求类型,其过程类似于 CHAP 认证,CephxServiceHandlerCephxClientHandler 各生成一个 64 位随机数,然后组合并计算其哈希值,如果各自得到的哈希值一致,则认证成功,monclient 将得到 AUTH 服务的 session key 与 ticket。

紧接着,monclient 将向 mon 请求其它服务的 ticket,即代码中的 CEPHX_GET_PRINCIPAL_SESSION_KEY 请求类型,而其在请求过程中所拿的授权凭证就是前面得到的 AUTH 服务的 ticket,在请求成功之后,monclient 将得到各服务的 session key 以及对应的 ticket。

对于 mgr/osd/mds 而言,还一个请求各自 rotating key 的过程,即代码中的 CEPHX_GET_ROTATING_KEY 请求类型,之所以要请求 rotating key,是因为访问这些服务的客户端(不一定是 librados 客户端,也可以是集群的其它服务进程)所持有的 ticket 需要使用 rotating key 进行解密。

AUTH ticket 的组成如下:

uint32_t service_id
__u8 service_ticket_v
CephXServiceTicket msg_a // encrypted by entity key
  CryptoKey session_key;
  utime_t validity;
CephXTicketBlob blob
  uint64_t secret_id;
  bufferlist blob; // encrypted by AUTH rotating key + old AUTH session key
    struct CephXServiceTicketInfo {
      AuthTicket ticket;
      CryptoKey session_key;
    };

对于 AUTH ticket,monclient 第一次请求得到的 ticket 只被 entity 的 key 进行了加密,而后续更新得到的 ticket 将先被 AUTH 服务的 rotating key 加密,然后再被老的 AUTH session key 加密。

MON/MGR/OSD/MDS ticket 的组成如下:

uint32_t service_id
__u8 service_ticket_v
CephXServiceTicket msg_a // encrypted by AUTH session key
  CryptoKey session_key;
  utime_t validity;
CephXTicketBlob blob
  uint64_t secret_id;
  bufferlist blob; // encrypted by service rotating key
    struct CephXServiceTicketInfo {
      AuthTicket ticket;
      CryptoKey session_key;
    };

session key 用在访问服务的授权过程中,更具体的:

CEPHX_GET_PRINCIPAL_SESSION_KEY 过程中,直接调用 CEPH_ENTITY_TYPE_AUTHCephXTicketHandler::build_authorizer(),或者用在访问 MON/MGR/OSD/MDS 服务的建链过程中,monclient 通过调用 MonClient::build_authorizer(service_id) 最终调用 CephXTicketHandler::build_authorizer()

前面提到,授权实际上包括两个过程,一是在与服务进程建链过程中将 entity 的权限设置在会话中,一是在处理具体的访问请求时校验相应的权限。第一阶段的处理在访问 MON 服务和 MGR/OSD/MDS 时稍有不同。对于 MON 服务而言,客户端在认证过程中 caps 通过 AuthMonitor::prep_auth(...) 附带的设置在 mon 的会话信息中,而对于 MGR/OSD/MDS 服务而言,客户端需要显式的调用 MonClient::build_authorizer(service_id),然后在服务端的 connect 消息处理时调用 CephxAuthorizeHandler::verify_authorizer(...) 校验授权 ticket 的合法性,然后调用 Dispatcher::ms_handle_authentication(conn) 将 caps 字符串转成具体的 caps 设置到会话中,如下图所示。

4

mon

mon 需要访问 MON/MGR 服务,因此它同样需要访问这两种服务的授权 ticket,而 mon 自身的认证隐含在访问其他 MON 服务的过程中。

MON 服务只有一个固定的全局唯一的 entity key(即所有的 mon 进程共用同一个 key),没有 rotating key(因此生成 ticket 所需的 service secret id 参数为 -1),其访问 MGR 所需的 ticket 的与 monclient 请求的 MGR ticket 并无二致,但访问 MON/MGR 服务所需的 ticket 更具体的即 authorizer)都是临时生成的,因此不会像 monclient 一样定时去更新 ticket 。具体的实现可以阅读代码 Monitor::ms_get_authorizer(...) 的实现。

5

cephx 在认证和授权之外,还有一个签名的功能,其实就是对消息的基本信息进行哈希计算,当前的实现其实和 cephx 并没有太大的关联,其流程如下图所示。

6

权限(caps)表示方法

我们常见在权限标识方法举例如下:

mon 'allow *'
mon 'allow profile osd'
osd 'allow profile rbd'
osd 'allow class-read object_prefix rbd_children, allow rwx pool=images'

MON/MGR/OSD/MDS 每种服务都有自己的二进制权限记录方式,但其外部表现都是一个字符串,当然每种字符串有不同的语法规则。

MON 的权限表示方式,可以参考 MonCap.h/cc 的实现。其中需要特别注意 MON 有 allow service mds rw 这种表现形式,其中的 service 类型并不是我们前面一直提到的 AUTH/MON/MGR/OSD/MDS 这种服务,而是指的 paxos service;

MGR 复用了 MON 的权限表示方式;

OSD 可以参考 OSDCap.h/cc 的实现;

MDS 可以参考 MDSAuthCaps.h/cc 的实现;

权限判断最终都体现在各自的 is_capable 函数实现上。

各种类型的服务权限都有其底层的表现形式,所谓的 profile 类型实际上只是一系列权限定义的简称,最终都会转换成其底层的表现形式,比如 mgr 的 allow profile rbd 在底层实际上展开得到的权限为空,即实际上并没有赋予任何权限。

补充说明

在新代码中,allow profile xxxprofile xxx 是等价的。

补充一个权限清空导致类似硬盘坏块的故障分析:

cephx 分两个部分:认证 + 授权

  1. 客户端与集群建立联系的第一步就是向 mon 请求认证;
  2. 认证成功之后客户端会向 mon 请求 mon/osd/mds/mgr 4 个服务的授权 ticket(ticket 中包含客户端的权限信息);
  3. 客户端与 mon 之间 socket 断掉之后的重新建立会话过程也会重复上面两个步骤;
  4. 客户端与 osd 之间在建立会话时 osd 会校验客户端携带的授权 ticket 的合法性(但是不检测权限);
  5. 客户端对 osd 的访问权限信息在会话建立成功后保存在会话信息中;
  6. 后续 osd 对客户端的所有 op 都会依据会话中保存的权限信息进行权限检测;

依据上面的描述,一个刚开始正常访问的客户端在删除权限之后要经过怎样的过程才能导致其 op 因为权限问题而被拒绝:

  1. 客户端要与 mon 重新建立会话,这样才能更新客户端访问 osd 服务的授权 ticket;
  2. 由于 ticket 有 ttl,因此即使不重新建立会话,默认 1 小时之后也会更新 ticket;
  3. 客户端要与 osd 重新建立会话,这样才能更新客户端与 osd 之间会话保存的权限信息(即用新的 ticket 来更新会话中保存的权限);
  4. 客户端与 osd 重新建立会话有多种途径,如:1)网络故障导致 socket 重新建链,2)socket 链路无 IO 15 分钟之后自动断链,3)客户端进程重启,4)osd 进程重启;
  5. 经过上面的步骤,osd 会话中的权限信息得到更新,从而得知客户端的权限已经被删除,因此后续的所有客户端 op 都会返回 -EPERM(通过初步阅读代码,librbd、qemu rbd 驱动,应该都透传了此处的错误码),最后在操作系统的块层返回 IO 错误,导致文件系统只读;

附件

附件为 librados 客户端访问 mon、以及 librados 客户端访问 osd 在 cephx 和 none 两种认证和授权系统下的报文交互流程。

cephx-rados-mon.pcapng

cephx-rados-mon-osd.pcapng

none-rados-mon.pcapng

none-rados-mon-osd.pcapng