Kubernetes 单点登录详解

原文:Kubernetes Single Sign On - A detailed guide

作者:Ben Dixon

本文中我们将会为 Kuebernetes 构建一个完备的单点登录系统,这个系统会为 kubectl、Web 应用的 Ingress,以及 Docker 镜像仓库和 Gitea 提供服务,本文中会涉及多数单点登录模型,对于 Gitlab、Kibana、Grafana 等其它应用,应该也是适用的。

整个方案中使用了以 OpenLDAP 为后端的 KeyCloak 服务。OpenLDAP 能满足 Gitea 的需求,但基于 OIDC 的 kubectl 单点登录之类的功能是不需要它的。

本文中的一些亮点:

  1. 通过浏览器完成 kubectl 的单点登录;
  2. 用同样简单、但更安全的注解来替换 Ingress 的 Basic 认证注解;
  3. 使用完整的 ACL(访问控制列表)来进行 Docker 容器镜像的推送和拉取。

前提条件

我们假设你能够使用 Kubectl 访问 Kubernetes 集群(集群中包含 CSI 支持),在其中创建一个名为 identity 的命名空间。能够使用 Helm 3。最后假设你使用 Nginx Ingress 控制器,并包含 Cert Manager 组件,并用 Cert Manager 为集群提供一个称为 letsencrypt-production 的 SSL 证书签发者。

如果你的配置不满足上述要求,一些主要步骤应该还是一致的,Ingress 注解可能会有一些不同。本文中涉及的源码位于 https://github.com/TalkingQuickly/kubernetes-sso-guide.git,可以 Clone 到本地:

git clone https://github.com/TalkingQuickly/kubernetes-sso-guide.git

后续所有命令都认为你的当前目录是上述源码的根目录。

部署 OpenLDAP

因为 stable Chart 仓库已经淘汰,OpenLDAP 的 Chart 也受到波及。因为还没有更新,本文会使用最近的版本。

openldap/values-openldap.yml 中包含了 values 样例,我们可以做一些因地制宜的修改。

# Default configuration for openldap as environment variables. These get injected directly in the container.
# Use the env variables from https://github.com/osixia/docker-openldap#beginner-guide
env:
  LDAP_ORGANISATION: "Talking Quickly Demo"
  LDAP_DOMAIN: "ssotest.staging.talkingquickly.co.uk"
  LDAP_BACKEND: "hdb"
  LDAP_TLS: "true"
  LDAP_TLS_ENFORCE: "false"
  LDAP_REMOVE_CONFIG_AFTER_SETUP: "true"
  LDAP_READONLY_USER: "true"
  LDAP_READONLY_USER_USERNAME: readonly
  LDAP_READONLY_USER_MASSWORD: password

# Default Passwords to use, stored as a secret. If unset, passwords are auto-generated.
# You can override these at install time with
# helm install openldap --set openldap.adminPassword=<passwd>,openldap.configPassword=<passwd>
adminPassword: admin
configPassword: 9h8sdfg9sdgfjsdfg8sdgsdfjgklsdfg8sdgfhj

customLdifFiles:
  initial-ous.ldif: |-
    dn: ou=People,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk
    objectClass: organizationalUnit
    ou: People

    dn: ou=Group,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk
    objectClass: organizationalUnit
    ou: Group

上一节配置中,我使用 dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk 作为 Kubernetes 集群的基础域。我们的 LDAP 服务仅对内提供服务,因此不需要映射到 DNS。

customLdifFiles 的内容是用于对 LDAP 数据库进行初始化的。这里提供了两个 organizationalUnit,类型为 People 的项目会用于保存个人信息,另外一个类型为 Group 的则会用于保存群组信息。OU 可以近似的看做是传统文件系统中的文件夹。OpenLDAP OUs 一文中详细解释了这方面的内容。

现在就可以用下面的命令来安装 OpenLDAP 了:

helm upgrade --install openldap ./charts/openldap --values openldap/values-openldap.yml

安装过程中会输出一些成功信息,以及访问该服务的示例。

注意下面几个用于获取配置和密码的命令:

kubectl get secret --namespace identity openldap -o jsonpath="{.data.LDAP_ADMIN_PASSWORD}" | base64 --decode; echo
kubectl get secret --namespace identity openldap -o jsonpath="{.data.LDAP_CONFIG_PASSWORD}" | base64 --decode; echo

使用 OpenLDAP 客户端

我们最后会使用 KeyCloak 来管理 LDAP 目录,在那之前可以熟悉一下 OpenLDAP 客户端。Splunk 有文章介绍了这个客户端

首先用 kubectl proxy 来开放 LDAP 服务:

kubectl port-forward --namespace identity \
      $(kubectl get pods -n identity --selector='release=openldap' -o jsonpath='{.items[0].metadata.name}') \
      3890:389

在另一个终端里,执行下面的命令(包括 OSX 在内的多数 Unix 系统都安装了 ldapsearch,如果没有的话,就需要进行安装,例如 Debian 发行版就需要安装 ldap-utils):

ldapsearch -x -H ldap://localhost:3890 \
    -b dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk \
    -D "cn=admin,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk" \
    -w password

命令会返回如下信息:

objectClass: organization
o: Talking Quickly's Demo
dc: k4stest4

# admin, k4stest4.talkingquickly.co.uk
dn: cn=admin,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: admin
description: LDAP administrator
userPassword:: e1NTSEF9YjM1a0hLYXVwcDlvcGU5R1N2UE5qcFBLd3FxdUorWFk=

# People, k4stest4.talkingquickly.co.uk
dn: ou=People,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk
objectClass: organizationalUnit
ou: People

# Group, k4stest4.talkingquickly.co.uk
dn: ou=Group,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk
objectClass: organizationalUnit
ou: Group

# search result
search: 2
result: 0 Success

# numResponses: 6
# numEntries: 4

Stack Overflow 上有一篇帖子描述了 dndc 这些名词的详情。

注意我的测试域 ssotest.staging.talkingquickly.co.uk 的结果是一个 dn,其中包含了一组逗号分隔的 dc 列表:dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk。这里看到我们 customLdiffFiles 定义的 ou 已经建立了。

现在我们已经有了 LDAP 服务器,通过简单的测试,检查了我们保存在服务之中的数据。接下来就可以安装 KeyCloak 了。

安装 KyeCloak

Helm 3 提倡使用去中心化的仓库替代原有的仓库,所以在安装 Keycloak 之前要首先加入新的仓库:

helm repo add codecentric https://codecentric.github.io/helm-charts

有兴趣的话还可以看看这个 Chart 的详细信息

为了进行一些基础配置,我们需要配置 Ingress,并启用 Postgres 存储数据。

这里假设你的集群中包含了 Ingress 和存储支持。我用域名通配符 *.ssotest.staging.talkingquickly.co.uk 指向测试集群。所以我们的 keycloak/values-keycloak 看起来是这样的:

extraEnv: |
  - name: KEYCLOAK_LOGLEVEL
    value: DEBUG
  - name: KEYCLOAK_USER
    value: admin
  - name: KEYCLOAK_PASSWORD
    value: as897gsdfs766dfsgjhsdf
  - name: PROXY_ADDRESS_FORWARDING
    value: "true"

ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
  rules:
    - host: sso.ssotest.staging.talkingquickly.co.uk
      paths:
        - /
args:
  - -Dkeycloak.profile.feature.docker=enabled

  tls:
  - hosts:
    - sso.ssotest.staging.talkingquickly.co.uk
    secretName: keycloak-tld-secret

