AWS云上部署KeyCloak的这些坑你踩过没有

背景

我司需要为前端应用和后端微服务提供统一的认证鉴权服务,需要支持Oauth2标准,并且提供RBAC(Role Based Access Control)支持,即基于角色的访问控制,同时还需要支持Multi-Cloud。经过调研选型,最终决定采用KeyCloak来实现上述需求。

关于KeyCloak

Keycloak 是一款开源的身份及访问控制软件,提供单点登录(SSO)功能,支持 OpenID Connect、OAuth 2.0、SAML 2.0 标准协议。Keycloak 提供可自定义的用户界面,用于登录,注册和帐户管理等。截至2018年3月,红帽公司负责管理这一JBoss社区项目,并将其作为他们RH-SSO产品的上游项目。从概念的角度上来说,该工具的目的是,只用少量编码甚至不用编码,就能很容易地使应用程序和服务更安全。

基础设施架构

image.png

上图是我们在AWS上部署KeyCloak时的采用的架构图。我们使用Amazon Fargate服务运行Amazon ECS集群。 Amazon Fargate 是一种针对容器的无服务器(Serverless)计算服务。我们无需管理服务器,并且可以隔离应用程序,为每个应用程序单独分配资源并提高安全性。为确保高系统可用性,Amazon ECS服务中定义了两个任务,位于不同的AZ(可用区),因此,如果一个任务无法提供服务,则另一任务将继续提供服务。外部请求通过ALB(Application Load Balancer)访问ECS。数据库类型为Amazon RDS for MySQL。

Amazon ECS支持Docker,可以部署和运行Keycloak的容器映像。我们使用的是JBOSS社区提供的镜像,版本为16.1.1。

基本配置

数据库配置

KeyCloak默认使用H2数据库,为了使用Amazon RDS for MySQL,我们参考镜像说明,在环境变量中添加了如下配置:

Generic variable names can be used to configure any Database type, defaults may vary depending on the Database.

  • DB_ADDR: Specify hostname of the database (optional). For postgres only, you can provide a list of hostnames separated by comma to failover alternative host. The hostname can be the host only or pair of host and port, as example host1,host2 or host1:5421,host2:5436 or host1,host2:5000. And keycloak will append DB_PORT (if specify) to the hosts without port, otherwise it will append the default port 5432, again to the address without port only.
  • DB_PORT: Specify port of the database (optional, default is DB vendor default port)
  • DB_DATABASE: Specify name of the database to use (optional, default is keycloak).
  • DB_USER: Specify user to use to authenticate to the database (optional, default is ``).
  • DB_PASSWORD: Specify user's password to use to authenticate to the database (optional, default is ``).

开放统计信息

为了向ALB和ECS报告容器是否运行正常,需要打开查看统计信息的Endpoint,按照镜像的说明文档,我们设置了 KEYCLOAK_STATISTICS=all

Keycloak image can collect some statistics for various subsystem which will then be available in the management console and the /metrics endpoint. You can enable it with the KEYCLOAK_STATISTICS environment variables which take a list of statistics to enable.

集群配置

按照上面架构图的设计,我们会同时运行多个KeyCloak实例,而这些实例需要互相识别,以组成一个集群,共享内存数据。参照镜像说明文档,我们设置了一下环境变量,其中JGROUPS_DISCOVERY_PROTOCOL 设为 dns.DNS_PING

Clustering

Replacing the default discovery protocols (PING for the UDP stack and MPING for the TCP one) can be achieved by defining some additional environment variables:

  • JGROUPS_DISCOVERY_PROTOCOL - name of the discovery protocol, e.g. dns.DNS_PING
  • JGROUPS_DISCOVERY_PROPERTIES - an optional parameter with the discovery protocol properties in the following format: PROP1=FOO,PROP2=BAR
  • JGROUPS_TRANSPORT_STACK - an optional name of the transport stack to use udp or tcp are possible values. Default: tcp

导入初试设定

有些初始设置,我们需要在容器启动时自动导入,免去在页面登录,手动配置的烦恼。参照文档,我们设置了KEYCLOAK_IMPORT

Importing a realm

To create an admin account and import a previously exported realm run:

docker run -e KEYCLOAK_USER=\<USERNAME> -e KEYCLOAK_PASSWORD=\<PASSWORD> \\
   -e KEYCLOAK_IMPORT=/tmp/example-realm.json -v /tmp/example-realm.json:/tmp/example-realm.json jboss/keycloak
复制代码

开始踩坑

