165 KiB
第 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 形式强调):
- 服务端加密的请求必须通过安全 HTTPS 连接发送,否则等于把密钥明文送到网络上。
- 如果使用代理做 SSL 终止(SSL termination),则必须先启用
rgw trust forwarded https,RGW 才会把被转发的请求视为安全请求。 - 服务端加密密钥必须是 256 位长度、base64 编码。
引自: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:
PutBucketEncryptionGetBucketEncryptionDeleteBucketEncryption
引自:Encryption
1.5 自动加密(仅供测试)
可以在 ceph.conf 中设置 rgw crypt default encryption key,强制加密所有没有另行指定加密模式的对象。该配置接受一个 base64 编码的 256 位密钥。
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
二、密钥管理集成
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 命令行工具,需要先设置:
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 |
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:
vault policy write rgw-kv-policy -<<EOF
path "secret/data/*" {
capabilities = ["read"]
}
EOF
Transit secret engine:
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,可能需要):
vault policy write old-rgw-transit-policy -<<EOF
path "transit/export/encryption-key/*" {
capabilities = ["read"]
}
EOF
Token 认证
⚠️ 官方 note:生产环境绝不要使用 root token 配合 Ceph。
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 认证机制。
rgw crypt vault auth = agent
rgw crypt vault addr = http://127.0.0.1:8100
配置 AppRole:
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):
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):
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):
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"
}
启动常驻守护进程:
/usr/local/bin/vault agent -config=/usr/local/etc/vault/rgw-agent.hcl
启动后,agent 应在 localhost 的 8100 端口监听,可用 vault 命令与之交互验证。
2.1.4 Vault Namespaces
Vault 企业版支持 namespace(命名空间)概念,用于在同一个组织内为不同团队做集中管理并保证彼此隔离(租户)。
rgw crypt vault namespace = tenant1
2.1.5 在 Vault 中创建密钥
⚠️ 服务端加密密钥必须 256 位长、base64 编码。
KV engine:
vault kv put secret/myproject/mybucketkey key=$(openssl rand -base64 32)
样例输出:
====== 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:
vault write -f transit/keys/mybucketkey
该命令创建一个 keyring,默认包含类型为 aes256-gcm96 的密钥。验证:
vault read transit/keys/mybucketkey
样例输出:
Key Value
--- -----
derived false
exportable false
name mybucketkey
type aes256-gcm96
2.1.6 配置 Ceph Object Gateway
# 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 结尾(这是此前文档记载的配置),会自动选中此模式 |
兼容旧部署 |
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 要带上路径):
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):
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
2.2 KMIP(kmip.rst)
KMIP 可作为安全密钥管理服务用于 SSE-KMS。交互流程与 Vault 相同(客户端建密钥 → 带 key ID 上传 → RGW 向 KMIP 取密钥 → 加密 → 落 OSD)。
官方给出的配置步骤顺序:
Setting KMIP Access for CephCreating Keys in KMIPConfigure the Ceph Object GatewayUpload 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 创建。流程是:
- 先配置 Ceph 使用 KMIP(见下)并尝试使用它;
- 该尝试会失败,但会在 SKLM 中留下一个 "untrusted client device certificate"(不受信任的客户端设备证书);
- 在 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 客户端库。
准备环境:
virtualenv $HOME/my-kmip-env
source $HOME/my-kmip-env/bin/activate
pip install pykmip
准备客户端配置文件:
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 里执行):
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
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 上传对象
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。
2.3 OpenStack Barbican(barbican.rst)
OpenStack Barbican 可作为安全密钥管理服务用于服务端加密。
官方给出的步骤顺序:
Configure KeystoneCreate a Keystone userConfigure the Ceph Object GatewayCreate a key in Barbican
2.3.1 配置 Keystone
Barbican 依赖 Keystone 对其密钥做授权与访问控制,参见 OpenStack Keystone Integration(本册第六章)。
2.3.2 创建 Keystone 用户
创建一个供 Ceph Object Gateway 取回密钥使用的新用户,例如:
user = rgwcrypt-user
pass = rgwcrypt-password
tenant = rgwcrypt
2.3.3 在 Barbican 中创建密钥
⚠️ 服务端加密密钥必须 256 位长、base64 编码。 请求 Barbican 必须在
X-Auth-Token头中带有效的 Keystone token。
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"
}
响应:
{"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:
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
}
}
响应:
{"acl_ref": "http://barbican.example.com:9311/v1/secrets/d1e7ef3b-f841-4b7c-90b2-b7d90ca2d723/acl"}
2.3.4 配置 Ceph Object Gateway
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 时:
rgw keystone barbican tenant = rgwcrypt
使用 API version 3 时:
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 |
三、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 操作"这一事实。
四、角色与 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
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 |
示例:
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\"\]\}\]\}
响应:
{
"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
radosgw-admin role delete --role-name={role-name}
| 参数 | 说明 | 类型 |
|---|---|---|
role-name |
Role 的名称 | String |
radosgw-admin role delete --role-name=S3Access1
注意:只有当 role 没有挂载任何权限策略时才能删除它。
获取 Role
radosgw-admin role get --role-name={role-name}
| 参数 | 说明 | 类型 |
|---|---|---|
role-name |
Role 的名称 | String |
radosgw-admin role get --role-name=S3Access1
响应同创建时的 JSON 结构(id / name / path / arn / create_date / max_session_duration / assume_role_policy_document)。
列出 Role
radosgw-admin role list [--path-prefix ={path prefix}]
| 参数 | 说明 | 类型 |
|---|---|---|
path-prefix |
用于过滤 role 的路径前缀。不指定则列出所有 role | String |
radosgw-admin role list --path-prefix="/application"
响应为 role 对象数组。
更新 Role 的 Assume Role Policy Document
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 |
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
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 |
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:
radosgw-admin role-policy put --role-name=S3Access1 --policy-name=Policy1 --infile policy-document.json
列出 Role 上挂载的 Policy 名称
radosgw-admin role policy get --role-name={role-name}
| 参数 | 说明 | 类型 |
|---|---|---|
role-name |
Role 的名称 | String |
radosgw-admin role-policy list --role-name=S3Access1
[
"Policy1"
]
💡 补充:源文中该小节的命令原型写作
radosgw-admin role policy get,但给出的示例却是radosgw-admin role-policy list。二者在官方源文中并存,实际使用时以radosgw-admin role-policy list为准。
获取 Role 上挂载的指定 Policy
radosgw-admin role policy get --role-name={role-name} --policy-name={policy-name}
| 参数 | 说明 | 类型 |
|---|---|---|
role-name |
Role 的名称 | String |
policy-name |
Policy 的名称 | String |
radosgw-admin role-policy get --role-name=S3Access1 --policy-name=Policy1
{
"Permission policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:*\"],\"Resource\":\"arn:aws:s3:::example_bucket\"}]}"
}
删除 Role 上挂载的 Policy
radosgw-admin role policy delete --role-name={role-name} --policy-name={policy-name}
| 参数 | 说明 | 类型 |
|---|---|---|
role-name |
Role 的名称 | String |
policy-name |
Policy 的名称 | String |
radosgw-admin role-policy delete --role-name=S3Access1 --policy-name=Policy1
更新 Role
radosgw-admin role update --role-name={role-name} --max-session-duration={max-session-duration}
| 参数 | 说明 | 类型 |
|---|---|---|
role-name |
Role 的名称 | String |
max-session-duration |
Role 的最大会话时长 | String |
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 的用户:
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):
<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):
<PolicyNames>
<member>Policy1</member>
</PolicyNames>
GetRolePolicy 响应(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):
<TagRoleResponse>
<ResponseMetadata>
<RequestId>tx000000000000000000004-00611f337e-1027-default</RequestId>
</ResponseMetadata>
</TagRoleResponse>
ListRoleTags 响应(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):
<UntagRoleResponse>
<ResponseMetadata>
<RequestId>tx000000000000000000007-00611f337e-1027-default</RequestId>
</ResponseMetadata>
</UntagRoleResponse>
UpdateRole 响应(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 标签
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
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:
{"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:
{"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:
{"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。
radosgw-admin caps add --uid="TESTER" --caps="roles=*"
完整流程(TESTER1 assume 由 TESTER 创建的 role,按挂载在 role 上的权限策略访问 TESTER 拥有的 S3 资源):
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)
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
-
取 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。 -
用 curl 取配置文档:
curl -k -v \
-X GET \
-H "Content-Type: application/x-www-form-urlencoded" \
"http://localhost:8000/auth/realms/quickstart/.well-known/openid-configuration" \
| jq .
- 从步骤 2 的响应中取
jwks_uri的值,获取 IDP 的证书:
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 .
-
把响应中
x5c的内容复制到文件certificate.crt,在开头加-----BEGIN CERTIFICATE-----,结尾加-----END CERTIFICATE-----。 -
用 OpenSSL 取证书指纹:
openssl x509 -in certificate.crt -fingerprint -noout
- 得到 SHA1 指纹,形如:
SHA1 Fingerprint=F7:D7:B3:51:5D:D0:D3:19:DD:21:9A:43:A9:EA:72:7A:D6:06:52:87
- 去掉冒号得到最终 thumbprint,可作为在 IAM 中创建 OpenID Connect Provider 实体的输入:
F7D7B3515DD0D319DD219A43A9EA727AD6065287
引自:STS in Ceph
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:
radosgw-admin caps add --uid="TESTER" --caps="user-policy=*"
需要挂到用户 TESTER1 上的策略:
{"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:authenticationcondition key 用法;BoolIfExists表示键不存在时按真处理。
4.3.5 配置
最小配置:
[client.radosgw.gateway]
rgw sts key = {sts key for encrypting the session token}
rgw s3 auth use sts = true
与 Keystone 配合的完整配置集:
[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 配合的完整配置集:
[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:
ceph-authtool --gen-print-key
生成的 key 会是 base64 编码(编码后的 key 还会包含 key type 与 timestamp)。
注意:STS 与 S3 API 默认共存于同一命名空间,可通过同一 endpoint 访问。
4.3.6 STS Lite + Keystone 完整示例
步骤 1:生成 EC2 凭证。
openstack ec2 credentials create
+------------+--------------------------------------------------------+
| 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 换取临时凭证。
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 调用。
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' 这段):
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
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 的示例:
{
"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 会失败。
{
"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):
{
"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):
{
"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):
{
"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):
{
"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):
{
"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 上附着的标签。
{
"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)上的标签。
{
"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:
# -*- 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)
五、桶策略(bucketpolicy.rst)
Bucket policies 在 Ceph 的 Luminous 版本中引入。
Ceph Object Gateway 支持 Amazon S3 policy 语言的一个子集,应用于桶。
5.1 创建与删除
桶策略通过标准 S3 操作管理,而不是 radosgw-admin。用 s3cmd 设置/删除策略:
$ 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:CurrentTimeaws:EpochTimeaws:PrincipalTypeaws:Refereraws:SecureTransportaws:SourceIpaws:UserAgentaws: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 的匹配方式,取决于所使用的具体后端。
六、外部身份源集成
6.1 OpenStack Keystone(keystone.rst)
可以把 Ceph Object Gateway 与 OpenStack 的身份服务 Keystone 集成。这让网关接受 Keystone 作为用户权威(users authority):
- 被 Keystone 授权访问网关的用户,会在 Ceph Object Gateway 上被自动创建(如果此前不存在);
- 被 Keystone 判定有效的 token,网关也认为有效。
6.1.1 配置项
[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 权限。
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 替换为:
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:
openstack service create --name=swift \
--description="Swift Service" \
object-store
+-------------+----------------------------------+
| Field | Value |
+-------------+----------------------------------+
| description | Swift Service |
| enabled | True |
| id | 37c4c0e79571404cb4644201a4a6e5ee |
| name | swift |
| type | object-store |
+-------------+----------------------------------+
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
+--------------+------------------------------------------+
| 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 |
+--------------+------------------------------------------+
查看:
$ openstack endpoint show object-store
⚠️ 注意:如果你的 radosgw
ceph.conf设置了rgw swift account in url = true,那么object-storeendpoint 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 的桶,需要启用:
rgw swift account in url = true
Keystone 的 object-store endpoint 也必须相应配置为包含 AUTH_%(project_id)s 后缀:
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 |
6.2 LDAP 认证(ldap-auth.rst)
versionadded:: Jewel
可以把 Ceph Object Gateway 的认证委托给 LDAP 服务器。
6.2.1 工作原理
- Ceph Object Gateway 从 token 中提取用户的 LDAP 凭证;
- 用用户名构造一个 search filter;
- Object Gateway 使用配置好的 service account 在目录中搜索匹配条目;
- 如果找到条目,Object Gateway 尝试用 token 中的密码 bind 到找到的 distinguished name;
- 如果凭证有效,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 连接:
# 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
"objectclass=inetorgperson"
Object Gateway 仍会按常规用 token 中的用户名和 rgw_ldap_dnattr 的值生成 search filter,然后与 rgw_search_filter 中的 partial filter 组合。取决于用户名与设置,最终 search filter 可能是:
"(&(uid=hari)(objectclass=inetorgperson))"
因此用户 hari 只有在:① 在 LDAP 目录中被找到;② object class 为 inetorgperson;③ 提供了有效密码 时,才会被授予访问。
② 指定 complete filter(完整过滤器)
完整过滤器必须包含 @USERNAME@ token,认证时会替换为用户名。此时不再使用 rgw_ldap_dnattr 参数。例如,把有效用户限制在特定组:
"(&(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。
# export RGW_ACCESS_KEY_ID="<username>"
# export RGW_SECRET_ACCESS_KEY="<password>"
# radosgw-token --encode
也可以手工生成 —— 把下面这段 JSON 做 base64 编码即可(当没有安装 radosgw-token 工具时):
{
"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 指定在客户端或环境变量中:
# export AWS_ACCESS_KEY_ID=<base64-encoded token generated by radosgw-token>
# export AWS_SECRET_ACCESS_KEY="" # 定义为空字符串,否则工具可能抱怨缺少环境变量
⚠️ access token 是 base64 编码的 JSON 结构,其中包含明文形式的 LDAP 凭证。除非你想分享你的明文密码,否则不要分享它!
6.3 OpenID Connect Provider(oidc.rst)
要在 RGW 与 IDP 之间建立信任,必须先在 RGW 中创建描述 OpenID Connect Provider 的实体。
调用这些 REST admin API 需要创建带 admin caps 的用户:
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 |
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 |
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 |
POST "<hostname>?Action=Action=GetOpenIDConnectProvider
&OpenIDConnectProviderArn=arn:aws:iam:::oidc-provider/localhost:8080/auth/realms/quickstart"
6.3.4 ListOpenIDConnectProviders
列出所有 IDP 的信息。
| 参数 | 说明 | 类型 |
|---|---|---|
| None | 无参数 | — |
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 |
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 |
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 |
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=与单个&。
6.4 Keycloak(keycloak.rst)
如果 Keycloak 被配置为 OpenID Connect Identity Provider,移动应用和 Web 应用就可以用它来认证用户。利用认证过程返回的 web token,应用可以调用 AssumeRoleWithWebIdentity,获得一组临时 S3 凭证,并用这些凭证发起 S3 调用。
6.4.1 集成步骤
① 配置 Keycloak 与 RGW 通信 —— 添加以下配置项:
[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:
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):
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 中给用户添加标签
- 添加用户(Keycloak 管理界面);
- 填写用户详情;
- 设置用户凭证;
- 在用户的 'attributes' 标签页 添加标签;
- 添加 protocol mapper,把用户属性映射到 client。
完成这些步骤后,标签 Department 会出现在 JWT(web token)中,位于 https://aws.amazon.com/tags 命名空间下。
验证方式:对 JWT 做 token introspection,使用 client id 与 client secret:
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 .
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
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 执行该决策。
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"
}
}
}
响应:
{"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的全部字段。上表字段仅覆盖示例中出现的字段,更多字段文档未明确。
七、多因素认证 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> |
创建:
# 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> ]
列出:
# radosgw-admin mfa list --uid=<user-id>
查看:
# radosgw-admin mfa get --uid=<user-id> --totp-serial=<serial>
删除:
# radosgw-admin mfa remove --uid=<user-id> --totp-serial=<serial>
检查(测试 TOTP token pin,用于验证 TOTP 功能是否正常):
# radosgw-admin mfa check --uid=<user-id> --totp-serial=<serial> \
--totp-pin=<pin>
重新同步(用于时间偏移场景,需要喂入两个连续的 pin:前一个 pin 与当前 pin):
# 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>)。本册保留源文原样并在此提示。
八、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
上传脚本:
# radosgw-admin script put --infile={lua-file-path} --context={prerequest|postrequest|background|getdata|putdata} [--tenant={tenant-name}]
上传
background上下文的脚本时,不应指定 tenant 名。
cephadm 环境下:
# cephadm shell radosgw-admin script put --infile=/rootfs/{lua-file-path} --context={prerequest|postrequest|background|getdata|putdata} [--tenant={tenant-name}]
打印脚本内容到标准输出:
# radosgw-admin script get --context={preRequest|postRequest|background|getdata|putdata} [--tenant={tenant-name}]
删除脚本:
# 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 场景下打印源对象与目标对象信息
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
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
if Request.Response.HTTPStatusCode ~= 200 then
RGWDebugLog("request is bad, use ops log")
rc = Request.Log()
RGWDebugLog("ops log return code: " .. rc)
end
示例 4:改写错误消息(响应内容改写)
if Request.Response.HTTPStatusCode == 500 then
Request.Response.Message = "<Message> something bad happened :-( </Message>"
end
示例 5:给对象追加客户端原本没有发送的 metadata
在 prerequest 上下文中:
if Request.RGWOp == 'put_obj' then
Request.HTTP.Metadata["x-amz-meta-mydata"] = "my value"
end
在 postrequest 上下文中查看 metadata:
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:
# radosgw-admin script-package add --package=lua-cjson --allow-compilation
# radosgw-admin script-package add --package=luasocket --allow-compilation
第二步:启动一个监听 Unix socket 的服务端,例如用 netcat:
# rm -f /tmp/socket
# nc -vklU /tmp/socket
第三步:重启 radosgw,并把下面脚本上传到 postrequest 上下文:
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:
if Request.Bucket.Name == "my-bucket" then
Request.Trace.Enable = true
end
如果 RGW 上已启用 tracing,则 Request.Trace.Enable 为 true,因此应对所有不匹配该桶名的请求关闭 tracing。在 prerequest 上下文中:
if Request.Bucket.Name ~= "my-bucket" then
Request.Trace.Enable = false
end
注意:修改
Request.Trace.Enable不会改变 tracer 本身的状态,只是针对该请求启用或禁用 tracing。
示例 8:为请求 trace 追加信息
在 postrequest 上下文中,可以向请求的 trace 添加属性和事件:
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 上下文中加入:
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)与可写响应字段。下面把二者组合成一个可直接使用的骨架(属本册补充,非源文原文):
-- 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 的原子计数,可做简单限流:
-- 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 实例私有的
RGWtable,因此是单实例维度的计数,不是集群全局限流。
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 状态 |
九、硬件加速
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 环境准备
- 需要 QuickAssist Accelerator 硬件,且内核态的 QAT 驱动必须已加载以驱动该硬件。
- out-of-tree QAT 驱动包可从 Intel Quickassist Technology 下载;
- QATlib 可从 qatlib 下载,用于 in-tree QAT driver。
- ⚠️ out-of-tree QAT 驱动正在逐步迁移到 in-tree driver + QATlib。
- 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并保持一致:
#...
# 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参数,可用以下命令验证与设置:
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。从源码构建时:
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:
plugin crypto accelerator = crypto_qat
③ QAT Support for Compression
For out-of-tree QAT:
- 构建前确保 QAT 驱动与 QATzip 都已安装;
- 除
ICP_ROOT外,把环境变量QZ_ROOT设置为 QATzip 源码树的根目录; - 配置以下 CMake 选项:
./do_cmake.sh -DWITH_QATDRV=ON -DWITH_QATZIP=ON -DWITH_SYSTEM_QATZIP=ON -DWITH_QATLIB=OFF
- 设置环境变量以明确 QAT 配置文件中 User Process Instance Section 的 section 名:
export QAT_SECTION_NAME=CEPH
For in-tree QAT:
- 确保系统满足 QATlib System Requirements;QATlib 可从预编译包或源码安装。安装后可运行
cpa_sample_code检查 QAT 环境是否正常; - 若使用 QATlib 源码,Ceph 的
cmake构建默认启用 qatlib 和 qatzip 选项,常规编译已包含 QAT-compressor 相关代码:
./do_cmake.sh
- 若使用系统上的预编译包,构建 Ceph 时需配置:
./do_cmake.sh -DWITH_SYSTEM_QATLIB=ON -DWITH_SYSTEM_QATZIP=ON
For both:
编辑 Ceph 的 central config DB 或配置文件(通常是 ceph.conf)以启用 QAT 对 zlib 压缩的支持:
qat compressor enabled=true
设置 RGW 的压缩方法:
# 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已启用。
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)。
软件栈结构:
+----------------------------------+
| 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 示例:
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 会自动启用,无需手工构建。以下是手工构建流程(供开发者研究其构建方式):
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 压缩的支持:
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 ④ 启用。
十、RGW 管理 API 入口(api.rst)
页面标题为 librgw (Python)。
rgw python 模块提供类文件(file-like)的访问方式来访问 rgw。
API Reference:源文以 Sphinx 的 automodule 指令自动生成 rgw 模块的 API 文档,成员包括 LibRGWFS 与 FileHandle。
.. automodule:: rgw
:members: LibRGWFS, FileHandle
💡 补充:
api.rst源文本身极短(仅 17 行),具体的 API 签名与文档由构建期从rgwpython 模块自动生成,因此本册范围内无法展开LibRGWFS与FileHandle的方法列表,文档未明确。
十一、安全配置检查清单
汇总本册涉及的能力、对应配置项、建议值与风险。所有默认值均取自 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 |