IAM身份提供商安全配置最佳实践
本最佳实践仅针对IAM旧版控制台中基于SAML协议的虚拟用户SSO和基于OIDC协议的虚拟用户SSO这两种配置进行指导说明,不包含基于SAML协议的IAM用户SSO,也不包含IAM新版控制台中的基于SAML协议的信任委托SSO和基于OIDC协议的信任委托SSO这两种配置。
联邦登录的两个环节
IAM作为服务提供方,负责授权环节的配置管理,如果使用者在授权环节配置不当,则可能导致权限范围失控。
- 身份准入:哪些IdP用户可以登录华为云。
- 权限授予:登录华为云的IdP用户拥有哪些操作权限。
| SAML协议要素 | OIDC协议要素 |
|---|---|
| 断言(Assertion) | OIDC令牌(ID Token) |
| 断言的验签 | OIDC令牌的验签 |
| 断言Subject元素中的NameID(__NAMEID__) | OIDC令牌中的sub声明 |
| 断言AttributeStatement元素中的属性(如Groups、FirstName、LastName) | OIDC令牌中的声明(如Groups、FirstName、LastName) |
| 属性中携带多个AttributeValue(多值属性) | 数组形式的多值声明 |
即下文出现的“SAML断言”均可替换为“OIDC令牌”理解,“断言中的属性”均可替换为“OIDC令牌中的声明”理解,不再单独给出OIDC协议的配置案例。
IAM身份提供商安全配置的常见问题与最佳实践
- 常见问题一:身份准入配置过宽,导致任意IdP用户均可登录华为云 假设您已经按照身份提供商概述配置好了基于SAML协议或OIDC协议的虚拟用户身份提供商,并在IAM中创建了ecs_readonly用户组,给该用户组授予了ECS ReadOnlyAccess的系统策略,其中IAM身份提供商的身份转换规则如下:
[ { "remote": [ { "type": "__NAMEID__" } ], "local": [ { "user": { "name": "FederationUser" } } ] }, { "remote": [ { "any_one_of": [ "idp_ecs_readonly" ], "type": "Groups" } ], "local": [ { "group": { "name": "ecs_readonly" } } ] } ]上述身份转换规则一共有两条规则,第一条为身份准入规则,第二条为权限授予规则,表达的含义为:- 在SAML断言验签通过后,只要SAML断言的Subject元素中存在NameID子元素,那么就可以将IdP用户映射为一个华为云虚拟用户,这个虚拟用户名称为FederationUser。
- 如果SAML断言的AttributeStatement元素中存在Groups属性,Groups属性中只要有任意一个属性值为idp_ecs_readonly,那么这个名称为FederationUser的虚拟用户就拥有IAM中ecs_readonly用户组的权限。
这个身份转换规则有以下两个问题:- 在remote侧的映射中使用了“__NAMEID__”这个通用type。
- 将第一条身份准入规则单独抽取出来但是没有使用条件运算符。
一般情况下,SAML断言的Subject元素中都会携带NameID子元素,而该规则未配置条件运算符进行校验,导致IdP中的所有用户均可通过第一条规则登录华为云。即便用户不属于IdP侧的idp_ecs_readonly用户组,也仅会影响其登录华为云后是否被授予相应权限,并不能阻止其完成登录。
- 最佳实践一:合理配置身份准入规则,只允许目标IdP用户登录华为云 正确的做法可以将身份准入规则和权限授予规则合并在一起,然后利用条件运算符限制身份准入:
[ { "remote": [ { "type": "FirstName" }, { "type": "LastName" }, { "any_one_of": [ "idp_ecs_readonly" ], "type": "Groups" } ], "local": [ { "user": { "name": "{0} {1}" } }, { "group": { "name": "ecs_readonly" } } ] } ]在上述身份转换规则中,并没有对remote中的FirstName、LastName这两个type添加条件运算符,因为需要将它们的值传递到local的user.name的占位符中,但是因为将身份准入规则和权限授予规则合并在一起了,整个身份转换规则都会受到any_one_of这个条件运算符控制,表达式的含义为:
在SAML断言验签通过后,当断言中存在Groups属性且Groups属性中有一个属性值为idp_ecs_readonly时,如果断言中的FirstName与LastName属性也存在,那么就可以将IdP用户映射为一个华为云虚拟用户,这个虚拟用户名称为${FirstName} ${LastName},且该虚拟用户拥有IAM中ecs_readonly用户组的权限。
这样便可以做到只允许IdP侧idp_ecs_readonly用户组的用户登录华为云,而其他不相关的IdP合法用户无法登录华为云,即使他们也有FirstName和LastName属性。
针对最佳实践一中的身份转换规则,如果any_one_of配置空数组,例如:"any_one_of": [],则表示拒绝所有登录,不论IdP用户在IdP侧是否加入了用户组。
- 常见问题二:未进行最小化权限授予,导致任意登录用户获取较大权限
[ { "remote": [ { "type": "FirstName" }, { "type": "LastName" } ], "local": [ { "user": { "name": "{0} {1}" } }, { "group": { "name": "admin" } } ] } ]上述身份转换规则,在问题一的基础上又引入了权限授予过大的问题,表达的含义为:
在SAML断言验签通过后,如果断言中的FirstName与LastName属性存在,那么就可以将IdP用户映射为一个华为云虚拟用户,这个虚拟用户名称为${FirstName} ${LastName},且该虚拟用户拥有IAM中admin用户组的权限。
一般来说,IdP中的所有用户都拥有FirstName与LastName这种表示姓名的基础属性,因此上述规则等价于IdP中的所有用户都拥有操作华为云资源的所有权限。
- 最佳实践二:按照工作职能分组,进行最小化权限授予 正确的做法是对权限授予规则进行拆分,将IdP中的不同用户按照工作职能分配IdP的不同用户组中,然后将IdP用户组映射到IAM对应的用户组中,最后根据不同的IAM用户组进行最小化权限授权。假设您已经在IAM中创建了所需的ecs_readonly、ecs_admin用户组,并分别授予了ECS ReadOnlyAccess、ECS FullAccess的系统策略,那么一个最小化权限授予的身份转换规则如下:
[ { "remote": [ { "type": "FirstName" }, { "type": "LastName" }, { "any_one_of": [ "idp_ecs_readonly" ], "type": "Groups" } ], "local": [ { "user": { "name": "{0} {1}" } }, { "group": { "name": "ecs_readonly" } } ] }, { "remote": [ { "type": "FirstName" }, { "type": "LastName" }, { "any_one_of": [ "idp_ecs_admin" ], "type": "Groups" } ], "local": [ { "user": { "name": "{0} {1}" } }, { "group": { "name": "ecs_admin" } } ] } ]在上述身份转换规则中,根据工作职能拆分为两条规则,将允许ECS只读用户分配到idp_ecs_readonly用户组中,将ECS管理员用户分配到idp_ecs_admin用户组中,表达的含义为:- 在SAML断言验签通过后,当断言中存在Groups属性且Groups属性中有一个属性值为idp_ecs_readonly时,如果断言中的FirstName与LastName属性也存在,那么就可以将IdP用户映射为一个华为云虚拟用户,这个虚拟用户名称为${FirstName} ${LastName},且该虚拟用户拥有IAM中ecs_readonly用户组的权限。
- 在SAML断言验签通过后,当断言中存在Groups属性且Groups属性中有一个属性值为idp_ecs_admin时,如果断言中的FirstName与LastName属性也存在,那么就可以将IdP用户映射为一个华为云虚拟用户,这个虚拟用户名称为${FirstName} ${LastName},且该虚拟用户拥有IAM中ecs_admin用户组的权限。
这样便做到了最小化权限授予,IdP中不同用户组的用户联邦登录后分别拥有不同的权限,需要有新用户登录华为云时,只需要将其加入IdP中的相应用户组即可。
- 常见问题三:将IdP用户组直接映射到IAM用户组时,未配置条件运算符导致身份准入失控 在使用身份转换规则将IdP的用户组直接映射到IAM的用户组时,如果remote侧对Groups这个type仅做存在性检查而未使用条件运算符,则身份准入效果取决于IdP对Groups属性的处理行为:
[ { "remote": [ { "type": "FirstName" }, { "type": "LastName" }, { "type": "Groups" } ], "local": [ { "user": { "name": "{0} {1}" } }, { "groups": "{2}" } ] } ]上述身份转换规则表达的含义为:
在SAML断言验签通过后,如果断言中的FirstName、LastName、Groups属性都存在,那么就可以将IdP用户映射为一个华为云虚拟用户,这个虚拟用户名称为${FirstName} ${LastName},并且将IdP侧的Groups属性值直接作为IAM侧的用户组名进行匹配,实际该虚拟用户是否拥有权限取决于IdP侧的用户组是否在IAM中真实存在。
注意,这个时候如果IdP用户未在IdP侧加入任何用户组,那么其身份准入效果取决于IdP对Groups属性的处理行为:表3 IdP用户未加入任何用户组时IdP的处理行为 IdP用户未加入任何用户组时IdP的处理行为
IAM识别到的Groups属性状态
身份准入结果
IdP发送的断言不存在Groups属性
属性不存在
登录失败
IdP发送的断言存在Groups属性但值为空
属性存在,值为空字符串
登录成功
也就是说,如果IdP在用户未加入任何用户组时仍发送空值的Groups属性,则该规则等价于所有拥有FirstName和LastName的IdP用户均可登录华为云,虽然登录后没有任何用户组权限,但仍然存在身份准入配置过宽的问题。
- 最佳实践三:将IdP用户组直接映射到IAM用户组时,应配合条件运算符确保身份准入可控 如果需要将IdP的用户组名直接映射到IAM的同名用户组,正确的做法是使用条件运算符进行身份准入控制:
[ { "remote": [ { "type": "FirstName" }, { "type": "LastName" }, { "type": "Groups" }, { "any_one_of": [ "idp_ecs_readonly", "idp_ecs_admin" ], "type": "Groups" } ], "local": [ { "user": { "name": "{0} {1}" } }, { "groups": "{2}" } ] } ]在上述身份转换规则中,在remote中有两个Groups的type,这是因为拥有any_one_of的Groups的type只用于做身份准入,确保了只有在断言的Groups属性中包含idp_ecs_readonly或idp_ecs_admin的用户才能登录华为云,它不能提取{2}的值。所以,想要在local的groups中仍然使用{2}直接从断言中提取IdP的用户组名,就必须在remote中额外携带一个不带条件运算符的Groups的type。整体表达的含义为:
在SAML断言验签通过后,当断言的Groups属性中存在idp_ecs_readonly或idp_ecs_admin时,如果断言中的FirstName与LastName属性也存在,那么就可以将IdP用户映射为一个华为云虚拟用户,这个虚拟用户名称为${FirstName} ${LastName},并且将IdP侧的Groups属性值直接作为IAM侧的用户组名进行匹配,实际该虚拟用户是否拥有权限仍然取决于IdP侧的用户组是否在IAM中真实存在;
这样即使IdP在用户未加入任何用户组时发送了空值的Groups属性,any_one_of条件运算符也会校验失败从而阻止该用户登录华为云。
IAM身份提供商使用的注意事项
- 注意事项一:针对断言中AttributeStatement元素的同名独立属性,IAM身份转换规则只会取最后一个属性的值 例如,对于最佳实践三中的身份转换规则,如果IdP断言中的AttributeStatement元素如下:
<saml:AttributeStatement> <saml:Attribute Name="Groups" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">idp_ecs_readonly</saml:AttributeValue> </saml:Attribute> <saml:Attribute Name="Groups" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">idp_other</saml:AttributeValue> </saml:Attribute> <saml:Attribute FriendlyName="First Name" Name="FirstName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">FirstName</saml:AttributeValue> </saml:Attribute> <saml:Attribute FriendlyName="Last Name" Name="LastName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">LastName</saml:AttributeValue> </saml:Attribute> </saml:AttributeStatement>那么,针对上述断言,IAM在local中取到的groups为["idp_other"],因此不满足any_one_of运算符中的值,所以会登录失败。想要传递IdP中多个用户组标准做法是使用多值属性,IdP断言设定Groups为多值属性的示例如下:<saml:AttributeStatement> <saml:Attribute Name="Groups" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">idp_ecs_readonly</saml:AttributeValue> <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">idp_other</saml:AttributeValue> </saml:Attribute> <saml:Attribute FriendlyName="First Name" Name="FirstName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">FirstName</saml:AttributeValue> </saml:Attribute> <saml:Attribute FriendlyName="Last Name" Name="LastName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">LastName</saml:AttributeValue> </saml:Attribute> </saml:AttributeStatement>针对上述断言,IAM在local中取到的groups为["idp_ecs_readonly", "idp_other"],因此满足any_one_of运算符中的值,所以登录成功。
上述注意事项源于SAML的XML结构允许在AttributeStatement元素中重复出现同名Attribute元素。在基于OIDC协议的虚拟用户SSO中,OIDC令牌为JSON格式,声明的名称唯一,不存在同名独立声明只取最后一个值的问题;如需传递多个用户组,在OIDC协议下直接使用数组形式的多值声明即可,例如"Groups": ["idp_ecs_readonly", "idp_other"]。