一切都准备就绪了,我们将镜像发布到AWS上,满心期待能看到KeyCloak的登录页面,然后看到的却是一堆错误。没想到是个坑,更让我们没想到的是,这还是个连环坑

一号坑:数据库连接失败

一开始 ECS 的任务无法启动成功,看了日志后发现是数据库连接失败。奇怪的是,同样的镜像配置,我们在本地Docker环境可以正常运行,一样 AWS 就挂了。仔细分析了以下日志后发现,应该和 SSL 的设定相关。

Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
Caused by: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)
Caused by: java.sql.SQLException: javax.resource.ResourceException: IJ000453: Unable to get managed connection for java:jboss/datasources/KeycloakDS
Caused by: javax.resource.ResourceException: IJ000453: Unable to get managed connection for java:jboss/datasources/KeycloakDS
复制代码

解决办法

因为 ECS 和 RDS 处于同一个 VPC ,所以他们直接的连接并不需要使用 SSL 加密。我们在环境变量中增加了如下配置后,问题解决了。

- JDBC_PARAMS: useSSL=false
复制代码

二号坑:无法导入初始设定

ECS 任务虽然启动成功,但是初始并没有被导入。调查之后发现,需要打开脚本上传开关 upload_scripts=enabled。另外我们发现,如果使用KEYCLOAK_IMPORT,只会在第一次启动时导入设定,如果数据库中已经存在该Realm,则不会执行导入。而我们希望的是,在每次设定文件修改之后,都能导入到 AWS 环境中。于是,在参考 KeyCloak官方文档 之后,我们删除了 KEYCLOAK_IMPORT 设定,而改用如下 migraion 设定。

解决办法

- JAVA_OPTS_APPEND=-Dkeycloak.profile.feature.upload_scripts=enabled \
-Dkeycloak.migration.action=import \
-Dkeycloak.migration.provider=singleFile \
-Dkeycloak.migration.realmName=my-realm \
-Dkeycloak.migration.usersExportStrategy=REALM_FILE \
-Dkeycloak.migration.file=/tmp/my-realm.json
复制代码

三号坑:Health Check失败

我们发现 ECS 任务在启动成功后不久,就会被 ALB 强制关闭,原因是 ALB 对 ECS 任务的 Health Check 失败了,ALB 认为 ECS 任务没有在正常运行,所以强制将其关闭,并重新启动新的 ECS 任务。

解决办法

虽然我们设置了 KEYCLOAK_STATISTICS=all ,但是还有一个隐藏选项需要打开。首先 ECS 的任务配置中,需要将 9990 端口打开。然后要在 JVM 系统参数中加入如下配置:

- JAVA_OPTS_APPEND=
... 
-Djboss.bind.address=0.0.0.0 
...
复制代码

四号坑:登录后无限重定向

现在终于可以看到 KeyCloak 后台登录界面了,不过不要大意,后面还有坑。在输入用户名和密码之后,浏览器的页面出现了多次重定向,然后就停留在一个雪白的页面上。

WTF ?!

image.png

调查了很久之后,终于在官方文档一个不起眼的角落发现了一丝蛛丝马迹。文档中很实诚的描述了这个问题会导致 an infinite number of redirects (无限重定向)。

If you try to authenticate with Keycloak to your application, but authentication fails with an infinite number of redirects in your browser and you see the errors like this in the Keycloak server log:

2017-11-27 14:50:31,587 WARN  [org.keycloak.events] (default task-17) type=LOGIN_ERROR, 
realmId=master, clientId=null, userId=null, ipAddress=aa.bb.cc.dd, error=expired_code, 
restart_after_timeout=true
复制代码

it probably means that your load balancer needs to be set to support sticky sessions.

解决办法

修改 AWS ALB Targe Group 的设置, 将 Stickness 开关打开。

image.png

五号坑:无法开启集群模式

好了,现在可以登录到 KeyCloak 管理页面了,看似一切配置正常了,于是打开前端页面,未登录的用户被拦截重定向到 KeyCloak 用户登录页面,登录成功后返回前端页面。此时前端页面拿到了 Access Token, 就当我以为大功告成的时候,意外还是发生了,当拿着这个 Token 去访问后端 API 的时候,有些访问成功了,有些访问失败了。失败的原因是 401 Unauthorized。更奇怪的是,同一个 API,同样的参数,这次访问失败了,下次访问又成功了!!!

image.png

这个问题在本地 Docker 环境同样无法复现,在谷歌找了一圈也没有发现任何头绪,最后只能靠自己推理了。由于这个问题是随机出现的,而且本地环境没有问题,初步推测是由于多节点部署造成的。比如KeyCloak有 AB 两个节点,其中 B 节点是问题节点,由于请求是 ALB 随机分配的,那么只有当请求被分发到 B 节点时才会出错。

