RustFS Policy 引擎深度解析:AWS 兼容的访问控制与条件策略评估机制
【免费下载链接】rustfs🚀2.3x faster than MinIO for 4KB object payloads. RustFS is an open-source, S3-compatible high-performance object storage system supporting migration and coexistence with other S3-compatible platforms such as MinIO and Ceph.项目地址: https://gitcode.com/GitHub_Trending/rus/rustfs
导读
本文以 RustFS 分布式对象存储的独立策略引擎 crate(crates/policy)为核心,系统讲解其如何实现 AWS S3 兼容的 Bucket Policy 与身份策略(Identity Policy)解析、校验与动态评估。你会掌握 RustFS 策略的 JSON 语法、Deny/Allow判定顺序、条件运算符(StringEquals、IpAddress、ForAllValues等)的底层实现,以及内置默认策略(readwrite、consoleAdmin、KMS 角色模板)的真实语义,并能够基于源码与测试用例自行验证策略行为。
一、策略引擎在 RustFS 中的定位
RustFS 是一个开源的 S3 兼容高性能对象存储系统,支持与 MinIO、Ceph 等其他 S3 兼容平台迁移与共存。在它的整体架构中,访问控制横跨多个 crate:crates/iam负责用户、组与 STS 会话,crates/credentials提供凭据与 JWT 声明(claims),而crates/policy则专职负责策略本身的建模、解析、校验与求值——即"谁(Principal)在什么资源(Resource)上被允许(Allow)或禁止(Deny)执行哪些操作(Action),并且满足哪些条件(Condition)"。
从 crates/policy/src/lib.rs 可以看到该 crate 的公开模块划分:
arn:ARN(Amazon Resource Name)类型解析;auth:认证上下文(credentials)适配;policy:核心策略模型,包括Policy、BucketPolicy、Statement、Effect、ActionSet、ResourceSet、Principal、Functions(条件函数集)等;format、serde_datetime、service_type、utils:序列化、日期时间与工具支持。
其中policy子模块又按职责拆分为action、effect、function(含string、number、date、addr、bool_null、binary、key、key_name等条件函数实现)、principal、resource、statement、variables等文件,每个文件对应一个可独立验证的语义单元。
二、策略 JSON 模型:Policy 与 BucketPolicy
2.1 两种策略类型的分工
源码中定义了两套并行模型(见 crates/policy/src/policy/policy.rs):
| 类型 | 适用场景 | 特点 |
|---|---|---|
Policy | 身份策略(IAM 用户/角色/组附加的策略) | 无Principal字段,判定时以调用者身份(account)隐式作为主体 |
BucketPolicy | 存储桶级资源策略(PutBucketPolicy下发) | 每条语句包含Principal字段,用于限定外部主体 |
两者共享相同的Statement结构,区别在于BucketPolicy使用BPStatement,额外携带Principal,且校验规则更严格——Bucket Policy 不允许出现 KMS 相关的 Action 或 Resource(见 crates/policy/src/policy/statement.rs 中的KmsUnsupportedInBucketPolicy校验)。
2.2 顶层结构字段
Policy与BucketPolicy均通过 serde 反序列化,字段使用rename对齐 AWS 官方格式,且开启deny_unknown_fields拒绝未知字段:
Version:策略语法版本,默认且唯一合法值为2012-10-17(源码常量DEFAULT_VERSION,见 crates/policy/src/policy/policy.rs);空值或非法值在校验阶段直接返回InvalidVersion错误;ID/Id:可选标识符,Policy兼容 AWS 的ID大写形式,BucketPolicy同时兼容历史遗留的"ID"键(源码中的RUSTFS_COMPAT_TODO注释说明了这一兼容性考虑);Statement:语句数组,为策略的实际判定单元。
2.3 Statement 语句结构
Statement的字段与 AWS IAM 策略元素一一对应(见 crates/policy/src/policy/statement.rs):
| JSON 字段 | Rust 字段 | 语义 |
|---|---|---|
Sid | sid | 语句标识符 |
Effect | effect | Allow或Deny |
Action | actions | 允许/禁止匹配的操作集合 |
NotAction | not_actions | 操作取反集合 |
Resource | resources | 匹配的资源集合(ARN) |
NotResource | not_resources | 资源取反集合 |
Condition | conditions | 条件求值函数集 |
注意ActionSet与ResourceSet的序列化行为:无论包含多少个元素,始终以 JSON 数组形式输出(见 crates/policy/src/policy/action.rs 的Serialize实现),以保证与 AWS S3 规范及 AWS SDK 客户端兼容;但反序列化时两者都宽容地同时接受单字符串或字符串数组两种输入形式(visit_str/visit_seq双 visitor)。crates/policy/tests/policy_is_allowed.rs 中的单元测试专门验证了单字符串与数组两种格式解析结果完全等价。
三、Effect 与判定算法:先 Deny 后 Allow
3.1 Effect 的语义实现
Effect枚举仅含Allow与Deny两个变体(见 crates/policy/src/policy/effect.rs)。其核心逻辑在一个极简方法中体现:
pub fn is_allowed(&self, allowed: bool) -> bool { if matches!(self, Self::Allow) { return allowed; } !allowed }也就是说:Allow语句直接透传条件求值结果;Deny语句则对求值结果取反——条件匹配成功时语句返回"不允许"。
3.2 整个策略的判定顺序
Policy::is_allowed(见 crates/policy/src/policy/policy.rs)实现了 AWS 风格的显式拒绝优先语义,流程如下:
- 先遍历所有
Deny语句:只要任一 Deny 语句匹配(其is_allowed返回 false),整个请求立即被拒绝; deny_only模式短路:若调用方只关心显式拒绝(例如审计场景),没有任何 Deny 命中则直接放行;- Owner 免检:若请求方是存储桶/资源所有者(
is_owner == true),直接放行(Allows all permissions); - 再遍历所有
Allow语句:任一 Allow 语句匹配即放行; - 否则默认拒绝(
false)。
BucketPolicy::is_allowed采用同样的顺序,但不包含deny_only模式(见 crates/policy/src/policy/policy.rs)。
这一"显式拒绝 > 隐式拒绝 > 显式允许"的顺序,保证运维人员可以用一条Deny语句精确封锁高危操作(如删除),而不会被后续宽松的Allow覆盖。
3.3 语句级匹配管线
每条语句的is_allowed(见 crates/policy/src/policy/statement.rs)依次经过四道关卡,全部通过才算语句匹配:
- Action 匹配:
ActionSet::is_match对每个动作执行通配符匹配(*与?),并实现了一个特殊的兼容规则——s3:GetObjectVersion可以匹配s3:GetObject(见 crates/policy/src/policy/action.rs);NotAction则反向匹配; - Resource 匹配:将请求的 bucket/object 组装为资源字符串后与
ResourceSet做通配符比对(细节见下节); - Principal 匹配(仅 Bucket Policy):调用
Principal::is_match(account); - Condition 求值:对
Condition函数集执行异步求值,最终结果交给Effect::is_allowed反转。
语句中Action与NotAction、Resource与NotResource不能同时出现,也不能同时为空,否则校验返回对应的 IAM 错误(NonAction、BothActionAndNotAction、NonResource、BothResourceAndNotResource,见 crates/policy/src/policy/policy.rs 的错误枚举)。
四、Action 与 Resource:四种命名空间与通配符匹配
4.1 四类 Action 命名空间
Action枚举(见 crates/policy/src/policy/action.rs)划分出四个互不混淆的命名空间:
| 前缀 | 枚举 | 示例 |
|---|---|---|
s3: | S3Action | s3:GetObject、s3:PutObject、s3:ListBucket |
admin: | AdminAction | admin:Prometheus、admin:ServerInfo |
sts: | StsAction | sts:AssumeRole |
kms: | KmsAction | kms:Decrypt、kms:GenerateDataKey |
解析时依据前缀分发(见 crates/policy/src/policy/action.rs),其中裸通配符*被特判为s3:*(S3Action::AllActions)。同一语句内禁止混合多个 Action 家族(MixedActionFamilies错误),但ActionSet::is_match的匹配本身基于通配符字符串比对,支持s3:Get*这类前缀通配。
4.2 资源 ARN 与通配符语义
Resource枚举支持两种 ARN 前缀(见 crates/policy/src/policy/resource.rs):
arn:aws:s3:::——S3 桶与对象,如arn:aws:s3:::mybucket/*;arn:aws:kms:::——KMS 密钥,如arn:aws:kms:::key/reports-*。
匹配过程(is_match_with_resolver)包含三个关键细节:
- 路径清洗:先对请求资源做
path::clean规范化(消除..、.段),再执行精确比对或通配符比对。资源匹配的测试用例(crates/policy/src/policy/resource.rs)明确验证了arn:aws:s3:::attacker-bucket/*无法匹配attacker-bucket/../victim-bucket/evil.txt这类路径穿越输入,从测试层面锁死了目录穿越攻击面; - 条件变量替换:资源模式中的
${aws:...}、${s3:...}等占位符会先由VariableResolver或条件映射中的公共键值进行替换(${aws:username}、${s3:prefix}等); - KMS 密钥级作用域:KMS 语句按
key/<key_id>段匹配请求中的密钥标识,支持?单字符通配,且请求侧 KMS 资源同样先做路径清洗(key/mykey/../otherkey不会匹配key/mykey)。
KMS 资源语法校验也相当严格(见 crates/policy/src/policy/resource.rs):裸*合法;key/<id>要求 id 非空且不含/与\;alias/<name>要求非空;带 region/account 的完整 ARN 形式(arn:aws:kms:us-east-1:...)会被拒绝。
五、Condition 条件求值:运算符与量化器
5.1 支持的条件运算符
Condition枚举(见 crates/policy/src/policy/function/condition.rs)覆盖了 AWS IAM 策略的常见运算符族,按值类型分组:
| 值类型 | 运算符 |
|---|---|
| 字符串 | StringEquals、StringNotEquals、StringEqualsIgnoreCase、StringNotEqualsIgnoreCase、StringLike、StringNotLike |
| ARN | ArnEquals、ArnNotEquals、ArnLike、ArnNotLike |
| 数值 | NumericEquals、NumericNotEquals、NumericLessThan、NumericLessThanEquals、NumericGreaterThan、NumericGreaterThanEquals、NumericGreaterThanIfExists |
| 日期时间 | DateEquals、DateNotEquals、DateLessThan、DateLessThanEquals、DateGreaterThan、DateGreaterThanEquals |
| 网络 | IpAddress、NotIpAddress(基于ipnetworkcrate 做 CIDR 匹配) |
| 布尔/空值 | Bool、Null |
| 二进制 | BinaryEquals |
| 修饰符 | 任意运算符追加IfExists后缀(如StringLikeIfExists),当键缺失时条件自动成立 |
每个运算符对应function/目录下的一个独立实现文件:string.rs、number.rs、date.rs、addr.rs、bool_null.rs、binary.rs,便于单独单元测试。
5.2 量化器:ForAllValues 与 ForAnyValue
当条件键携带多个请求值时(如对象标签键集合),需要量化器来定义聚合语义。Quantifier枚举(见 crates/policy/src/policy/function.rs)提供三种模式:
- 无前缀:运算符对整个值集合求值(单值键语义);
ForAnyValue::至少一个请求值满足运算符即成立;键缺失时不满足;ForAllValues::所有请求值都满足才成立;键缺失时空真(vacuous)成立。
源码注释特别强调了一个易错点:对于否定类运算符(StringNotEquals、StringNotLike等),否定必须先作用于每个请求值,再由量化器聚合;若先聚合再取反,会把ForAllValues与ForAnyValue的语义颠倒。这也是 crates/policy/src/policy/function/condition.rs 中is_negate只对NotIpAddress做整体取反、而字符串否定族通过内部negate参数处理的原因——避免双重取反。crates/policy/src/policy/function/condition.rs 的单元测试test_string_not_equals_no_double_negation直接验证了这一行为。
5.3 条件键命名空间
条件键通过KeyName枚举按前缀分派(见 crates/policy/src/policy/function/key_name.rs):s3:、aws:、ldap:、sts:、jwt:、svc:六个命名空间。其中:
aws:SourceIp、aws:Username、aws:UserID、aws:Groups、aws:CurrentTime、aws:EpochTime、aws:SecureTransport、aws:Referer、aws:UserAgent等为全局环境键;s3:x-amz-*键镜像请求头(如s3:x-amz-server-side-encryption、s3:x-amz-copy-source、s3:prefix、s3:delimiter、s3:max-keys、s3:LocationConstraint);jwt:*与ldap:*键来源于认证声明与 LDAP 上下文(jwt:groups、jwt:sub、ldap:groups等)。
关键的安全设计是is_server_derived(见 crates/policy/src/policy/function/key_name.rs):aws:、jwt:、ldap:、sts:、svc:命名空间以及非x-amz-的s3:键,其值必须来自服务端已验证的状态(身份、认证声明、连接信息),组装条件映射的代码会拒绝请求方直接以同名头注入这些键,防止攻击者仅靠伪造请求头满足aws:userid或jwt:groups条件。crate层面通过is_server_derived_condition_key对外暴露该判定(见 crates/policy/src/policy/function.rs)。
5.4 IfExists 语义与函数集求值
Functions结构将条件分为三组存储——for_any_value、for_all_values、for_normal,反序列化时依据ForAnyValue:/ForAllValues:前缀分流(见 crates/policy/src/policy/function.rs),并拒绝重复的条件运算符键。求值时三组条件全部通过才算通过(AND 语义),任一失败即短路返回 false。IfExists包装器在键完全缺失时直接返回 true(见 crates/policy/src/policy/function/condition.rs),序列化时则以运算符名+IfExists形式输出(如StringEqualsIfExists),且支持嵌套(StringEqualsIfExistsIfExists)并保证 round-trip 无损。
六、Principal:Bucket Policy 的主体限定
Principal(见 crates/policy/src/policy/principal.rs)支持 AWS 与 Service 两类主体:
AWS:账号或 ARN 主体,通配符*表示任意主体;Service:服务主体,如logging.s3.amazonaws.com(日志投递服务)。
反序列化兼容三种输入形态:裸字符串"*"、{"AWS": "..."}对象、{"AWS": [...]}数组;空对象或非法字符串会被拒绝。序列化时遵循 AWS API 格式——AWS集合仅含单个*时输出为字符串,多个元素时输出为数组。is_match对账号执行简单的通配符匹配(见 crates/policy/src/policy/principal.rs)。
一个完整的 Bucket Policy 示例(允许指定账号对桶内对象执行读写,且拒绝来自特定网段的访问):
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam::123456789012:root"]}, "Action": ["s3:GetObject", "s3:PutObject"], "Resource": ["arn:aws:s3:::mybucket/*"] }, { "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": ["arn:aws:s3:::mybucket/*"], "Condition": { "NotIpAddress": { "aws:SourceIp": ["10.0.0.0/8"] } } } ] }七、内置默认策略与 KMS 角色模板
Policy::default模块(见 crates/policy/src/policy/policy.rs)通过DEFAULT_POLICIES静态表内置了 8 个开箱即用的策略,全部附带sts:AssumeRole允许语句以支持 STS 会话扮演:
| 策略名 | 授予内容 |
|---|---|
readwrite | s3:*全部 S3 操作(S3Action::AllActions)+ AssumeRole |
readonly | s3:GetBucketLocation、s3:GetObject、s3:GetBucketQuota+ AssumeRole |
writeonly | s3:PutObject+ AssumeRole |
diagnostics | 诊断类 admin 操作:admin:Profiling、admin:Trace、admin:ConsoleLog、admin:ServerInfo、admin:TopLocks、admin:HealthInfo、admin:Prometheus、admin:BandwidthMonitor+ AssumeRole |
consoleAdmin | admin:*、kms:*、s3:*+ AssumeRole(管理控制台全权) |
KMSKeyAdministrator | KMS 密钥生命周期管理:kms:DescribeKey、kms:ListKeys、kms:EnableKey、kms:DisableKey、kms:RotateKey、kms:DeleteKey+ AssumeRole |
KMSKeyUser | KMS 数据面使用:kms:GenerateDataKey、kms:Decrypt、kms:DescribeKey+ AssumeRole |
KMSAuditor | KMS 只读审计:kms:DescribeKey、kms:ListKeys+ AssumeRole |
三个 KMS 模板体现了职责分离(separation of duties)设计:密钥管理员能管理密钥生命周期但永远不能加密/解密;密钥使用者能加密解密但不能管理;审计者只能读取元数据。且它们故意不授予任何 S3 或 admin 权限,也不包含kms:Configure、kms:ServiceControl、kms:ClearCache、kms:Backup、kms:Restore等集群级管理操作(这些只保留给consoleAdmin)。源码注释(crates/policy/src/policy/policy.rs)明确说明了这一边界。
这些模板默认以*通配所有密钥,运维可按工作负载将其收窄为arn:aws:kms:::key/<key_id>形式。crates/policy/src/policy/policy.rs 的测试验证了收窄后的策略确实拒绝其他密钥(reports-2026放行、payroll-2026拒绝),而 crates/policy/src/policy/policy.rs 的系列测试则分别验证了管理员不可加密、使用者不可管理、审计者只读、模板不授予 KMS 之外任何权限等约束。
八、校验、合并与解析:策略生命周期管理
8.1 解析入口
策略 JSON 通过Policy::parse_config(data: &[u8])解析(见 crates/policy/src/policy/policy.rs):先用 serde 反序列化,再调用validate()全量校验,两步缺一不可。Validatortrait 为所有策略构件提供统一的is_valid入口。
8.2 校验规则汇总
Policy::is_valid与Statement::is_valid(见 crates/policy/src/policy/statement.rs)共同执行以下检查:
Version必须为空或2012-10-17;- 每条语句的
Effect合法; Action与NotAction二选一且至少一个非空;- 同一语句 Action 家族不可混合(S3/Admin/STS/KMS 四选一);
Resource与NotResource二选一且至少一个非空(Admin/STS/KMS 语句例外,允许资源为空);- KMS 资源只能出现在纯 KMS 语句中(
KmsResourceWithNonKmsAction); - Bucket Policy 语句禁止出现 KMS Action 或 KMS Resource(
KmsUnsupportedInBucketPolicy); - 各集合内部的资源模式逐个合法性检查。
8.3 去重与合并
merge_policies将多个策略合并为一个(见 crates/policy/src/policy/policy.rs),合并后调用drop_duplicate_statements删除完全重复的语句。值得注意的是Statement的PartialEq实现(见 crates/policy/src/policy/statement.rs)逐一比较所有影响匹配的字段(Effect、Action、NotAction、Resource、NotResource、Condition),源码注释明确警示:任何新增的影响匹配的字段都必须加入比较,否则语义不同的语句可能被静默删除,从而缩小 Deny 覆盖范围造成安全隐患。
九、动态求值:Args 与变量解析
策略求值不是纯静态的,它依赖每次请求的动态上下文。Args结构(见 crates/policy/src/policy/policy.rs)封装了完整求值上下文:
account:请求账号/主体;groups:所属组;action:本次请求的 S3/Admin/STS/KMS 操作;bucket、object:请求目标;conditions:本次请求的条件键值映射(来源于请求头、来源 IP、认证声明等);is_owner:是否资源所有者;claims:认证令牌携带的声明(JWT claims),含roleArn与附加策略声明;deny_only:是否仅评估 Deny 语句。
VariableResolver(见 crates/policy/src/policy/statement.rs 与variables.rs)负责将策略中的${aws:...}、${jwt:...}等变量替换为实际值:账号 ID、用户名(优先取 claims 中的parent声明)、条件键值等。资源模式与条件值在求值前都会经过变量解析,从而实现"一条策略模板 + 动态上下文 = 精确匹配"的能力。
十、与既有标签条件的集成:ExistingObjectTag 检测
policy.rs中实现了一组与对象标签(tagging)强相关的辅助判定(见 crates/policy/src/policy/policy.rs):
is_existing_object_tag_condition_key:识别ExistingObjectTag、s3:ExistingObjectTag及其/后缀形式;policy_uses_existing_object_tag_conditions/bucket_policy_uses_existing_object_tag_conditions:检测整个策略是否引用标签条件;policy_needs_existing_object_tag_for_args/bucket_policy_needs_existing_object_tag_for_args:进一步判断到达条件求值阶段的具体语句是否可能求值标签条件,供数据面在读取对象标签前判断是否需要额外加载标签元数据,避免无谓的 IO。
这些函数在 crates/policy/src/policy/policy.rs 的解析测试中得到验证——示例策略使用s3:ExistingObjectTag/security = "public"控制GetObject与DeleteObjectTagging,并用ForAllValues:StringLike+s3:RequestObjectTagKeys限制上传对象允许携带的标签键。
十一、测试矩阵与可靠性保障
该 crate 的测试覆盖相当完善,是理解其行为边界的绝佳入口:
| 测试文件 | 验证重点 |
|---|---|
| crates/policy/tests/policy_is_allowed.rs | 基于test_case的矩阵化is_allowed判定测试,覆盖 Allow/Deny、条件匹配、所有者放行等组合 |
| crates/policy/tests/bucket_iam_authz_matrix.rs | Bucket Policy 与 IAM 交叉授权矩阵 |
| crates/policy/tests/quantified_negation.rs | 量化器与否定运算符的语义测试 |
| crates/policy/tests/policy_eval_proptest.rs | 基于proptest的属性测试,随机生成策略与请求上下文验证求值不变量 |
此外 crates/policy/src/policy/resource.rs 内置了 20 余条资源匹配test_case(含路径穿越防护、KMS 密钥通配、别名永不匹配密钥等边界),crates/policy/src/policy/function.rs 则验证了条件运算符的序列化 round-trip 与重复键拒绝。
十二、工程集成要点
从 crates/policy/Cargo.toml 可以看到该 crate 的依赖与特性设计:
- hotpath 特性族(
hotpath、hotpath-alloc、hotpath-cpu):与rustfs-config、rustfs-credentials、rustfs-crypto同步启用,用于热路径(请求处理主链路)的性能追踪/分配观测/CPU 属性归因,说明策略求值发生在每请求的数据面上,其性能至关重要; - 依赖
rustfs-credentials(获取 IAM 策略声明名)与rustfs-config(含opa特性),并与jsonwebtoken、jiff、time、ipnetwork、moka(缓存)、reqwest等配合,支撑 JWT 声明解析、日期时间比较、CIDR 匹配与策略缓存。
策略求值全程为异步(async fn is_allowed),与 Tokio 运行时无缝集成;moka缓存暗示运行时存在策略对象缓存以降低高频请求的解析开销。
十三、上手验证:如何在本地跑通策略语义
该 crate 作为 RustFS workspace 的独立成员,可以直接在仓库根目录运行其测试来验证上述所有行为:
# 运行策略核心语义测试(含矩阵化判定、属性测试、KMS 角色模板约束) cargo test -p rustfs-policy # 仅运行资源匹配与条件运算符的单元测试 cargo test -p rustfs-policy --lib # 运行全部集成测试(bucket_iam_authz_matrix 等) cargo test -p rustfs-policy --test bucket_iam_authz_matrix当前仓库工作区根目录为rustfs/,上述命令需在包含 Cargo.toml 的仓库根目录执行。测试通过即代表策略解析、校验、判定与序列化行为符合源码注释所声明的契约。
结语
RustFS Policy 引擎并非简单照搬 AWS 策略语法,而是在四个层面做了工程化落地:严格的双模型校验(身份策略 vs Bucket Policy)、显式拒绝优先的判定管线、带量化器与 IfExists 修饰的完整条件函数族,以及服务端派生键防注入的安全设计。配合内置的 8 个默认策略与职责分离的 KMS 角色模板,开发者可以在完全兼容 AWS S3 策略语义的前提下,直接复用这套引擎构建细粒度的对象存储访问控制体系。若需深入特定运算符的边界行为,crates/policy/src/policy/function/下的每个实现文件与对应测试用例都是最可靠的参考。
【免费下载链接】rustfs🚀2.3x faster than MinIO for 4KB object payloads. RustFS is an open-source, S3-compatible high-performance object storage system supporting migration and coexistence with other S3-compatible platforms such as MinIO and Ceph.项目地址: https://gitcode.com/GitHub_Trending/rus/rustfs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考