# 第 12 册 · RGW 对象存储篇(四):安全、鉴权、加密与高级特性 > 基于 Ceph 官方文档整理 | 文档版本:**Tentacle v20.2.4** | 抓取日期:2026-09-17 | 源文:ceph/ceph@v20.2.4 `doc/radosgw/` > 官网入口:https://docs.ceph.com/en/tentacle/radosgw/ --- ## 本册导读 RGW 对象存储篇共四册,本册为**第四册**,聚焦 Ceph Object Gateway(radosgw)的**安全与高级特性**,即从"能跑起来"走向"能安全可控地对外提供服务"所需要的全部能力。 | 册 | 主题 | 本册关系 | |---|---|---| | 第 9 册 | RGW 基础与部署 | 前置:把网关跑起来 | | 第 10 册 | RGW 多站点与数据同步 | 前置:跨集群复制 | | 第 11 册 | RGW 运维与可观测性 | 平行:日常管理 | | **第 12 册(本册)** | **安全、鉴权、加密与高级特性** | **加密 → 密钥管理 → IAM/STS → 桶策略 → 外部身份源 → MFA → Lua 扩展 → 硬件加速** | 本册覆盖的 20 个官方页面: | 分类 | 页面 | |---|---| | 加密 | `encryption` | | 密钥管理 | `vault`、`kmip`、`barbican` | | 身份与访问 | `iam`、`role`、`STS`、`STSLite`、`session-tags`、`bucketpolicy` | | 外部身份源 | `keystone`、`ldap-auth`、`oidc`、`keycloak`、`opa` | | 多因素认证 | `mfa` | | 可编程扩展 | `lua-scripting` | | 硬件加速 | `qat-accel`、`uadk-accel` | | API 入口 | `api` | **阅读建议** - 如果你负责**数据合规**:优先读第一、二章(SSE-S3 / SSE-C / SSE-KMS 与 Vault/KMIP/Barbican 集成)。 - 如果你负责**多租户权限体系**:优先读第三、四、五章(IAM → Role/STS → Bucket Policy),这三章存在明确的依赖与优先级关系。 - 如果你要**对接公司统一身份**:读第六章(Keystone / LDAP / OIDC / Keycloak / OPA)。 - 如果你要做**网关侧定制逻辑**(改写 header、鉴权、限流、审计):读第八章 Lua 脚本扩展,这是本册体量最大、字段最多的一章。 **本册的默认值来源标注**:rst 源文中 `.. confval::` 的默认值是构建期占位符,凡涉及默认值处,本册均改为从 `src/common/options/*.yaml.in` 查询真实默认值,并标注「(默认值取自 src/common/options/*.yaml.in @ v20.2.4)」。 --- ## 一、服务端加密(encryption.rst) ### 1.1 基本概念 Ceph Object Gateway 支持对上传对象做**服务端加密(Server-Side Encryption, SSE)**。所谓"服务端加密"的含义是: - 数据**通过 HTTP 以明文形式**发送给 RGW; - RGW 把数据**以加密形式**存入 Ceph 存储集群(OSD)。 因此有两条硬性前提(官方以 `note` 形式强调): 1. **服务端加密的请求必须通过安全 HTTPS 连接发送**,否则等于把密钥明文送到网络上。 2. 如果使用代理做 SSL 终止(SSL termination),则**必须先启用 `rgw trust forwarded https`**,RGW 才会把被转发的请求视为安全请求。 3. **服务端加密密钥必须是 256 位长度、base64 编码。** > 引自:[Encryption](https://docs.ceph.com/en/tentacle/radosgw/encryption/) ### 1.2 三种密钥管理模式对比 RGW 提供 3 种密钥管理方式,对应 Amazon S3 的三套规范。 | 模式 | 全称 | 密钥由谁管理 | 每次请求是否带密钥 | 对应的 Amazon 规范 | Ceph 侧是否需要配置 | 当前支持的 KMS 后端 | |---|---|---|---|---|---|---| | **SSE-C** | Customer-Provided Keys(客户提供密钥) | **客户端**全权管理,客户端必须自己记住"哪个对象用了哪把密钥" | 是,读/写加密数据的每个请求都要带上密钥 | `Amazon SSE-C` | **不需要任何特殊 Ceph 配置** | 不适用(无 KMS) | | **SSE-KMS** | Key Management Service | **管理员**把密钥存放在安全的密钥管理服务中,RGW 按需取回用于加解密 | 否,请求只带 key ID | `Amazon SSE-KMS` | 需要配置 `rgw crypt s3 kms backend` 及对应后端参数 | **Barbican、Vault、KMIP** 三者已实现;原则上任何 KMS 都可以接 | | **SSE-S3** | S3-Managed Keys | 密钥**仍然存放在 Vault 中**,但由 Ceph **自动创建、自动删除、按需取回**,对用户完全不可见 | 否 | `Amazon SSE-S3` | 需要配置 SSE-S3 相关 Vault 参数 | **目前仅实现 Vault**;原则上任何 KMS 都可以接 | 关键差异提炼: - **SSE-C**:密钥不出客户端,RGW 不保存任何密钥,客户端丢了密钥就等于丢了数据;零服务端配置成本。 - **SSE-KMS**:密钥集中在 KMS,管理员可控可轮换可吊销;桶拥有者以 key ID 引用密钥。 - **SSE-S3**:对应用最友好(不用管密钥),代价是密钥生命周期由 Ceph 托管,且**只能接 Vault**。 官方同时提醒:**当同时使用 SSE-KMS 和 SSE-S3 时,应把二者指向不同的容器**(可以用不同的 Vault 实例、分别挂载的 transit 实例、或同一 transit 挂载点下的不同分支),通过 `rgw_crypt_vault_prefix` 与 `rgw_crypt_sse_s3_vault_prefix` 区分;给 SSE-KMS 桶拥有者授予 Vault 权限时,**不应**让他们能碰 SSE-S3 的密钥——那部分只应由 Ceph 自己操作。 ### 1.3 请求头 > ⚠️ **本册范围内 `encryption.rst` 并未逐项列出 SSE 请求头**,它只指向 Amazon 的 SSE-C / SSE-KMS / SSE-S3 规范页面。因此下表只收录**本册其它源文中确实出现过**的头部名(来自 `bucketpolicy.rst` 的 condition key 列表),未在源文中出现的头部一律标注「文档未明确」。 | 请求头 | 出现位置 | 说明 | |---|---|---| | `x-amz-server-side-encryption` | `bucketpolicy.rst` 中 `s3:PutObject` 的 condition key `s3:x-amz-server-side-encryption` | 指定服务端加密算法/模式 | | `x-amz-server-side-encryption-aws-kms-key-id` | `bucketpolicy.rst` 中 `s3:PutObject` 的 condition key `s3:x-amz-server-side-encryption-aws-kms-key-id` | 指定 SSE-KMS 的 key ID | | `x-amz-server-side-encryption-customer-algorithm` | `bucketpolicy.rst` 中 `s3:PutObject` 的 condition key `s3:x-amz-server-side-encryption-customer-algorithm` | 指定 SSE-C 的客户密钥算法 | | SSE-C 的密钥本体头(AWS 中为 `x-amz-server-side-encryption-customer-key`) | 未出现 | **文档未明确** | | SSE-C 的密钥 MD5 头(AWS 中为 `x-amz-server-side-encryption-customer-key-MD5`) | 未出现 | **文档未明确** | > 💡 补充:上表前三项之所以能被确认,是因为 RGW 的桶策略引擎把这些 HTTP 头暴露成了 policy condition key(`s3:x-amz-*` 前缀),这间接证明了 RGW 在 `PutObject` 路径上确实读取这些头。 ### 1.4 桶加密 API RGW 支持 Bucket Encryption APIs,用于以桶为粒度配置 SSE-S3 或 SSE-KMS: - `PutBucketEncryption` - `GetBucketEncryption` - `DeleteBucketEncryption` > 引自:[Encryption](https://docs.ceph.com/en/tentacle/radosgw/encryption/) ### 1.5 自动加密(仅供测试) 可以在 `ceph.conf` 中设置 `rgw crypt default encryption key`,强制加密**所有没有另行指定加密模式的对象**。该配置接受一个 base64 编码的 256 位密钥。 ```ini rgw crypt default encryption key = 4YSmvJtBv0aZ7geVgAsdpRnLBEwWSWlMIGnRS8a9TSA= ``` 配置项: | 配置项 | 类型 | 默认值 | 说明 | |---|---|---|---| | `rgw_crypt_default_encryption_key` | str | **文档未明确**(`rgw.yaml.in` 中该条目无 `default:` 字段,level 为 `dev`) | base64 编码的 256 位密钥,用于强制加密未指定加密模式的对象 | > ⚠️ 官方以 `important` 强调:**此模式仅用于诊断/测试目的!** ceph 配置文件不是安全的密钥存储方式,以这种方式意外暴露的密钥应视为**已泄露**。 相关安全开关: | 配置项 | 类型 | 默认值 | 说明 | |---|---|---|---| | `rgw_crypt_require_ssl` | bool | `true`(默认值取自 src/common/options/rgw.yaml.in @ v20.2.4) | 携带加密密钥头的请求必须通过 ssl 发送 | | `rgw_crypt_suppress_logs` | bool | `true`(默认值取自 src/common/options/rgw.yaml.in @ v20.2.4) | 抑制可能打印出客户端密钥的日志 | | `rgw_trust_forwarded_https` | bool | **文档未明确**(`rgw.yaml.in` 中该条目无 `default:` 字段) | 当 radosgw 前有代理做 SSL 终止时,信任 `Forwarded` 与 `X-Forwarded-Proto` 头判断连接是否安全。**服务端加密功能需要此选项。** 若前方没有受信任的代理,**绝不要开启**,否则恶意用户可在任意请求中伪造这些头 | > 引自:[Encryption](https://docs.ceph.com/en/tentacle/radosgw/encryption/) --- ## 二、密钥管理集成 RGW 的 SSE-KMS / SSE-S3 需要一个外部 KMS。当前官方实现了三套:Vault、KMIP、Barbican。 ### 2.1 HashiCorp Vault(vault.rst) #### 2.1.1 整体交互流程 ``` +---------+ +---------+ +-------+ +-------+ | Client | | RadosGW | | Vault | | OSD | +---------+ +---------+ +-------+ +-------+ | create secret | | | | key for key ID | | | |-----------------+---------------->| | | | | | | upload object | | | | with key ID | | | |---------------->| request secret | | | | key for key ID | | | |---------------->| | | |<----------------| | | | return secret | | | | key | | | | | | | | encrypt object | | | | with secret key | | | |--------------+ | | | | | | | | |<-------------+ | | | | | | | | store encrypted | | | | object | | | |------------------------------>| ``` 要点:客户端只传 **key ID**;RGW 用 key ID 向 Vault 换取密钥,在 RGW 侧完成加密,密文落到 OSD。 下文示例使用 Vault 命令行工具,需要先设置: ```bash export VAULT_ADDR='https://vault-server-fqdn:8200' ``` #### 2.1.2 Vault Secrets Engines RGW 当前支持两种 secrets engine: | Engine | 启用命令 | RGW 侧配置项取值 | |---|---|---| | **KV secrets engine version 2** | `vault secrets enable -path secret kv-v2` | `rgw crypt vault secret engine = kv` | | **Transit engine** | `vault secrets enable transit` | `rgw crypt vault secret engine = transit` | > 引自:[HashiCorp Vault Integration](https://docs.ceph.com/en/tentacle/radosgw/vault/) #### 2.1.3 Vault 认证方式 官方支持两种:`Token authentication` 与 `Vault agent`。 **为什么强烈建议用 Vault agent**:Vault 中除 root token 外的绝大多数 token 都有生命周期限制,需要周期性地"重新初始认证"或"续期(renew)",而 **Ceph 自身没有任何逻辑去做这两件事**。最简单的正确做法是**同时运行 Vault agent,由它刷新 token 文件**;在这种模式下可以用文件系统权限来限制谁能使用 token。 Vault agent 也可以配置成**代理模式**:它自动为请求加上 token 再转发给真实 Vault server,同时仍然负责 token 续期。此时**必须保护好 RGW 到 Vault agent 的网络路径**(例如让 agent 只监听 localhost)。 ##### Token Policies(RGW 所需的最小权限) 所有 Vault token 的权限由挂在其上的 policy 决定。应当只授予配置所必需的最小权限。 **KV secret engine**: ```bash vault policy write rgw-kv-policy -< ⚠️ 官方 note:**生产环境绝不要使用 root token 配合 Ceph。** ```ini rgw crypt vault auth = token rgw crypt vault token file = /run/.rgw-vault-token rgw crypt vault addr = https://vault-server-fqdn:8200 ``` 出于安全考虑,**token 文件必须只有 Object Gateway 可读**。 ##### Vault Agent Vault agent 是一个客户端守护进程,负责完成对 Vault 的认证并管理 token 续期与缓存,通常与 Object Gateway 运行在同一主机上。借助 Vault agent,可以使用 AppRole、AWS、Certs、JWT、Azure 等其它 Vault 认证机制。 ```ini rgw crypt vault auth = agent rgw crypt vault addr = http://127.0.0.1:8100 ``` 配置 AppRole: ```bash vault write auth/approle/role/rgw-ap \ token_policies=rgw-transit-policy,default \ token_max_ttl=60m ``` 获取 `role-id`(保存到例如 `/usr/local/etc/vault/.rgw-ap-role-id`): ```bash vault read auth/approle/role/rgw-ap/role-id -format=json | \ jq -r .data.role_id ``` 获取 `secret-id`(保存到例如 `/usr/local/etc/vault/.rgw-ap-secret-id`): ```bash vault read auth/approle/role/rgw-ap/role-id -format=json | \ jq -r .data.role_id ``` > 💡 补充:源文中"Get the secret-id"小节给出的命令与"Get the role-id"完全相同(都是读 `role-id` 并取 `.data.role_id`),这显然是官方文档的复制粘贴错误。正确的 secret-id 获取命令应当是 `vault write -f auth/approle/role/rgw-ap/secret-id -format=json | jq -r .data.secret_id`,但本册遵循"不编造"原则,仅指出源文此处存疑,不将其作为可执行命令推荐。 Vault agent 配置文件示例(`rgw-agent.hcl`): ```hcl pid_file = "/run/rgw-vault-agent-pid" auto_auth { method "AppRole" { mount_path = "auth/approle" config = { role_id_file_path ="/usr/local/etc/vault/.rgw-ap-role-id" secret_id_file_path ="/usr/local/etc/vault/.rgw-ap-secret-id" remove_secret_id_file_after_reading ="false" } } } cache { use_auto_auth_token = true } listener "tcp" { address = "127.0.0.1:8100" tls_disable = true } vault { address = "https://vault-server-fqdn:8200" } ``` 启动常驻守护进程: ```bash /usr/local/bin/vault agent -config=/usr/local/etc/vault/rgw-agent.hcl ``` 启动后,agent 应在 localhost 的 8100 端口监听,可用 `vault` 命令与之交互验证。 #### 2.1.4 Vault Namespaces Vault 企业版支持 namespace(命名空间)概念,用于在同一个组织内为不同团队做集中管理并保证彼此隔离(租户)。 ```ini rgw crypt vault namespace = tenant1 ``` #### 2.1.5 在 Vault 中创建密钥 > ⚠️ 服务端加密密钥必须 256 位长、base64 编码。 **KV engine**: ```bash vault kv put secret/myproject/mybucketkey key=$(openssl rand -base64 32) ``` 样例输出: ```text ====== Metadata ====== Key Value --- ----- created_time 2019-08-29T17:01:09.095824999Z deletion_time n/a destroyed false version 1 ``` 注意:KV secrets engine 中 secret 以 key-value 对形式存储,**RGW 期望 key 名必须是 `key`**,即 secret 必须是 `key=` 形式。 **Transit engine**: 为 transit engine 创建的密钥**不应再标记为 exportable**: ```bash vault write -f transit/keys/mybucketkey ``` 该命令创建一个 keyring,默认包含类型为 `aes256-gcm96` 的密钥。验证: ```bash vault read transit/keys/mybucketkey ``` 样例输出: ```text Key Value --- ----- derived false exportable false name mybucketkey type aes256-gcm96 ``` #### 2.1.6 配置 Ceph Object Gateway ```ini # 1. 启用 Vault 作为 SSE-KMS 后端 rgw crypt s3 kms backend = vault # 2. 选择认证方式(二选一) rgw crypt vault auth = token rgw crypt vault token file = /run/.rgw-vault-token rgw crypt vault addr = https://vault-server-fqdn:8200 # 或 rgw crypt vault auth = agent rgw crypt vault addr = http://localhost:8100 # 3. 选择 secrets engine(二选一) rgw crypt vault secret engine = kv # 或 rgw crypt vault secret engine = transit # 4. 可选:Vault namespace rgw crypt vault namespace = tenant1 # 5. 可选:限制 RGW 取密钥的 URL 路径前缀 rgw crypt vault prefix = /v1/secret/data # 或 rgw crypt vault prefix = /v1/transit # 6. 自定义 SSL 证书 rgw crypt vault verify ssl = true rgw crypt vault ssl cacert = /etc/ceph/vault.ca rgw crypt vault ssl clientcert = /etc/ceph/vault.crt rgw crypt vault ssl clientkey = /etc/ceph/vault.key ``` 其中 `vault.ca` 是 CA 证书,`vault.key` / `vault.crt` 是为 RGW 访问 Vault server 生成的私钥与 SSL 证书。**官方强烈建议 `rgw crypt vault verify ssl` 设为 `true`;设为 `false` 非常危险,在这样高安全要求的环境中必须避免。** #### 2.1.7 Vault 配置项逐项表格 | 配置项 | 类型 | 默认值(`src/common/options/*.yaml.in` @ v20.2.4) | 说明 | |---|---|---|---| | `rgw_crypt_s3_kms_backend` | str | `barbican` | SSE-KMS 加密密钥存放位置。可选值枚举:`barbican`、`vault`、`testing`、`kmip` | | `rgw_crypt_sse_s3_backend` | str | `vault` | SSE-S3 加密密钥存放位置。**唯一合法取值是 `vault`** | | `rgw_crypt_vault_auth` | str | `token` | 与 Vault 交互使用的认证方式。枚举值:`token`、`agent` | | `rgw_crypt_vault_token_file` | str | 文档未明确(无 `default:`) | 认证方式为 `token` 时,token 文件路径(安全上应仅 Rados Gateway 可读) | | `rgw_crypt_vault_addr` | str | 文档未明确(无 `default:`) | Vault server 基地址,例如 `http://vaultserver:8200` | | `rgw_crypt_vault_prefix` | str | 文档未明确(无 `default:`) | Vault secret URL 前缀,用于限制 RGW 可访问的 secret 空间子集,例如 `/v1/secret/data` | | `rgw_crypt_vault_secret_engine` | str | `transit` | 用于取回加密密钥的 Vault Secret Engine:`kv-v2` 或 `transit` | | `rgw_crypt_vault_namespace` | str | 文档未明确(无 `default:`) | Vault Namespace,用于选择租户,例如 `acme/tenant1`(仅 Vault 企业版可用) | | `rgw_crypt_vault_verify_ssl` | bool | `true` | RGW 是否校验 Vault server 的 SSL 证书 | | `rgw_crypt_vault_ssl_cacert` | str | 文档未明确(无 `default:`) | 访问 Vault server 使用的自定义 CA 证书路径 | | `rgw_crypt_vault_ssl_clientcert` | str | 文档未明确(无 `default:`) | 访问 Vault server 使用的自定义客户端证书路径 | | `rgw_crypt_vault_ssl_clientkey` | str | 文档未明确(无 `default:`) | 客户端证书所需的私钥路径 | | `rgw_crypt_sse_s3_vault_auth` | str | `token` | SSE-S3 + Vault 的认证方式。枚举值:`token`、`agent` | | `rgw_crypt_sse_s3_vault_token_file` | str | 文档未明确(无 `default:`) | SSE-S3 场景下 token 文件路径 | | `rgw_crypt_sse_s3_vault_addr` | str | 文档未明确(无 `default:`) | SSE-S3 场景 Vault server 基地址 | | `rgw_crypt_sse_s3_vault_prefix` | str | 文档未明确(无 `default:`) | SSE-S3 场景 Vault secret URL 前缀 | | `rgw_crypt_sse_s3_vault_secret_engine` | str | `transit` | SSE-S3 场景的 Secret Engine,**唯一合法取值是 `transit`** | | `rgw_crypt_sse_s3_vault_namespace` | str | 文档未明确(无 `default:`) | SSE-S3 场景 Vault Namespace | | `rgw_crypt_sse_s3_vault_verify_ssl` | bool | `true` | SSE-S3 场景是否校验 Vault SSL 证书 | | `rgw_crypt_sse_s3_vault_ssl_cacert` | str | 文档未明确(无 `default:`) | SSE-S3 场景自定义 CA 证书路径 | | `rgw_crypt_sse_s3_vault_ssl_clientcert` | str | 文档未明确(无 `default:`) | SSE-S3 场景客户端证书路径 | | `rgw_crypt_sse_s3_vault_ssl_clientkey` | str | 文档未明确(无 `default:`) | SSE-S3 场景客户端私钥路径 | | `rgw_crypt_sse_s3_key_template` | str | `%bucket_id` | SSE-S3 在 Vault 中每桶密钥的模板。可含 `%bucket_id`(展开为桶 marker,即分配给该桶的唯一 uuid)与 `%owner_id`(展开为拥有者 id);其它 `%` 用法保留、不应使用。**若模板包含 `%bucket_id`,桶被删除时关联密钥会被自动移除** | | `rgw_crypt_kmip_kms_key_template` | str | 文档未明确(无 `default:`) | SSE-KMS 场景下 kmip 密钥名模板 | | `rgw_crypt_kmip_s3_key_template` | str | `$keyid` | SSE-S3 场景下 kmip 密钥模板 | > 默认值说明:上表所有非空默认值均取自 `src/common/options/rgw.yaml.in` @ v20.2.4。 #### 2.1.8 Transit Engine 兼容性支持 Transit engine 对旧版 Ceph 有兼容支持(旧版把 transit engine 当作简单的密钥仓库使用)。可用 `compat` 选项配置: | 取值 | 语义 | 适用 | |---|---|---| | `transit compat=0` | **完全关闭向后兼容**。未来版本将以此为默认值;当前版本的新装环境使用它是安全的 | 全新安装 | | `transit compat=1` | **当前版本的正常默认值**。对新创建的对象启用新引擎,同时仍允许旧引擎用于旧对象。此时给 Ceph 的 Vault token 必须**同时具备新旧两套 transit policy** | 默认 | | `transit compat=2` | 强制只使用旧引擎。**若 Vault prefix 以 `export/encryption-key` 结尾(这是此前文档记载的配置),会自动选中此模式** | 兼容旧部署 | ```ini rgw crypt vault secret engine = transit compat=0 rgw crypt vault secret engine = transit compat=1 rgw crypt vault secret engine = transit compat=2 ``` #### 2.1.9 上传对象 **KV engine**(注意 key ID 要带上路径): ```bash aws --endpoint=http://radosgw:8000 s3 cp plaintext.txt s3://mybucket/encrypted.txt --sse=aws:kms --sse-kms-key-id myproject/mybucketkey ``` **Transit engine(new flavor)**: ```bash aws --endpoint=http://radosgw:8000 s3 cp plaintext.txt s3://mybucket/encrypted.txt --sse=aws:kms --sse-kms-key-id mybucketkey ``` RGW 会从 Vault 取回密钥、加密对象并存入桶;任何下载请求都会自动从 Vault 取回对应密钥并解密。 密钥的取回 URL 由三段拼接而成:`rgw crypt vault addr`(基地址)+ `rgw crypt vault prefix`(可选前缀)+ key ID。 - KV 示例:`http://vaultserver:8200/v1/secret/data/myproject/mybucketkey` - Transit 示例:`http://vaultserver:8200/v1/transit/mybucketkey` > 引自:[HashiCorp Vault Integration](https://docs.ceph.com/en/tentacle/radosgw/vault/) --- ### 2.2 KMIP(kmip.rst) KMIP 可作为安全密钥管理服务用于 SSE-KMS。交互流程与 Vault 相同(客户端建密钥 → 带 key ID 上传 → RGW 向 KMIP 取密钥 → 加密 → 落 OSD)。 官方给出的配置步骤顺序: 1. `Setting KMIP Access for Ceph` 2. `Creating Keys in KMIP` 3. `Configure the Ceph Object Gateway` 4. `Upload object` 开始之前需要完成三件事:把 ceph 与 KMIP 中的客户端信息关联、配置 ceph 使用该客户端信息、在 KMIP 中创建 1 个或多个密钥。 #### 2.2.1 配置 KMIP 对 Ceph 的访问 这部分高度依赖具体 KMIP 实现支持的机制。官方描述了两种实现: | 实现 | 定位 | 认证方式 | |---|---|---| | **IBM Security Guardium Key Lifecycle Manager (SKLM)** | 商业产品,支持良好 | 证书认证 | | **PyKMIP** | 小型 Python 项目,**仅适合实验与测试** | 信任证书,无特殊注册流程 | **IBM SKLM** 证书可以是自签(例如用 openssl 创建),也可以由 SKLM 创建。流程是: 1. 先配置 Ceph 使用 KMIP(见下)并尝试使用它; 2. 该尝试会**失败**,但会在 SKLM 中留下一个 **"untrusted client device certificate"(不受信任的客户端设备证书)**; 3. 在 Web 界面中把它升级为已注册客户端,完成注册。 具体位置:`Advanced Configuration` → `Client Device Communication Certificates`,选择 `Modify SSL/KMIP Certificates for Clients`,然后打开 `allow the server to trust this certificate and communicate...` 开关。 **PyKMIP** 没有特殊注册流程,**只要证书被信任即可**,但证书必须由 pykmip 信任的 CA 签发。PyKMIP 还偏好证书带有 "extended key usage" 扩展,不过可以通过在服务端配置中指定 `enable_tls_client_auth=False` 规避。 #### 2.2.2 在 KMIP 中创建密钥 可以用实现自带的 Web 界面/管理工具,也可以用 KMIP 协议本身。PyKMIP 提供了 Python 客户端库。 准备环境: ```bash virtualenv $HOME/my-kmip-env source $HOME/my-kmip-env/bin/activate pip install pykmip ``` 准备客户端配置文件: ```bash cat <$HOME/my-kmip-configuration [client] host={hostname} port=5696 certfile={clientcert} keyfile={clientkey} ca_certs={clientca} ssl_version=PROTOCOL_TLSv1_2 EOF ``` 其中 `{hostname}` 替换为 kmip 主机名,`{clientcert}` `{clientkey}` `{clientca}` 替换为合适的 PEM 证书路径(例如你为 ceph 创建的那个)。 创建密钥的 Python 脚本(可直接粘到 shell 里执行): ```python from kmip.pie import client from kmip import enums import ssl import os import sys import json c = client.ProxyKmipClient(config_file=os.environ['HOME']+"/my-kmip-configuration") while True: l=sys.stdin.readline() keyname=l.strip() if keyname == "": break with c: key_id = c.create( enums.CryptographicAlgorithm.AES, 256, operation_policy_name='default', name=keyname, cryptographic_usage_mask=[ enums.CryptographicUsageMask.ENCRYPT, enums.CryptographicUsageMask.DECRYPT ] ) c.activate(key_id) attrs = c.get_attributes(uid=key_id) r = {} for a in attrs[1]: r[str(a.attribute_name)] = str(a.attribute_value) print (json.dumps(r)) ``` 在 shell 提示符下,python 会先用 `>>>` 再 `...` 读入脚本,之后无提示地逐行读入并处理名字,直到遇到空行或 EOF(^D)或出错。 #### 2.2.3 配置 Ceph Object Gateway ```ini rgw crypt s3 kms backend = kmip rgw crypt kmip ca path: /etc/ceph/kmiproot.crt rgw crypt kmip client cert: /etc/ceph/kmip-client.crt rgw crypt kmip client key: /etc/ceph/private/kmip-client.key rgw crypt kmip kms key template: pykmip-$keyid ``` > 💡 补充:源文中这段示例的写法是 `rgw crypt kmip ca path: /etc/ceph/kmiproot.crt`(用冒号而非等号),与 Ceph 配置文件惯用的 `key = value` 不同。实际 ceph.conf 中应写作 `rgw_crypt_kmip_ca_path = /etc/ceph/kmiproot.crt`。本册保留源文原样以忠实记录,同时提示此差异。 **kmip key template** 描述 ceph 在去 kmip 查找之前如何改写传进来的名字,**默认值就是 `$keyid`**。如果不想让 ceph 看到你所有的 kmip 密钥,可以用它把 ceph 限制在 kmip 密钥命名空间的指定子集内。 KMIP 配置项: | 配置项 | 类型 | 默认值(`rgw.yaml.in` @ v20.2.4) | 说明 | |---|---|---|---| | `rgw_crypt_s3_kms_backend` | str | `barbican` | 设为 `kmip` 启用 KMIP 后端 | | `rgw_crypt_kmip_addr` | str | 文档未明确(无 `default:`) | kmip server 地址 | | `rgw_crypt_kmip_ca_path` | str | 文档未明确(无 `default:`) | kmip servers 的 ca | | `rgw_crypt_kmip_username` | str | 文档未明确(无 `default:`) | 通过用户名认证时使用 | | `rgw_crypt_kmip_password` | str | 文档未明确(无 `default:`) | 与 username 配合,可选 | | `rgw_crypt_kmip_client_cert` | str | 文档未明确(无 `default:`) | 使用客户端证书连接 | | `rgw_crypt_kmip_client_key` | str | 文档未明确(无 `default:`) | 使用客户端证书连接 | | `rgw_crypt_kmip_kms_key_template` | str | 文档未明确(无 `default:`) | SSE-KMS 的 kmip 密钥名模板 | | `rgw_crypt_kmip_s3_key_template` | str | `$keyid` | SSE-S3 的 kmip 密钥模板 | #### 2.2.4 上传对象 ```bash aws --endpoint=http://radosgw:8000 s3 cp plaintext.txt \ s3://mybucket/encrypted.txt --sse=aws:kms --sse-kms-key-id mybucketkey ``` > 💡 补充:源文在 KMIP 页面同时给了 "for the kv engine" 与 "for the transit engine" 两段完全相同的示例命令,这是从 Vault 页面复制过来的残留(KMIP 没有 kv/transit engine 概念)。两条命令实际等价。 密钥名由 key template 构造,把 `$keyid` 替换为请求中提供的 key。按上面的配置,radosgw 会取名为 `pykmip-mybucketkey` 的 secret。 > 引自:[KMIP Integration](https://docs.ceph.com/en/tentacle/radosgw/kmip/) --- ### 2.3 OpenStack Barbican(barbican.rst) OpenStack Barbican 可作为安全密钥管理服务用于服务端加密。 官方给出的步骤顺序: 1. `Configure Keystone` 2. `Create a Keystone user` 3. `Configure the Ceph Object Gateway` 4. `Create a key in Barbican` #### 2.3.1 配置 Keystone Barbican 依赖 Keystone 对其密钥做授权与访问控制,参见 OpenStack Keystone Integration(本册第六章)。 #### 2.3.2 创建 Keystone 用户 创建一个供 Ceph Object Gateway 取回密钥使用的新用户,例如: ```text user = rgwcrypt-user pass = rgwcrypt-password tenant = rgwcrypt ``` #### 2.3.3 在 Barbican 中创建密钥 > ⚠️ 服务端加密密钥必须 256 位长、base64 编码。 > 请求 Barbican 必须在 `X-Auth-Token` 头中带有效的 Keystone token。 ```http POST /v1/secrets HTTP/1.1 Host: barbican.example.com:9311 Accept: */* Content-Type: application/json X-Auth-Token: 7f7d588dd29b44df983bc961a6b73a10 Content-Length: 299 { "name": "my-key", "expiration": "2016-12-28T19:14:44.180394", "algorithm": "aes", "bit_length": 256, "mode": "cbc", "payload": "6b+WOZ1T3cqZMxgThRcXAQBrS5mXKdDUphvpxptl9/4=", "payload_content_type": "application/octet-stream", "payload_content_encoding": "base64" } ``` 响应: ```json {"secret_ref": "http://barbican.example.com:9311/v1/secrets/d1e7ef3b-f841-4b7c-90b2-b7d90ca2d723"} ``` 响应中的 `d1e7ef3b-f841-4b7c-90b2-b7d90ca2d723` 就是可在任意 SSE-KMS 请求中使用的 key id。 **新创建的密钥对 `rgwcrypt-user` 是不可访问的**,必须通过 ACL 授予权限。假设 `rgwcrypt-user` 的 Keystone id 是 `906aa90bd8a946c89cdff80d0869460f`: ```http PUT /v1/secrets/d1e7ef3b-f841-4b7c-90b2-b7d90ca2d723/acl HTTP/1.1 Host: barbican.example.com:9311 Accept: */* Content-Type: application/json X-Auth-Token: 7f7d588dd29b44df983bc961a6b73a10 Content-Length: 101 { "read":{ "users":[ "906aa90bd8a946c89cdff80d0869460f" ], "project-access": true } } ``` 响应: ```json {"acl_ref": "http://barbican.example.com:9311/v1/secrets/d1e7ef3b-f841-4b7c-90b2-b7d90ca2d723/acl"} ``` #### 2.3.4 配置 Ceph Object Gateway ```ini rgw crypt s3 kms backend = barbican rgw barbican url = http://barbican.example.com:9311 rgw keystone barbican user = rgwcrypt-user rgw keystone barbican password = rgwcrypt-password ``` 使用 Keystone API version 2 时: ```ini rgw keystone barbican tenant = rgwcrypt ``` 使用 API version 3 时: ```ini rgw keystone barbican project rgw keystone barbican domain ``` #### 2.3.5 Barbican 相关配置项 | 配置项 | 类型 | 默认值(`rgw.yaml.in` @ v20.2.4) | 说明 | |---|---|---|---| | `rgw_crypt_s3_kms_backend` | str | `barbican` | 设为 `barbican` 启用 Barbican 后端(这也是默认值) | | `rgw_barbican_url` | str | 文档未明确(无 `default:`) | Barbican server 的 URL | | `rgw_keystone_barbican_user` | str | 文档未明确(无 `default:`) | 用于访问 barbican secrets 的 Keystone 用户 | | `rgw_keystone_barbican_password` | str | 文档未明确(无 `default:`) | 该 barbican 用户的密码 | | `rgw_keystone_barbican_tenant` | str | 文档未明确(无 `default:`) | Keystone v2.0 下 barbican 用户的 tenant | | `rgw_keystone_barbican_project` | str | 文档未明确(无 `default:`) | Keystone v3 下 barbican 用户的 project | | `rgw_keystone_barbican_domain` | str | 文档未明确(无 `default:`) | Keystone v3 下 barbican 用户的 domain | > 引自:[OpenStack Barbican Integration](https://docs.ceph.com/en/tentacle/radosgw/barbican/) --- ## 三、IAM(iam.rst) > `versionadded:: Squid` Ceph Object Gateway 支持 **Amazon IAM API 的一个子集**,用于对 account 的 users、roles 及相关 policies 做 RESTful 管理。该 REST API 与 S3 使用**同一个 HTTP endpoint** 提供服务。 ### 3.1 授权(Authorization) - 默认情况下,**只有 Account Root Users(账户根用户)** 被授权使用 IAM API; - 且只能看到**自己账户下**的资源; - 账户根用户可以通过 policies 把这些权限委托给账户内的其它 users 或 roles。 ### 3.2 支持的 IAM Actions(逐项) #### Users | Action | Remarks | |---|---| | `CreateUser` | 文档未明确 | | `GetUser` | 文档未明确 | | `UpdateUser` | 文档未明确 | | `DeleteUser` | 文档未明确 | | `ListUsers` | 文档未明确 | | `CreateAccessKey` | 文档未明确 | | `UpdateAccessKey` | 文档未明确 | | `DeleteAccessKey` | 文档未明确 | | `ListAccessKeys` | 文档未明确 | | `PutUserPolicy` | 文档未明确 | | `GetUserPolicy` | 文档未明确 | | `DeleteUserPolicy` | 文档未明确 | | `ListUserPolicies` | 文档未明确 | | `AttachUserPolicies` | 文档未明确 | | `DetachUserPolicy` | 文档未明确 | | `ListAttachedUserPolicies` | 文档未明确 | #### Groups | Action | Remarks | |---|---| | `CreateGroup` | 文档未明确 | | `GetGroup` | 文档未明确 | | `UpdateGroup` | 文档未明确 | | `DeleteGroup` | 文档未明确 | | `ListGroups` | 文档未明确 | | `AddUserToGroup` | 文档未明确 | | `RemoveUserFromGroup` | 文档未明确 | | `ListGroupsForUser` | 文档未明确 | | `PutGroupPolicy` | 文档未明确 | | `GetGroupPolicy` | 文档未明确 | | `DeleteGroupPolicy` | 文档未明确 | | `ListGroupPolicies` | 文档未明确 | | `AttachGroupPolicies` | 文档未明确 | | `DetachGroupPolicy` | 文档未明确 | | `ListAttachedGroupPolicies` | 文档未明确 | #### Roles | Action | Remarks | |---|---| | `CreateRole` | 文档未明确 | | `GetRole` | 文档未明确 | | `UpdateRole` | 文档未明确 | | `UpdateAssumeRolePolicy` | 文档未明确 | | `DeleteRole` | 文档未明确 | | `ListRoles` | 文档未明确 | | `TagRole` | 文档未明确 | | `UntagRole` | 文档未明确 | | `ListRoleTags` | 文档未明确 | | `PutRolePolicy` | 文档未明确 | | `GetRolePolicy` | 文档未明确 | | `DeleteRolePolicy` | 文档未明确 | | `ListRolePolicies` | 文档未明确 | | `AttachRolePolicies` | 文档未明确 | | `DetachRolePolicy` | 文档未明确 | | `ListAttachedRolePolicies` | 文档未明确 | #### OpenIDConnectProvider | Action | Remarks | |---|---| | `CreateOpenIDConnectProvider` | 文档未明确 | | `GetOpenIDConnectProvider` | 文档未明确 | | `DeleteOpenIDConnectProvider` | 文档未明确 | | `ListOpenIDConnectProviders` | 文档未明确 | > 💡 补充:上表所有 `Remarks` 列在官方 rst 源文中均为空单元格,本册按"不编造"原则统一标注「文档未明确」。 ### 3.3 Managed Policies(托管策略) 以下托管策略可用于 `AttachGroupPolicy`、`AttachRolePolicy` 和 `AttachUserPolicy`: | 托管策略名 | ARN | Version(默认) | |---|---|---| | `IAMFullAccess` | `arn:aws:iam::aws:policy/IAMFullAccess` | v2 | | `IAMReadOnlyAccess` | `arn:aws:iam::aws:policy/IAMReadOnlyAccess` | v4 | | `AmazonSNSFullAccess` | `arn:aws:iam::aws:policy/AmazonSNSFullAccess` | v1 | | `AmazonSNSReadOnlyAccess` | `arn:aws:iam::aws:policy/AmazonSNSReadOnlyAccess` | v1 | | `AmazonS3FullAccess` | `arn:aws:iam::aws:policy/AmazonS3FullAccess` | v2 | | `AmazonS3ReadOnlyAccess` | `arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess` | v3 | ### 3.4 IAM policy 语法 > 💡 补充:`iam.rst` 本身**没有**定义 policy 的语法结构,它只列出支持的 action 与托管策略。本册范围内的 policy 语法证据来自 `role.rst`、`STS.rst`、`session-tags.rst`、`bucketpolicy.rst` 中的大量示例,其共同结构如下,字段含义一并列出: | 字段 | 类型 | 是否必填 | 含义 | 出现位置 | |---|---|---|---|---| | `Version` | string | 示例中恒为 `2012-10-17` | policy 语言版本 | 所有示例 | | `Statement` | object 或 array | 是 | 一条或多条语句 | 所有示例 | | `Statement[].Effect` | string | 是 | `Allow` 或 `Deny` | 所有示例 | | `Statement[].Principal` | object | trust policy 中必填 | 指定被授权的实体。形态如 `{"AWS":["arn:aws:iam:::user/TESTER"]}` 或 `{"Federated":["arn:aws:iam:::oidc-provider/"]}` | `role.rst`、`STS.rst`、`session-tags.rst` | | `Statement[].Action` | string 或 array | 是 | 动作,如 `s3:*`、`sts:AssumeRole`、`sts:AssumeRoleWithWebIdentity`、`sts:TagSession`、`sts:GetSessionToken` | 全部 | | `Statement[].Resource` | string 或 array | 是 | 资源 ARN,如 `arn:aws:s3:::example_bucket`、`arn:aws:s3::t1tenant:my-test-bucket`、`*` | 全部 | | `Statement[].Condition` | object | 否 | 条件块,见下 | `session-tags.rst`、`STSLite.rst` | `Condition` 支持的操作符(本册源文中出现过的): | 操作符 | 出现位置 | 语义 | |---|---|---| | `StringEquals` | `session-tags.rst`、`STS.rst` | 字符串相等 | | `BoolIfExists` | `STSLite.rst` | 布尔值判断(键不存在时视为真) | | `ForAllValues:StringEquals` | `session-tags.rst` | 检验请求中的每个 tag key 都是 policy 中 tag keys 的子集 | ### 3.5 IAM 与 bucket policy 的关系 | 维度 | IAM policy | Bucket policy | |---|---|---| | 挂载对象 | account 内的 user / group / role | 桶 | | 管理方式 | IAM REST API(`PutUserPolicy` / `PutRolePolicy` 等)与 `radosgw-admin role policy put` | 标准 S3 操作(`s3cmd setpolicy` / `delpolicy`),**不用 radosgw-admin** | | 是否支持 user/group/role | 支持(Squid 起) | **不支持**在 user/group/role 上设置策略(`bucketpolicy.rst` 明确说明 "We do not yet support setting policies on users, groups, or roles") | | 引入版本 | Squid | Luminous(部分 bucket 操作在 Mimic 补齐) | > 💡 补充:关于"IAM policy 与 bucket policy 谁的优先级更高",本册源文**没有**给出显式裁决规则,因此标注:**文档未明确**。可确认的只有 `bucketpolicy.rst` 中"bucket policy 设置的策略同时约束 Swift 与 S3 操作"这一事实。 > 引自:[Ceph Object Gateway IAM API](https://docs.ceph.com/en/tentacle/radosgw/iam/) --- ## 四、角色与 STS ### 4.1 Role(role.rst) #### 4.1.1 Role 概念 Role(角色)与 user 类似,也有决定"能做什么、不能做什么"的权限策略挂在它上面。区别在于: - Role **可以被任何需要它的身份 assume(承担)**; - 当用户 assume 一个 role 时,会得到**一组动态创建的临时凭证(temporary credentials)**; - Role 可用来把访问权限**委托**给本来没有权限访问某些 S3 资源的用户、应用和服务。 #### 4.1.2 radosgw-admin 命令 ##### 创建 Role ```bash radosgw-admin role create --role-name={role-name} [--path=="{path to the role}"] [--assume-role-policy-doc={trust-policy-document}] ``` | 参数 | 说明 | 类型 | |---|---|---| | `role-name` | Role 的名称 | String | | `path` | Role 的路径,**默认值为斜杠 `/`** | String | | `assume-role-policy-doc` | 授予实体 assume 该 role 权限的信任关系策略文档 | String | 示例: ```bash radosgw-admin role create --role-name=S3Access1 --path=/application_abc/component_xyz/ --assume-role-policy-doc=\{\"Version\":\"2012-10-17\",\"Statement\":\[\{\"Effect\":\"Allow\",\"Principal\":\{\"AWS\":\[\"arn:aws:iam:::user/TESTER\"\]\},\"Action\":\[\"sts:AssumeRole\"\]\}\]\} ``` 响应: ```json { "id": "ca43045c-082c-491a-8af1-2eebca13deec", "name": "S3Access1", "path": "/application_abc/component_xyz/", "arn": "arn:aws:iam:::role/application_abc/component_xyz/S3Access1", "create_date": "2018-10-17T10:18:29.116Z", "max_session_duration": 3600, "assume_role_policy_document": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"arn:aws:iam:::user/TESTER\"]},\"Action\":[\"sts:AssumeRole\"]}]}" } ``` ##### 删除 Role ```bash radosgw-admin role delete --role-name={role-name} ``` | 参数 | 说明 | 类型 | |---|---|---| | `role-name` | Role 的名称 | String | ```bash radosgw-admin role delete --role-name=S3Access1 ``` > 注意:**只有当 role 没有挂载任何权限策略时才能删除它。** ##### 获取 Role ```bash radosgw-admin role get --role-name={role-name} ``` | 参数 | 说明 | 类型 | |---|---|---| | `role-name` | Role 的名称 | String | ```bash radosgw-admin role get --role-name=S3Access1 ``` 响应同创建时的 JSON 结构(`id` / `name` / `path` / `arn` / `create_date` / `max_session_duration` / `assume_role_policy_document`)。 ##### 列出 Role ```bash radosgw-admin role list [--path-prefix ={path prefix}] ``` | 参数 | 说明 | 类型 | |---|---|---| | `path-prefix` | 用于过滤 role 的路径前缀。**不指定则列出所有 role** | String | ```bash radosgw-admin role list --path-prefix="/application" ``` 响应为 role 对象数组。 ##### 更新 Role 的 Assume Role Policy Document ```bash radosgw-admin role-trust-policy modify --role-name={role-name} --assume-role-policy-doc={trust-policy-document} ``` | 参数 | 说明 | 类型 | |---|---|---| | `role-name` | Role 的名称 | String | | `assume-role-policy-doc` | 授予实体 assume 该 role 权限的信任关系策略文档 | String | ```bash radosgw-admin role-trust-policy modify --role-name=S3Access1 --assume-role-policy-doc=\{\"Version\":\"2012-10-17\",\"Statement\":\[\{\"Effect\":\"Allow\",\"Principal\":\{\"AWS\":\[\"arn:aws:iam:::user/TESTER2\"\]\},\"Action\":\[\"sts:AssumeRole\"\]\}\]\} ``` 该例把 Principal 从 `TESTER` 改为 `TESTER2`。 ##### 添加 / 更新 Role 上挂载的 Policy ```bash radosgw-admin role policy put --role-name={role-name} --policy-name={policy-name} --policy-doc={permission-policy-doc} ``` | 参数 | 说明 | 类型 | |---|---|---| | `role-name` | Role 的名称 | String | | `policy-name` | Policy 的名称 | String | | `policy-doc` | 权限策略文档 | String | ```bash radosgw-admin role-policy put --role-name=S3Access1 --policy-name=Policy1 --policy-doc=\{\"Version\":\"2012-10-17\",\"Statement\":\[\{\"Effect\":\"Allow\",\"Action\":\[\"s3:*\"\],\"Resource\":\"arn:aws:s3:::example_bucket\"\}\]\} ``` 以文件形式传 `policy-doc`: ```bash radosgw-admin role-policy put --role-name=S3Access1 --policy-name=Policy1 --infile policy-document.json ``` ##### 列出 Role 上挂载的 Policy 名称 ```bash radosgw-admin role policy get --role-name={role-name} ``` | 参数 | 说明 | 类型 | |---|---|---| | `role-name` | Role 的名称 | String | ```bash radosgw-admin role-policy list --role-name=S3Access1 ``` ```json [ "Policy1" ] ``` > 💡 补充:源文中该小节的命令原型写作 `radosgw-admin role policy get`,但给出的示例却是 `radosgw-admin role-policy list`。二者在官方源文中并存,实际使用时以 `radosgw-admin role-policy list` 为准。 ##### 获取 Role 上挂载的指定 Policy ```bash radosgw-admin role policy get --role-name={role-name} --policy-name={policy-name} ``` | 参数 | 说明 | 类型 | |---|---|---| | `role-name` | Role 的名称 | String | | `policy-name` | Policy 的名称 | String | ```bash radosgw-admin role-policy get --role-name=S3Access1 --policy-name=Policy1 ``` ```json { "Permission policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:*\"],\"Resource\":\"arn:aws:s3:::example_bucket\"}]}" } ``` ##### 删除 Role 上挂载的 Policy ```bash radosgw-admin role policy delete --role-name={role-name} --policy-name={policy-name} ``` | 参数 | 说明 | 类型 | |---|---|---| | `role-name` | Role 的名称 | String | | `policy-name` | Policy 的名称 | String | ```bash radosgw-admin role-policy delete --role-name=S3Access1 --policy-name=Policy1 ``` ##### 更新 Role ```bash radosgw-admin role update --role-name={role-name} --max-session-duration={max-session-duration} ``` | 参数 | 说明 | 类型 | |---|---|---| | `role-name` | Role 的名称 | String | | `max-session-duration` | Role 的最大会话时长 | String | ```bash radosgw-admin role update --role-name=S3Access1 --max-session-duration=43200 ``` > 注意:**该命令目前只能用于更新 max-session-duration。** #### 4.1.3 Role 的 REST API 除上述 `radosgw-admin` 命令外,也可用 REST API 操作 role。请求参数及解释参见上一小节。 调用 REST admin API 需要创建带 admin caps 的用户: ```bash radosgw-admin --uid TESTER --display-name "TestUser" --access_key TESTER --secret test123 user create radosgw-admin caps add --uid="TESTER" --caps="roles=*" ``` | 操作 | 请求示例 | |---|---| | 创建 Role | `POST "?Action=CreateRole&RoleName=S3Access&Path=/application_abc/component_xyz/&AssumeRolePolicyDocument=\{...\}"` | | 删除 Role | `POST "?Action=DeleteRole&RoleName=S3Access"` | | 获取 Role | `POST "?Action=GetRole&RoleName=S3Access"` | | 列出 Role | `POST "?Action=ListRoles&RoleName=S3Access&PathPrefix=/application"` | | 更新 Assume Role Policy | `POST "?Action=UpdateAssumeRolePolicy&RoleName=S3Access&PolicyDocument=\{...\}"` | | 添加/更新 Role Policy | `POST "?Action=PutRolePolicy&RoleName=S3Access&PolicyName=Policy1&PolicyDocument=\{...\}"` | | 列出 Role Policy 名 | `POST "?Action=ListRolePolicies&RoleName=S3Access"` | | 获取 Role Policy | `POST "?Action=GetRolePolicy&RoleName=S3Access&PolicyName=Policy1"` | | 删除 Role Policy | `POST "?Action=DeleteRolePolicy&RoleName=S3Access&PolicyName=Policy1"` | | 给 Role 打标签 | `POST "?Action=TagRole&RoleName=S3Access&Tags.member.1.Key=Department&Tags.member.1.Value=Engineering"` | | 列出 Role 标签 | `POST "?Action=ListRoleTags&RoleName=S3Access"` | | 删除 Role 标签 | `POST "?Action=UntagRoles&RoleName=S3Access&TagKeys.member.1=Department"` | | 更新 Role | `POST "?Action=UpdateRole&RoleName=S3Access&MaxSessionDuration=43200"` | **CreateRole 响应(XML)**: ```xml 8f41f4e0-7094-4dc0-ac20-074a881ccbc5 S3Access /application_abc/component_xyz/ arn:aws:iam:::role/application_abc/component_xyz/S3Access 2018-10-23T07:43:42.811Z 3600 {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["arn:aws:iam:::user/TESTER"]},"Action":["sts:AssumeRole"]}]} ``` **ListRolePolicies 响应(XML)**: ```xml Policy1 ``` **GetRolePolicy 响应(XML)**: ```xml Policy1 S3Access {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:CreateBucket"],"Resource":"arn:aws:s3:::example_bucket"}]} ``` **TagRole 响应(XML)**: ```xml tx000000000000000000004-00611f337e-1027-default ``` **ListRoleTags 响应(XML)**: ```xml Department Engineering tx000000000000000000005-00611f337e-1027-default ``` **UntagRole 响应(XML)**: ```xml tx000000000000000000007-00611f337e-1027-default ``` **UpdateRole 响应(XML)**: ```xml tx000000000000000000007-00611f337e-1027-default ``` > 注意:**UpdateRole API 目前也只能用于更新 max-session-duration。** > Role 可以有**多值(multivalued)标签**,这些标签也可以在 CreateRole REST API 中一并传入。**AWS 不支持多值 role tags。** #### 4.1.4 用 boto3 操作 role 标签 ```python import boto3 access_key = 'TESTER' secret_key = 'test123' iam_client = boto3.client('iam', aws_access_key_id=access_key, aws_secret_access_key=secret_key, endpoint_url='http://s3.us-east.localhost:8000', region_name='' ) policy_document = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Federated\":[\"arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart\"]},\"Action\":[\"sts:AssumeRoleWithWebIdentity\"],\"Condition\":{\"StringEquals\":{\"localhost:8080/auth/realms/quickstart:sub\":\"user1\"}}}]}" print ("\n Creating Role with tags\n") tags_list = [ {'Key':'Department','Value':'Engineering'} ] role_response = iam_client.create_role( AssumeRolePolicyDocument=policy_document, Path='/', RoleName='S3Access', Tags=tags_list, ) print ("Adding tags to role\n") response = iam_client.tag_role( RoleName='S3Access', Tags= [ {'Key':'CostCenter','Value':'123456'} ] ) print ("Listing role tags\n") response = iam_client.list_role_tags( RoleName='S3Access' ) print (response) print ("Untagging role\n") response = iam_client.untag_role( RoleName='S3Access', TagKeys=[ 'Department', ] ) ``` > 引自:[Role](https://docs.ceph.com/en/tentacle/radosgw/role/) --- ### 4.2 完整 STS(STS.rst) Secure Token Service(STS)是 AWS 中用于为联邦用户返回一组临时安全凭证的 Web 服务。Ceph Object Gateway 实现了 **STS API 的一个子集**,提供用于身份与访问管理的临时凭证: - 这些临时凭证可用于发起后续 S3 调用,由 Ceph Object Gateway 中的 STS engine 完成认证; - 临时凭证的权限可以通过**作为 STS API 参数传入的 IAM policy** 进一步收紧。 > ⚠️ 官方 note:**STS 与 S3 API 共存于同一命名空间,二者可通过同一 endpoint 访问。** #### 4.2.1 AssumeRole 返回一组可用于**跨账户访问**的临时凭证。临时凭证的权限是**Role 上挂载的权限策略**与**AssumeRole API 传入的策略**二者共同允许的交集。 | 参数 | 类型 | 是否必填 | 说明 | |---|---|---|---| | `RoleArn` | String | **Required** | 要 assume 的 Role 的 ARN | | `RoleSessionName` | String | **Required** | 被 assume 的 role session 的标识符 | | `Policy` | String | Optional | JSON 格式的 IAM Policy | | `DurationSeconds` | Integer | Optional | 会话时长(秒),**默认值为 3600** | | `ExternalId` | String | Optional | 当 role 在另一个账户中被 assume 时可能使用的唯一 Id | | `SerialNumber` | String | Optional | 与发起 AssumeRole 调用的用户关联的 MFA 设备 Id | | `TokenCode` | String | Optional | 若被 assume 的 role 的 trust policy 要求 MFA,则由 MFA 设备提供的值 | #### 4.2.2 AssumeRoleWithWebIdentity 为已被 OpenID Connect / OAuth2.0 身份提供者(IDP)认证过的 Web/移动应用用户返回一组临时凭证。**目前 Keycloak 已完成与 RGW 的测试与集成。** | 参数 | 类型 | 是否必填 | 说明 | |---|---|---|---| | `RoleArn` | String | **Required** | 要 assume 的 Role 的 ARN | | `RoleSessionName` | String | **Required** | 被 assume 的 role session 的标识符 | | `Policy` | String | Optional | JSON 格式的 IAM Policy | | `DurationSeconds` | Integer | Optional | 会话时长(秒),**默认值为 3600** | | `ProviderId` | String | Optional | IDP 域名的完全限定主机部分。**仅对 OAuth2.0 token 有效,对 OpenID Connect token 无效** | | `WebIdentityToken` | String | **Required** | 应用在 IDP 认证用户后获得的 OpenID Connect / OAuth2.0 token | **前置条件**:在调用 `AssumeRoleWithWebIdentity` 之前,需要**先在 RGW 中创建 OpenID Connect Provider 实体**(即 Web 应用所认证的那个 IDP)。 **信任关系的建立方式**:在 role 的 trust policy 中添加 condition,只允许满足条件的应用访问。**JWT 的所有 claims 都可以在 role trust policy 的 condition 中使用。** 使用 `aud` claim 的 policy: ```json {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":["arn:aws:iam:::oidc-provider/"]},"Action":["sts:AssumeRoleWithWebIdentity"],"Condition":{"StringEquals":{" :app_id":""}}}]} ``` 上例 condition 中的 `app_id` 必须匹配传入 token 的 `aud` claim。 使用 `sub` claim 的 policy: ```json {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":["arn:aws:iam:::oidc-provider/"]},"Action":["sts:AssumeRoleWithWebIdentity"],"Condition":{"StringEquals":{" :sub":""}}}]} ``` 使用 `azp` claim 的 policy: ```json {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":["arn:aws:iam:::oidc-provider/"]},"Action":["sts:AssumeRoleWithWebIdentity"],"Condition":{"StringEquals":{" :azp":""}}}]} ``` **影子用户(shadow user)**:每个联邦用户都会对应创建一个影子用户,其 user id 由传入 web token 的 `sub` 字段派生。该用户创建在**独立的命名空间 `oidc`** 中,以避免与 rgw 中其它 user id 冲突。user id 格式为 `$$`,其中通过 oidc provider 认证的用户其 `user-namespace` 就是 `oidc`。 **会话标签**:RGW 支持在传给 `AssumeRoleWithWebIdentity` 的 web token 中携带 Session tags,详见本章 4.4 节。 #### 4.2.3 STS 配置项 | 配置项 | 类型 | 默认值(`rgw.yaml.in` @ v20.2.4) | 说明 | |---|---|---|---| | `rgw_sts_key` | str | 文档未明确(无 `default:`) | 用于加密/解密 role session token 的 key。应由 `ceph-authtool --gen-print-key` 生成,是 base64 编码的 CryptoKey。**同一 zone 内所有 radosgw 实例必须使用相同 key;multisite 配置下同一 realm 内所有 zone 必须使用相同 key** | | `rgw_s3_auth_use_sts` | bool | `false` | S3 认证是否使用 STS | | `rgw_sts_entry` | str | `sts` | 内部 STS 请求的 URL 路径前缀 | | `rgw_sts_max_session_duration` | uint | `43200` | `GetSessionToken` 返回的临时凭证的 `durationSeconds` 上限 | | `rgw_sts_min_session_duration` | uint | `900` | `AssumeRole*` 调用返回的临时凭证的 `durationSeconds` 下限 | | `rgw_s3_auth_order` | str | `sts, external, local` | S3 认证策略的尝试顺序,逗号分隔的引擎列表。默认顺序是**先尝试所有已配置的外部引擎,再尝试本地 RADOS 认证** | | `rgw_s3_auth_disable_signature_url` | bool | `false` | 是否禁用 presigned URL 认证。启用后,任何使用 V2 或 V4 签名预签名的请求都会被拒绝 | #### 4.2.4 示例:AssumeRole 前置:确保用户 TESTER 被赋予 `roles` capability。 ```bash radosgw-admin caps add --uid="TESTER" --caps="roles=*" ``` 完整流程(TESTER1 assume 由 TESTER 创建的 role,按挂载在 role 上的权限策略访问 TESTER 拥有的 S3 资源): ```python import boto3 iam_client = boto3.client('iam', aws_access_key_id=, aws_secret_access_key=, endpoint_url=, region_name='' ) policy_document = '''{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["arn:aws:iam:::user/TESTER1"]},"Action":["sts:AssumeRole"]}]}''' role_response = iam_client.create_role( AssumeRolePolicyDocument=policy_document, Path='/', RoleName='S3Access', ) role_policy = '''{"Version":"2012-10-17","Statement":{"Effect":"Allow","Action":"s3:*","Resource":"arn:aws:s3:::*"}}''' response = iam_client.put_role_policy( RoleName='S3Access', PolicyName='Policy1', PolicyDocument=role_policy ) sts_client = boto3.client('sts', aws_access_key_id=, aws_secret_access_key=, endpoint_url=, region_name='', ) response = sts_client.assume_role( RoleArn=role_response['Role']['Arn'], RoleSessionName='Bob', DurationSeconds=3600 ) s3client = boto3.client('s3', aws_access_key_id = response['Credentials']['AccessKeyId'], aws_secret_access_key = response['Credentials']['SecretAccessKey'], aws_session_token = response['Credentials']['SessionToken'], endpoint_url=, region_name='',) bucket_name = 'my-bucket' s3bucket = s3client.create_bucket(Bucket=bucket_name) resp = s3client.list_buckets() ``` #### 4.2.5 示例:AssumeRoleWithWebIdentity(Keycloak 作为 IDP) ```python import boto3 iam_client = boto3.client('iam', aws_access_key_id=, aws_secret_access_key=, endpoint_url=, region_name='' ) oidc_response = iam_client.create_open_id_connect_provider( Url= ], ThumbprintList=[ ] ) policy_document = '''{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":["arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/demo"]},"Action":["sts:AssumeRoleWithWebIdentity"],"Condition":{"StringEquals":{"localhost:8080/auth/realms/demo:app_id":"customer-portal"}}}]}''' role_response = iam_client.create_role( AssumeRolePolicyDocument=policy_document, Path='/', RoleName='S3Access', ) role_policy = '''{"Version":"2012-10-17","Statement":{"Effect":"Allow","Action":"s3:*","Resource":"arn:aws:s3:::*"}}''' response = iam_client.put_role_policy( RoleName='S3Access', PolicyName='Policy1', PolicyDocument=role_policy ) sts_client = boto3.client('sts', aws_access_key_id=, aws_secret_access_key=, endpoint_url=, region_name='', ) response = client.assume_role_with_web_identity( RoleArn=role_response['Role']['Arn'], RoleSessionName='Bob', DurationSeconds=3600, WebIdentityToken= ) s3client = boto3.client('s3', aws_access_key_id = response['Credentials']['AccessKeyId'], aws_secret_access_key = response['Credentials']['SecretAccessKey'], aws_session_token = response['Credentials']['SessionToken'], endpoint_url=, region_name='',) bucket_name = 'my-bucket' s3bucket = s3client.create_bucket(Bucket=bucket_name) resp = s3client.list_buckets() ``` > 💡 补充:源文示例中 `sts_client` 创建后调用的是 `client.assume_role_with_web_identity`(变量名不一致,应为 `sts_client`)。这是官方示例的笔误,本册保留原样并在此提示。 #### 4.2.6 如何获取 OpenID Connect Provider IDP 的 thumbprint 1. 取 IDP 的 URL,追加 `/.well-known/openid-configuration` 得到配置文档 URL。例如 IDP URL 为 `http://localhost:8000/auth/realms/quickstart`,则文档 URL 为 `http://localhost:8000/auth/realms/quickstart/.well-known/openid-configuration`。 2. 用 curl 取配置文档: ```bash curl -k -v \ -X GET \ -H "Content-Type: application/x-www-form-urlencoded" \ "http://localhost:8000/auth/realms/quickstart/.well-known/openid-configuration" \ | jq . ``` 3. 从步骤 2 的响应中取 `jwks_uri` 的值,获取 IDP 的证书: ```bash curl -k -v \ -X GET \ -H "Content-Type: application/x-www-form-urlencoded" \ "http://$KC_SERVER/$KC_CONTEXT/realms/$KC_REALM/protocol/openid-connect/certs" \ | jq . ``` 4. 把响应中 `x5c` 的内容复制到文件 `certificate.crt`,在开头加 `-----BEGIN CERTIFICATE-----`,结尾加 `-----END CERTIFICATE-----`。 5. 用 OpenSSL 取证书指纹: ```bash openssl x509 -in certificate.crt -fingerprint -noout ``` 6. 得到 SHA1 指纹,形如: ```text SHA1 Fingerprint=F7:D7:B3:51:5D:D0:D3:19:DD:21:9A:43:A9:EA:72:7A:D6:06:52:87 ``` 7. 去掉冒号得到最终 thumbprint,可作为在 IAM 中创建 OpenID Connect Provider 实体的输入: ```text F7D7B3515DD0D319DD219A43A9EA727AD6065287 ``` > 引自:[STS in Ceph](https://docs.ceph.com/en/tentacle/radosgw/STS/) --- ### 4.3 STS Lite(STSLite.rst) #### 4.3.1 定位与价值 STS Lite 是 STS 的扩展,构建在 STS 的某个 API 之上,目的是**降低外部 IDP(如 Keystone、LDAP)的负载**。 工作原理:用一组 AWS 凭证与外部 IDP 完成认证后,返回一组临时安全凭证;这些临时凭证可用于发起后续 S3 调用,由 Ceph 中的 STS engine 完成认证 —— 从而**减少对 Keystone / LDAP 服务器的压力**。 同时,**本地用户也可以通过 STS Lite API 获得临时的、权限受限的凭证**。 #### 4.3.2 与完整 STS 的区别 | 维度 | 完整 STS | STS Lite | |---|---|---| | 提供的 API | `AssumeRole`、`AssumeRoleWithWebIdentity` | `GetSessionToken` | | 主要场景 | 跨账户访问;Web/移动应用通过 OIDC/OAuth2 IDP 联邦 | 降低 Keystone/LDAP 负载;本地用户取临时受限凭证 | | 是否需要 Role | **需要**(`RoleArn` 必填) | **不需要**(无 RoleArn 参数) | | 临时凭证权限来源 | Role 权限策略 ∩ API 传入策略 | **与原始 AWS 凭证的权限相同** | | 会话时长上限配置 | `rgw_sts_min_session_duration`(下限 900) | `rgw_sts_max_session_duration`(默认上限 43200) | | 与 Keystone/LDAP 配合 | 不直接依赖 | 可配合 Keystone 或 LDAP 使用 | #### 4.3.3 GetSessionToken 返回一组与某组 AWS 凭证对应的临时凭证。在与 Keystone / LDAP 完成初次认证后,返回的临时凭证可用于发起后续 S3 调用。**临时凭证拥有的权限与该 AWS 凭证相同。** | 参数 | 类型 | 是否必填 | 说明 | |---|---|---|---| | `DurationSeconds` | Integer | Optional | 凭证有效的秒数。**默认值为 3600**;**默认最大值为 43200**,可通过 `rgw sts max session duration` 配置 | | `SerialNumber` | String | Optional | 与发起 GetSessionToken 调用的用户关联的 MFA 设备 Id | | `TokenCode` | String | Optional | 若要求 MFA,则由 MFA 设备提供的值 | #### 4.3.4 必需的 user policy 管理员需要挂载策略,以允许用户用其**永久凭证**调用 `GetSessionToken`,并且**只允许**用 GetSessionToken 返回的临时凭证发起后续 S3 操作。 挂载策略的用户需要 admin caps: ```bash radosgw-admin caps add --uid="TESTER" --caps="user-policy=*" ``` 需要挂到用户 `TESTER1` 上的策略: ```json {"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"s3:*","Resource":["*"],"Condition":{"BoolIfExists":{"sts:authentication":"false"}}},{"Effect":"Allow","Action":"sts:GetSessionToken","Resource":"*","Condition":{"BoolIfExists":{"sts:authentication":"false"}}}]} ``` 该策略的语义:当 `sts:authentication` 为 false(即不是用 STS 临时凭证认证的请求)时,`Deny` 所有 s3 操作,只 `Allow` `sts:GetSessionToken`。 > 💡 补充:这是本册源文中唯一出现的 `sts:authentication` condition key 用法;`BoolIfExists` 表示键不存在时按真处理。 #### 4.3.5 配置 最小配置: ```ini [client.radosgw.gateway] rgw sts key = {sts key for encrypting the session token} rgw s3 auth use sts = true ``` 与 Keystone 配合的完整配置集: ```ini [client.{your-rgw-name}] rgw_sts_key = {sts key for encrypting/ decrypting the session token, base64 encoded} rgw_s3_auth_use_sts = true rgw keystone url = {keystone server url:keystone server admin port} rgw keystone admin project = {keystone admin project name} rgw keystone admin tenant = {keystone service tenant name} rgw keystone admin domain = {keystone admin domain name} rgw keystone api version = {keystone api version} rgw keystone implicit tenants = {true for private tenant for each new user} rgw keystone admin password = {keystone service tenant user name} rgw keystone admin user = keystone service tenant user password} rgw keystone accepted roles = {accepted user roles} rgw keystone token cache size = {number of tokens to cache} rgw s3 auth use keystone = true ``` 与 LDAP 配合的完整配置集: ```ini [client.{your-rgw-name}] rgw_sts_key = {sts key for encrypting/ decrypting the session token, base64 encoded} rgw_s3_auth_use_sts = true rgw_s3_auth_use_ldap = true rgw_ldap_uri = {LDAP server to use} rgw_ldap_binddn = {Distinguished Name (DN) of the service account} rgw_ldap_secret = {password for the service account} rgw_ldap_searchdn = {base in the directory information tree for searching users} rgw_ldap_dnattr = {attribute being used in the constructed search filter to match a username} rgw_ldap_searchfilter = {search filter} ``` > 💡 补充:源文 Keystone 配置集中 `rgw keystone admin password` 与 `rgw keystone admin user` 两行的说明文字**互相错位**了(password 行的说明写成 "user name",user 行的说明写成 "user password}")。本册保留源文原样,并在此提示。 **生成 `rgw_sts_key`**: ```bash ceph-authtool --gen-print-key ``` 生成的 key 会是 base64 编码(编码后的 key 还会包含 key type 与 timestamp)。 > 注意:**STS 与 S3 API 默认共存于同一命名空间,可通过同一 endpoint 访问。** #### 4.3.6 STS Lite + Keystone 完整示例 **步骤 1**:生成 EC2 凭证。 ```bash openstack ec2 credentials create ``` ```text +------------+--------------------------------------------------------+ | Field | Value | +------------+--------------------------------------------------------+ | access | b924dfc87d454d15896691182fdeb0ef | | links | {u'self': u'http://192.168.0.15/identity/v3/users/ | | | 40a7140e424f493d8165abc652dc731c/credentials/ | | | OS-EC2/b924dfc87d454d15896691182fdeb0ef'} | | project_id | c703801dccaf4a0aaa39bec8c481e25a | | secret | 6a2142613c504c42a94ba2b82147dc28 | | trust_id | None | | user_id | 40a7140e424f493d8165abc652dc731c | +------------+--------------------------------------------------------+ ``` **步骤 2**:用步骤 1 的凭证调用 `GetSessionToken` 换取临时凭证。 ```python import boto3 access_key = secret_key = client = boto3.client('sts', aws_access_key_id=access_key, aws_secret_access_key=secret_key, endpoint_url=, region_name='', ) response = client.get_session_token( DurationSeconds=43200 ) ``` **步骤 3**:用临时凭证发起 S3 调用。 ```python s3client = boto3.client('s3', aws_access_key_id = response['Credentials']['AccessKeyId'], aws_secret_access_key = response['Credentials']['SecretAccessKey'], aws_session_token = response['Credentials']['SessionToken'], endpoint_url=, region_name='') bucket = s3client.create_bucket(Bucket='my-new-shiny-bucket') response = s3client.list_buckets() for bucket in response["Buckets"]: print("{name}\t{created}".format( name = bucket['Name'], created = bucket['CreationDate'], )) ``` LDAP 下使用 `GetSessionToken` 的步骤类似。 #### 4.3.7 已知限制与规避方法 **Keystone 目前只支持 S3 请求**,因此为了成功认证 STS 请求,需要给 boto 打补丁 —— 修改 `botocore/auth.py`,加入第 13–16 行(即下面代码块中 `if service_name == 'sts': self._service_name = 's3'` 这段): ```python class SigV4Auth(BaseSigner): """ Sign a request with Signature V4. """ REQUIRES_REGION = True def __init__(self, credentials, service_name, region_name): self.credentials = credentials # We initialize these value here so the unit tests can have # valid values. But these will be overridden in ``add_auth`` # later for real requests. self._region_name = region_name if service_name == 'sts': self._service_name = 's3' else: self._service_name = service_name ``` > 引自:[STS Lite](https://docs.ceph.com/en/tentacle/radosgw/STSLite/) --- ### 4.4 会话标签(session-tags.rst) #### 4.4.1 什么是会话标签 Session tags(会话标签)是**在联邦用户时传入的键值对**(目前仅支持作为传给 `AssumeRoleWithWebIdentity` 的 web token 的一部分传入)。 - 会话标签会作为 **`aws:PrincipalTag`** 随会话凭证(临时凭证)一起返回(由 STS 返回); - 这些 Principal Tags **由两部分组成**:① web token 中传入的 session tags;② 被 assume 的 role 上附着的 tags; - 标签**必须始终指定在 `https://aws.amazon.com/tags` 这个命名空间下**。 IDP 在 web token 中传入 session tags 的示例: ```json { "jti": "947960a3-7e91-4027-99f6-da719b0d4059", "exp": 1627438044, "nbf": 0, "iat": 1627402044, "iss": "http://localhost:8080/auth/realms/quickstart", "aud": "app-profile-jsp", "sub": "test", "typ": "ID", "azp": "app-profile-jsp", "auth_time": 0, "session_state": "3a46e3e7-d198-4a64-8b51-69682bcfc670", "preferred_username": "test", "email_verified": false, "acr": "1", "https://aws.amazon.com/tags": [ { "principal_tags": { "Department": [ "Engineering", "Marketing" ] } } ], "client_id": "app-profile-jsp", "username": "test", "active": true } ``` **关键规则**:如果联邦用户传入的 web token 中含有 session tags,那么 **trust policy 必须具备 `sts:TagSession` 权限**,否则 `AssumeRoleWithWebIdentity` 会失败。 ```json { "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":["sts:AssumeRoleWithWebIdentity","sts:TagSession"], "Principal":{"Federated":["arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart"]}, "Condition":{"StringEquals":{"localhost:8080/auth/realms/quickstart:sub":"test"}} }] } ``` #### 4.4.2 标签键(Tag Keys)逐项 可在 role 的 trust policy 或 role 的 permission policy 中使用的 tag key 共 5 个: | # | Tag Key | 用途 | 可用位置 | |---|---|---|---| | 1 | `aws:RequestTag` | 把**请求中传入**的键值对与 role trust policy 中的键值对比较。在 `AssumeRoleWithWebIdentity` 场景下,IDP 在 web token 中传入的 session tags 可作为 `aws:RequestTag` 用在 role 的 trust policy 中,据此决定联邦用户是否被允许 assume 该 role | role 的 trust policy | | 2 | `aws:PrincipalTag` | 把**挂在 principal 上**的键值对与 policy 中的键值对比较。在 `AssumeRoleWithWebIdentity` 场景下,IDP 在 web token 中传入的 session tags 在用户认证通过后会出现在临时凭证的 Principal tags 中,可作为 `aws:PrincipalTag` 用在 role 的 **permission policy** 中 | role 的 permission policy | | 3 | `iam:ResourceTag` | 把**挂在资源上**的键值对与 policy 中的键值对比较。在 `AssumeRoleWithWebIdentity` 场景下,挂在 role 上的 tags 可与 trust policy 中的比较,以决定是否允许用户 assume 该 role。RGW 已支持针对 role 的 tagging / list tags / untag 的 REST API | role 的 trust policy | | 4 | `aws:TagKeys` | 把**请求中的 tags** 与 **policy 中的 tags** 比较。在 `AssumeRoleWithWebIdentity` 场景下可用于在允许用户 assume role 之前检查 role trust policy 中的 tag keys。**也可用于 role 的 permission policy** | role 的 trust policy 与 permission policy | | 5 | `s3:ResourceTag` | 把 **s3 资源(bucket 或 object)上的 tags** 与 role 的 permission policy 中的 tags 比较。**AWS 中不允许这样用** | role 的 permission policy | **1. `aws:RequestTag` 示例(trust policy)**: ```json { "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":["sts:AssumeRoleWithWebIdentity","sts:TagSession"], "Principal":{"Federated":["arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart"]}, "Condition":{"StringEquals":{"aws:RequestTag/Department":"Engineering"}} }] } ``` **2. `aws:PrincipalTag` 示例(permission policy)**: ```json { "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":["s3:*"], "Resource":["arn:aws:s3::t1tenant:my-test-bucket","arn:aws:s3::t1tenant:my-test-bucket/*"], "Condition":{"StringEquals":{"aws:PrincipalTag/Department":"Engineering"}} }] } ``` **3. `iam:ResourceTag` 示例(trust policy)**: ```json { "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":["sts:AssumeRoleWithWebIdentity","sts:TagSession"], "Principal":{"Federated":["arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart"]}, "Condition":{"StringEquals":{"iam:ResourceTag/Department":"Engineering"}} }] } ``` > 要使上例生效,需要给 role 附上 `Department=Engineering` 标签。 **4. `aws:TagKeys` 示例(trust policy)**: ```json { "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":["sts:AssumeRoleWithWebIdentity","sts:TagSession"], "Principal":{"Federated":["arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart"]}, "Condition":{"ForAllValues:StringEquals":{"aws:TagKeys":["Department"]}} }] } ``` `ForAllValues:StringEquals` 检验**请求中的每个 tag key 都是 policy 中 tag keys 的子集**,因此上述 condition 限制了请求中可以传入的 tag keys。 **5. `s3:ResourceTag` 示例(permission policy)**: ```json { "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":["s3:PutBucketTagging"], "Resource":["arn:aws:s3::t1tenant:my-test-bucket\","arn:aws:s3::t1tenant:my-test-bucket/*"] }, { "Effect":"Allow", "Action":["s3:*"], "Resource":["*"], "Condition":{"StringEquals":{"s3:ResourceTag/Department":\"Engineering"}} } } ``` > 要使上例生效,需要给希望应用此策略的 bucket(以及 object)附上 `Department=Engineering` 标签。 > 💡 补充:源文此 JSON 片段中的引号转义明显有误(`arn:aws:s3::t1tenant:my-test-bucket\"` 与 `\"Engineering"`),本册按原样保留以忠实记录,实际书写时应为合法 JSON 字符串。 #### 4.4.3 更多带标签的 policy 示例 **示例 1**:用请求中传入的标签与 role 上附着的标签匹配,来决定是否允许 assume role。`aws:RequestTag` 是 JWT(access token)中的传入标签,`iam:ResourceTag` 是被 assume 的 role 上附着的标签。 ```json { "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":["sts:AssumeRoleWithWebIdentity","sts:TagSession"], "Principal":{"Federated":["arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart"]}, "Condition":{"StringEquals":{"aws:RequestTag/Department":"${iam:ResourceTag/Department}"}} }] } ``` **示例 2**:用 principal tags 与 s3 资源标签匹配来评估 role 的 permission policy。`aws:PrincipalTag` 是随临时凭证传入的标签,`s3:ResourceTag` 是挂在 s3 资源(object/bucket)上的标签。 ```json { "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":["s3:PutBucketTagging"], "Resource":["arn:aws:s3::t1tenant:my-test-bucket\","arn:aws:s3::t1tenant:my-test-bucket/*"] }, { "Effect":"Allow", "Action":["s3:*"], "Resource":["*"], "Condition":{"StringEquals":{"s3:ResourceTag/Department":"${aws:PrincipalTag/Department}"}} } } ``` > 💡 补充:这两个示例展示了**策略变量插值**(`${iam:ResourceTag/Department}`、`${aws:PrincipalTag/Department}`)的用法 —— 这与 `bucketpolicy.rst` 中"bucket policies 还不支持字符串插值(string interpolation)"的说明形成对比:插值能力出现在 **role 的 trust / permission policy** 中,而**桶策略不支持**。 #### 4.4.4 会话标签的属性(限制) | # | 属性 | |---|---| | 1 | Session Tags **可以是多值的**(多值 session tags 在 AWS 中不支持) | | 2 | IDP 最多允许传入 **50 个** session tags | | 3 | key 的最大长度为 **128 字符** | | 4 | value 的最大长度为 **256 字符** | | 5 | tag 与 value **不能以 `aws:` 开头** | #### 4.4.5 s3 Resource Tags 如前所述,`s3:ResourceTag` 键可用于在 RGW 中对 s3 操作授权(**AWS 不允许**)。`s3:ResourceTag` 指挂在 object 或 bucket 上的 tags,tags 可通过相应的 REST API 挂到 object 或 bucket 上。 下表说明**授权某个操作时使用的是哪种 s3 资源标签类型**(bucket tags 还是 object tags): | Operation | Tag type | |---|---| | `GetObject` | Object tags | | `GetObjectTags` | Object tags | | `DeleteObjectTags` | Object tags | | `DeleteObject` | Object tags | | `PutACLs` | Object tags | | `InitMultipart` | Object tags | | `AbortMultipart` | Object tags | | `ListMultipart` | Object tags | | `GetAttrs` | Object tags | | `PutObjectRetention` | Object tags | | `GetObjectRetention` | Object tags | | `PutObjectLegalHold` | Object tags | | `GetObjectLegalHold` | Object tags | | `PutObjectTags` | Bucket tags | | `GetBucketTags` | Bucket tags | | `PutBucketTags` | Bucket tags | | `DeleteBucketTags` | Bucket tags | | `GetBucketReplication` | Bucket tags | | `DeleteBucketReplication` | Bucket tags | | `GetBucketVersioning` | Bucket tags | | `SetBucketVersioning` | Bucket tags | | `GetBucketWebsite` | Bucket tags | | `SetBucketWebsite` | Bucket tags | | `DeleteBucketWebsite` | Bucket tags | | `StatBucket` | Bucket tags | | `ListBucket` | Bucket tags | | `GetBucketLogging` | Bucket tags | | `GetBucketLocation` | Bucket tags | | `DeleteBucket` | Bucket tags | | `GetLC` | Bucket tags | | `PutLC` | Bucket tags | | `DeleteLC` | Bucket tags | | `GetCORS` | Bucket tags | | `PutCORS` | Bucket tags | | `GetRequestPayment` | Bucket tags | | `SetRequestPayment` | Bucket tags | | `PutBucketPolicy` | Bucket tags | | `GetBucketPolicy` | Bucket tags | | `DeleteBucketPolicy` | Bucket tags | | `PutBucketObjectLock` | Bucket tags | | `GetBucketObjectLock` | Bucket tags | | `GetBucketPolicyStatus` | Bucket tags | | `PutBucketPublicAccessBlock` | Bucket tags | | `GetBucketPublicAccessBlock` | Bucket tags | | `DeleteBucketPublicAccessBlock` | Bucket tags | | `GetACLs` | bucket ACLs 用 Bucket tags;object ACLs 用 Object tags | | `PutACLs` | bucket ACLs 用 Bucket tags;object ACLs 用 Object tags | | `PutObject` | 源 object 的 Object tags;目标 bucket 的 Bucket tags | | `CopyObject` | 源 object 的 Object tags;目标 bucket 的 Bucket tags | > 💡 补充:源文原始表格中 `PutACLs` 同时出现在 Object tags 组与 GetACLs/PutACLs 组,本册保留了两处出现。 #### 4.4.6 完整示例代码(role / bucket / object 打标签 + 策略中使用 tag keys) 假设 IDP 在 JWT(access token)中传入标签 `Department=Engineering`: ```python # -*- coding: utf-8 -*- import boto3 import json from nose.tools import eq_ as eq access_key = 'TESTER' secret_key = 'test123' endpoint = 'http://s3.us-east.localhost:8000' s3client = boto3.client('s3', aws_access_key_id = access_key, aws_secret_access_key = secret_key, endpoint_url = endpoint, region_name='',) s3res = boto3.resource('s3', aws_access_key_id = access_key, aws_secret_access_key = secret_key, endpoint_url = endpoint, region_name='',) iam_client = boto3.client('iam', aws_access_key_id=access_key, aws_secret_access_key=secret_key, endpoint_url=endpoint, region_name='' ) bucket_name = 'test-bucket' s3bucket = s3client.create_bucket(Bucket=bucket_name) bucket_tagging = s3res.BucketTagging(bucket_name) Set_Tag = bucket_tagging.put(Tagging={'TagSet':[{'Key':'Department', 'Value': 'Engineering'}]}) try: response = iam_client.create_open_id_connect_provider( Url='http://localhost:8080/auth/realms/quickstart', ClientIDList=[ 'app-profile-jsp', 'app-jee-jsp' ], ThumbprintList=[ 'F7D7B3515DD0D319DD219A43A9EA727AD6065287' ] ) except ClientError as e: print ("Provider already exists") policy_document = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Federated\":[\"arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart\"]},\"Action\":[\"sts:AssumeRoleWithWebIdentity\",\"sts:TagSession\"],\"Condition\":{\"StringEquals\":{\"aws:RequestTag/Department\":\"${iam:ResourceTag/Department}\"}}}]}" role_response = "" print ("\n Getting Role \n") try: role_response = iam_client.get_role( RoleName='S3Access' ) print (role_response) except ClientError as e: if e.response['Code'] == 'NoSuchEntity': print ("\n Creating Role \n") tags_list = [ {'Key':'Department','Value':'Engineering'}, ] role_response = iam_client.create_role( AssumeRolePolicyDocument=policy_document, Path='/', RoleName='S3Access', Tags=tags_list, ) print (role_response) else: print("Unexpected error: %s" % e) role_policy = "{\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Allow\",\"Action\":\"s3:*\",\"Resource\":\"arn:aws:s3:::*\",\"Condition\":{\"StringEquals\":{\"s3:ResourceTag/Department\":[\"${aws:PrincipalTag/Department}\"]}}}}" response = iam_client.put_role_policy( RoleName='S3Access', PolicyName='Policy1', PolicyDocument=role_policy ) sts_client = boto3.client('sts', aws_access_key_id='abc', aws_secret_access_key='def', endpoint_url = endpoint, region_name = '', ) print ("\n Assuming Role with Web Identity\n") response = sts_client.assume_role_with_web_identity( RoleArn=role_response['Role']['Arn'], RoleSessionName='Bob', DurationSeconds=900, WebIdentityToken='') s3client2 = boto3.client('s3', aws_access_key_id = response['Credentials']['AccessKeyId'], aws_secret_access_key = response['Credentials']['SecretAccessKey'], aws_session_token = response['Credentials']['SessionToken'], endpoint_url='http://s3.us-east.localhost:8000', region_name='',) bucket_body = 'this is a test file' tags = 'Department=Engineering' key = "test-1.txt" s3_put_obj = s3client2.put_object(Body=bucket_body, Bucket=bucket_name, Key=key, Tagging=tags) eq(s3_put_obj['ResponseMetadata']['HTTPStatusCode'],200) s3_get_obj = s3client2.get_object(Bucket=bucket_name, Key=key) eq(s3_get_obj['ResponseMetadata']['HTTPStatusCode'],200) ``` > 引自:[Session tags for Attribute Based Access Control in STS](https://docs.ceph.com/en/tentacle/radosgw/session-tags/) --- ## 五、桶策略(bucketpolicy.rst) > *Bucket policies 在 Ceph 的 Luminous 版本中引入。* Ceph Object Gateway 支持 **Amazon S3 policy 语言的一个子集**,应用于桶。 ### 5.1 创建与删除 桶策略**通过标准 S3 操作管理,而不是 `radosgw-admin`**。用 `s3cmd` 设置/删除策略: ```bash $ cat > examplepol { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam::usfolks:user/fred:subuser"]}, "Action": "s3:PutObjectAcl", "Resource": [ "arn:aws:s3:::happybucket/*" ] }] } $ s3cmd setpolicy examplepol s3://happybucket $ s3cmd delpolicy s3://happybucket ``` ### 5.2 支持的 Action(完整列表) 当前只支持以下 action: | # | Action | # | Action | |---|---|---|---| | 1 | `s3:AbortMultipartUpload` | 26 | `s3:GetObjectVersionAcl` | | 2 | `s3:CreateBucket` | 27 | `s3:GetObjectVersion` | | 3 | `s3:DeleteBucketPolicy` | 28 | `s3:GetObjectVersionTorrent` | | 4 | `s3:DeleteBucket` | 29 | `s3:GetReplicationConfiguration` | | 5 | `s3:DeleteBucketWebsite` | 30 | `s3:IPAddress` | | 6 | `s3:DeleteObject` | 31 | `s3:NotIpAddress` | | 7 | `s3:DeleteObjectVersion` | 32 | `s3:ListAllMyBuckets` | | 8 | `s3:DeleteReplicationConfiguration` | 33 | `s3:ListBucketMultipartUploads` | | 9 | `s3:GetAccelerateConfiguration` | 34 | `s3:ListBucket` | | 10 | `s3:GetBucketAcl` | 35 | `s3:ListBucketVersions` | | 11 | `s3:GetBucketCORS` | 36 | `s3:ListMultipartUploadParts` | | 12 | `s3:GetBucketLocation` | 37 | `s3:PutAccelerateConfiguration` | | 13 | `s3:GetBucketLogging` | 38 | `s3:PutBucketAcl` | | 14 | `s3:GetBucketNotification` | 39 | `s3:PutBucketCORS` | | 15 | `s3:GetBucketPolicy` | 40 | `s3:PutBucketLogging` | | 16 | `s3:GetBucketRequestPayment` | 41 | `s3:PutBucketNotification` | | 17 | `s3:GetBucketTagging` | 42 | `s3:PutBucketPolicy` | | 18 | `s3:GetBucketVersioning` | 43 | `s3:PutBucketRequestPayment` | | 19 | `s3:GetBucketWebsite` | 44 | `s3:PutBucketTagging` | | 20 | `s3:GetLifecycleConfiguration` | 45 | `s3:PutBucketVersioning` | | 21 | `s3:GetObjectAcl` | 46 | `s3:PutBucketWebsite` | | 22 | `s3:GetObject` | 47 | `s3:PutLifecycleConfiguration` | | 23 | `s3:GetObjectTorrent` | 48 | `s3:PutObjectAcl` | | 24 | `s3:GetObjectVersionAcl` | 49 | `s3:PutObject` | | 25 | `s3:GetObjectVersion` | 50 | `s3:PutObjectVersionAcl` | | | | 51 | `s3:PutReplicationConfiguration` | | | | 52 | `s3:RestoreObject` | ### 5.3 限制 | 限制 | 说明 | |---|---| | 不支持在 user / group / role 上设置策略 | "We do not yet support setting policies on users, groups, or roles" | | Account ID 的替代 | 用 RGW 的 **tenant 标识符**代替 Amazon 的 12 位 account ID。未来可能允许给 tenant 分配 account ID,但**目前**若要在 AWS S3 与 RGW S3 之间使用策略,**必须在创建用户时使用 Amazon account ID 作为 tenant ID** | | 命名空间差异 | AWS 中所有 tenant 共享单一命名空间;**RGW 给每个 tenant 各自的桶命名空间**。未来可能提供 AWS 式 'flat' 桶命名空间选项。**当前访问属于另一个 tenant 的桶,要在 S3 请求中用 `"tenant:bucket"` 寻址** | | 授权粒度 | AWS 中桶策略可把访问权限授予另一个账户,该账户拥有者再用用户权限授予具体用户。由于 RGW 还不支持 user / role / group 权限,**账户拥有者目前必须直接把权限授予具体用户**;并且**把整个账户授予桶的访问权限,等于把权限授予了该账户下的所有用户** | | 字符串插值 | **桶策略尚不支持字符串插值(string interpolation)** | ### 5.4 全局 Condition Keys 对所有请求,支持的 condition keys 为: - `aws:CurrentTime` - `aws:EpochTime` - `aws:PrincipalType` - `aws:Referer` - `aws:SecureTransport` - `aws:SourceIp` - `aws:UserAgent` - `aws:username` 此外,对 bucket 与 object 请求还支持若干 **s3 condition keys**。 > *对下列 bucket 相关操作的支持是在 Ceph 的 Mimic 版本中加入的。* ### 5.5 Bucket 相关操作的 Condition Keys | Permission | Condition Keys | Comments | |---|---|---| | `s3:createBucket` | `s3:x-amz-acl`、`s3:x-amz-grant-`(其中 perm 取 `read` / `write` / `read-acp` / `write-acp` / `full-control` 之一) | 文档未明确 | | `s3:ListBucket` 与 `s3:ListBucketVersions` | `s3:prefix`、`s3:delimiter`、`s3:max-keys` | 文档未明确 | | `s3:PutBucketAcl` | `s3:x-amz-acl`、`s3:x-amz-grant-` | 文档未明确 | ### 5.6 Object 相关操作的 Condition Keys | Permission | Condition Keys | Comments | |---|---|---| | `s3:PutObject` | `s3:x-amz-acl` 与 `s3:x-amz-grant-` | 文档未明确 | | `s3:PutObject` | `s3:x-amz-copy-source` | 文档未明确 | | `s3:PutObject` | `s3:x-amz-server-side-encryption` | 文档未明确 | | `s3:PutObject` | `s3:x-amz-server-side-encryption-aws-kms-key-id` | 文档未明确 | | `s3:PutObject` | `s3:x-amz-server-side-encryption-customer-algorithm` | 文档未明确 | | `s3:PutObject` | `s3:x-amz-metadata-directive` | PUT 与 COPY 中用于 overwrite/preserve COPY 请求里的 metadata | | `s3:PutObject` | `s3:RequestObjectTag/` | 文档未明确 | | `s3:PutObjectAcl`、`s3:PutObjectVersionAcl` | `s3:x-amz-acl` 与 `s3-amz-grant-`;`s3:ExistingObjectTag/` | 文档未明确 | | `s3:PutObjectTagging`、`s3:PutObjectVersionTagging` | `s3:RequestObjectTag/`;`s3:ExistingObjectTag/` | 文档未明确 | | `s3:GetObject`、`s3:GetObjectVersion` | `s3:ExistingObjectTag/` | 文档未明确 | | `s3:GetObjectAcl`、`s3:GetObjectVersionAcl` | `s3:ExistingObjectTag/` | 文档未明确 | | `s3:GetObjectTagging`、`s3:GetObjectVersionTagging` | `s3:ExistingObjectTag/` | 文档未明确 | | `s3:DeleteObjectTagging`、`s3:DeleteObjectVersionTagging` | `s3:ExistingObjectTag/` | 文档未明确 | > 官方补充:随着近期重写的 Authentication/Authorization 子系统完成集成,未来可能支持更多。 ### 5.7 Swift - **Swift 下没有办法设置桶策略**; - 但**已设置的桶策略同时约束 Swift 与 S3 操作**; - Swift 凭证与策略中指定的 Principal 的匹配方式,**取决于所使用的具体后端**。 > 引自:[Bucket Policies](https://docs.ceph.com/en/tentacle/radosgw/bucketpolicy/) --- ## 六、外部身份源集成 ### 6.1 OpenStack Keystone(keystone.rst) 可以把 Ceph Object Gateway 与 OpenStack 的身份服务 Keystone 集成。这让网关**接受 Keystone 作为用户权威(users authority)**: - 被 Keystone 授权访问网关的用户,**会在 Ceph Object Gateway 上被自动创建**(如果此前不存在); - 被 Keystone 判定有效的 token,网关也认为有效。 #### 6.1.1 配置项 ```ini [client.radosgw.gateway] rgw keystone api version = {keystone api version} rgw keystone url = {keystone server url:keystone server admin port} rgw keystone admin token = {keystone admin token} rgw keystone admin token path = {path to keystone admin token} #preferred rgw keystone accepted roles = {accepted user roles} rgw keystone token cache size = {number of tokens to cache} rgw keystone implicit tenants = {true for private tenant for each new user} ``` 也可以为 Keystone 配置 service tenant / user / password(对应 OpenStack Identity API v2.0),这与 OpenStack 服务通常的配置方式类似,可以**避免在配置文件中设置共享密钥 `rgw keystone admin token`** —— 官方建议在**生产环境中禁用** `rgw keystone admin token`。service tenant 的凭证应具备 admin 权限。 ```ini rgw keystone admin user = {keystone service tenant user name} rgw keystone admin password = {keystone service tenant user password} rgw keystone admin password = {keystone service tenant user password path} # preferred rgw keystone admin tenant = {keystone service tenant name} ``` #### 6.1.2 用户/租户/角色映射 - 一个 Ceph Object Gateway 用户被映射到 Keystone 的一个 `tenant`; - 一个 Keystone 用户可能在不同 tenant 上被分配不同角色; - 网关拿到 ticket 后,查看其中的 **tenant** 与分配给该 ticket 的 **用户角色**,然后依据 `rgw keystone accepted roles` 配置决定接受或拒绝请求。 **API v3** 时应把 `rgw keystone admin tenant` 替换为: ```ini rgw keystone admin domain = {keystone admin domain name} rgw keystone admin project = {keystone admin project name} ``` **兼容旧版本**:`rgw keystone implicit tenants` 也可以设为 `s3` 或 `swift`,效果是**切分身份空间**——被指定的协议只使用 implicit tenants,另一个协议永不使用 implicit tenants。(某些旧版 ceph 只支持 swift 使用 implicit tenants。) #### 6.1.3 Keystone 侧配置(Ocata 及以后) Keystone 本身需要配置为把 Ceph Object Gateway 作为 object-storage endpoint: ```bash openstack service create --name=swift \ --description="Swift Service" \ object-store ``` ```text +-------------+----------------------------------+ | Field | Value | +-------------+----------------------------------+ | description | Swift Service | | enabled | True | | id | 37c4c0e79571404cb4644201a4a6e5ee | | name | swift | | type | object-store | +-------------+----------------------------------+ ``` ```bash openstack endpoint create --region RegionOne \ --publicurl "http://radosgw.example.com:8080/swift/v1" \ --adminurl "http://radosgw.example.com:8080/swift/v1" \ --internalurl "http://radosgw.example.com:8080/swift/v1" \ swift ``` ```text +--------------+------------------------------------------+ | Field | Value | +--------------+------------------------------------------+ | adminurl | http://radosgw.example.com:8080/swift/v1 | | id | e4249d2b60e44743a67b5e5b38c18dd3 | | internalurl | http://radosgw.example.com:8080/swift/v1 | | publicurl | http://radosgw.example.com:8080/swift/v1 | | region | RegionOne | | service_id | 37c4c0e79571404cb4644201a4a6e5ee | | service_name | swift | | service_type | object-store | +--------------+------------------------------------------+ ``` 查看: ```bash $ openstack endpoint show object-store ``` > ⚠️ 注意:如果你的 radosgw `ceph.conf` 设置了 `rgw swift account in url = true`,那么 `object-store` endpoint URL **必须**包含后缀 `/v1/AUTH_%(tenant_id)s`(而不仅仅是 `/v1`)。 Keystone URL 是 Keystone 的 admin RESTful API URL;admin token 是 Keystone 内部为 admin 请求配置的 token。 **自签名证书**:OpenStack Keystone 可能以自签名 SSL 证书终止。此时 radosgw 要么在运行的节点上安装 Keystone 的 ssl 证书,要么把配置项 `rgw keystone verify ssl` 设为 false,使 radosgw 完全不校验 ssl 证书(类似 OpenStack 客户端的 `--insecure` 开关)。 #### 6.1.4 跨 Project(Tenant)访问 要让一个 project(旧称 tenant)访问属于另一个 project 的桶,需要启用: ```ini rgw swift account in url = true ``` Keystone 的 object-store endpoint 也必须相应配置为包含 `AUTH_%(project_id)s` 后缀: ```bash openstack endpoint create --region RegionOne \ --publicurl "http://radosgw.example.com:8080/swift/v1/AUTH_$(project_id)s" \ --adminurl "http://radosgw.example.com:8080/swift/v1/AUTH_$(project_id)s" \ --internalurl "http://radosgw.example.com:8080/swift/v1/AUTH_$(project_id)s" \ swift ``` #### 6.1.5 Keystone 与 S3 API 集成 即使使用 S3 API(带 AWS 风格的 access key 和 secret key),只要设置了 `rgw s3 auth use keystone` 选项,也可以用 Keystone 做认证。 #### 6.1.6 Service Token 支持 Service tokens 可用于支持 RadosGW Keystone 集成**在请求中同时携带有效 service token 时允许过期的 token**: - 用 `rgw keystone service token enabled` 启用; - 用 `rgw keystone service token accepted roles` 指定哪些角色被视为 service roles; - 用 `rgw keystone expired token cache expiration` 调整"被 service token 放行的过期 token"的缓存过期时间 —— **该值必须低于 Keystone 配置中的 `[token]/allow_expired_window`**。 启用后:若 `X-Auth-Token` 头中的 token 已过期,但 `X-Service-Token` 头中含有具备 accepted roles 的有效 token,则该过期 token 会被放行。这可以让使用用户 token 的**长运行进程**在 token 过期后继续工作。 #### 6.1.7 Keystone 配置项逐项表格 | 配置项 | 类型 | 默认值(`rgw.yaml.in` @ v20.2.4) | 说明 | |---|---|---|---| | `rgw_keystone_api_version` | — | **文档未明确**(该配置项未在 `rgw.yaml.in` / `global.yaml.in` 中定义) | Keystone API 版本 | | `rgw_keystone_url` | str | 文档未明确(无 `default:`) | Keystone server 的 URL | | `rgw_keystone_admin_token` | str | 文档未明确(无 `default:`) | 用于 Keystone 请求的 admin token(共享密钥)。**已 DEPRECATED**。admin token 认证**优先于** admin 凭证认证 | | `rgw_keystone_admin_token_path` | str | 文档未明确(无 `default:`) | 包含 admin token 的文件路径(推荐,避免暴露 token) | | `rgw_keystone_admin_user` | str | 文档未明确(无 `default:`) | Keystone admin 用户(OpenStack Identity API v2 下的 Service User) | | `rgw_keystone_admin_password` | str | 文档未明确(无 `default:`) | OpenStack admin 用户密码(v2)。**已 DEPRECATED**,推荐改用 `rgw_keystone_admin_password_path` | | `rgw_keystone_admin_password_path` | str | 文档未明确(无 `default:`) | 包含 Keystone admin 密码的文件路径 | | `rgw_keystone_admin_tenant` | str | 文档未明确(无 `default:`) | Keystone admin 用户的 tenant(v2 下 Service Tenant) | | `rgw_keystone_admin_project` | str | 文档未明确(无 `default:`) | Keystone admin 用户的 project(v3)。**若未指定,则使用 `rgw keystone admin tenant` 的值** | | `rgw_keystone_admin_domain` | str | 文档未明确(无 `default:`) | Keystone admin 用户的 domain(v3) | | `rgw_keystone_accepted_roles` | str | `Member, admin` | 做 Keystone 认证时,只有具备这些角色之一的用户才会被服务 | | `rgw_keystone_accepted_admin_roles` | str | 文档未明确(无 `default:`) | 允许用户获得 admin 特权的角色列表 | | `rgw_keystone_accepted_reader_roles` | str | 文档未明确(无 `default:`) | 只能用于读操作的角色列表 | | `rgw_keystone_token_cache_size` | int | `10000` | 每个 Keystone token 缓存的最大条目数。未被缓存的 token 在认证时需要 RGW 访问 Keystone server | | `rgw_keystone_verify_ssl` | bool | `true` | 向 keystone 发起 token 请求时是否校验 SSL 证书 | | `rgw_keystone_implicit_tenants` | str | `false` | 通过 Keystone 认证时,是否隐式地在同名 tenant 中创建新用户。可限定为仅 S3 或仅 SWIFT。枚举值:`false`、`true`、`swift`、`s3`、`both`、`0`、`1`、`none` | | `rgw_keystone_service_token_enabled` | bool | `false` | 允许请求携带 `X-Service-Token`,使 `X-Auth-Token` 中的过期 token 可用 | | `rgw_keystone_service_token_accepted_roles` | str | `admin` | 创建 service token 的用户必须具备这些角色之一,才被视为有效 service user | | `rgw_keystone_expired_token_cache_expiration` | int | `3600` | 被 service token 放行的过期 token 的新过期时间 = 当前时间 + 该秒数。**必须低于 Keystone 的 `[token]/allow_expired_window`** | | `rgw_s3_auth_use_keystone` | bool | `false` | S3 认证是否使用 Keystone | | `rgw_s3_auth_use_rados` | bool | `true` | S3 认证是否使用存储在 RADOS 中的凭证 | | `rgw_swift_account_in_url` | bool | `false` | Swift account 是否编码在 URI 路径中(`AUTH_`)。跨 project 访问桶时必须设为 `true` | > 引自:[Integrating with OpenStack Keystone](https://docs.ceph.com/en/tentacle/radosgw/keystone/) --- ### 6.2 LDAP 认证(ldap-auth.rst) > `versionadded:: Jewel` 可以把 Ceph Object Gateway 的认证**委托给 LDAP 服务器**。 #### 6.2.1 工作原理 1. Ceph Object Gateway 从 token 中提取用户的 LDAP 凭证; 2. 用用户名构造一个 search filter; 3. Object Gateway 使用配置好的 service account 在目录中搜索匹配条目; 4. 如果找到条目,Object Gateway 尝试用 token 中的密码 bind 到找到的 distinguished name; 5. 如果凭证有效,bind 成功,Object Gateway 授予访问,并以该用户名创建 radosgw-user。 可以通过**把搜索的 base 限制在特定 organizational unit**,或**指定自定义 search filter**(例如要求特定的组成员身份、自定义 object class 或属性)来限制允许的用户范围。 > ⚠️ LDAP 凭证必须在服务器上可用才能执行 LDAP 认证。请确保把 `rgw` 的日志级别设得足够低,以隐藏 base64 编码的凭证 / access token。 #### 6.2.2 前置要求 | 要求 | 说明 | |---|---| | **LDAP 或 Active Directory** | 一个运行中的、Ceph Object Gateway 可访问的 LDAP 实例 | | **Service account** | 供 Ceph Object Gateway 使用的、具备搜索权限的 LDAP 凭证 | | **User account** | LDAP 目录中至少有一个用户账号 | | **不要让 LDAP 用户与本地用户重名** | 不应对本地用户和通过 LDAP 认证的用户使用相同的用户名 —— Ceph Object Gateway 无法区分它们,会当作同一个用户处理 | #### 6.2.3 健全性检查 用 `ldapsearch` 工具验证 service account 或 LDAP 连接: ```bash # ldapsearch -x -D "uid=ceph,ou=system,dc=example,dc=com" -W \ -H ldaps://example.com -b "ou=users,dc=example,dc=com" 'uid=*' dn ``` > ⚠️ 请确保使用与 Ceph 配置文件中相同的 LDAP 参数,以排除可能的问题。 #### 6.2.4 配置项 | 配置项 | 说明 | 默认值(`rgw.yaml.in` @ v20.2.4) | |---|---|---| | `rgw_s3_auth_use_ldap` | 设为 `true` 启用 S3 的 LDAP 认证 | `false`(bool) | | `rgw_s3_auth_use_ldap` | 设为 `true` 启用 S3 的 LDAP 认证 | `false`(bool) | | `rgw_ldap_uri` | 指定使用的 LDAP 服务器。**务必使用 `ldaps://:` 形式**,以免明文凭证在网络上传输 | 文档未明确(`default:` 字段为空) | | `rgw_ldap_binddn` | Ceph Object Gateway 使用的 service account 的 Distinguished Name (DN) | `uid=admin,cn=users,dc=example,dc=com` | | `rgw_ldap_secret` | 包含 `rgw_ldap_binddn` 凭证的文件路径 | `/etc/openldap/secret` | | `rgw_ldap_searchdn` | 在 directory information tree 中搜索用户的 base。可以是你的 users organizational unit 或更具体的 OU | `cn=users,cn=accounts,dc=example,dc=com` | | `rgw_ldap_dnattr` | 构造 search filter 时用于匹配用户名的属性。取决于你的 DIT,通常是 `uid` 或 `cn`。生成的 filter 串形如 `cn=some_username` | `uid` | | `rgw_ldap_searchfilter` | 若未指定,Object Gateway 会用 `rgw_ldap_dnattr` 自动构造 search filter。用该参数可以非常灵活地收窄允许的用户范围 | 文档未明确(无 `default:`) | #### 6.2.5 用自定义 search filter 限制用户访问 `rgw_search_filter` 参数有两种用法: **① 指定 partial filter(部分过滤器)以进一步限制构造出的 search filter** ```text "objectclass=inetorgperson" ``` Object Gateway 仍会按常规用 token 中的用户名和 `rgw_ldap_dnattr` 的值生成 search filter,然后与 `rgw_search_filter` 中的 partial filter 组合。取决于用户名与设置,最终 search filter 可能是: ```text "(&(uid=hari)(objectclass=inetorgperson))" ``` 因此用户 `hari` 只有在:① 在 LDAP 目录中被找到;② object class 为 `inetorgperson`;③ 提供了有效密码 时,才会被授予访问。 **② 指定 complete filter(完整过滤器)** 完整过滤器**必须包含 `@USERNAME@` token**,认证时会替换为用户名。此时**不再使用** `rgw_ldap_dnattr` 参数。例如,把有效用户限制在特定组: ```text "(&(uid=@USERNAME@)(memberOf=cn=ceph-users,ou=groups,dc=mycompany,dc=com))" ``` > ⚠️ 在 LDAP 搜索中使用 `memberOf` 属性需要你具体的 LDAP 服务器实现提供服务端支持。 #### 6.2.6 生成 LDAP 认证的 access token `radosgw-token` 工具基于 LDAP 用户名和密码生成 access token,会输出一个 base64 编码的字符串,即 access token。 ```bash # export RGW_ACCESS_KEY_ID="" # export RGW_SECRET_ACCESS_KEY="" # radosgw-token --encode ``` 也可以手工生成 —— 把下面这段 JSON 做 base64 编码即可(当没有安装 `radosgw-token` 工具时): ```json { "RGW_TOKEN": { "version": 1, "type": "ldap", "id": "your_username", "key": "your_clear_text_password_here" } } ``` > ⚠️ 官方以 `important` 强调:**access token 是一个 base64 编码的 JSON 结构,其中包含了明文形式的 LDAP 凭证。** #### 6.2.7 使用 access token 用你惯用的 S3 客户端,把该 token 作为 access key 指定在客户端或环境变量中: ```bash # export AWS_ACCESS_KEY_ID= # export AWS_SECRET_ACCESS_KEY="" # 定义为空字符串,否则工具可能抱怨缺少环境变量 ``` > ⚠️ **access token 是 base64 编码的 JSON 结构,其中包含明文形式的 LDAP 凭证。除非你想分享你的明文密码,否则不要分享它!** > 引自:[LDAP Authentication](https://docs.ceph.com/en/tentacle/radosgw/ldap-auth/) --- ### 6.3 OpenID Connect Provider(oidc.rst) 要在 RGW 与 IDP 之间建立信任,必须**先在 RGW 中创建描述 OpenID Connect Provider 的实体**。 调用这些 REST admin API 需要创建带 admin caps 的用户: ```bash radosgw-admin --uid TESTER --display-name "TestUser" --access_key TESTER --secret test123 user create radosgw-admin caps add --uid="TESTER" --caps="oidc-provider=*" ``` #### 6.3.1 CreateOpenIDConnectProvider 在 RGW 中创建 OpenID Connect Provider 实体。 | 参数 | 说明 | 类型 | |---|---|---| | `ClientIDList.member.N` | 需要访问 S3 资源的 Client Ids 列表 | Array of Strings | | `ThumbprintList.member.N` | OpenID Connect IDP 服务器证书的 thumbprint 列表。**最多允许 5 个 thumbprints** | Array of Strings | | `Url` | IDP 的 URL | String | ```text POST "?Action=Action=CreateOpenIDConnectProvider &ThumbprintList.list.1=F7D7B3515DD0D319DD219A43A9EA727AD6065287 &ClientIDList.list.1=app-profile-jsp &Url=http://localhost:8080/auth/realms/quickstart" ``` #### 6.3.2 DeleteOpenIDConnectProvider 删除 RGW 中的 OpenID Connect Provider 实体。 | 参数 | 说明 | 类型 | |---|---|---| | `OpenIDConnectProviderArn` | Create API 返回的 IDP 的 ARN | String | ```text POST "?Action=Action=DeleteOpenIDConnectProvider &OpenIDConnectProviderArn=arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart" ``` #### 6.3.3 GetOpenIDConnectProvider 获取某个 IDP 的信息。 | 参数 | 说明 | 类型 | |---|---|---| | `OpenIDConnectProviderArn` | Create API 返回的 IDP 的 ARN | String | ```text POST "?Action=Action=GetOpenIDConnectProvider &OpenIDConnectProviderArn=arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart" ``` #### 6.3.4 ListOpenIDConnectProviders 列出所有 IDP 的信息。 | 参数 | 说明 | 类型 | |---|---|---| | None | 无参数 | — | ```text POST "?Action=Action=ListOpenIDConnectProviders ``` #### 6.3.5 AddClientIDToOpenIDConnectProvider 向创建 OpenIDConnectProvider 时注册的 client id 列表中**添加**一个 client id。 | 参数 | 说明 | 类型 | |---|---|---| | `OpenIDConnectProviderArn` | Create API 返回的 IDP 的 ARN | String | | `ClientID` | 要添加到已有 OpenIDConnectProvider 的 Client Id | String | ```text POST "?Action=Action=AddClientIDToOpenIDConnectProvider &OpenIDConnectProviderArn=arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart &ClientID=app-jee-jsp" ``` #### 6.3.6 RemoveClientIDFromOpenIDConnectProvider 从创建 OpenIDConnectProvider 时注册的 client id 列表中**移除**一个 client id。 | 参数 | 说明 | 类型 | |---|---|---| | `OpenIDConnectProviderArn` | Create API 返回的 IDP 的 ARN | String | | `ClientID` | 要从已有 OpenIDConnectProvider 移除的 Client ID | String | ```text POST "?Action=Action=RemoveClientIDFromOpenIDConnectProvider &OpenIDConnectProviderArn=arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart &ClientID=app-jee-jsp" ``` #### 6.3.7 UpdateOpenIDConnectProviderThumbprint 用给定的列表更新 OpenIDConnectProvider 已有的 thumbprint 列表。**该 API 会移除已有 thumbprint 列表,并用输入的 thumbprint 列表替换它。** | 参数 | 说明 | 类型 | |---|---|---| | `OpenIDConnectProviderArn` | Create API 返回的 IDP 的 ARN | String | | `ThumbprintList.member.N` | OpenID Connect IDP 服务器证书的 thumbprint 列表。**最多允许 5 个 thumbprints** | Array of Strings | ```text POST "?Action=Action=UpdateOpenIDConnectProviderThumbprint &OpenIDConnectProviderArn=arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart &&ThumbprintList.list.1=ABCDB3515DD0D319DD219A43A9EA727AD6061234" ``` > 💡 补充:源文中所有 OIDC 示例的查询串都写作 `?Action=Action=...`(`Action=` 重复),以及最后一例的 `&&ThumbprintList`。这是官方文档的排版/笔误,本册保留原样并在此提示;实际请求应为单个 `Action=` 与单个 `&`。 > 引自:[OpenID Connect Provider in RGW](https://docs.ceph.com/en/tentacle/radosgw/oidc/) --- ### 6.4 Keycloak(keycloak.rst) 如果 Keycloak 被配置为 OpenID Connect Identity Provider,移动应用和 Web 应用就可以用它来认证用户。利用认证过程返回的 web token,应用可以调用 `AssumeRoleWithWebIdentity`,获得一组临时 S3 凭证,并用这些凭证发起 S3 调用。 #### 6.4.1 集成步骤 **① 配置 Keycloak 与 RGW 通信** —— 添加以下配置项: ```ini [client.radosgw.gateway] rgw sts key = {sts key for encrypting/ decrypting the session token} rgw s3 auth use sts = true ``` **② 获取 web token** 用 `client_credentials` grant type,配合 client id 与 client secret 获取 access token: ```bash KC_REALM=demo KC_CLIENT= KC_CLIENT_SECRET= KC_SERVER=:8080 KC_CONTEXT=auth # Request Tokens for credentials KC_RESPONSE=$( \ curl -k -v -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "scope=openid" \ -d "grant_type=client_credentials" \ -d "client_id=$KC_CLIENT" \ -d "client_secret=$KC_CLIENT_SECRET" \ "http://$KC_SERVER/$KC_CONTEXT/realms/$KC_REALM/protocol/openid-connect/token" \ | jq . ) KC_ACCESS_TOKEN=$(echo $KC_RESPONSE| jq -r .access_token) ``` 用 `password` grant type 为**特定用户**获取 access token(需要 client id、client secret、username、password): ```bash KC_REALM=demo KC_USERNAME= KC_PASSWORD= KC_CLIENT= KC_CLIENT_SECRET= KC_SERVER=:8080 KC_CONTEXT=auth # Request Tokens for credentials KC_RESPONSE=$( \ curl -k -v -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "scope=openid" \ -d "grant_type=password" \ -d "client_id=$KC_CLIENT" \ -d "client_secret=$KC_CLIENT_SECRET" \ -d "username=$KC_USERNAME" \ -d "password=$KC_PASSWORD" \ "http://$KC_SERVER/$KC_CONTEXT/realms/$KC_REALM/protocol/openid-connect/token" \ | jq . ) KC_ACCESS_TOKEN=$(echo $KC_RESPONSE| jq -r .access_token) ``` 得到的 `KC_ACCESS_TOKEN` 即可用于调用 `AssumeRoleWithWebIdentity`。 #### 6.4.2 在 Keycloak 中给用户添加标签 1. 添加用户(Keycloak 管理界面); 2. 填写用户详情; 3. 设置用户凭证; 4. 在用户的 **'attributes' 标签页** 添加标签; 5. 添加 **protocol mapper**,把用户属性映射到 client。 完成这些步骤后,标签 `Department` 会出现在 JWT(web token)中,位于 **`https://aws.amazon.com/tags`** 命名空间下。 **验证方式**:对 JWT 做 token introspection,使用 `client id` 与 `client secret`: ```bash KC_REALM=demo KC_CLIENT= KC_CLIENT_SECRET= KC_SERVER=:8080 KC_CONTEXT=auth curl -k -v \ -X POST \ -u "$KC_CLIENT:$KC_CLIENT_SECRET" \ -d "token=$KC_ACCESS_TOKEN" \ "http://$KC_SERVER/$KC_CONTEXT/realms/$KC_REALM/protocol/openid-connect/token/introspect" \ | jq . ``` > 引自:[Integrating Keycloak with RadosGW](https://docs.ceph.com/en/tentacle/radosgw/keycloak/) --- ### 6.5 Open Policy Agent(opa.rst) OPA(Open Policy Agent)是一个轻量级通用策略引擎,可以与服务**协同部署(co-located)**,可作为 sidecar、主机级守护进程或库来集成。服务可以通过执行查询把策略决策卸载给 OPA,从而**把策略执行与策略决策解耦**。 #### 6.5.1 配置 OPA 向 OPA 加载自定义策略,控制用户可以访问哪些资源;也可以把相关的数据或上下文加载进 OPA 以辅助决策。策略与数据可通过以下方式加载: - OPA 的 RESTful APIs - OPA 的 *bundle* 特性(从远程 HTTP 服务器下载策略和数据) - 文件系统 #### 6.5.2 配置 Ceph Object Gateway ```ini rgw use opa authz = {use opa server to authorize client requests} rgw opa url = {opa server url:opa server port} rgw opa token = {opa bearer token} rgw opa verify ssl = {verify opa server ssl certificate} ``` | 配置项 | 类型 | 默认值(`rgw.yaml.in` @ v20.2.4) | 说明 | |---|---|---|---| | `rgw_use_opa_authz` | bool | `false` | 是否使用 OPA server 为客户端请求做授权 | | `rgw_opa_url` | str | 文档未明确(无 `default:`) | OPA server 的 URL | | `rgw_opa_token` | str | 文档未明确(无 `default:`) | OPA 用于认证客户端请求的 Bearer token | | `rgw_opa_verify_ssl` | bool | `true` | RGW 是否校验 OPA server 的 SSL 证书 | #### 6.5.3 RGW-OPA 集成的工作方式 用户完成认证后,OPA 被用来检查该用户是否有权在资源上执行给定动作。OPA 返回 **allow 或 deny** 决策,回送给 RGW 并由 RGW 执行该决策。 ```http POST /v1/data/ceph/authz HTTP/1.1 Host: opa.example.com:8181 Content-Type: application/json { "input": { "method": "GET", "subuser": "subuser", "user_info": { "user_id": "john", "display_name": "John" }, "bucket_info": { "bucket": { "name": "Testbucket", "bucket_id": "testbucket" }, "owner": "john" } } } ``` 响应: ```json {"result": true} ``` RGW 向 endpoint **`/v1/data/ceph/authz`** 发起 POST 请求,其中 **`ceph` 是 package 名,`authz` 是 rule 名**。请求体中包含用户、资源以及要在资源上执行的动作信息。 | `input` 字段 | 说明 | |---|---| | `method` | 要执行的 HTTP 方法(示例为 `GET`) | | `subuser` | 子用户 | | `user_info.user_id` | 用户 id | | `user_info.display_name` | 用户显示名 | | `bucket_info.bucket.name` | 桶名 | | `bucket_info.bucket.bucket_id` | 桶 id | | `bucket_info.owner` | 桶的拥有者 | > 💡 补充:源文只给出了这一个示例请求,未穷举 `input` 的全部字段。上表字段仅覆盖示例中出现的字段,更多字段**文档未明确**。 > 引自:[Open Policy Agent Integration](https://docs.ceph.com/en/tentacle/radosgw/opa/) --- ## 七、多因素认证 MFA(mfa.rst) > `versionadded:: Mimic` S3 的 MFA(multifactor authentication)特性允许**用户要求在某些桶上删除对象时使用一次性密码(one-time password)**。这些桶需要通过 S3 API 配置为**启用 versioning 且启用 MFA**。 基于时间的一次性密码(TOTP)token 可以通过 `radosgw-admin` 分配给用户。每个 token 有一个 secret seed 和一个分配给它的 serial id。Token 可以被添加到用户上、被列出、被移除,也可以被重新同步(re-synchronized)。 ### 7.1 Multisite 注意事项 MFA IDs 设置在**用户元数据**上,但**实际的 MFA 一次性密码配置驻留在本地 zone 的 OSD 中**。因此,在 multi-site 环境中,**建议为不同的 zone 使用不同的 token**。 ### 7.2 术语 | 术语 | 说明 | |---|---| | `TOTP` | Time-based One Time Password,基于时间的一次性密码 | | `token serial` | 表示 TOTP token ID 的字符串 | | `token seed` | 用于计算 TOTP 的秘密种子 | | `totp seconds` | 生成 TOTP 所使用的时间分辨率 | | `totp window` | 校验 token 时,在当前 token 之前和之后要检查的 TOTP token 数量 | | `totp pin` | 某个时刻 TOTP token 的有效值 | ### 7.3 管理命令 | 操作 | 命令 | |---|---| | 创建新的 MFA TOTP token | `radosgw-admin mfa create --uid= --totp-serial= --totp-seed= [ --totp-seed-type= ] [ --totp-seconds= ] [ --totp-window= ]` | | 列出 MFA TOTP tokens | `radosgw-admin mfa list --uid=` | | 查看 MFA TOTP token | `radosgw-admin mfa get --uid= --totp-serial=` | | 删除 MFA TOTP token | `radosgw-admin mfa remove --uid= --totp-serial=` | | 检查 MFA TOTP token | `radosgw-admin mfa check --uid= --totp-serial= --totp-pin=` | | 重新同步 MFA TOTP token | `radosgw-admin mfa resync --uid= --totp-serial= --totp-pin= --totp=pin=` | **创建**: ```bash # radosgw-admin mfa create --uid= \ --totp-serial= \ --totp-seed= \ [ --totp-seed-type= ] \ [ --totp-seconds= ] \ [ --totp-window= ] ``` **列出**: ```bash # radosgw-admin mfa list --uid= ``` **查看**: ```bash # radosgw-admin mfa get --uid= --totp-serial= ``` **删除**: ```bash # radosgw-admin mfa remove --uid= --totp-serial= ``` **检查**(测试 TOTP token pin,用于验证 TOTP 功能是否正常): ```bash # radosgw-admin mfa check --uid= --totp-serial= \ --totp-pin= ``` **重新同步**(用于时间偏移场景,**需要喂入两个连续的 pin**:前一个 pin 与当前 pin): ```bash # radosgw-admin mfa resync --uid= --totp-serial= \ --totp-pin= --totp=pin= ``` > 💡 补充:源文 resync 命令中当前 pin 的参数写作 `--totp=pin=`,与前面的 `--totp-pin=` 形式不一致,疑为官方文档笔误(应为 `--totp-pin=`)。本册保留源文原样并在此提示。 > 引自:[RGW Support for Multifactor Authentication](https://docs.ceph.com/en/tentacle/radosgw/mfa/) --- ## 八、Lua 脚本扩展(lua-scripting.rst) > `versionadded:: Pacific` 该特性允许用户把执行上下文(execution context)指派给 Lua 脚本。 ### 8.1 脚本上下文(Contexts) | Context | 执行时机 | |---|---| | `prerequest` | 在每个操作执行**之前**执行脚本 | | `postrequest` | 在每个操作执行**之后**执行脚本 | | `background` | 在**指定的时间间隔**内执行 | | `getdata` | 在**对象被下载**时,作用于对象数据 | | `putdata` | 在**对象被上传**时,作用于对象数据 | | `preRequest` | 在每个操作执行**之前**执行脚本 | | `postRequest` | 在每个操作执行**之后**执行脚本 | > 💡 补充:源文中 `prerequest`/`postrequest` 与 `preRequest`/`postRequest` 两组写法并存(前者用于 `script put`,后者用于 `script get` / `script rm`)。由于源文明确说明"字段名大小写不敏感"(见 8.4 节),两组写法实际等价,本册保留源文原样。 补充说明: - request(pre 或 post)或 data(get 或 put)上下文的脚本,**可以被限制为只作用于属于特定 tenant 的用户的操作**; - request 上下文脚本可以访问 request 中的字段,**可以修改某些字段**,也可以访问 `Global RGW Table`; - data 上下文脚本可以访问对象的内容,以及 request 字段和 `Global RGW Table`; - **所有上下文都可以使用 Lua 语言的全部特性**。 ### 8.2 资源限制 | 限制 | 默认值 | 调整方式 | |---|---|---| | 单个脚本在上下文中执行可用的**内存** | **500K byte**(包含 Lua 使用的所有库,但**不包含**由 RGW 自身管理、可从 Lua 访问的内存) | `rgw_lua_max_memory_per_state`。**注意:Lua 及其标准库的基本开销约 32K bytes。设为 0 表示禁用该限制** | | 单个 Lua 脚本执行的**最大运行时间** | **1000 毫秒**。超出该运行时间的脚本会被终止 | `rgw_lua_max_runtime_per_state`。**设为 0 表示禁用运行时限制** | > ⚠️ 警告 1:**修改内存限制时要谨慎。如果当前内存使用量超过了新设的限制,background state 中此前存储的所有数据都会丢失。** > ⚠️ 警告 2:**禁用运行时限制可能导致脚本无界执行,从而造成过度的资源消耗,并可能影响 RADOS gateway 的可用性。** | 配置项 | 类型 | 默认值(`rgw.yaml.in` @ v20.2.4) | 说明 | |---|---|---|---| | `rgw_lua_max_memory_per_state` | uint | `128000` | 单个 lua state 可为自己分配的最大字节数。**不包含**任何可从 lua 访问、但由 RGW 管理的内存。设 0 则仅受系统限制 | | `rgw_lua_max_runtime_per_state` | uint | `1000` | 每个 Lua state 的最大运行时间(毫秒)。超出则脚本被终止。设 0 则无限制 | | `rgw_lua_enable` | bool | `true` | 是否启用 lua scripting(startup 标志,需重启生效) | | `rgw_luarocks_location` | str | `/tmp/rgw_luarocks/$name` | luarocks 从 allowlist 安装包的目录(startup 标志,需重启生效) | > 💡 补充:源文正文写"一个脚本在上下文中执行最多可使用 500K byte 内存",而 `rgw.yaml.in` 中 `rgw_lua_max_memory_per_state` 的默认值为 `128000`(即 128K)。二者不一致,本册同时记录两处原始表述,不做裁决。 ### 8.3 Lua 包管理(allowlist) 默认情况下脚本中可以使用**所有 Lua 标准库**。为了允许脚本使用额外的 Lua 模块,支持把包加入 allowlist: - 向 allowlist 添加 Lua 包、或从中移除包,**并不会安装或卸载它**。要使改动生效,需要调用 "reload" 命令。 - 此外,**allowlist 中的所有包会在 radosgw 重启时用 luarocks 包管理器重新安装**。 - 要添加含有需要编译的 C 源代码的包,使用 `--allow-compilation` 标志;此时主机上需要有可用的 C 编译器。 - Lua 包安装在、并使用于 **radosgw 本地的一个目录**,即 allowlist 中的 Lua 包与主机上任何可用的 Lua 包是**隔离**的。默认该目录为 `/tmp/luarocks/`,其前缀部分(`/tmp/luarocks/`)可通过 `rgw_luarocks_location` 配置项改到别处。 - **注意:该参数不应设置为 luarocks 安装包的默认位置之一**(例如 `$HOME/.luarocks`、`/usr/lib64/lua`、`/usr/share/lua`)。 ### 8.4 脚本管理 CLI **上传脚本**: ```bash # radosgw-admin script put --infile={lua-file-path} --context={prerequest|postrequest|background|getdata|putdata} [--tenant={tenant-name}] ``` > 上传 `background` 上下文的脚本时,**不应指定 tenant 名**。 cephadm 环境下: ```bash # cephadm shell radosgw-admin script put --infile=/rootfs/{lua-file-path} --context={prerequest|postrequest|background|getdata|putdata} [--tenant={tenant-name}] ``` **打印脚本内容到标准输出**: ```bash # radosgw-admin script get --context={preRequest|postRequest|background|getdata|putdata} [--tenant={tenant-name}] ``` **删除脚本**: ```bash # radosgw-admin script rm --context={preRequest|postRequest|background|getdata|putdata} [--tenant={tenant-name}] ``` ### 8.5 包管理 CLI | 操作 | 命令 | |---|---| | 添加包到 allowlist | `# radosgw-admin script-package add --package={package name} [--allow-compilation]` | | 添加**指定版本**的包到 allowlist | `# radosgw-admin script-package add --package='{package name} {package version}' [--allow-compilation]` | | 从 allowlist 移除包 | `# radosgw-admin script-package rm --package={package name}` | | 从 allowlist 移除**指定版本**的包 | `# radosgw-admin script-package rm --package='{package name} {package version}'` | | 打印 allowlist 中的包列表 | `# radosgw-admin script-package list` | | 把 allowlist 的改动应用到所有 RGW | `# radosgw-admin script-package reload` | 规则: - 当添加一个**已存在于列表中的包的**不同版本时,**新添加的版本会覆盖已有版本**。 - 添加包时**不指定版本**,则会添加该包的**最新版本**。 - 移除包时**不指定版本**,则该包的**任何已存在版本都会被移除**。 ### 8.6 Context Free Functions(与上下文无关的函数) #### Debug Log `RGWDebugLog()` 函数接受一个字符串,把它以**优先级 20** 打印到 debug log。每条日志消息带有前缀 **`Lua INFO:`**。该函数**没有返回值**。 ### 8.7 Request Fields(逐项表格) > ⚠️ 官方 warning:**该特性是实验性的。字段可能在未来被移除或重命名。** > 说明(官方 note): > - 尽管 Lua 是大小写敏感的语言,**radosgw 提供的字段名是大小写不敏感的**;**函数名仍然大小写敏感**。 > - 标记为 "optional" 的字段**可能为 nil 值**。 > - 标记为 "iterable" 的字段**可以被 `pairs()` 函数使用,也可以配合 `#` 长度操作符**。 > - **所有 table 字段都可以用方括号操作符 `[]`**。 > - `time` 类型的字段是字符串,格式为 `%Y-%m-%d %H:%M:%S`。 | Field | Type | Description | Iterable | Writeable | Optional | |---|---|---|---|---|---| | `Request.RGWOp` | string | radosgw operation | no | no | no | | `Request.DecodedURI` | string | decoded URI | no | no | no | | `Request.ContentLength` | integer | size of the request | no | no | no | | `Request.GenericAttributes` | table | string to string generic attributes map | yes | no | no | | `Request.Response` | table | response to the request | no | no | no | | `Request.Response.HTTPStatusCode` | integer | HTTP status code | no | **yes** | no | | `Request.Response.HTTPStatus` | string | HTTP status text | no | **yes** | no | | `Request.Response.RGWCode` | integer | radosgw error code | no | **yes** | no | | `Request.Response.Message` | string | response message | no | **yes** | no | | `Request.SwiftAccountName` | string | swift account name | no | no | yes | | `Request.Bucket` | table | info on the bucket | no | no | no | | `Request.Bucket.Tenant` | string | tenant of the bucket | no | no | yes | | `Request.Bucket.Name` | string | bucket name(**仅在 `prerequest` 上下文中可写**) | no | **yes** | no | | `Request.Bucket.Marker` | string | bucket marker (initial id) | no | no | yes | | `Request.Bucket.Id` | string | bucket id | no | no | yes | | `Request.Bucket.ZoneGroupId` | string | zone group of the bucket | no | no | yes | | `Request.Bucket.CreationTime` | time | creation time of the bucket | no | no | yes | | `Request.Bucket.MTime` | time | modification time of the bucket | no | no | yes | | `Request.Bucket.Quota` | table | bucket quota | no | no | yes | | `Request.Bucket.Quota.MaxSize` | integer | bucket quota max size | no | no | no | | `Request.Bucket.Quota.MaxObjects` | integer | bucket quota max number of objects | no | no | no | | `Reques.Bucket.Quota.Enabled` | boolean | bucket quota is enabled | no | no | no | | `Request.Bucket.Quota.Rounded` | boolean | bucket quota is rounded to 4K | no | no | no | | `Request.Bucket.PlacementRule` | table | bucket placement rule | no | no | yes | | `Request.Bucket.PlacementRule.Name` | string | bucket placement rule name | no | no | no | | `Request.Bucket.PlacementRule.StorageClass` | string | bucket placement rule storage class | no | no | no | | `Request.Bucket.User` | string | owning user/account id | no | no | yes | | `Request.Object` | table | info on the object | no | no | yes | | `Request.Object.Name` | string | object name | no | no | no | | `Request.Object.Instance` | string | object version | no | no | no | | `Request.Object.Id` | string | object id | no | no | no | | `Request.Object.Size` | integer | object size | no | no | no | | `Request.Object.MTime` | time | object mtime | no | no | no | | `Request.CopyFrom` | table | information on copy operation | no | no | yes | | `Request.CopyFrom.Tenant` | string | tenant of the object copied from | no | no | no | | `Request.CopyFrom.Bucket` | string | bucket of the object copied from | no | no | no | | `Request.CopyFrom.Object` | table | object copied from. See: `Request.Object` | no | no | yes | | `Request.ObjectOwner` | table | object owner | no | no | no | | `Request.ObjectOwner.DisplayName` | string | object owner display name | no | no | no | | `Request.ObjectOwner.User` | string | owning user/account id. See: `Request.Bucket.User` | no | no | yes | | `Request.ZoneGroup.Name` | string | name of zone group | no | no | no | | `Request.ZoneGroup.Endpoint` | string | endpoint of zone group | no | no | no | | `Request.UserAcl` | table | user ACL | no | no | no | | `Request.UserAcl.Owner` | table | user ACL owner. See: `Request.ObjectOwner` | no | no | no | | `Request.UserAcl.Grants` | table | user ACL map of string to grant(注意:**没有 Id 的 grants 在迭代时不会出现**,且通过方括号只能访问其中的一个) | yes | no | no | | `Request.UserAcl.Grants[""]` | table | user ACL grant | no | no | no | | `Request.UserAcl.Grants[""].Type` | integer | user ACL grant type | no | no | no | | `Request.UserAcl.Grants[""].User` | string | user ACL grant user/account id | no | no | no | | `Request.UserAcl.Grants[""].GroupType` | integer | user ACL grant group type | no | no | yes | | `Request.UserAcl.Grants[""].Referer` | string | user ACL grant referer | no | no | yes | | `Request.BucketAcl` | table | bucket ACL. See: `Request.UserAcl` | no | no | no | | `Request.ObjectAcl` | table | object ACL. See: `Request.UserAcl` | no | no | no | | `Request.Environment` | table | string to string environment map | yes | no | no | | `Request.Policy` | table | policy | no | no | yes | | `Request.Policy.Text` | string | policy text | no | no | no | | `Request.Policy.Id` | string | policy Id | no | no | yes | | `Request.Policy.Statements` | table | list of string statements | yes | no | no | | `Request.UserPolicies` | table | list of user policies | yes | no | no | | `Request.UserPolicies[]` | table | user policy. See: `Request.Policy` | no | no | no | | `Request.RGWId` | string | radosgw host id: `--` | no | no | no | | `Request.HTTP` | table | HTTP header | no | no | no | | `Request.HTTP.Parameters` | table | string to string parameter map | yes | no | no | | `Request.HTTP.Resources` | table | string to string resource map | yes | no | no | | `Request.HTTP.Metadata` | table | string to string metadata map | yes | **yes** | no | | `Request.HTTP.StorageClass` | string | storage class | no | **yes** | yes | | `Request.HTTP.Host` | string | host name | no | no | no | | `Request.HTTP.Method` | string | HTTP method | no | no | no | | `Request.HTTP.URI` | string | URI | no | no | no | | `Request.HTTP.QueryString` | string | HTTP query string | no | no | no | | `Request.HTTP.Domain` | string | domain name | no | no | no | | `Request.Time` | time | request time | no | no | no | | `Request.Dialect` | string | "S3" or "Swift" | no | no | no | | `Request.Id` | string | request Id | no | no | no | | `Request.TransactionId` | string | transaction Id | no | no | no | | `Request.Tags` | table | object tags map | yes | no | no | | `Request.User` | table | user that triggered the request | no | no | no | | `Request.User.Tenant` | string | triggering user tenant | no | no | no | | `Request.User.Id` | string | triggering user id | no | no | no | | `Request.Trace` | table | info on trace | no | no | no | | `Request.Trace.Enable` | boolean | tracing is enabled | no | **yes** | no | > 💡 补充:源文表格中 `Reques.Bucket.Quota.Enabled` 少了一个 `t`(应为 `Request.Bucket.Quota.Enabled`)。本册保留源文原始拼写以忠实记录,实际书写时请使用 `Request.Bucket.Quota.Enabled`。 **可写字段汇总**(这是 Lua 能做"改写/拦截"的能力边界): | 可写字段 | 上下文限制 | |---|---| | `Request.Response.HTTPStatusCode` | 无限制说明 | | `Request.Response.HTTPStatus` | 无限制说明 | | `Request.Response.RGWCode` | 无限制说明 | | `Request.Response.Message` | 无限制说明 | | `Request.Bucket.Name` | **仅 `prerequest` 上下文** | | `Request.HTTP.Metadata` | 无限制说明 | | `Request.HTTP.StorageClass` | 无限制说明 | | `Request.Trace.Enable` | 无限制说明 | ### 8.8 Request Functions #### Operations Log `Request.Log()` 函数把请求打印到 operations log。**该函数没有参数**,成功返回 `0`,失败返回错误码。 #### Tracing Tracing 函数**只能在 `postrequest` 上下文中使用**。 | 函数 | 说明 | |---|---| | `Request.Trace.SetAttribute(, )` | 为请求的 trace 设置属性。第一个参数 `key` 必须是字符串;第二个参数 `value` 可以是字符串或数字(integer 或 double)。之后可以用该属性定位特定的 traces | | `Request.Trace.AddEvent(, )` | 向请求 trace 的**第一个 span** 添加一个事件。事件由事件名、事件时间和零个或多个事件属性定义。该函数接受一个或两个参数:第一个是包含事件 `name` 的字符串,其次是事件 `attributes`(对无属性的事件可省略)。**事件属性必须是一个字符串 table** | ### 8.9 请求阻断与错误处理 #### 脚本执行错误 如果 Lua 脚本因**语法错误或运行时错误**失败,RGW 会记录该错误,而**触发脚本的请求仍会照常通过**。 #### 请求阻断与返回值 脚本的返回值决定 RGW 如何处理该请求: | 目的 | 做法 | |---|---| | **阻断请求** | 脚本必须返回值 **`RGW_ABORT_REQUEST`**。RGW 将其解释为 `-EPERM`,并停止处理该请求 | | **继续请求** | 不返回值,或返回任何其它值/类型,都被视为成功 | #### 返回值上下文 Lua 脚本的返回值**只在 `prerequest` 上下文中被评估**,在其它任何 RGW 请求处理上下文中都被忽略。 被 Lua 阻断的请求,其 HTTP 响应状态码**默认为 403 (Forbidden)**。该响应码可用 `Request.Response.HTTPStatusCode` 与 `Request.Response.HTTPStatus` 修改。 **如果请求以这种方式被 abort,那么 `data` 和 `postrequest` 上下文也会被 abort。** ### 8.10 Background Context `background` 上下文可用于分析、监控、为其它上下文的执行缓存数据等目的。 - Background 脚本执行的**默认间隔为 5 秒**。 ### 8.11 Data Context `getdata` 与 `putdata` 两个上下文都拥有以下字段: | 字段 | 说明 | |---|---| | `Data` | **只读且可迭代(逐字节)**。如果对象被分多个 chunk 上传或读取,则 `Data` 字段**一次只持有一个 chunk 的数据** | | `Offset` | 该 chunk 在**整个对象中的偏移量** | | `Request` 字段与 background 的 `RGW` table | 在这些上下文中也可用 | ### 8.12 Global RGW Table `RGW` Lua table **可从所有上下文访问**,它会保存在执行期间写入其中的数据,以便后续在**其它执行(同一上下文或不同上下文)**中读取和使用。 | 属性 | 说明 | |---|---| | 作用域 | **每个 RGW 实例都有自己私有的、短暂的(ephemeral)`RGW` Lua table**,守护进程重启时丢失。**注意 `background` 上下文脚本会在每个实例上运行** | | 容量上限 | table 中**最多 100,000 个条目**。每个条目是 string key,其 value 的**组合长度不超过 1KB**。若条目数或条目大小超出这些限制,**Lua 脚本会带错误中止** | | 索引与值类型 | `RGW` Lua table 使用 **string 索引**,可存储类型为 **string、integer、double、boolean** 的值 | #### Increment / Decrement 函数 由于 `RGW` table 中的条目可能被多处同时访问,需要一种**原子地**增减其中数值的方法: | 函数 | 说明 | |---|---| | `RGW.increment(, [value])` | 把 `key` 的值增加 `value`;若未提供 `value`,则增加 1 | | `RGW.decrement(, [value])` | 把 `key` 的值减少 `value`;若未提供 `value`,则减少 1 | - 如果 `key` 的值**不是数值类型**,脚本执行会失败; - 如果尝试用**非数值**做增量或减量,脚本执行会失败。 ### 8.13 Lua 代码示例 #### 示例 1:在 copy 场景下打印源对象与目标对象信息 ```lua function print_object(object) RGWDebugLog(" Name: " .. object.Name) RGWDebugLog(" Instance: " .. object.Instance) RGWDebugLog(" Id: " .. object.Id) RGWDebugLog(" Size: " .. object.Size) RGWDebugLog(" MTime: " .. object.MTime) end if Request.CopyFrom and Request.Object and Request.CopyFrom.Object then RGWDebugLog("copy from object:") print_object(Request.CopyFrom.Object) RGWDebugLog("to object:") print_object(Request.Object) end ``` #### 示例 2:用"通用函数"打印 ACLs ```lua function print_owner(owner) RGWDebugLog("Owner:") RGWDebugLog(" Display Name: " .. owner.DisplayName) RGWDebugLog(" Id: " .. owner.User.Id) RGWDebugLog(" Tenant: " .. owner.User.Tenant) end function print_acl(acl_type) index = acl_type .. "ACL" acl = Request[index] if acl then RGWDebugLog(acl_type .. "ACL Owner") print_owner(acl.Owner) RGWDebugLog(" there are " .. #acl.Grants .. " grant for owner") for k,v in pairs(acl.Grants) do RGWDebugLog(" Grant Key: " .. k) RGWDebugLog(" Grant Type: " .. v.Type) RGWDebugLog(" Grant Group Type: " .. v.GroupType) RGWDebugLog(" Grant Referer: " .. v.Referer) RGWDebugLog(" Grant User Tenant: " .. v.User.Tenant) RGWDebugLog(" Grant User Id: " .. v.User.Id) end else RGWDebugLog("no " .. acl_type .. " ACL in request: " .. Request.Id) end end print_acl("User") print_acl("Bucket") print_acl("Object") ``` #### 示例 3:仅在出错时使用 operations log ```lua if Request.Response.HTTPStatusCode ~= 200 then RGWDebugLog("request is bad, use ops log") rc = Request.Log() RGWDebugLog("ops log return code: " .. rc) end ``` #### 示例 4:改写错误消息(响应内容改写) ```lua if Request.Response.HTTPStatusCode == 500 then Request.Response.Message = " something bad happened :-( " end ``` #### 示例 5:给对象追加客户端原本没有发送的 metadata 在 `prerequest` 上下文中: ```lua if Request.RGWOp == 'put_obj' then Request.HTTP.Metadata["x-amz-meta-mydata"] = "my value" end ``` 在 `postrequest` 上下文中查看 metadata: ```lua RGWDebugLog("number of metadata entries is: " .. #Request.HTTP.Metadata) for k, v in pairs(Request.HTTP.Metadata) do RGWDebugLog("key=" .. k .. ", " .. "value=" .. v) end ``` #### 示例 6:用模块构造基于 Unix socket、JSON 编码的 "access log" **第一步**:把下列包加入 allowlist: ```bash # radosgw-admin script-package add --package=lua-cjson --allow-compilation # radosgw-admin script-package add --package=luasocket --allow-compilation ``` **第二步**:启动一个监听 Unix socket 的服务端,例如用 netcat: ```bash # rm -f /tmp/socket # nc -vklU /tmp/socket ``` **第三步**:重启 radosgw,并把下面脚本上传到 `postrequest` 上下文: ```lua if Request.RGWOp == "get_obj" then local json = require("cjson") local socket = require("socket") local unix = require("socket.unix") local s = assert(unix()) E = {} msg = {bucket = (Request.Bucket or (Request.CopyFrom or E).Bucket).Name, time = Request.Time, operation = Request.RGWOp, http_status = Request.Response.HTTPStatusCode, error_code = Request.Response.HTTPStatus, object_size = Request.Object.Size, trans_id = Request.TransactionId} assert(s:connect("/tmp/socket")) assert(s:send(json.encode(msg).."\n")) assert(s:close()) end ``` #### 示例 7:只对特定桶的请求开启 tracing Tracing 默认是禁用的,所以应为这个特定桶启用 tracing: ```lua if Request.Bucket.Name == "my-bucket" then Request.Trace.Enable = true end ``` 如果 RGW 上**已启用 tracing**,则 `Request.Trace.Enable` 为 true,因此应对所有不匹配该桶名的请求关闭 tracing。在 `prerequest` 上下文中: ```lua if Request.Bucket.Name ~= "my-bucket" then Request.Trace.Enable = false end ``` > 注意:**修改 `Request.Trace.Enable` 不会改变 tracer 本身的状态**,只是针对该请求启用或禁用 tracing。 #### 示例 8:为请求 trace 追加信息 在 `postrequest` 上下文中,可以向请求的 trace 添加属性和事件: ```lua Request.Trace.AddEvent("lua script execution started") Request.Trace.SetAttribute("HTTPStatusCode", Request.Response.HTTPStatusCode) event_attrs = {} for k,v in pairs(Request.GenericAttributes) do event_attrs[k] = v end Request.Trace.AddEvent("second event", event_attrs) ``` #### 示例 9:用熵值检测对象是否被加密 对象的熵值可用于判断对象是否被加密。下面的脚本计算上传对象的熵与大小,并打印到 debug log。在 `putdata` 上下文中加入: ```lua function object_entropy() local byte_hist = {} local byte_hist_size = 256 for i = 1,byte_hist_size do byte_hist[i] = 0 end local total = 0 for i, c in pairs(Data) do local byte = c:byte() + 1 byte_hist[byte] = byte_hist[byte] + 1 total = total + 1 end entropy = 0 for _, count in ipairs(byte_hist) do if count ~= 0 then local p = 1.0 * count / total entropy = entropy - (p * math.log(p)/math.log(byte_hist_size)) end end return entropy end local full_name = Request.Bucket.Name.."\\"..Request.Object.Name RGWDebugLog("entropy of chunk of: " .. full_name .. " at offset:" .. tostring(Offset) .. " is: " .. tostring(object_entropy())) RGWDebugLog("payload size of chunk of: " .. full_name .. " is: " .. #Data) ``` #### 示例 10:阻断请求(鉴权 / 限流的骨架) > 💡 补充:源文没有直接给出"阻断"的完整示例,但给出了阻断机制(返回 `RGW_ABORT_REQUEST`)与可写响应字段。下面把二者组合成一个可直接使用的骨架(属本册补充,非源文原文): ```lua -- prerequest 上下文:只放行指定桶,其它一律 403 if Request.Bucket.Name ~= "allowed-bucket" then Request.Response.HTTPStatusCode = 403 Request.Response.HTTPStatus = "Forbidden" Request.Response.Message = "blocked by lua policy" return RGW_ABORT_REQUEST end ``` 配合 `RGW` table 的原子计数,可做简单限流: ```lua -- prerequest 上下文:对某用户做每实例计数(注意 RGW table 是每个实例私有的) local key = "cnt:" .. Request.User.Id RGW.increment(key) if RGW[key] > 100 then Request.Response.HTTPStatusCode = 429 Request.Response.HTTPStatus = "Too Many Requests" return RGW_ABORT_REQUEST end ``` > ⚠️ 该限流示例基于**每个 RGW 实例私有**的 `RGW` table,因此是**单实例维度**的计数,不是集群全局限流。 ### 8.14 调试与限制小结 | 主题 | 要点 | |---|---| | 调试手段 | `RGWDebugLog()`(优先级 20,前缀 `Lua INFO:`);`Request.Log()`(写入 operations log) | | 脚本错误的影响 | 语法/运行时错误**只记录日志,请求照常通过** —— 这意味着 Lua 逻辑出错不会变成显式拒绝,需要靠日志发现 | | 阻断 | 仅在 `prerequest` 返回 `RGW_ABORT_REQUEST` 才有效;默认 403,可改写 | | 阻断的连带影响 | 请求被 abort 时,`data` 与 `postrequest` 上下文也会 abort | | 内存限制 | 默认 500K(正文)/ 128000(`rgw_lua_max_memory_per_state` 默认值),改小可能导致 background state 数据丢失 | | 运行时间限制 | 默认 1000 ms,禁用可能导致 RGW 可用性受损 | | 字段稳定性 | Request Fields 是**实验性**特性,字段可能被移除或重命名 | | 包隔离 | allowlist 的包安装在 radosgw 本地目录,与主机 Lua 包隔离;新增/移除需 `script-package reload` | | `background` 上下文 | 不能指定 tenant;在每个 RGW 实例上都运行;默认 5 秒间隔 | | `RGW` table | 每实例私有、重启丢失;上限 100,000 条目,单条目 ≤1KB;仅 string 索引;值类型限 string/integer/double/boolean | | Tracing 函数 | 仅 `postrequest` 可用;`Request.Trace.Enable` 只影响当前请求,不改 tracer 状态 | > 引自:[Lua Scripting](https://docs.ceph.com/en/tentacle/radosgw/lua-scripting/) --- ## 九、硬件加速 ### 9.1 Intel QAT(qat-accel.rst) Intel QAT(QuickAssist Technology)可以通过**把实际的加密与压缩请求卸载(offload)到硬件 QuickAssist 加速器**来提供扩展的加速服务;对这类特定的计算密集型负载,硬件加速器在成本与功耗上都比通用 CPU 更高效。 #### 9.1.1 QAT 在软件栈中的位置 - 应用开发者通过 **QAT API** 访问 QuickAssist 特性,它是 QuickAssist 技术的顶层 API,负责客户应用与 QuickAssist 加速驱动之间的对接; - QAT API 访问 **QuickAssist driver**,驱动再去驱动 QuickAssist Accelerator 硬件;驱动负责把加速服务暴露给应用软件; - 用户可以直接写 QAT API,也可以经由已被他人(包括 Intel)使能的框架来使用 QAT,例如 **zlib\***、**OpenSSL\* libcrypto\***、以及 **Linux\* Kernel Crypto Framework**。 #### 9.1.2 环境准备 1. **需要 QuickAssist Accelerator 硬件**,且内核态的 QAT 驱动必须已加载以驱动该硬件。 - out-of-tree QAT 驱动包可从 Intel Quickassist Technology 下载; - **QATlib** 可从 qatlib 下载,用于 **in-tree QAT driver**。 - ⚠️ out-of-tree QAT 驱动正在逐步迁移到 in-tree driver + QATlib。 2. **QAT 加密的实现直接基于 QAT API**(包含在驱动包中);而 **QAT 压缩支持依赖 QATzip 项目** —— 一个构建在 QAT API 之上的用户态库。截至撰写时(2024 年 7 月),QATzip 加速 **gzip 的压缩与解压**。 #### 9.1.3 实现方式差异 | 能力 | 实现路径 | 说明 | |---|---|---| | **QAT 加密** | 基于**原生 QAT API** 的 crypto plugin 加入 crypto framework | 虽然 Ceph 已合并 OpenSSL support for RGW encryption,且 Intel 提供了 QAT Engine for OpenSSL,但**该 QAT Engine 目前只支持链式操作(chained operations)**,意味着 Ceph 无法基于 OpenSSL crypto plugin 利用 QAT 硬件特性做 crypto 操作,因此额外加入了基于原生 QAT API 的插件 | | **QAT 压缩** | 基于**用户态 QATzip 库**,通过**一个 QAT 加速的通用工具类**实现(**不是** compressor plugin) | 该通用工具类可以透明地加速已有的压缩类型,但**截至撰写时只支持 zlib compressor**。也就是说,若有 QAT 硬件,可用该工具类加速 zlib compressor | #### 9.1.4 配置步骤 **① 前置条件** *For out-of-tree QAT*: - 确保已安装版本 **v1.7.L.4.14.0 或更高**的 out-of-tree QAT 驱动; - 为 QAT 驱动包根目录设置环境变量 **`ICP_ROOT`**; - 必须修改 QAT 配置文件。例如对 Intel QuickAssist Adapter 8970 产品,修改 `/etc/` 下的 `c6xx_dev0/1/2.conf` 并保持一致: ```ini #... # User Process Instance Section ############################################## [CEPH] NumberCyInstances = 1 NumberDcInstances = 1 NumProcesses = 8 LimitDevAccess = 1 # Crypto - User instance #0 Cy0Name = "SSL0" Cy0IsPolled = 1 # List of core affinities Cy0CoreAffinity = 0 # Data Compression - User instance #0 Dc0Name = "Dc0" Dc0IsPolled = 1 # List of core affinities Dc0CoreAffinity = 0 ``` *For in-tree QAT*: - 确保系统满足 QATlib System Requirements; - **Intel VT-d 与 SR-IOV 参数必须在平台 BIOS 中启用**; - 某些 QATlib 特性需要较新的内核驱动或固件版本; - 支持的平台需包含 **4xxx Intel Communications device 或更新**; - **必须启用 `intel_iommu` 参数**,可用以下命令验证与设置: ```bash cat /proc/cmdline | grep intel_iommu=on sudo sh -c 'echo "@qat - memlock 204800" >> /etc/security/limits.conf' sudo su -l $USER ``` **② QAT-based Encryption for RGW** 必须设置 CMake 选项 **`WITH_QATDRV=ON`**。从源码构建时: ```bash cd ceph ./do_cmake.sh -DWITH_QATDRV=ON cd build ininja ``` > ⚠️ **QAT 配置文件中的 section 名必须是 `CEPH`**,因为 Ceph crypto 源码中把 section name 设置为 `CEPH`。 > 💡 补充:源文中的构建命令写作 `ininja`,疑为 `ninja` 的笔误,本册保留原样并在此提示。 编辑 Ceph 配置文件(通常是 `ceph.conf`)以使用 QAT-based crypto plugin: ```ini plugin crypto accelerator = crypto_qat ``` **③ QAT Support for Compression** *For out-of-tree QAT*: - 构建前确保 QAT 驱动与 QATzip 都已安装; - 除 `ICP_ROOT` 外,把环境变量 **`QZ_ROOT`** 设置为 QATzip 源码树的根目录; - 配置以下 CMake 选项: ```bash ./do_cmake.sh -DWITH_QATDRV=ON -DWITH_QATZIP=ON -DWITH_SYSTEM_QATZIP=ON -DWITH_QATLIB=OFF ``` - 设置环境变量以明确 QAT 配置文件中 User Process Instance Section 的 section 名: ```bash export QAT_SECTION_NAME=CEPH ``` *For in-tree QAT*: - 确保系统满足 QATlib System Requirements;QATlib 可从预编译包或源码安装。安装后可运行 **`cpa_sample_code`** 检查 QAT 环境是否正常; - 若使用 QATlib 源码,Ceph 的 `cmake` 构建**默认启用 qatlib 和 qatzip 选项**,常规编译已包含 QAT-compressor 相关代码: ```bash ./do_cmake.sh ``` - 若使用系统上的预编译包,构建 Ceph 时需配置: ```bash ./do_cmake.sh -DWITH_SYSTEM_QATLIB=ON -DWITH_SYSTEM_QATZIP=ON ``` *For both*: 编辑 Ceph 的 central config DB 或配置文件(通常是 `ceph.conf`)以启用 QAT 对 **zlib** 压缩的支持: ```ini qat compressor enabled=true ``` 设置 RGW 的压缩方法: ```bash # for storage class(STANDARD) radosgw-admin zone placement modify --rgw-zone=default --placement-id=default-placement --compression=zlib # or create a new storage class(COLD) and define data pool(default.rgw.cold.data) radosgw-admin zonegroup placement add --rgw-zonegroup default --placement-id default-placement --storage-class COLD radosgw-admin zone placement add --rgw-zone default --placement-id default-placement --storage-class COLD --compression zlib --data-pool default.rgw.cold.data ``` #### 9.1.5 QAT 配置项 以下 QAT 相关设置可加到 Ceph 配置文件(通常 `ceph.conf`)的 **`[client.rgw.{instance-name}]`** 段下。 | 配置项 | 类型 | 默认值(`*.yaml.in` @ v20.2.4) | 说明 | |---|---|---|---| | `qat_compressor_enabled` | bool | `false`(来自 `src/common/options/global.yaml.in`) | 若可用,启用 Intel QAT 对压缩的加速支持 | | `qat_compressor_session_max_number` | uint | `256`(来自 `src/common/options/global.yaml.in`) | 使用 QAT compressor 时,Qatzip 内的最大 session 数 | | `qat_compressor_busy_polling` | bool | `false`(来自 `src/common/options/global.yaml.in`) | 开启 QAT busy polling 以降低延迟,代价是可能增加 CPU 使用 | | `plugin_crypto_accelerator` | str | `crypto_isal`(来自 `src/common/options/global.yaml.in`) | 使用的 crypto accelerator 库;启用 QAT 加密时设为 `crypto_qat` | #### 9.1.6 验证方式 - 安装 QATlib 后运行 **`cpa_sample_code`**,可检查 QAT 环境是否正常; - 用 `cat /proc/cmdline | grep intel_iommu=on` 验证 `intel_iommu` 已启用。 > 引自:[QAT Acceleration for Encryption and Compression](https://docs.ceph.com/en/tentacle/radosgw/qat-accel/) --- ### 9.2 鲲鹏 UADK(uadk-accel.rst) UADK 是一个让用户**以统一、安全、高效的方式访问硬件加速器**的框架,由 **UACCE、libwd** 以及许多其它算法库组成。 #### 9.2.1 UADK 在软件栈中的位置 UADK 是一个**通用用户态加速器框架**,使用 **SVA(Shared Virtual Addressing,共享虚拟寻址)** 为加密与压缩算法的硬件加速提供统一编程接口。 UADK 包含 **UACCE(Unified/User-space-access-intended Accelerator Framework)**,它使支持 SVA 的硬件加速器能够适配 UADK。 **目前 HiSilicon Kunpeng(鲲鹏)硬件加速器已注册到 UACCE**。通过 UADK 框架,用户可以使用硬件加速器而非 CPU 来运行加密与压缩算法,从而释放 CPU 算力、提升计算性能。 用户可以通过**在字符设备上做用户态操作**来访问硬件加速器,也可以经由已支持 UADK 的框架来使用(例如 **OpenSSL\* libcrypto\***、**DPDK**、**Linux\* Kernel Crypto Framework**)。 软件栈结构: ```text +----------------------------------+ | apps | +----+------------------------+----+ | | | | +-------+--------+ +-------+-------+ | scheduler | | alg libraries | +-------+--------+ +-------+-------+ | | | | | | | +--------+------+ | | vendor drivers| | +-+-------------+ | | | | +--+------------------+--+ | libwd | User +----+-------------+-----+ -------------------------------------------------- Kernel +--+-----+ +------+ | uacce | | smmu | +---+----+ +------+ | +---+------------------+ | vendor kernel driver | +----------------------+ -------------------------------------------------- +----------------------+ | HW Accelerators | +----------------------+ ``` #### 9.2.2 环境要求 UADK 由 UACCE、vendor drivers 和算法层组成。**UADK 要求硬件加速器支持 SVA,操作系统支持 IOMMU 与 SVA**。硬件加速器由内核态驱动以不同的字符设备形式注册到 UACCE。 #### 9.2.3 配置 **① 内核要求** 必须确保所使用的 Linux kernel 版本支持 UACCE,应为 **5.9 或更高**,且**启用 SVA(Shared Virtual Addressing)**。UACCE 可以构建为可加载模块,也可以构建进内核。以下是为 HiSilicon Kunpeng 平台构建带硬件加速器的 UACCE 示例: ```bash CONFIG_IOMMU_SVA_LIB=y CONFIG_ARM_SMMU=y CONFIG_ARM_SMMU_V3=y CONFIG_ARM_SMMU_V3_SVA=y CONFIG_PCI_PASID=y CONFIG_UACCE=y CONFIG_CRYPTO_DEV_HISI_QM=y CONFIG_CRYPTO_DEV_HISI_ZIP=y ``` 确保上述内核配置项全部被选中。 **② UADK 启用** 如果架构是 **`aarch64`**,会**自动下载 UADK 源码**构建静态库。在其它 CPU 架构上构建时,可通过在编译命令行中加入 **`-DWITH_UADK=true`** 来启用 UADK。**注意 UADK 可能并不兼容所有架构。** **③ 手工构建 UADK** 若架构为 `aarch64`,UADK 会自动启用,**无需手工构建**。以下是手工构建流程(供开发者研究其构建方式): ```bash git clone https://github.com/Linaro/uadk.git cd uadk mkdir build ./autogen.sh ./configure --prefix=$PWD/build make make install ``` > ⚠️ 不使用 `--prefix` 时,UADK 默认安装到 `/usr/local/lib`。若遇到错误 `cannot find -lnuma`,请安装 `libnuma-dev` 包。 **④ 启用配置** 编辑 Ceph 配置文件(通常 `ceph.conf`)以启用 UADK 对 **zlib** 压缩的支持: ```ini uadk_compressor_enabled=true ``` #### 9.2.4 UADK 配置项 | 配置项 | 类型 | 默认值(`src/common/options/global.yaml.in` @ v20.2.4) | 说明 | |---|---|---|---| | `uadk_compressor_enabled` | bool | `false` | 若可用,启用 UADK 对压缩的加速支持 | > 源文明确说明:`global.yaml.in` 中 `uadk_compressor_enabled` 的默认值为 **`false`**。 #### 9.2.5 验证方式 > 💡 补充:源文**未给出** UADK 的显式验证命令(QAT 页面给出了 `cpa_sample_code`)。可确认的验证手段是内核侧检查 `CONFIG_UACCE` 等配置项是否被选中,以及确认 zlib 压缩已按 9.2.3 ④ 启用。 > 引自:[UADK Acceleration for Compression](https://docs.ceph.com/en/tentacle/radosgw/uadk-accel/) --- ## 十、RGW 管理 API 入口(api.rst) 页面标题为 **librgw (Python)**。 `rgw` python 模块提供**类文件(file-like)的访问方式**来访问 rgw。 **API Reference**:源文以 Sphinx 的 `automodule` 指令自动生成 `rgw` 模块的 API 文档,成员包括 **`LibRGWFS`** 与 **`FileHandle`**。 ```text .. automodule:: rgw :members: LibRGWFS, FileHandle ``` > 💡 补充:`api.rst` 源文本身极短(仅 17 行),具体的 API 签名与文档由构建期从 `rgw` python 模块自动生成,因此**本册范围内无法展开** `LibRGWFS` 与 `FileHandle` 的方法列表,**文档未明确**。 > 引自:[librgw (Python)](https://docs.ceph.com/en/tentacle/radosgw/api/) --- ## 十一、安全配置检查清单 汇总本册涉及的能力、对应配置项、建议值与风险。所有默认值均取自 `src/common/options/*.yaml.in` @ v20.2.4;"建议值"一列中属于本册补充建议的,均在备注中说明。 | # | 能力 | 配置项 | 默认值 | 建议值 | 风险 | |---|---|---|---|---|---| | 1 | 加密请求必须走 SSL | `rgw_crypt_require_ssl` | `true` | **`true`** | 关闭后密钥头可能以明文传输 | | 2 | 抑制可能泄露客户端密钥的日志 | `rgw_crypt_suppress_logs` | `true` | **`true`** | 关闭后日志可能打印客户端密钥 | | 3 | SSL 终止代理后的安全判定 | `rgw_trust_forwarded_https` | 文档未明确 | **仅在前方确有受信任代理时设为 `true`** | 无代理却开启 → 任意用户可伪造 `Forwarded` / `X-Forwarded-Proto` 头 | | 4 | SSE-KMS 后端选择 | `rgw_crypt_s3_kms_backend` | `barbican` | 按实际 KMS 选 `vault` / `kmip` / `barbican` | 选错会导致取密钥失败 | | 5 | SSE-S3 后端 | `rgw_crypt_sse_s3_backend` | `vault` | `vault`(唯一合法值) | 无其它选择 | | 6 | Vault 认证方式 | `rgw_crypt_vault_auth` | `token` | **`agent`**(生产) | 用 `token` 且不用 agent 刷新 → token 过期后无法解密;**绝不能用 root token** | | 7 | Vault SSL 校验 | `rgw_crypt_vault_verify_ssl` | `true` | **`true`** | 设为 `false` 官方明确称"非常危险" | | 8 | Vault token 文件权限 | `rgw_crypt_vault_token_file` | 文档未明确 | 仅 radosgw 可读(如 `0600` + 属主 ceph) | 权限过宽导致密钥泄露 | | 9 | SSE-S3 Vault SSL 校验 | `rgw_crypt_sse_s3_vault_verify_ssl` | `true` | **`true`** | 同 #7 | | 10 | SSE-KMS 与 SSE-S3 隔离 | `rgw_crypt_vault_prefix` / `rgw_crypt_sse_s3_vault_prefix` | 文档未明确 | **指向不同容器** | 不隔离时桶拥有者可能碰到 SSE-S3 密钥 | | 11 | 测试用自动加密 | `rgw_crypt_default_encryption_key` | 文档未明确(level: `dev`) | **生产禁用** | ceph.conf 不是安全的密钥存储方式,泄露即视为已泄露 | | 12 | KMIP 密钥命名空间收敛 | `rgw_crypt_kmip_kms_key_template` | 文档未明确 | 设为带固定前缀的模板(如 `pykmip-$keyid`) | 不设时 Ceph 可见全部 kmip 密钥 | | 13 | STS 启用 | `rgw_s3_auth_use_sts` | `false` | 需要 STS/STSLite 时设 `true` | 未启用则临时凭证无法认证 | | 14 | STS 会话密钥 | `rgw_sts_key` | 文档未明确 | 用 `ceph-authtool --gen-print-key` 生成;**同 zone / 同 realm 内所有实例一致** | 各实例不一致 → 临时凭证跨实例失效 | | 15 | STS 会话时长上限 | `rgw_sts_max_session_duration` | `43200` | 按业务设定,不宜过大 | 过大 → 凭证泄露后暴露窗口长 | | 16 | AssumeRole* 会话时长下限 | `rgw_sts_min_session_duration` | `900` | 保持默认或按需上调 | 过小 → 频繁换取凭证 | | 17 | S3 认证引擎顺序 | `rgw_s3_auth_order` | `sts, external, local` | 明确指定,避免隐式回退 | 顺序不当可能导致认证绕过到非预期引擎 | | 18 | 禁用 presigned URL | `rgw_s3_auth_disable_signature_url` | `false` | 高安全场景设 `true` | 开启后所有 V2/V4 预签名请求被拒 | | 19 | Keystone admin token | `rgw_keystone_admin_token` | 文档未明确(**DEPRECATED**) | 改用 `rgw_keystone_admin_token_path` 或 admin user/password | 明文写在配置文件中 | | 20 | Keystone admin 密码 | `rgw_keystone_admin_password` | 文档未明确(**DEPRECATED**) | 改用 `rgw_keystone_admin_password_path` | 明文写在配置文件中 | | 21 | Keystone 可接受角色 | `rgw_keystone_accepted_roles` | `Member, admin` | 按最小权限收敛 | 过宽 → 不该被服务的用户也能访问 | | 22 | Keystone SSL 校验 | `rgw_keystone_verify_ssl` | `true` | **`true`** | 关闭后不校验 Keystone 证书 | | 23 | Keystone token 缓存 | `rgw_keystone_token_cache_size` | `10000` | 按并发调整 | 过小 → 频繁访问 Keystone | | 24 | service token 放行过期 token | `rgw_keystone_service_token_enabled` | `false` | 仅在确有长运行进程需求时开启 | 开启后过期 token 可被放行 | | 25 | service token 可接受角色 | `rgw_keystone_service_token_accepted_roles` | `admin` | 收敛到专门的 service role | 过宽 → 更多用户可让过期 token 生效 | | 26 | 过期 token 缓存时长 | `rgw_keystone_expired_token_cache_expiration` | `3600` | **必须低于 Keystone 的 `[token]/allow_expired_window`** | 超过则该机制失效/行为异常 | | 27 | LDAP 启用 | `rgw_s3_auth_use_ldap` | `false` | 需要 LDAP 时设 `true` | — | | 28 | LDAP 传输加密 | `rgw_ldap_uri` | 文档未明确 | **必须用 `ldaps://:`** | 用 `ldap://` 会明文传输凭证 | | 29 | LDAP 搜索范围收敛 | `rgw_ldap_searchdn` / `rgw_ldap_searchfilter` | `cn=users,cn=accounts,dc=example,dc=com` / 文档未明确 | 限定到具体 OU / 组 | 过宽 → 目录内所有用户都可认证 | | 30 | LDAP 用户名冲突 | — | — | **禁止 LDAP 用户与本地用户重名** | RGW 无法区分,会当作同一用户处理 | | 31 | LDAP 日志级别 | — | — | 把 `rgw` 日志级别设得足够低 | 日志可能泄露 base64 编码的凭证/access token | | 32 | OPA 授权 | `rgw_use_opa_authz` | `false` | 有 OPA 时设 `true` | 未启用则不做额外授权检查 | | 33 | OPA SSL 校验 | `rgw_opa_verify_ssl` | `true` | **`true`** | 关闭后不校验 OPA 证书 | | 34 | OPA bearer token | `rgw_opa_token` | 文档未明确 | 妥善保管 | 泄露 → 可伪造对 OPA 的查询 | | 35 | Lua 脚本总开关 | `rgw_lua_enable` | `true` | 不用时关闭 | 开启时脚本在每个请求上执行,可能拖慢网关 | | 36 | Lua 内存上限 | `rgw_lua_max_memory_per_state` | `128000` | 谨慎调整 | **改小到低于当前用量 → background state 数据全部丢失** | | 37 | Lua 运行时间上限 | `rgw_lua_max_runtime_per_state` | `1000` | 保持默认,**不要设为 0** | 设为 0 → 无界执行,可能拖垮 RGW 可用性 | | 38 | Lua 包安装目录 | `rgw_luarocks_location` | `/tmp/rgw_luarocks/$name` | **不要**设为 `$HOME/.luarocks`、`/usr/lib64/lua`、`/usr/share/lua` | 设为默认位置会与主机 Lua 包混在一起 | | 39 | QAT 压缩加速 | `qat_compressor_enabled` | `false` | 有 QAT 硬件时设 `true` | 无硬件却开启 → 行为异常 | | 40 | QAT 加密加速器 | `plugin_crypto_accelerator` | `crypto_isal` | 需要 QAT 加密时设 `crypto_qat`(且需 `WITH_QATDRV=ON` 构建、QAT 配置 section 名为 `CEPH`) | 未以 `WITH_QATDRV=ON` 构建则插件不可用 | | 41 | UADK 压缩加速 | `uadk_compressor_enabled` | `false` | aarch64 + 鲲鹏加速器时设 `true` | 架构不兼容时 UADK 不可用 | | 42 | MFA 多站点 | — | — | **不同 zone 使用不同 token** | MFA OTP 配置驻留在本地 zone 的 OSD,多站点共用 token 会出现不一致 | | 43 | 桶策略不支持用户级策略 | — | — | 账户拥有者需**直接**授权到具体用户 | 把整个账户授权给桶 = 授权给该账户下所有用户 | | 44 | 桶策略字符串插值 | — | — | 不要在桶策略里依赖插值 | 桶策略**不支持**字符串插值(role policy 支持) | --- ## 文档未明确项 本册范围内,官方源文未给出明确说明的事项,汇总如下(不编造、不推断): | # | 事项 | 所属章节 / 页面 | |---|---|---| | 1 | SSE-C 的密钥本体请求头名与 MD5 头名(AWS 中为 `x-amz-server-side-encryption-customer-key` 等) | `encryption` | | 2 | SSE 各模式的完整请求头清单(`encryption.rst` 只指向 Amazon 规范,未逐项列出) | `encryption` | | 3 | `rgw_crypt_default_encryption_key` 的默认值(`rgw.yaml.in` 中无 `default:` 字段,level 为 `dev`) | `encryption` | | 4 | `rgw_trust_forwarded_https` 的默认值 | `encryption` | | 5 | `rgw_keystone_api_version` 的完整定义(未在 `rgw.yaml.in` / `global.yaml.in` 中找到该条目) | `keystone` | | 6 | `rgw_barbican_url`、`rgw_keystone_barbican_*`、`rgw_keystone_admin_*`、`rgw_keystone_url` 等一批 str 配置的默认值(均无 `default:` 字段) | `barbican`、`keystone` | | 7 | `rgw_crypt_vault_token_file`、`rgw_crypt_vault_addr`、`rgw_crypt_vault_prefix`、`rgw_crypt_vault_namespace`、各 SSL 路径配置的默认值 | `vault` | | 8 | `rgw_crypt_kmip_*` 中除 `rgw_crypt_kmip_s3_key_template`(`$keyid`)外的默认值 | `kmip` | | 9 | `rgw_ldap_uri` 的默认值(`default:` 字段为空)与 `rgw_ldap_searchfilter` 的默认值 | `ldap-auth` | | 10 | `rgw_opa_url`、`rgw_opa_token` 的默认值 | `opa` | | 11 | OPA `input` 结构中除 `method`、`subuser`、`user_info.*`、`bucket_info.*` 之外的其它字段 | `opa` | | 12 | IAM API 中全部 60 个 action 的 Remarks(官方表格该列全为空) | `iam` | | 13 | IAM policy 与 bucket policy 的优先级裁决规则(源文未给出显式规则) | `iam`、`bucketpolicy` | | 14 | `AssumeRole` / `AssumeRoleWithWebIdentity` / `GetSessionToken` 的**响应结构字段清单**(源文只给出 `response['Credentials']['AccessKeyId' \| 'SecretAccessKey' \| 'SessionToken']` 的用法,未给出完整响应 schema) | `STS`、`STSLite` | | 15 | `rgw_sts_key` 的默认值(无 `default:` 字段) | `STS`、`STSLite` | | 16 | IAM 中 `sts:authentication` condition key 的完整语义(仅出现 `BoolIfExists` 一种用法) | `STSLite` | | 17 | `lua-scripting` 中"500K byte"与 `rgw_lua_max_memory_per_state` 默认值 `128000` 的不一致如何裁决 | `lua-scripting` | | 18 | background 上下文的执行间隔**是否可配置**、用哪个配置项(源文只说"默认间隔为 5 秒") | `lua-scripting` | | 19 | `Reques.Bucket.Quota.Enabled` 是否为 `Request.Bucket.Quota.Enabled` 的拼写错误(源文原样保留) | `lua-scripting` | | 20 | Lua `RGW` table 是否支持跨实例共享(源文明确说每实例私有,但未说明是否提供共享机制) | `lua-scripting` | | 21 | UADK 的显式验证命令(QAT 给出了 `cpa_sample_code`,UADK 未给) | `uadk-accel` | | 22 | `api.rst`(librgw Python)中 `LibRGWFS` 与 `FileHandle` 的方法清单(由构建期 `automodule` 生成,rst 源文中无内容) | `api` | | 23 | MFA 中 MFA 与桶 versioning 的具体绑定方式(源文只说"桶需通过 S3 api 配置为 versioning 且 MFA 启用",未给出具体 API/命令) | `mfa` | | 24 | Vault agent 场景下 secret-id 的正确获取命令(源文该处与 role-id 命令重复,疑为笔误) | `vault` |