Files
wiki/存储/Ceph/知识库/12-RGW-安全与高级特性篇.md
2026-09-18 13:49:11 +08:00

3826 lines
165 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第 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 -<<EOF
path "secret/data/*" {
capabilities = ["read"]
}
EOF
```
**Transit secret engine**:
```bash
vault policy write rgw-transit-policy -<<EOF
path "transit/keys/*" {
capabilities = [ "create", "update" ]
denied_parameters = {"exportable" = [], "allow_plaintext_backup" = [] }
}
path "transit/keys/*" {
capabilities = ["read", "delete"]
}
path "transit/keys/" {
capabilities = ["list"]
}
path "transit/keys/+/rotate" {
capabilities = [ "update" ]
}
path "transit/*" {
capabilities = [ "update" ]
}
EOF
```
**兼容旧版 Ceph 的附加 policy**(若此前用旧版 Ceph 配合 transit engine,可能需要):
```bash
vault policy write old-rgw-transit-policy -<<EOF
path "transit/export/encryption-key/*" {
capabilities = ["read"]
}
EOF
```
##### Token 认证
> ⚠️ 官方 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=<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 <<EOF >$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/<URL of IDP>"]}` | `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 "<hostname>?Action=CreateRole&RoleName=S3Access&Path=/application_abc/component_xyz/&AssumeRolePolicyDocument=\{...\}"` |
| 删除 Role | `POST "<hostname>?Action=DeleteRole&RoleName=S3Access"` |
| 获取 Role | `POST "<hostname>?Action=GetRole&RoleName=S3Access"` |
| 列出 Role | `POST "<hostname>?Action=ListRoles&RoleName=S3Access&PathPrefix=/application"` |
| 更新 Assume Role Policy | `POST "<hostname>?Action=UpdateAssumeRolePolicy&RoleName=S3Access&PolicyDocument=\{...\}"` |
| 添加/更新 Role Policy | `POST "<hostname>?Action=PutRolePolicy&RoleName=S3Access&PolicyName=Policy1&PolicyDocument=\{...\}"` |
| 列出 Role Policy 名 | `POST "<hostname>?Action=ListRolePolicies&RoleName=S3Access"` |
| 获取 Role Policy | `POST "<hostname>?Action=GetRolePolicy&RoleName=S3Access&PolicyName=Policy1"` |
| 删除 Role Policy | `POST "<hostname>?Action=DeleteRolePolicy&RoleName=S3Access&PolicyName=Policy1"` |
| 给 Role 打标签 | `POST "<hostname>?Action=TagRole&RoleName=S3Access&Tags.member.1.Key=Department&Tags.member.1.Value=Engineering"` |
| 列出 Role 标签 | `POST "<hostname>?Action=ListRoleTags&RoleName=S3Access"` |
| 删除 Role 标签 | `POST "<hostname>?Action=UntagRoles&RoleName=S3Access&TagKeys.member.1=Department"` |
| 更新 Role | `POST "<hostname>?Action=UpdateRole&RoleName=S3Access&MaxSessionDuration=43200"` |
**CreateRole 响应(XML)**:
```xml
<role>
<id>8f41f4e0-7094-4dc0-ac20-074a881ccbc5</id>
<name>S3Access</name>
<path>/application_abc/component_xyz/</path>
<arn>arn:aws:iam:::role/application_abc/component_xyz/S3Access</arn>
<create_date>2018-10-23T07:43:42.811Z</create_date>
<max_session_duration>3600</max_session_duration>
<assume_role_policy_document>{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["arn:aws:iam:::user/TESTER"]},"Action":["sts:AssumeRole"]}]}</assume_role_policy_document>
</role>
```
**ListRolePolicies 响应(XML)**:
```xml
<PolicyNames>
<member>Policy1</member>
</PolicyNames>
```
**GetRolePolicy 响应(XML)**:
```xml
<GetRolePolicyResult>
<PolicyName>Policy1</PolicyName>
<RoleName>S3Access</RoleName>
<Permission_policy>{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:CreateBucket"],"Resource":"arn:aws:s3:::example_bucket"}]}</Permission_policy>
</GetRolePolicyResult>
```
**TagRole 响应(XML)**:
```xml
<TagRoleResponse>
<ResponseMetadata>
<RequestId>tx000000000000000000004-00611f337e-1027-default</RequestId>
</ResponseMetadata>
</TagRoleResponse>
```
**ListRoleTags 响应(XML)**:
```xml
<ListRoleTagsResponse>
<ListRoleTagsResult>
<Tags>
<member>
<Key>Department</Key>
<Value>Engineering</Value>
</member>
</Tags>
</ListRoleTagsResult>
<ResponseMetadata>
<RequestId>tx000000000000000000005-00611f337e-1027-default</RequestId>
</ResponseMetadata>
</ListRoleTagsResponse>
```
**UntagRole 响应(XML)**:
```xml
<UntagRoleResponse>
<ResponseMetadata>
<RequestId>tx000000000000000000007-00611f337e-1027-default</RequestId>
</ResponseMetadata>
</UntagRoleResponse>
```
**UpdateRole 响应(XML)**:
```xml
<UpdateRoleResponse>
<UpdateRoleResult>
<ResponseMetadata>
<RequestId>tx000000000000000000007-00611f337e-1027-default</RequestId>
</ResponseMetadata>
</UpdateRoleResult>
</UpdateRoleResponse>
```
> 注意:**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/<URL of IDP>"]},"Action":["sts:AssumeRoleWithWebIdentity"],"Condition":{"StringEquals":{"<URL of IDP> :app_id":"<aud>"}}}]}
```
上例 condition 中的 `app_id` 必须匹配传入 token 的 `aud` claim。
使用 `sub` claim 的 policy:
```json
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":["arn:aws:iam:::oidc-provider/<URL of IDP>"]},"Action":["sts:AssumeRoleWithWebIdentity"],"Condition":{"StringEquals":{"<URL of IDP> :sub":"<sub>"}}}]}
```
使用 `azp` claim 的 policy:
```json
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":["arn:aws:iam:::oidc-provider/<URL of IDP>"]},"Action":["sts:AssumeRoleWithWebIdentity"],"Condition":{"StringEquals":{"<URL of IDP> :azp":"<azp>"}}}]}
```
**影子用户(shadow user)**:每个联邦用户都会对应创建一个影子用户,其 user id 由传入 web token 的 `sub` 字段派生。该用户创建在**独立的命名空间 `oidc`** 中,以避免与 rgw 中其它 user id 冲突。user id 格式为 `<tenant>$<user-namespace>$<sub>`,其中通过 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=<access_key of TESTER>,
aws_secret_access_key=<secret_key of TESTER>,
endpoint_url=<IAM 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=<access_key of TESTER1>,
aws_secret_access_key=<secret_key of TESTER1>,
endpoint_url=<STS 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=<S3 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=<access_key of TESTER>,
aws_secret_access_key=<secret_key of TESTER>,
endpoint_url=<IAM URL>,
region_name=''
)
oidc_response = iam_client.create_open_id_connect_provider(
Url=<URL of the OpenID Connect Provider,
ClientIDList=[
<Client id registered with the IDP>
],
ThumbprintList=[
<Thumbprint of the IDP>
]
)
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=<access_key of TESTER1>,
aws_secret_access_key=<secret_key of TESTER1>,
endpoint_url=<STS URL>,
region_name='',
)
response = client.assume_role_with_web_identity(
RoleArn=role_response['Role']['Arn'],
RoleSessionName='Bob',
DurationSeconds=3600,
WebIdentityToken=<Web Token>
)
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=<S3 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 = <ec2 access key>
secret_key = <ec2 secret key>
client = boto3.client('sts',
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
endpoint_url=<STS 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=<S3 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='<web-token>')
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>`(其中 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-<perm>` | 文档未明确 |
### 5.6 Object 相关操作的 Condition Keys
| Permission | Condition Keys | Comments |
|---|---|---|
| `s3:PutObject` | `s3:x-amz-acl` 与 `s3:x-amz-grant-<perm>` | 文档未明确 |
| `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/<tag-key>` | 文档未明确 |
| `s3:PutObjectAcl`、`s3:PutObjectVersionAcl` | `s3:x-amz-acl` 与 `s3-amz-grant-<perm>`;`s3:ExistingObjectTag/<tag-key>` | 文档未明确 |
| `s3:PutObjectTagging`、`s3:PutObjectVersionTagging` | `s3:RequestObjectTag/<tag-key>`;`s3:ExistingObjectTag/<tag-key>` | 文档未明确 |
| `s3:GetObject`、`s3:GetObjectVersion` | `s3:ExistingObjectTag/<tag-key>` | 文档未明确 |
| `s3:GetObjectAcl`、`s3:GetObjectVersionAcl` | `s3:ExistingObjectTag/<tag-key>` | 文档未明确 |
| `s3:GetObjectTagging`、`s3:GetObjectVersionTagging` | `s3:ExistingObjectTag/<tag-key>` | 文档未明确 |
| `s3:DeleteObjectTagging`、`s3:DeleteObjectVersionTagging` | `s3:ExistingObjectTag/<tag-key>` | 文档未明确 |
> 官方补充:随着近期重写的 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_<account>`)。跨 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://<fqdn>:<port>` 形式**,以免明文凭证在网络上传输 | 文档未明确(`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="<username>"
# export RGW_SECRET_ACCESS_KEY="<password>"
# 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=<base64-encoded token generated by radosgw-token>
# 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 "<hostname>?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 "<hostname>?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 "<hostname>?Action=Action=GetOpenIDConnectProvider
&OpenIDConnectProviderArn=arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart"
```
#### 6.3.4 ListOpenIDConnectProviders
列出所有 IDP 的信息。
| 参数 | 说明 | 类型 |
|---|---|---|
| None | 无参数 | — |
```text
POST "<hostname>?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 "<hostname>?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 "<hostname>?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 "<hostname>?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=<client id>
KC_CLIENT_SECRET=<client secret>
KC_SERVER=<host>: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=<username>
KC_PASSWORD=<userpassword>
KC_CLIENT=<client id>
KC_CLIENT_SECRET=<client secret>
KC_SERVER=<host>: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=<client id>
KC_CLIENT_SECRET=<client secret>
KC_SERVER=<host>: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=<user-id> --totp-serial=<serial> --totp-seed=<seed> [ --totp-seed-type=<hex\|base32> ] [ --totp-seconds=<num-seconds> ] [ --totp-window=<twindow> ]` |
| 列出 MFA TOTP tokens | `radosgw-admin mfa list --uid=<user-id>` |
| 查看 MFA TOTP token | `radosgw-admin mfa get --uid=<user-id> --totp-serial=<serial>` |
| 删除 MFA TOTP token | `radosgw-admin mfa remove --uid=<user-id> --totp-serial=<serial>` |
| 检查 MFA TOTP token | `radosgw-admin mfa check --uid=<user-id> --totp-serial=<serial> --totp-pin=<pin>` |
| 重新同步 MFA TOTP token | `radosgw-admin mfa resync --uid=<user-id> --totp-serial=<serial> --totp-pin=<prev-pin> --totp=pin=<current-pin>` |
**创建**:
```bash
# radosgw-admin mfa create --uid=<user-id> \
--totp-serial=<serial> \
--totp-seed=<seed> \
[ --totp-seed-type=<hex|base32> ] \
[ --totp-seconds=<num-seconds> ] \
[ --totp-window=<twindow> ]
```
**列出**:
```bash
# radosgw-admin mfa list --uid=<user-id>
```
**查看**:
```bash
# radosgw-admin mfa get --uid=<user-id> --totp-serial=<serial>
```
**删除**:
```bash
# radosgw-admin mfa remove --uid=<user-id> --totp-serial=<serial>
```
**检查**(测试 TOTP token pin,用于验证 TOTP 功能是否正常):
```bash
# radosgw-admin mfa check --uid=<user-id> --totp-serial=<serial> \
--totp-pin=<pin>
```
**重新同步**(用于时间偏移场景,**需要喂入两个连续的 pin**:前一个 pin 与当前 pin):
```bash
# radosgw-admin mfa resync --uid=<user-id> --totp-serial=<serial> \
--totp-pin=<prev-pin> --totp=pin=<current-pin>
```
> 💡 补充:源文 resync 命令中当前 pin 的参数写作 `--totp=pin=`,与前面的 `--totp-pin=` 形式不一致,疑为官方文档笔误(应为 `--totp-pin=<current-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/<entity name>`,其前缀部分(`/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["<name>"]` | table | user ACL grant | no | no | no |
| `Request.UserAcl.Grants["<name>"].Type` | integer | user ACL grant type | no | no | no |
| `Request.UserAcl.Grants["<name>"].User` | string | user ACL grant user/account id | no | no | no |
| `Request.UserAcl.Grants["<name>"].GroupType` | integer | user ACL grant group type | no | no | yes |
| `Request.UserAcl.Grants["<name>"].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[<index>]` | table | user policy. See: `Request.Policy` | no | no | no |
| `Request.RGWId` | string | radosgw host id: `<host>-<zone>-<zonegroup>` | 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(<key>, <value>)` | 为请求的 trace 设置属性。第一个参数 `key` 必须是字符串;第二个参数 `value` 可以是字符串或数字(integer 或 double)。之后可以用该属性定位特定的 traces |
| `Request.Trace.AddEvent(<name>, <attributes>)` | 向请求 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(<key>, [value])` | 把 `key` 的值增加 `value`;若未提供 `value`,则增加 1 |
| `RGW.decrement(<key>, [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 = "<Message> something bad happened :-( </Message>"
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://<fqdn>:<port>`** | 用 `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` |