postgresql:
  enabled: true
  postgresqlPassword: asdfaso97sadfjylfasdsf78

用如下命令执行安装:

helm upgrade --install keycloak codecentric/keycloak --values keycloak/values-keycloak.yml

values-keycloak.yml 中使用环境变量 KEYCLOAK_USER 以及 KEYCLOAK_PASSWORD 设置了 keycloak 的用户名和密码,我们将会用这个信息来访问 Keycloak 的控制台。

我们根据前面的 Ingress 设置来浏览控制台页面(例如 https://sso.ssotest.staging.talkingquickly.co.uk),然后输入用户名密码完成登录。这里可以创建用户和即将使用 Keycloak 进行单点登录的应用程序。

注意登录管理控制台和服务用户登录是各自独立的。

在 Keycloak 中我们可以创建多个 realms,代表不同的认证服务。例如我们可能要给内部系统创建一个 realm,另外给客户创建独立的 realm。缺省 realm 命名为 master,根据前面的 Ingress 定义,用户可以访问 https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master/account 进行登录。访问 https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master 能够获取该 realm 的有用信息。

这里有一个容易混淆的环节就是使用管理员凭据而非在特定 realm 中创建的普通用户的凭据进行登录。

Kubernetes 中的 Keycloak 和 OpenLDAP

完成了 Keycloak 和 OpenLDAP 的部署之后,可以进行进一步的配置,把两个系统连接起来,让 Keycloak 使用 OpenLDAP 存储用户数据。

在 Keycloak 中配置 OpenLDAP

用管理用户登录到 Keycloak 控制台,进入 User FederationAdd Provider 下拉列表中选择 ldap。按照如下数值填写关键字段:

  • Edit Mode: Writable
  • Sync Registrations: On
  • Vendor: Other
  • Connection URL: ldap://openldap.identity.svc.cluster.local; you’ll need to change identity to match the namespace you’re working in)
  • Users DN: ou=People,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk; you’ll need to change the dc entries to match your base dn. Note that here we’re telling Keycloak that users are stored in our People ou, created from the customLdiffFiles.
  • Authentication Type: simple
  • Bind DN: cn=admin,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk again, updating the dc entries to match your base dn
  • Bind Credentials: Set this to the admin password we used for ldapsearch earlier

填写完成后保存数据。这个配置数据和缺省数据稍有不同,需要确认 memberOf 属性是否正常工作。在这方面,Github 上有一个很长的 Issue。有些应用需要这个功能来根据分组来进行访问控制。

可以用 ldapsearch 验证 memberOf 是否正常工作,我们需要使用搜索操作符来进行搜索:

ldapsearch -x -H ldap://localhost:3890 -b dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk -D "cn=admin,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk" "+" -w password

要在本地运行这个命令,需要在本地进行流量转发:

kubectl port-forward --namespace identity \
      $(kubectl get pods -n identity --selector='release=openldap' -o jsonpath='{.items[0].metadata.name}') \
      3890:389

用户管理和测试

点击 Keycloak 管理门户左侧的 Users,选择 Add User。填写并保存用户数据之后,就可以用 ldapsearch 来检查用户是否已经成功创建。可以用端口转发的方式来进行验证:

kubectl port-forward --namespace identity \
      $(kubectl get pods -n identity --selector='release=openldap' -o jsonpath='{.items[0].metadata.name}') \
      3890:389

然后进行搜索:

ldapsearch -x -H ldap://localhost:3890 -b dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk -D "cn=admin,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk" "+" -w password

这里应该用真实密码代替此处的 password

输出内容中应该能看到如下内容:

# talkingquickly, People, ssotest.staging.talkingquickly.co.uk
dn: uid=talkingquickly1,ou=People,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk
uid: talkingquickly1
objectClass: inetOrgPerson
objectClass: organizationalPerson
mail: ben+1@hillsbede.co.uk
sn: Dixon
cn: Ben

这表明用户已经成功建立。接下来配置一下用户组。我们浏览 Groups 页面,加入一个 Administrator 组,重新运行 ldapsearch 命令,我们不会看到任何变化,群组没有出现。

返回 Users 页,选择或者新建一个用户,并进入 Groups 页面把用户加入群组。回到 Users,会看到一个空列表,必须选择 View all users 才能看到用户信息。

完成这些动作之后,重新运行 ldapsearch,会看到类似下面的内容:

# Administrators, Group, k4stest4.talkingquickly.co.uk
dn: cn=Administrators,ou=Group,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk
objectClass: groupOfNames
cn: Administrators
member: cn=empty-membership-placeholder
member: uid=talkingquickly,ou=People,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk

这些输出表示我们的群组已经建立,并且已经有用户成为群组的成员。

如果我们查看一下我们的用户条目,点击 +,会看到包含一行内容:

memberOf: cn=Administrators,ou=Group,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk

说明 memberOf 功能正常。

本文不会涉及更多 ldapsearch 的内容,ldapsearch Examples 中包含了很多的详细用例。

用 OIDC 登录 Kubernetes

使用 Kubernetes 集群的团队要面临的一个常见痛点就是管理连接集群的配置文件。一般的解决办法,要么是使用 KUBECONFIG 环境变量指定配置文件,其中包含了硬编码的凭据;要么就是使用自定义脚本来包装 AWS 或者 GCP 的客户端。

本节中我们会把 Kubernetes 和 Keycloak 集成起来,这样在我们执行 kubectl 或者 helm 命令的时候,如果用户没能完成认证,就会打开浏览器窗口进行登录,这样就无需使用 KUBECONFIG 变量了。

我们也会配置基于群组的访问控制,所以我们可以创建一个 KubernetesAdminstrators 组,从而让组中所有用户具备 cluster-admin 权限。

如果我们在 Keycloak 中移除用户(或者从特定组中移除用户),对应用户就会失去权限。

我们会使用 OpenID Connect。官网文档中介绍了这一特性的原理。

缺省情况下,要给 Kubernetes 加入 OIDC 认证配置,需要修改 API Server 的参数。只有一些托管 Kubernetes 产品(例如 AWS 和 GCP)提供了这种手段,用于连接它们各自的 IAM 系统。

我们将使用来自 JetStack 的 kube-oidc-proxy 来解决这个问题。这个工具提供一个代理服务器来管理 OIDC 认证,用户连接到这个代理服务器时,服务会给通过认证的用户提供所需的权限。这种方法是通用的,也就是说我们可以用同样的方法来管理所有的托管和非托管集群。

设置 Keycloak

首先我们要在 Keycloak 中创建一个新客户端,其 ID 为 kube-oidc-proxy,协议为 openid-connect,并且设置该客户端的参数:

  • Access Typeconfidential,这需要生成一个应用 Secret。
  • Valid Redirect URLshttp://localhost:8000http://localhost:18000kubelogin 会使用这些网址作为回调,在 kubectl 进行登录时,就会打开浏览器窗口进行 Keycloak 进行认证。

保存新应用,就会出现一个新的 Credentials 标签,需要在这个标签里生成客户端 Secret,这个 Secret 将会在后续步骤中和 Client ID 一起使用。

设置 Kube OIDC Proxy

客户端创建之后,就要配置 Kube OIDC Proxy 了。在 kube-oidc-proxy/values-kube-oidc.yml 包含一个样本:

oidc:
  clientId: kube-oidc-proxy
  issuerUrl: https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master
  usernameClaim: sub

extraArgs:
  v: 10

ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
    nginx.ingress.kubernetes.io/backend-protocol: HTTPS
  hosts:
    - host: kube.ssotest.staging.talkingquickly.co.uk
      paths:
        - /
  tls:
    - secretName: oidc-proxy-tls
      hosts:
        - kube.ssotest.staging.talkingquickly.co.uk

有几个需要进行定制的点:

  • issuerUrl:Keycloak 的实例地址,其中包含了 realm 信息(这里我们使用的是缺省的 master realm)。
  • Ingress 定义的主机名。这个 URL 会用来替代 Kubernetes API 的 URL,我们的 SSO 登录设置完毕之后,Kubeconfig 文件会指向这个地址而非原有的 Kubernetes API。

extraArgs 中设置的 v: 10 要求 Kube OIDC Proxy 输出详细日志,便于排查问题。在生产环境中可以删除这一行。

接下来用 Helm 安装 Kube OIDC Proxy:

helm upgrade --install kube-oidc-proxy ./charts/kube-oidc-proxy --values kube-oidc-proxy/values-kube-oidc.yml

Kube OIDC Proxy 启动之后,就可以配置 kubectl 了。最简单的方式就是使用 kubelogin。它是一个 kubectl 的插件,安装插件之后,如果执行 kubectl,就会打开一个浏览器窗口,让用户在其中登录 Keycloak。登录之后它会负责刷新 Token,并负责会话过期之后的重新认证。

项目主页提供了该插件的安装方法,homebrew 用户可以用 brew install int128/kubelogin/kubelogin 轻松完成,否则的话,推荐用 krew 管理 kubectl 插件,这样就可以用 kubectl krew install oidc-login 进行安装了。

接下来就是创建一个 kubeconfig.yml 文件,内容(kubelogin/kuebconfig.yml)如下:

apiVersion: v1
clusters:
- cluster:
    server: https://kube.ssotest.staging.talkingquickly.co.uk
  name: default
contexts:
- context:
    cluster: default
    namespace: identity
    user: oidc
  name: default
current-context: default
kind: Config
preferences: {}
users:
- name: oidc
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - oidc-login
      - get-token
      # - -v1
      - --oidc-issuer-url=https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master
      - --oidc-client-id=kube-oidc-proxy
      - --oidc-client-secret=a32807bc-4b5d-40b7-8391-91bb2b80fd30
      - --oidc-extra-scope=email
      - --grant-type=authcode
      command: kubectl
      env: null
      provideClusterInfo: false

需要修改的内容:

  • server:Kube OIDC Proxy 的 Ingress URL;
  • oidc-issuer-url:和 Kube OIDC Proxy 中配置的 Keycloak URL;
  • oidc-client-secret:Keycloak 客户端应用的 Secret;
  • -v1:可选项,用于输出更多日志信息。

接下来执行:

export KUBECONFIG=./kubelogin/kubeconfig.yml
kubectl get pods

本文不会涉及 kubeconfig 文件的管理方法,而如果你还没有这方面的管理经验,强烈推荐使用 direnvkubectx 的组合。我的 Debian 远程开发环境OSX 配置里面都提供了开箱可用的这两个工具。

export KUBECONFIG=./kubelogin/kubeconfig.yml 这个用法仅在同一个终端会话中生效,所以如果切换到新的终端,或者关闭重新打开你的终端,这个配置就会退回到 Shell 缺省的 KUBECONFIG 环境变量。

当我们执行上面命令的时候,会打开一个浏览器,用户需要在浏览器里登录 Keycloak。

然而我们会看到错误信息:

Error from server (Forbidden): pods is forbidden: User "oidcuser:7d7c2183-3d96-496a-9516-dda7538854c9" cannot list resource "pods" in API group "" in the namespace "identity"

Kubernetes 知道当前用户是 oidcuser:7d7c2183-3d96-496a-9516-dda7538854c9,说明我们的用户已经通过认证,但是这个用户当前却又无权进行任何操作。

我们可以创建一个 Cluster Role Binding,并把 cluster-admin 角色绑定上去。

我们需要另开一个终端,也就是我们还没有修改 KUBECONFIG 的终端会话,这样我们就会使用一个 cluster-admin 权限来操作集群了。

kubectl create clusterrolebinding oidc-cluster-admin --clusterrole=cluster-admin --user='oidcuser:OUR_USER_ID'

OURUSERID 替换为登录用户的 Keycloak ID(可以参看上面的消息)。

oidcuser: 前缀是 Kueb OIDC Proxy 配置中的 usernamePrefix:oidcuser:。这一措施能够防止不同的用户系统之间造成冲突。

使用 Keycloak 群组登录到 Kubernetes

前面的步骤让我们的 kubectl 用 Keycloak 用户登录集群。然而为每个用户创建一个 Cluster Role Binding 是个很麻烦的事情。

要解决这个问题就要靠群组,我们会对 OIDC 实现进行配置,使其感知到 Keycloak 的群组。我们可以在 Keycloak 创建一个 KubernetesAdmin 组,组中所有用户都使用同一个 Cluster Role Binding 被授予 cluster-admin 权限。

首先在 Keycloak 上创建一个 KubernetesAdmin 群组,然后在群组中创建一个新用户。

接下来要更新我们的 Keycloak 客户端,把用户所属群组的信息包含在 JWT 中。

要完成这个任务要回到 Keycloak 客户端 kube-oidc-client 条目中,并选择 Mapper 分页,点击 Create

输入如下内容:

  • NameGroups
  • Mapper TypeGroup Membership
  • Full Group PathOff

然后保存。

如果在 kubelogin/kubeconfig.yml 文件中取消 # - -v1 中的注释符,并删除 ~/.kube/cache/oidc-login/ 的内容然后执行 kubectl get pods,会再次进行登录,我们会看到 JWT 信息中包含了我们的群组信息:

{
  ...                                         
  "groups": [                                                       
    "DockerRegistry",                                             
    "Administrators",
    "KubernetesAdmins"
  ],             
  ...
}

我们可以创建一个 Cluster Role Binding,让每个 KubernetesAdmin 中的每个成员都有 cluster-admin 的访问能力:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: oidc-admin-group
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: oidcgroup:KubernetesAdmins

在我们的 Kube OIDC Proxy 配置中,使用 groupsPrefix: "oidcgroup:" 的配置为群组名称加上了 oidcgroup 前缀,防止和 Kubernetes 中的其他分组造成冲突。

把这个 YAML 提交给集群:

kubectl apply -f ./group-auth/cluster-role-binding.yml

然后就可以删除前面单独创建的 Cluster Role Binding:

kubectl delete clusterrolebinding oidc-cluster-admin

上述动作完成之后,就能用 kubectl get po 之类的简单命令来验证工作成果了。

上述授权只是用于测试的,实际工作中应该创建更加严格的授权,例如一些只能在特定命名空间中工作的用户。

使用 Ingress 注解为 Web 应用提供认证

本章节完成后,只需要给 Ingress 加入注解,就能用 Keycloak 为其他应用提供认证功能。这就给我们一种方便易用的替换 Basic Auth 的方法。

一部分运行在 Kubernetes 中的第三方应用都支持 OIDC 或者 LDAP 的登录。在部署自研应用时,可以使用 Keycloak 来管理登录,而无需额外开发 OIDC 或者 LDAP 的集成代码。

下面将使用 OAuth2 Proxy 给一个简单的 Nginx 容器加入认证功能。之后观察一下如何访问应用,如何解码 Keycloak JWT 并使用群组鉴权等特性。

可以用这种建议的方式对内部应用进行保护。在更复杂的环境中,还可以在 Keycloak 中创建一个 customers realm,把认证和鉴权工作都交给 Keycloak。

Keycloak Gatekeeper/Louketo 在 2020 年 8 月进入 EOL,因此我们使用 OAuth2 Proxy 来完成这一任务。

为 Nginx 服务器设置 Keycloak 认证

首先要对 OAuth2 Proxy 进行配置,使之对接到 Keycloak,并使用 Helm 进行部署。

接下来会用 Helm 部署官方的 Nginx 容器镜像,用它作为测试应用,然后我们会使用 Keycloak 结合 Ingress 注解来对其进行访问控制。

最后我们还要看看示例应用如何获得登录用的信息,从而有能力进行更细粒度的访问控制。

工作原理

Nginx 支持基于子请求结果的认证:当受保护页面接到请求时,Nginx 可以向一个额外的 URL 发送一个子请求,如果该 URL 返回了 2xx 的响应码,就允许接收这个请求,如果返回了 401 或 403,就会拒绝请求。

实际上我们无需深入理解上面的内容,因为 OAuth2 Proxy 连接到了 Keycloak 进行实际的认证,并给 Nginx 提供了适用的端点,来检查用户是否登录。

所以我们只要配置 OAuth2 Proxy 并给特定服务的 Ingress 加入合适的注解。

配置 OAuth2 Proxy

首先我们要在 Keycloak 创建一个客户端应用,创建一个新的 OpenID 连接应用,并作出如下设置:

  • Client IDoauth2-proxy
  • Access Typeconfidential
  • Valid Redirect URLshttps://oauth.ssotest.staging.talkingquickly.co.uk/oauth2/callback,注意替换其中的域名。

保存新建项目,并打开新出现的 Credentials 页面,注意其中的 Secret 内容。

最后进入 Mappers 页面,选择 Create

  • NameGroups
  • Mapper TypeGroup Membership
  • Token Claim Namegroups
  • 其它选项都设置为 On

保存数据。这一配置的含义是,属于本组的用户会返回给 OAuth2 Proxy 并随后返回给应用。

OAuth2 Proxy 有一个 Keycloak Provider,但是这里我们会使用通用的 OIDC Provider。这是一个更通用的解决方案,并提供了自动刷新 Cookie 等 Keycloak Provider 所没有的一些功能。目前 OAuth2 Proxy 团队还在讨论修改 Keycloak Provider 为 OIDC Provider。

下一步可以创建 OAuth2 Proxy 配置,在 oauth2-proxy/values-oauth2-proxy.yml 中包含一个样本:

# Oauth client configuration specifics
config:
  clientID: "oauth2-proxy"
  clientSecret: "YOUR_SECRET"
  # Create a new secret with the following command
  # openssl rand -base64 32 | head -c 32 | base64
  cookieSecret: "YOUR_COOKIE_SECRET"
  configFile: |-
    provider = "oidc"
    provider_display_name = "Keycloak"
    oidc_issuer_url = "YOUR_ISSUER"
    email_domains = [ "*" ]
    scope = "openid profile email"
    cookie_domain = ".ssotest.staging.talkingquickly.co.uk"
    whitelist_domains = ".ssotest.staging.talkingquickly.co.uk"
    pass_authorization_header = true
    pass_access_token = true
    pass_user_headers = true
    set_authorization_header = true
    set_xauthrequest = true
    cookie_refresh = "1m"
    cookie_expire = "30m"

ingress:
  enabled: true
  path: /
  hosts:
    - oauth.ssotest.staging.talkingquickly.co.uk
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
    nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
  tls:
    - secretName: oauth-proxy-tls
      hosts:
        - oauth.ssotest.staging.talkingquickly.co.uk

这里有一些需要自行配置的关键字段:

  • clientSecret:Keycloak 凭据页面里保存的 Secret
  • cookieSecret:可以用 openssl rand -base64 32 | head -c 32 | base64 命令随机生成;
  • loginurlredeemurlvalidate_url:应该根据实际的 URL 和 Realm 进行配置;
  • cookiedomainwhitelistdomain:需要根据实际的域名进行配置,例如这里使用的 .ssotest.staging.talkingquickly.co.uk
  • ingress hosts:设置为部署 OAuth2 Proxy 的 Ingress 主机名。

cookie_domainwhitelist_domain 都很重要,缺省情况下,OAuth2 Proxy 只对自己的主机名生效。所以 Cookie 只能指定到该主机名,重定向也只能对这个子域名生效。

缺省情况下,OAuth2 Proxy 会请求一个 api scope,这在 Keycloak 是不存在的,会返回 403 Invalid Scopes 的错误,因此要设置 scope = "openid profile email"

set_authorization_header 确保 JWT 被回传给 Nginx Ingress,这样就能确保 Header 被回传给应用,应用可以借此获取登录用户的信息。

最后,OAuth 报文中经常会传递较多的 Header 信息,因此这里设置 nginx.ingress.kubernetes.io/proxy-buffer-size: "16k",避免出现 Cookie "oauth2proxy not present" 或者 upstream sent too big header while reading response header from upstream 这样的错误。

安装 OAuth2 Proxy

同样地,因为 Stable 仓库即将淘汰,OAuth2 Proxy Chart 还没迁移到新的仓库,所以我们在样例中保留了最近的版本,可以用如下命令进行安装:

helm upgrade --install oauth2-proxy ./charts/oauth2-proxy --values oauth2-proxy/values-oauth2-proxy.yml

安装结束,就可以浏览 Ingress 域名,会看到出现了 Sign in with Keycloak 的选项。

如果我们不小心使用管理员而非在 OAuth2 Proxy 登录会看到 403 之类的错误。

如果我们成功地登录到了 Kyecloak,会被重定向到一个 404 页面,这是因为目前还没定义待认证页面。我们不应该直接访问这个 URL,正常情况下,认证流程应该在浏览受保护页面时被自动触发。所以前面的登录动作只是为了验证功能而已。

为应用加入认证

现在我们已经完成了 OAuth2 Proxy 的配置,接下来就可以安装一个示例应用,并在 Ingress 定义中加入注解,将应用置于认证保护之后。

在这个例子中会安装一个简单的 Nginx,它只会提供一个 Welcome to nginx 的静态页面,不同的是这个页面会要求登录认证。读者要注意不要把这个 Nginx 和 Nginx Ingress 混为一谈。

这里用了 Bitnami 的 Nginx Helm Chart:

helm repo add bitnami https://charts.bitnami.com/bitnami

用如下代码配置我们的应用:

serverBlock: |
  log_format    withauthheaders '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status  $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" "$http_x_auth_request_access_token"';

    add_header    x-auth-request-access-token "$http_x_auth_request_access_token";

  # HTTP Server
  server {
      # Port to listen on, can also be set in IP:PORT format
      listen  8080;

      include  "/opt/bitnami/nginx/conf/bitnami/*.conf";

      location /status {
          stub_status on;
          access_log   off;
          allow 127.0.0.1;
          deny all;
      }

      access_log /dev/stdout withauthheaders;
  }

ingress:
  enabled: true
  hostname: nginx-demo-app2.ssotest.staging.talkingquickly.co.uk
  tls: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-staging
    nginx.ingress.kubernetes.io/auth-url: "https://oauth.ssotest.staging.talkingquickly.co.uk/oauth2/auth"
    nginx.ingress.kubernetes.io/auth-signin: "https://oauth.ssotest.staging.talkingquickly.co.uk/oauth2/start?rd=$scheme://$best_http_host$request_uri"
    nginx.ingress.kubernetes.io/auth-response-headers: "x-auth-request-user, x-auth-request-email, x-auth-request-access-token"
    acme.cert-manager.io/http01-edit-in-place: "true"
    nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"

service:
  type: ClusterIP

这里定义的 serverBlock 和实际的认证流程无关。下面两条认证措施,都是把 Nginx 作为演示应用的:

  • 修改日志行为,在日志输出中加入 x-auth-request-access-token Header,这样我们就可以查看日志中的 Token,进行分析和测试。
  • 自动为入站请求和响应加入 x-auth-request-access-token Header,便于在浏览器进行查看。

把 Token 输出到日志是非常危险的操作,决不应该用在生产环境之中。

跟认证有关的行如下所示:

ingress:
  annotations:
    nginx.ingress.kubernetes.io/auth-url: "https://oauth.ssotest.staging.talkingquickly.co.uk/oauth2/auth"
    nginx.ingress.kubernetes.io/auth-signin: "https://oauth.ssotest.staging.talkingquickly.co.uk/oauth2/start?rd=$scheme://$best_http_host$request_uri"
    nginx.ingress.kubernetes.io/auth-response-headers: "x-auth-request-user, x-auth-request-email, x-auth-request-access-token"
    acme.cert-manager.io/http01-edit-in-place: "true"
    nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"

这里用 acme.cert-manager.io/http01-edit-in-place: "true" 注解来调用 Cert Manager 并设置响应 Header。nginx.ingress.kubernetes.io/proxy-buffer-size: "16k" 则是增加缓存用于存储 OAuth 2 Proxy Header。

nginx.ingress.kubernetes.io/auth-url 是第一处核心注解,指定了检查当前用户认证的 URL。当请求进入时,Nginx 会发送请求到这个网址,注意它发送的只是 Header 以及请求相关的 Cookie,而不包括 Body。URL 所对应的 Service(本例中就是 OAuth2 Proxy)负责根据 Cookie 和 Header 来验证用户的登录状态。如果用户已经过认证,服务会返回 2xx 状态码,然后这个请求就会传递给应用。如果未登录,请求就会转发给 nginx.ingress.kubernetes.io/auth-signin 中的 URL,启动认证流程。要给所有子域提供认证,就需要把 OAuth2 Proxy 的 Cookie 域设置为上级域名。

因为设置了 set_authorization_header = true,当请求认证通过后,OAuth2 Proxy 就会在返回给 Nginx 的 2xx 响应中加入 x-auth-request-access-token Header,其中包含了认证 Token,本例中这个 Token 是包含用户和会话的 JWT 信息。

缺省情况下,被认证应用是不能访问这个 Token 的,要获取认证用户的信息,或者该用户所属的群组,要获取这些信息,需要设置 nginx.ingress.kubernetes.io/auth-response-headers: "x-auth-request-user, x-auth-request-email, x-auth-request-access-token",它会通知 Nginx Ingress,从返回的 2xx 响应中获取这几个 Header,并传递给后端应用。这样后端应用就能够获取 Header,并解码 JWT 来获取用户的相关信息。

在这个简单的例子里,会把这些信息输出到日志里(不安全),并把信息响应给用户。如果访问示例应用的 Ingress URL,在本例中就是 https://nginx-demo-app2.ssotest.staging.talkingquickly.co.uk,就会要求登录,然后重定向到 Welcome to nginx! 页面。

可以用浏览器的网络页来观察请求内容,会看到响应中的 x-auth-request-access-token

如果我们把 Header 内容拷贝出来进行解码(例如 https://jwt.io)就会看到类似内容:

{
...
  "scope": "openid email profile",
  "email_verified": false,
  "name": "Ben Dixon",
  "groups": [
    "/DockerRegistry",
    "/KubernetesAdmins",
    "/Administrators"
  ],
  "preferred_username": "talkingquickly",
  "given_name": "Ben",
  "family_name": "Dixon",
  "email": "ben@talkingquickly.co.uk"
}

在更复杂的系统中,后端系统会用这里的信息进行判断,根据用户和群组信息来展示不同内容。

Token 过期

我们已经完成了两阶段认证。当请求认证通过之后,OAuth2 Proxy 会跟 Keycloak 进行通信,并取得 Access Token。只要 OAuth2 Proxy 的 Cookie 存在并且有效,相应的请求就不会需要重新使用 Keycloak 进行认证。

使用 JWT 时,很可能会遭遇一个问题就是过期(缺省情况下,Keycloak 的 Access Key 寿命只有一分钟)。这个问题会导致一个麻烦,用户通过了 OAuth2 Proxy 的验证,但是传递给 x-auth-request-access-token 的 JWT 已经过期了。如果我们在应用中对 JWT 进行验证,会抛出 Token 无效的异常。

下面的配置可以解决这个问题:

cookie_refresh = "1m"
cookie_expire = "30m"

cookie_refresh 告诉 OAuth2 Proxy 如果 OAuth2 Proxy Cookie 超过一分钟还没刷新,则刷新 Access Token。这就和 Keycloak 的行为一致了,不会再次把过期 Token 加入请求数据之中。

使用 OIDC Provider 而非 Keycloak Provider 的原因是,目前 Keycloak Provider 还不支持 Token 刷新。

cookie_expire 设置 OAuth2 Proxy 的 Cookie 生命周期为 30 分钟,超时之后用户会被转向 KeyCloak 重新进行验证。这就和 KeyCloak 的会话过期保持了同步。

限制特定群组的访问

在 OAuth2 Proxy 配置中加入一行 allowed_groups = ["/DemoAdmin"],就能够仅允许属于 DemoAdin 群组的用户进行登录。这种情况下,组外用户在登录失败的时候会看到一个 500 的返回码,而不是一个说明实际原因的页面。用户登录时,如果没有在许可组中找到记录,OAuth2 Proxy 就会返回 400,如果我们看看 Nginx 的日志,会看到 auth request unexpected status: 400 while sending to client 这样的信息。

所以这种方法虽然适用于简单的内部应用,但是在应用中处理群组鉴权会有更好的用户体验。

和 Token 打交道

jwt-ruby-example/main.rb 是一个简单的 Ruby 程序,其中包含了处理 Token 的技术。下面的代码很直白:

require 'jwt'

public_key_string = """
PUBLIC_KEY_GOES_HERE
"""

public_key = OpenSSL::PKey::RSA.new(public_key_string)

token = "TOKEN_GOES_HERE"

decoded_token = JWT.decode token, public_key, true, { algorithm: 'RS256' }

puts decoded_token

PUBLIC_KEY_GOES_HERE 需要用公钥来替换,可以在 Keycloak 的 Realm Settings -> Keys -> RS256 -> Public Key 中找到。

TOKEN_GOES_HERE 需要从我们的应用日志或者是 Headers 中获取并使用 ruby main.rb(在运行 bundle install 之后)解码获得。

注意 Keycloak Token 的缺省过期时间是 1 分钟,所以拷贝黏贴动作最好快一点。

输出内容会被解压为 Ruby Map。所以在完整的 Web 应用中(例如 Rails 或者 Sinatra),可以需要根据用户所属群组或者用户登录 Email 进行相应的判断。

Gitea 单点登录

Gitea 是一个开源的轻量级 Git 服务。顺便说一句,Gitea(尤其是和 Drone CI 协同)是我最喜欢的开源软件。

Gitea 的轻量和易用特性,非常适用于 Git Push 发布以及持续集成。

这里我们要配置 OpenLDAP 来进行中心化的用户管理并提供单点登录。我们也会配置 OpenID Connect,但是可能会有一些问题。

安装 Gitea

本文我们主要会聚焦于配置 Gitea 使用 LDAP 进行认证的过程,因此我们这里只会包括 Web 界面的设置。

可能需要根据集群现状编辑 gitea/values-gitea.yml 中的 Ingress 主机名以及 HTTPS 的相关配置:

itea:
  domain: gitea-keycloak.ssotest.staging.talkingquickly.co.uk 
  protocol: http
  installLock: "false"

ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-staging
  hosts:
    - host: gitea-keycloak.ssotest.staging.talkingquickly.co.uk
      paths: ['/']
  tls:
   - secretName: gitea-keycloak-https-secret
     hosts:
       - gitea-keycloak.ssotest.staging.talkingquickly.co.uk

编辑结束之后,用 Gitea 安装:

helm3 upgrade --install gitea-keycloak ./charts/gitea --values ./gitea/values-gitea.yml

安装结束之后,我们在 Gitea Pod 里用客户端创建一个初始用户 administrator,注意修改其中的 YOUR_PASSWORDYOUR_EMAIL 字段:

kubectl exec -it --namespace identity \
      $(kubectl get pods -n identity --selector='app.kubernetes.io/instance=gitea' -o jsonpath='{.items[0].metadata.name}') \
      -- gitea admin user create --username YOUR_EMAIL --password YOUR_PASSWORD --email YOUR_EMAIL --admin --access-token --must-change-password=false

这样我们就能登录 Gitea 实例了。

Gitea 的 LDAP 配置

Gitea 支持的 ODIC 登录仅适用于现存用户,不利于中心化的用户管理,所以我们选用 LDAP。

Github 上的 Gitea 仓库有个 Issue 解释了 OIDC 和 LDAP 的差异。

administrator 身份登录 Gitea,并进入 Site Administrator -> Authentication Sources,在 Add Authentication Source 选择 LDAP (via BindDN) 作为认证源。进行如下配置:

  • Authentication NameOpenLDAP
  • Security ProtocolUnencrypted
  • Hostopenldap.identity.svc.cluster.local
  • Port389
  • Bind DNcn=readonly,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk
  • Bind Password:这个应该是在 values-openldap.yml 文件中设置的只读用户密码;
  • User Search Baseou=People,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk 注意根据实际情况替换 dc
  • User Filter(&(objectClass=inetOrgPerson)(uid=%s)) 的配置允许所有用户登录 Gitea,当然可以创建更复杂的过滤器,只允许特定群组(例如 Engineers)成员登录,How to write ldap search filters 一文讲解了过滤器编写方面的知识。uid=%s 让用户可以使用用户名登录,可以修改过滤器同时匹配邮件和用户名;
  • Username Attributeuid
  • First Name Attributecn
  • Surname attributesn
  • Email Attributemail

Gitea LDAP 文档 中介绍了更多的细节内容。

注意如果我们要用配置管理工具管理 Gitea,可以使用前面创建管理账号的方法。要用 CLI 创建 LDAP 配置可以参考 Gitea 的命令文档

如果回到 Gitea 的登录页面,我们会发现可以直接用 Keycloak 中创建的用户凭据登录。注意要使用 master realm 中的用户而非管理用户。

这个过程没有重定向,密码认证会在后台执行。如果我们进入该用户的 security settings 页面,因为用户是外部托管的,无法在 Gitea 中修改密码,只能在 Keycloak 做管理。

配置 Gitea 使用 OpenID

OpenID Connect 无法在 Gitea 中使用完整的 SSO。(Gitea 的)底层用户必须已经存在,对于已经登录的用户是可以配置的,要配置 Keycloak 的 OIDC,从而用户可以通过 Keycloak 流程进行登录,而不用直接在 Gitea 中输入他们的 Keycloak 用户名和密码。

这种方式的唯一好处就是,扩展使用 Keycloak 让用户能够更方便的登录,少输入用户名和密码。所以这个选项不像其它方法一样完整。

在 Keycloak 管理界面中,进入侧面菜单的 Clients 页面,并选择 Create。给 Gitea 输入 Client ID 并设置 Client Protocolopenid-connect,设置如下内容:

保存之后,就可以在 Credentials 页面中找到客户端的 Secret。

在 Gitea 中进入 Site Administration 并选择 Authentication Sources,然后选择 Add Authentication Source,填写下列内容:

  • Authentication TypeOAuth2
  • Authentication NameKeycloak
  • OAuth2 ProviderOpenID Connect
  • Client IDgitea(创建客户端应用时输入的值);
  • Client SecretYOUR_SECRET(在 Keycloak 客户端页面的 Credentials 卡片上为 Gitea 客户端创建的 Secret);
  • OpenID Connect Auto Discovery URLhttps://YOUR_KEYCLOAK_INGRESS_URL/auth/realms/master/.well-known/openid-configuration 用 Keycloak 的 Ingress 主机名替换 YOUR_KEYCLOAK_INGRESS_URL

需要着重关注的是,Gitea 会在创建 Provider 的时候进行证书认证,所以如果 SSL 证书无效的时候是无法完成的。

在尝试登录之前,我们需要给在 Keycloak 中创建的用户设置一个密码。可以通过进入 Keycloak 用户页面,选择 Credentials 卡片并设置一个密码,如果设置 Temporary 为 1,则用户登录时,会被要求设置新密码。

这样我们就可以进入 Gitea 的登录页面,点击 Sign in with OpenID Connect 选项(如果已经登录,就需要登出当前用户)。这样会重定向到 Keycloak 登录页面,在这页面中我们可以用前面创建的 Keycloak 用户名进行登录。

使用 Keycloak 的用户名和密码进行登录之后,就会重定向回到 Gitea。

简易的 Docker 镜像库

本节我们会讲解如何使用 Keycloak 作为 Docker 镜像库的认证层。这样用户必须使用 Keycloak 中的有效凭据完成 docker login 才能够进行 pushpull。注意这里没有什么访问控制,所有 Keycloak 用户都能够对任何镜像执行任何动作。要进行细粒度的控制,还是需要使用 Harbor。

配置 Keycloak

Keycloak 的 Helm values 文件中需要加入如下内容:

args:
  - -Dkeycloak.profile.feature.docker=enabled

这样就能启用 Docker 镜像库的支持了。

Keycloak 中的 Docker registry 配置

在 Keycloak 中创建新的客户应用,命名为 simple-docker-registry,协议选择 docker-v2。在后续界面中进入 install 卡片,选择 Docker Compose YAML,然后点击 Download。我们并不是要使用 Docker Compose,这个过程是为了便捷地获取证书,用于创建 Kubernetes Secret。

创建 Secret

从 Keycloak 中得到 Docker Compose YAML 压缩包之后,解压到本地文件夹,会看到其中包含了 certs/localhost_trust_chain.pem 文件。我们可以用下面的命令创建 Kubernetes:

kubectl create secret generic docker-registry-auth-token-rootcertbundle --from-file YOUR_PATH_TO/certs/localhost_trust_chain.pem

上述命令中需要用本地路径来替换 YOUR_PATH_TO/certs/localhost_trust_chain.pem。执行之后就创建了一个名为 docker-registry-auth-token-rootcertbundle 的 Secret,其中包含了必要的证书内容。

配置镜像仓库

用 Helm 安装 Docker 镜像仓库的文档:https://github.com/twuni/docker-registry.helm。我们的配置文件大致如下:

configData:
  auth:
    token:
      realm: https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master/protocol/docker-v2/auth
      service: simple-docker-registry
      issuer: https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master
      rootcertbundle: /root-cert-bundle/localhost_trust_chain.pem
ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
  hosts:
    - registry-keycloak.ssotest.staging.talkingquickly.co.uk

  tls:
  - hosts:
    - registry-keycloak.ssotest.staging.talkingquickly.co.uk
    secretName: keycloak-registry-tls-secret

extraVolumes:
  - name: docker-registry-auth-token-rootcertbundle
    secret:
      secretName: docker-registry-auth-token-rootcertbundle

extraVolumeMounts:
  - mountPath: /root-cert-bundle
    name: docker-registry-auth-token-rootcertbundle
    readOnly: true

这里的配置来源于 Keycloak 文档configData 字段配置使用基于 Keycloak 的 Token 认证。

接下来需要更新 Ingress 定义,加入 Docker 镜像库的 URL,例如 registry-keycloak.ssotest.staging.talkingquickly.co.uknginx.ingress.kubernetes.io/proxy-body-size: "0" 这一行注解移除了 Ningx 的最大 Body 长度限制,避免推送大镜像时出现的 413 Request Entity Too Large 错误信息。

extraVolumes 节创建了一个存储卷,用来加载我们的 docker-registry-auth-token-rootcertbundle Secret。extraVolumeMounts 一节将这个卷加载到 Docker 镜像库的 /root-cert-bundle 路径,和 configData 中配置的 rootcertbundle 一致。

安装 Docker 镜像库

使用 Helm 安装镜像仓库:

helm repo add twuni https://helm.twun.io
helm upgrade --install simple-docker-registry twuni/docker-registry --values ./docker-registry/values-docker-registry.yml

测试镜像库

我们需要等待一段时间,让 LetsEncrypt 生成证书,我们可以用 kubectl get certificates 命令查看进度。完成之后就能够进行登录了:

docker login registry-keycloak.ssotest.staging.talkingquickly.co.uk

上边的 URL 应该替换为我们的 Ingress 地址。可以输入 Keycloak 用户名进行认证:

sername: someusername
Password:
WARNING! Your password will be stored unencrypted in /home/ben/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

测试一下镜像推送和拉取:

docker image tag SOME_IMAGE_REF registry-keycloak.ssotest.staging.talkingquickly.co.uk/SOME_NAME
docker push registry-keycloak.ssotest.staging.talkingquickly.co.uk/SOME_NAME
docker pull registry-keycloak.ssotest.staging.talkingquickly.co.uk/SOME_NAME

会看到两种操作都成功了。

登出再试试看:

docker logout registry-keycloak.ssotest.staging.talkingquickly.co.uk
docker pull registry-keycloak.ssotest.staging.talkingquickly.co.uk/SOME_NAME

会看到拒绝访问的错误信息。

在 Kubernetes 中使用

为了访问仓库中的镜像,需要创建合适的 Image Pull Secret,可以参看 Kubernetes 文档完成这个过程。

总结

我们现在就有了一个只允许 Keycloak 认证用户访问的 Docker 镜像库。想要更高级的配置,例如只有特定用户才能访问仓库,或者更细粒度的访问控制,应该换用 Harbor。

用 Keycloak 为 Harbor 提供单点登录,实现完整镜像仓库功能

安装 Harbor

官方提供了 Helm Chart。

和多数 Helm Chart 一样,values 文件内容相当丰富。接下来我们会定制 Ingress 内容和 TLS 证书。必须在安装完成之后使用 Web UI 或者 API 配置 OIDC。我们的 Values 配置包含如下内容:

expose:
  type: ingress
  tls:
    certSource: secret
    secret:
      secretName: harbor-ingress-tls
  ingress:
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-production

    hosts:
      core: core.harbor.ssotest.staging.talkingquickly.co.uk

harborAdminPassword: 85nsafg87ehfgk0fgsgfg6u
externalURL: https://core.harbor.ssotest.staging.talkingquickly.co.uk
secretKey: "8d10dlskeit8fhtg"

notary:
  enabled: false

metrics:
  enabled: true

有几个需要注意的配置:

  • certSourcesecret,其中包含 secretName: harbor-ingress-tls 告知 Harbor 使用 Cert Manager 为 Ingress 生成的证书,而不是自行生成。这样就避免 docker login 时出现 x509: certificate signed by unknown authority
  • core:Ingress URL 应该用 Harbor URL 替换;
  • harbourAdminPasswordexternalURL 以及 secretKey:都要替换为当前环境的配置内容,secretKey 应该是一个随机的 16 字符。

加入 Helm 仓库,并安装 Harbor:

helm repo add harbor https://helm.goharbor.io
helm upgrade --install harbor-registry harbor/harbor --values=./harbor/values-harbor.yml

命令结束之后,我们就可以使用 Ingress URL 访问 Harbor 了,用户名是 admin,密码则是 harbourAdminPassword 中的配置内容。这些组件启动需要一段时间,通常还会看到一些 CrashLoopBackoff 之类的信息出现。

记住还不能用 Harbor 管理员账号进行 docker login,目前 Harbor 中还没有任何的常规用户。因为要切换到 OIDC 登录,所以除了管理用户,我们不会创建任何常规用户。如果我们现在创建了测试用户,然后再删掉,我们还是不能切换到 OIDC 登录。

创建 Keycloak 客户端应用

在 Keycloak 中创建一个新的客户端应用,ID 为 harbor,客户端协议为 openid-connect,并进行如下配置:

  • Access Typeconfidential
  • Valid Redirect URIshttps://YOUR_HARBOR_CORE_INGRESS_DOMAIN/c/oidc/callback

然后保存客户端应用配置,记录 credentials 卡片的内容。

最后来到客户端应用的 Mappers 卡片创建如下的协议映射关系:

  • NameGroups
  • Mapper TypeGroup Membership
  • Token Claim Namegroups
  • All Other OptionsOn

配置 Harbor OIDC

通过 Ingress 地址使用用户名 adminharborAdminPassword 中配置的密码登录到 Harbor 界面。进入 Administration 页面,接下来打开 Configuration 并选择 Authentication 卡片,修改 Auth ModeOIDC,输入下列配置:

  • OIDC Provider NameKeycloak
  • OIDC Endpointhttps://YOUR_KEYCLOAK_BASE_URL/auth/realms/YOURREALM
  • OIDC Client IDharbor
  • OIDC Client Secret:前面记录的客户端应用凭据;
  • Group Claim Namegroups
  • OIDC Scopeopenid,profile,email,offline_access
  • Verify Certificate:根据实际情况决定;
  • Automatic Onboardingchecked
  • Username Claimpreferred_username

然后就可以使用 Test OIDC Server 按钮来测试配置,成功后点击 Save

测试

如果我们现在登出管理账号(或者启动一个匿名浏览器),回到 Harbor Core 的 Ingress URL,会看到一个 Login with OIDC Provider 选项。选择这个选项之后,会被重定向到 Keycloak 进行登录,这里我们应该使用一个常规 Keycloak 用户(Master Realm),不要用 Keycloak 管理用户。登录到 Harbor 之后,Harbor 会自动根据 Keycloak 的配置创建一个用户名。

回到管理员账号,进入AdministratorGroups,就会看到 Keycloak 用户所属群组已经被复制到 Harbor。这意味着可以把特定的群组连接到特定项目,从而自动授予用户某个项目的访问权限。

默认情况下所有用户都能创建项目。所有 Keycloak 用户都能登录到 Harbor,所以最好只允许管理员能够创建项目,可以用 Harbor 的 Administration/Configuration/System Settings 来完成这一限制。

可以用管理员身份进行测试,创建一个叫做 test1 的私有项目,浏览项目的 Members 卡片,选择 + Group,输出 /Administrators 作为群组名称,并设置角色为 Project Admin。 Keyclok 的 Administrators 所有用户都能自动被授予该项目的 Project Admin 权限。

使用 Docker

假设我们创建了叫做 test1 的私有项目,并让我们的 Keycloak Master Realm 账号能够访问这个项目,我们就能够使用 Docker 客户端登录了,例如 docker login core.harbor.ssotest.staging.talkingquickly.co.uk

然后输入 Keycloak 的 Master realm 中的用户名。而密码不应该使用 Keycloak 的密码,而是从 Harbor 获取,在用户页面右上角选择 User Profile,在其中拷贝 CLI 密码。

然后尝试推送镜像:

docker tag SOURCE_IMAGE[:TAG] core.harbor.ssotest.staging.talkingquickly.co.uk/test1/REPOSITORY[:TAG]
docker push core.harbor.ssotest.staging.talkingquickly.co.uk/test1/REPOSITORY[:TAG]

如果我们要在 CI 服务器或者 Kubernetes 中使用这个仓库,可以在 Harbor 中进入 Robot Accounts 卡片生成受限的访问 Token。

从命令行中配置 Harbor OIDC

Ansible 之类的自动化环境中是非常需要从命令行中完成任务的。因此 Harbor 提供了完善的 API,用管理员账号登录,点击底部的 Harbor API v2.0,就能通过 Swagger 查看 API 文档。缺省情况下可以用 YOUR_INGRESS_URL/api/v2.0/ 访问 API。要查看当前配置可以使用:

curl -u "admin:HARBOR_ADMIN_PASSWORD" -H "Content-Type: application/json" -ki YOUR_INGRESS_URL/api/v2.0/configurations

目前为止,Harbor 官网文档有些滞后,实际的命令返回内容中,为现有配置的可选项目提供了更好的概括信息。用命令行设置 OIDC 认证:

curl -X PUT -u "admin:YOUR_ADMIN_PASSWORD" -H "Content-Type: application/json" -ki YOUR_HARBOR_CORE_INGRESS_URL/api/v2.0/configurations -d'{"auth_mode":"oidc_auth", "oidc_name":"Keycloak Auth", "oidc_endpoint":"YOUR_KEYCLOAK_REALM_INGRESS", "oidc_client_id":"harbor", "oidc_client_secret":"YOUR_KEYCLOAK_CLIENT_SECRET", "oidc_scope":"openid,profile,email,offline_access", "oidc_groups_claim":"groups", "oidc_auto_onboard":"true", "oidc_user_claim":"preferred_username"}'

返回状态码为 200,表明我们已经成功地完成了 Keycloak 认证设置。然后做出限制,只有管理员才能创建项目:

curl -X PUT -u "admin:YOUR_ADMIN_PASSWORD" -H "Content-Type: application/json" -ki YOUR_HARBOR_CORE_INGRESS_URL/api/v2.0/configurations -d '{"project_creation_restriction":"adminonly"}'

Harbor API 非常全面,例如我们可以使用 API 创建项目,并为群组授权访问该项目。因此它的 Swagger API 非常值得一看。

在 Kubernetes 中使用

要在 Kubernetes 中访问这个仓库,需要根据官方文档,使用项目的 Robot Token 创建合适的 Image Pull Secret。

结论

我们现在有了一个自托管的 Docker 镜像仓库,并且完全集成了 Keycloak 进行认证。如果想要用 Ansible、Chef 之类的配置管理工具,还能使用命令行完成这些配置。

相关链接

  • Nginx Ingress 控制器:https://kubernetes.github.io/ingress-nginx/
  • OpenLDAP OUs:https://www.theurbanpenguin.com/openldap-ous/
  • LDAPSearch:https://www.splunk.com/en_us/blog/tips-and-tricks/ldapsearch-is-your-friend.html
  • CN 和 DC:https://stackoverflow.com/questions/18756688/what-are-cn-ou-dc-in-an-ldap-search
  • Keycloak Chart:https://github.com/codecentric/helm-charts/tree/master/charts/keycloak
  • Issue:https://github.com/osixia/docker-openldap/issues/304
  • 使用搜索操作符:https://docs.oracle.com/cd/E19623-01/820-6169/searching-for-special-entries-and-attributes.html
  • ldapsearch Examples:https://docs.oracle.com/cd/E19450-01/820-6169/ldapsearch-examples.html
  • Kubernetes 和 OpenID:https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens
  • kube-oidc-proxy:https://github.com/jetstack/kube-oidc-proxy
  • kubelogin:https://github.com/int128/kubelogin
  • kubeloginhttps://github.com/int128/kubelogin
  • Kube-Login:https://github.com/int128/kubelogin
  • krew:https://krew.sigs.k8s.io/docs/user-guide/setup/install/
  • direnv:https://direnv.net/
  • kubectx:https://github.com/ahmetb/kubectx
  • Debian 远程开发环境:http://www.talkingquickly.co.uk/2021/01/debian-dev-environment-for-remote-vscode/
  • OSX 配置:http://www.talkingquickly.co.uk/2021/01/macos-setup-with-ansible/
  • OAuth2 Proxy:https://github.com/oauth2-proxy/oauth2-proxy
  • 官方的 Nginx 容器镜像:https://hub.docker.com/_/nginx
  • 基于子请求结果的认证:https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/
  • Nginx Ingress:https://kubernetes.github.io/ingress-nginx/
  • https://jwt.io:`https://jwt.io/`
  • How to write ldap search filters:https://confluence.atlassian.com/kb/how-to-write-ldap-search-filters-792496933.html
  • Gitea LDAP 文档:https://docs.gitea.io/en-us/authentication/
  • Gitea 的命令文档:https://docs.gitea.io/en-us/command-line/
  • https://github.com/twuni/docker-registry.helmhttps://github.com/twuni/docker-registry.helm
  • Keycloak 文档:https://www.keycloak.org/docs/4.8/securing_apps/#docker-registry-configuration
  • Kubernetes 文档:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
  • Harbor 官方 Helm 文档:https://github.com/goharbor/harbor-helm
  • Harbor 的 values 文件:https://github.com/goharbor/harbor-helm/blob/master/values.yaml
  • Harbor 官网文档:https://goharbor.io/docs/1.10/install-config/configure-user-settings-cli/
  • Pull Secret 官方文档:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
Avatar
崔秀龙

简单,是大师的责任;我们凡夫俗子,能做到清楚就很不容易了。

comments powered by Disqus
上一页