前言
这篇文章是阅读 Arch Linux 团队部署、管理、整合身份与访问管理( Identity and Access Management)服务相关代码的笔记,会包含:
- 如何部署和管理 Keycloak 服务:Ansible 相关
- 如何管理 Keycloak 上的用户和权限:Terraform 相关
- 如何将 Web 站点接入通过 Keycloak:OAuth2.0、OIDC、Python、Authlib 相关
- 如何基于 Keycloak 中的信息,实现权限管理:装饰器和枚举类相关
IAM 服务
有什么用
先说说没有 IAM 服务会有什么问题。
一般团队内会有多种服务,比如:GitLab、Jira、Confluence、MatterMost、Grafana、Kibana、Argo 等,会产生的问题:
- 每个团队成员入职/离职时,需在每个服务上添加/删除账号
- 每个团队成员使用服务时,需在每个服务上输入一遍账号密码
- 每个团队成员需要管理多个服务的密码,如果密码泄漏且相同时,每个服务需要手动修改一遍
IAM 服务的作用:
- 统一的权限管理。授权服务器可以管理用户对各个应用的访问权限,例如,哪些用户可以访问哪些应用,用户在各个应用中的角色和权限是什么,另外用户账号的增删也只需要一遍。
- 单点登录。用户只需要在授权服务器上进行一次登录,就可以访问所有连接到该授权服务器的应用。提高用户体验,减少了用户需要记住多个凭证的负担。
- 降低业务复杂度。授权码流程可以将认证功能分离出业务代码,业务代码只需根据 OAuth2.0、OIDC 获取到的用户信息,更新和管理用户权限相关的用户表,而无需再每个业务内实现2FA、验证码等验证逻辑。
- 微服务改造的基础。微服务旨在将业务拆分成独立小服务(独立的数据库)从而降低复杂度、提高容错率和扩展性。IAM 在微服务架构中为各个微服务提供了集中、统一的身份验证和权限管理,从而实现安全性,减少重复验证的开销,并提高系统的可维护性和扩展性。
Keycloak 是什么
Keycloak 是 RedHat 开源的身份认证和授权服务实现。通过部署 Keycloak,我们就可以拥有自己的 IAM 服务,Arch Linux 团队的 IAM 服务就是基于 Keycloak。
Keycloak 的部署
Arch Linux 团队的 Keycloak 通过 Ansible 进行部署和管理,相关的 Playbook 和 Role 在这里:
关于 Playbook 和 Role: Arch Linux 团队使用 Ansible 和 Terraform 的目的是为了实现运维代码化而不是自动化运维工具。
运维代码化是:将运维操作像业务代码一样放到 Git 内,基于传统的开发流程实现运维的管理,开发者以管理代码的方式管理服务器(运维即代码),包括了解服务器上过往所有改动的细节。
回到 Ansible。Ansible Role(是一个有固定目录结构的文件夹)是对应一类中间件或操作的抽象,比如:
- common role: 用于服务器的初始化
- firewalld role: 用于配置防火墙规则
- sshd role 和 root_ssh role: 用于实现 sshd 配置管理和用户公钥证书管理
- prometheus-exporters role: 用于安装 node-exporter 实现服务器基本的监控数据暴露
- nginx role: 用于实现 nginx 的部署和配置管理
- certbot role: 用于实现 letsencrypt 证书自动申请和刷新
- postgres role: 用于实现 PostgreSQL 的部署和管理
- keycloak role: 用于实现 Keycloak 的部署和管理
- borg_client role: 用于实现备份系统的接入,实现服务器的每天自动备份
- fail2ban role: 用于实现机器人暴力攻击防御,比如 SSH 暴力登录,如果一个用户 15 分钟内登录失败 5 次,就把该用户的公网 IP 加入到黑名单,关小黑屋一天
- promtail role: 用于实现日志系统的接入,把服务器上 journald、pacman、nginx 等日志采集到 loki
Ansible Playbook(是一个文件)对应一个服务。如果 Role 是某一个操作的抽象,Playbook 就是按需选择要使用哪些 Role 然后像叠乐高一样把这些 Role 拼起来。
通过在 Playbook 内使用相同的 Role,可以实现运维代码的复用,提高运维操作的一致性,比如所有的服务器都有相同的 common role,这样初始化出来的服务器就是相同的了。
Keycloak 的部署操作如下(实际就是对应的 role 的功能):
- 初始化服务器
- 初始化 SSH 服务并添加允许访问服务器的开发者公钥
- 配置防火墙
- 配置主机监控
- 部署 Nginx、PostgreSQL、Keycloak 并配置 HTTPS 证书
- 配置备份、流控、日志收集
正因为 Arch Linux 团队实现了非常成熟的运维代码化,我才有机会了解他们实现 IAM 服务的细节(具体的细节不展开,见仓库中的代码)
Keycloak 的管理
Arch Linux IAM 服务的地址是: https://accounts.archlinux.org/
整个 Keycloak 中的信息通过 terraform 管理,比如:
- 用户所需的组(类似于 Linux 的用户组)
- 不同应用的 OIDC 配置(gitlab、grafana etc.)
- 认证流程(用户密码登录、2FA认证、WebAuth 认证等)
地址如下:keycloak.tf
terraform 管理分组信息的例子
terraform 定义分组信息的代码,见:terraform 代码
terraform 管理 OIDC 配置的例子
client_secret
这个字段(917行)实际传入的是一个引用,而不是具体的密钥。这里是值得借鉴的地方,通过采用 hashicorp/external provider 将私密信息存储到状态库,解决 terraform 代码化运维中私密信息如何存储问题。
权限管理的实现
在 security.archlinux.org 这个站点的 terraform OIDC 配置中
通过 Terraform 代码:
resource "keycloak_openid_group_membership_protocol_mapper" "group_membership_mapper" {
realm_id = "archlinux"
client_id = keycloak_openid_client.security_tracker_openid_client.id
name = "group-membership-mapper"
claim_name = "groups"
}
定义了一个 claim
,作用是:当用户通过 security.archlinux.org 向 Keycloak 发起授权码认证时,让 Keycloak 把用户归属的组的信息,以键名为 groups
的键值对方式提供给 security.archlinux.org 服务端。这样,服务端即可通过 Keycloak 的分组信息管理站点内用户的权限。
从 Keycloak 获取的 groups
字段的值是一个数组,比如 Arch Linux 安全团队的管理员的 groups
字段长这样:
['/Arch Linux Staff/Security Team/Members', '/Arch Linux Staff/Security Team/Admins']
表示这个成员首先是安全团队的 Members(成员),也是 Admins(管理员),这里跟分组信息的 Terraform 代码相对应。
下面是 security.archlinux.org 这个站点基于 authlib
实现授权码流程的 Python 代码:完整代码在这里
def sso_auth():
# 为了方便阅读做了删减
token = oauth.idp.authorize_access_token()
userinfo = token.get('userinfo')
idp_user_sub = userinfo.get('sub')
idp_groups = userinfo.get('groups')
user_role = get_user_role_from_idp_groups(idp_groups)
user = db.get(User, idp_id=idp_user_sub)
if user:
user.role = user_role
else:
salt = random_string()
user = db.create(User,
name=idp_username,
salt=salt,
password=hash_password(random_string(TRACKER_PASSWORD_LENGTH_MAX), salt),
role=user_role,
active=True,
idp_id=idp_user_sub)
db.session.add(user)
db.session.commit()
user = user_assign_new_token(user)
user.is_authenticated = True
login_user(user)
上面的代码实现了从 Keycloak 获取用户信息实现登录的操作,通过获取 sub
(即用户 ID) 查数据库,判断是否有这个用户(没有就创建),并通过groups
来判断用户的权限。
如何基于 groups
的信息实现授权管理呢?
见 get_user_role_from_idp_groups
的实现:
# idp_groups = userinfo.get('groups')
# user_role = get_user_role_from_idp_groups(idp_groups)
def get_user_role_from_idp_groups(idp_groups):
group_names_for_roles = {
SSO_ADMINISTRATOR_GROUP: UserRole.administrator,
SSO_SECURITY_TEAM_GROUP: UserRole.security_team,
SSO_REPORTER_GROUP: UserRole.reporter
}
eligible_roles = [group_names_for_roles[group] for group in idp_groups if group in group_names_for_roles]
if eligible_roles:
return sorted(eligible_roles, reverse=False)[0]
return None
上面的代码中定义了一个 名为:group_names_for_roles
的 python dict
,目的是:将从 Keycloak 获取到的分组信息的字符串,同业务代码中用于表示权限的枚举类实例做映射·。
SSO_ADMINISTRATOR_GROUP
,SSO_SECURITY_TEAM_GROUP
,SSO_REPORTER_GROUP
是项目配置文件配置项的值,跟下图红框内容相对应,见:配置文件
UserRole.administrator
、UserRole.security_team
、UserRole.reporter
是枚举类 UserRole
的枚举类实例,见:代码
通过将 User 表的 role 字段设为 UserRole
枚举类类型(见下图),实现用户权限从 Keycloak groups
键值对到数据库字段值的映射。
eligible_roles = [group_names_for_roles[group] for group in idp_groups if group in group_names_for_roles]
这段代码是 Python 的列表推导,通过遍历值为:['/Arch Linux Staff/Security Team/Members', '/Arch Linux Staff/Security Team/Admins']
的 idp_groups
数组(也就是从 groups
的值),如果该数组成员的值是 group_names_for_roles
这个字典的键,则将该键对应的枚举类实例添加到 eligible_roles
数组内。
下图中没有直接使用 SQLAlchemy 的 Enum 类型,是因为:UserRole(上图)的枚举类实例的值不是基础数据类型而是元组 'Administrator', 1
,故做了类型增强,比如:通过继承python Enum
类型,实现基于枚举值(元组)中第二个成员(整数值类型)为排序条件的、枚举类的子类 OrderedDatabaseEnum
class OrderedDatabaseEnum(DatabaseEnum):
def __init__(self, label, order):
super().__init__(db_repr=self.name, label=label)
self.order = order
def __lt__(self, other):
return self.order < other.order
这样,当 groups
为 ['/Arch Linux Staff/Security Team/Members', '/Arch Linux Staff/Security Team/Admins']
的团队成员,通过 get_user_role_from_idp_groups
函数映射出来的枚举类实例为:[UserRole.security_team, UserRole.administrator]
,因为 UserRole
的父类 OrderedDatabaseEnum
为元组类型的枚举类实例值实现了 __lt__
方法,所以枚举类实例所在的数组 [UserRole.security_team, UserRole.administrator]
就可以被排序。
sorted(eligible_roles, reverse=False)[0]
实际上就是对数组 eligible_roles
执行排序,让 [UserRole.security_team, UserRole.administrator]
变成 [UserRole.administrator, UserRole.security_team]
并只保留第一个数组成员作为代表用户权限的角色。一个团队成员,如果既有普通权限又有高级权限,那么我们就认为他是管理员角色。
在后面基于枚举类实例的权限判断逻辑中:
可以看到,当我们从 groups
获得代表权限的枚举类实例后,UserRole.administrator
出现在了 is_reporter
is_security_team
is_administrator
三个判断逻辑内,即管理员会有所有普通的权限。
业务代码中是如何校验用户权限的呢?这块是通过装饰器 permission_required
实现的:
上面的代码中:permission_required
是一个返回装饰器的装饰器。通过传入一个具体的枚举类属性方法(即下图中的 is_guest
/is_reporter
/is_security_team
/is_administrator
)返回一个可以装饰视图函数的装饰器:
故整个业务的认证流程是:
- 运维代码中:
- 通过 Ansible 部署 Keycloak
- 通过 Terraform 控制 Keycloak 管理用户,存储用户分组信息、并提供 OIDC 接入的能力
- 基于 OAuth2.0 和 OIDC 的授权码流程,通过 Terraform 配置 Keycloak,在 ID token 中添加名为
groups
的用户分组信息键值对,让业务代码获取到分组信息
- 业务代码中:
- 将
groups
键值对中的字符串信息转换成表示权限的枚举类实例 - 通过用户 ID 判断用户表内是否已经有该用户,没有就创建用户
- 根据
groups
信息更新用户的权限。即为当前用户的 role 属性指定一个表示当前用户权限的枚举类实例 - 最后通过 flask_login 的更新当前用户的 session id、并标记用户为经过认证的状态
- 将
总结
这篇文章是关于 Arch Linux 团队从基于 Ansible 自建 IAM 服务:Keycloak;到基于 terraform 管理 IAM 服务中的用户分组信息;再到基于 Flask、SQLAlchemy 实现认证和授权过程的记录。
如果你正在实现业务的 OAuth2.0、OIDC 整合,并希望项目有良好的 DevOps 实践,那这篇文章涉及到的代码:
- 代码化运维仓库:https://gitlab.archlinux.org/archlinux/infrastructure
- 业务代码仓库:https://github.com/archlinux/arch-security-tracker
是个不错的参考来源。另外,Arch Linux 的业务代码有相对完整的单元测试,这也是非常好的实践参考。