为了验证这个推测,我关闭了一个 ECS 的任务(即 KeyCloak 实例),此时运行中的任务只有一个,这个时候的请求要么全部成功,要么全部失败,不应该出现随机失败的情况。于是从前端验证了一下,果然不出我所料!单一节点运行时所有请求都成功了!

等一等!我们的 ALB 不是打开了 Sticky Session 吗? 同一个客户端的请求应该会被分发到同一个实例来处理呀。为什么会被随机分配AB 节点呢?其实这是因为对于后端 API 的请求,经过 ALB 之后会被分配到 Gateway 实例上,Gateway 再请求 KeyCloak 对 Token 进行验证。如果我们只启动一个 Gateway 实例,问题也同样不会再出现。

解决办法

根本原因还是 KeyCloak 的多个节点之间没有组网成功,没有以集群模式作为一个整体运行。可是我们明明配置了JGROUPS_DISCOVERY_PROTOCOL=dns.DNS_PING,应该可以通过 DNS 发现其他节点才对啊。正当我疑惑不解时,这篇文章中的一句话提醒了我。

TCPPING use TCP protocol with 7600 port. This can be used when multicast is not available, e.g. deployments cross DC, containers cross host. image.png

我们采用的 DNS_PING 同样是基于 TCP 的,是不是也会需要使用 7600 端口呢?于是我们在 ECS 任务中打开了 7600 端口,同时在 AWS Security Group 中也允许了 7600 端口的 inbound TCP 请求。

于是我们在 A 节点看到如下日志:

05:16:24,965 INFO  [org.infinispan.CLUSTER] (thread-7,ejb,ip-10-11-193-82) ISPN000093: Received new, MERGED cluster view for channel ejb: MergeView::[ip-10-11-193-82|1] (2) [ip-10-11-193-82, ip-10-11-193-51], 2 subgroups: [ip-10-11-193-82|0] (1) [ip-10-11-193-82], [ip-10-11-193-51|0] (1) [ip-10-11-193-51]",
05:16:24,967 INFO  [org.infinispan.CLUSTER] (thread-7,ejb,ip-10-11-193-82) ISPN100000: Node ip-10-11-193-51 joined the cluster",
复制代码

B 节点的日志如下:

05:16:24,967 INFO  [org.infinispan.CLUSTER] (thread-5,null,ip-10-11-193-51) ISPN000093: Received new, MERGED cluster view for channel ejb: MergeView::[ip-10-11-193-82|1] (2) [ip-10-11-193-82, ip-10-11-193-51], 2 subgroups: [ip-10-11-193-82|0] (1) [ip-10-11-193-82], [ip-10-11-193-51|0] (1) [ip-10-11-193-51]",
05:16:24,972 INFO  [org.infinispan.CLUSTER] (thread-5,null,ip-10-11-193-51) ISPN100000: Node ip-10-11-193-82 joined the cluster",
复制代码

组网成功,问题解决!!!

六号坑:出现单点故障

系统终于可以正常运转了,你以为这就大结局了吗?那就 too naive 了。

在进行可用性相关的测试时,我们发现如果关掉 KeyCloak 的一个节点,部分请求会失败,在重新登录之后又可以成功请求了。难道又是多节点造成的问题?集群模式不是已经在成功运行了吗?

仔细翻阅了官方文档之后,发现 KeyCloak 默认不会备份缓存数据,如果一个节点挂了,节点中的缓存数据就永久丢失了

By default Keycloak does NOT replicate caches like sessions, authenticationSessions, offlineSessions, loginFailures and a few others (See Eviction and Expiration for more details), which are configured as distributed caches when using a clustered setup. Entries are not replicated to every single node, but instead one or more nodes is chosen as an owner of that data. If a node is not the owner of a specific cache entry it queries the cluster to obtain it. What this means for failover is that if all the nodes that own a piece of data go down, that data is lost forever. By default, Keycloak only specifies one owner for data. So if that one node goes down that data is lost.

解决办法

在环境变量中增加如下配置,将备份数设置为 2 或者更多:

- CACHE_OWNERS_COUNT=2
- CACHE_OWNERS_AUTH_SESSIONS_COUNT=2
复制代码

总结

本文归纳了作者在 AWS 上部署 KeyCloak 时踩过的一些坑,网上类似的资料不多,希望对你有帮助。 码子不易,如果喜欢请点赞关注加分享,感谢!

参考链接

猜你喜欢

转载自juejin.im/post/7083046684342616095