当前位置:   article > 正文

深入理解 Kubernetes 中的用户与身份认证授权

kubelet user

本文转自 Cylon 的博客,原文:https://www.cnblogs.com/Cylon/p/16905335.html,版权归原作者所有。欢迎投稿,投稿请添加微信好友:cloud-native-yang

本章主要简单阐述 kubernetes 认证相关原理,最后以实验来阐述 kubernetes 用户系统的思路。

主要内容

  • 了解 kubernetes 各种认证机制的原理

  • 了解 kubernetes 用户的概念

  • 了解 kubernetes authentication webhook

  • 完成实验,如何将其他用户系统接入到 kubernetes 中的一个思路

Kubernetes 认证

在 Kubernetes apiserver 对于认证部分所描述的,对于所有用户访问 Kubernetes API(通过任何客户端,客户端库,kubectl 等)时都会经历 验证 (Authentication) , 授权 (Authorization), 和准入控制 (Admission control) 三个阶段来完成对 “用户” 进行授权,整个流程正如下图所示:

59785eefce91325a5e34cc14b5448646.png
图:Kubernetes API 请求的请求处理步骤图

其中在大多数教程中,在对这三个阶段所做的工作大致上为:

  • Authentication 阶段所指用于确认请求访问 Kubernetes API 用户是否为合法用户

  • Authorization 阶段所指的将是这个用户是否有对操作的资源的权限

  • Admission control 阶段所指控制对请求资源进行控制,通俗来说,就是一票否决权,即使前两个步骤完成

到这里了解到了 Kubernetes API 实际上做的工作就是 “人类用户” 与 kubernetes service account[1];那么就引出了一个重要概念就是 “用户” 在 Kubernetes 中是什么,以及用户在认证中的也是本章节的中心。

在 Kubernetes 官方手册中给出了 ”用户“  的概念,Kubernetes 集群中存在的用户包括 ”普通用户“ 与 “service account” 但是 Kubernetes  没有普通用户的管理方式,只是将使用集群的证书 CA 签署的有效证书的用户都被视为合法用户。

那么对于使得 Kubernetes 集群有一个真正的用户系统,就可以根据上面给出的概念将 Kubernetes 用户分为 ”外部用户“ 与  ”内部用户“。如何理解外部与内部用户呢?实际上就是有 Kubernetes 管理的用户,即在 kubernetes 定义用户的数据模型这种为  “内部用户” ,正如 service account;反之,非 Kubernetes 托管的用户则为 ”外部用户“  这中概念也更好的对 kubernetes 用户的阐述。

对于外部用户来说,实际上 Kubernetes 给出了多种用户概念[2],例如:

  • 拥有 kubernetes 集群证书的用户

  • 拥有 Kubernetes 集群 token 的用户(--token-auth-file 指定的静态 token)

  • 用户来自外部用户系统,例如 OpenID,LDAP,QQ connect, google identity platform 等

向外部用户授权集群访问的示例

场景 1:通过证书请求 k8s

该场景中 kubernetes 将使用证书中的 cn 作为用户,ou 作为组,如果对应 rolebinding/clusterrolebinding 给予该用户权限,那么请求为合法

  1. $ curl https://hostname:6443/api/v1/pods \
  2.  --cert ./client.pem \
  3.  --key ./client-key.pem \
  4.  --cacert ./ca.pem

接下来浅析下在代码中做的事情

确认用户是 apiserverAuthentication 阶段 做的事情,而对应代码在 pkg/kubeapiserver/authenticator[3] 下,整个文件就是构建了一系列的认证器,而 x.509 证书指是其中一个

  1. // 创建一个认证器,返回请求或一个k8s认证机制的标准错误
  2. func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, error) {
  3. ...
  4.  // X509 methods
  5.     // 可以看到这里就是将x509证书解析为user
  6.  if config.ClientCAContentProvider != nil {
  7.   certAuth := x509.NewDynamic(config.ClientCAContentProvider.VerifyOptions, x509.CommonNameUserConversion)
  8.   authenticators = append(authenticators, certAuth)
  9.  }
  10. ...

接下来看实现原理,NewDynamic 函数位于代码 k8s.io/apiserver/pkg/authentication/request/x509/x509.go[4]

通过代码可以看出,是通过一个验证函数与用户来解析为一个 Authenticator

  1. // NewDynamic returns a request.Authenticator that verifies client certificates using the provided
  2. // VerifyOptionFunc (which may be dynamic), and converts valid certificate chains into user.Info using the provided UserConversion
  3. func NewDynamic(verifyOptionsFn VerifyOptionFunc, user UserConversion) *Authenticator {
  4.  return &Authenticator{verifyOptionsFn, user}
  5. }

验证函数为 CAContentProvider 的方法,而 x509 部分实现为 k8s.io/apiserver/pkg/server/dynamiccertificates/dynamic_cafile_content.go.VerifyOptions[5];可以看出返回是一个 x509.VerifyOptions + 与认证的状态

  1. // VerifyOptions provides verifyoptions compatible with authenticators
  2. func (c *DynamicFileCAContent) VerifyOptions() (x509.VerifyOptions, bool) {
  3.  uncastObj := c.caBundle.Load()
  4.  if uncastObj == nil {
  5.   return x509.VerifyOptions{}, false
  6.  }
  7.  return uncastObj.(*caBundleAndVerifier).verifyOptions, true
  8. }

而用户的获取则位于  k8s.io/apiserver/pkg/authentication/request/x509/x509.go[6];可以看出,用户正是拿的证书的 CN,而组则是为证书的 OU

  1. // CommonNameUserConversion builds user info from a certificate chain using the subject's CommonName
  2. var CommonNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (*authenticator.Response, boolerror) {
  3.  if len(chain[0].Subject.CommonName) == 0 {
  4.   return nilfalsenil
  5.  }
  6.  return &authenticator.Response{
  7.   User: &user.DefaultInfo{
  8.    Name:   chain[0].Subject.CommonName,
  9.    Groups: chain[0].Subject.Organization,
  10.   },
  11.  }, truenil
  12. })

由于授权不在本章范围内,直接忽略至入库阶段,入库阶段由 RESTStorageProvider[7] 实现 这里,每一个 Provider 都提供了 Authenticator 这里包含了已经允许的请求,将会被对应的 REST 客户端写入到库中

  1. type RESTStorageProvider struct {
  2.  Authenticator authenticator.Request
  3.  APIAudiences  authenticator.Audiences
  4. }
  5. // RESTStorageProvider is a factory type for REST storage.
  6. type RESTStorageProvider interface {
  7.  GroupName() string
  8.  NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, error)
  9. }

场景 2:通过 token

该场景中,当 kube-apiserver 开启了 --enable-bootstrap-token-auth 时,就可以使用 Bootstrap Token 进行认证,通常如下列命令,在请求头中增加 Authorization: Bearer <token> 标识

  1. $ curl https://hostname:6443/api/v1/pods \
  2.   --cacert ${CACERT} \
  3.   --header "Authorization: Bearer <token>" \

接下来浅析下在代码中做的事情

可以看到,在代码 pkg/kubeapiserver/authenticator.New()[8] 中当 kube-apiserver 指定了参数 --token-auth-file=/etc/kubernetes/token.csv" 这种认证会被激活

  1. if len(config.TokenAuthFile) > 0 {
  2.     tokenAuth, err := newAuthenticatorFromTokenFile(config.TokenAuthFile)
  3.     if err != nil {
  4.         return nilnil, err
  5.     }
  6.     tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, tokenAuth))
  7. }

此时打开 token.csv 查看下 token 长什么样

  1. $ cat /etc/kubernetes/token.csv
  2. 12ba4f.d82a57a4433b2359,"system:bootstrapper",10001,"system:bootstrappers"

这里回到代码 k8s.io/apiserver/pkg/authentication/token/tokenfile/tokenfile.go.NewCSV[9] ,这里可以看出,就是读取 --token-auth-file= 参数指定的 tokenfile,然后解析为用户,record[1] 作为用户名,record[2] 作为 UID

  1. // NewCSV returns a TokenAuthenticator, populated from a CSV file.
  2. // The CSV file must contain records in the format "token,username,useruid"
  3. func NewCSV(path string) (*TokenAuthenticator, error) {
  4.  file, err := os.Open(path)
  5.  if err != nil {
  6.   return nil, err
  7.  }
  8.  defer file.Close()
  9.  recordNum := 0
  10.  tokens := make(map[string]*user.DefaultInfo)
  11.  reader := csv.NewReader(file)
  12.  reader.FieldsPerRecord = -1
  13.  for {
  14.   record, err := reader.Read()
  15.   if err == io.EOF {
  16.    break
  17.   }
  18.   if err != nil {
  19.    return nil, err
  20.   }
  21.   if len(record) < 3 {
  22.    return nil, fmt.Errorf("token file '%s' must have at least 3 columns (token, user name, user uid), found %d", path, len(record))
  23.   }
  24.   recordNum++
  25.   if record[0] == "" {
  26.    klog.Warningf("empty token has been found in token file '%s', record number '%d'", path, recordNum)
  27.    continue
  28.   }
  29.   obj := &user.DefaultInfo{
  30.    Name: record[1],
  31.    UID:  record[2],
  32.   }
  33.   if _, exist := tokens[record[0]]; exist {
  34.    klog.Warningf("duplicate token has been found in token file '%s', record number '%d'", path, recordNum)
  35.   }
  36.   tokens[record[0]] = obj
  37.   if len(record) >= 4 {
  38.    obj.Groups = strings.Split(record[3], ",")
  39.   }
  40.  }
  41.  return &TokenAuthenticator{
  42.   tokens: tokens,
  43.  }, nil
  44. }

而 token file 中配置的格式正是以逗号分隔的一组字符串,

  1. type DefaultInfo struct {
  2.  Name   string
  3.  UID    string
  4.  Groups []string
  5.  Extra  map[string][]string
  6. }

这种用户最常见的方式就是 kubelet 通常会以此类用户向控制平面进行身份认证,例如下列配置

  1. KUBELET_ARGS="--v=0 \
  2.     --logtostderr=true \
  3.     --config=/etc/kubernetes/kubelet-config.yaml \
  4.     --kubeconfig=/etc/kubernetes/auth/kubelet.conf \
  5.     --network-plugin=cni \
  6.     --pod-infra-container-image=registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.1 \
  7.     --bootstrap-kubeconfig=/etc/kubernetes/auth/bootstrap.conf"

/etc/kubernetes/auth/bootstrap.conf 内容,这里就用到了 kube-apiserver 配置的 --token-auth-file= 用户名,组必须为 system:bootstrappers

  1. apiVersion: v1
  2. clusters:
  3. - cluster:
  4.     certificate-authority-data: ......
  5.     server: https://10.0.0.4:6443
  6.   name: kubernetes
  7. contexts:
  8. - context:
  9.     cluster: kubernetes
  10.     user: system:bootstrapper
  11.   name: system:bootstrapper@kubernetes
  12. current-context: system:bootstrapper@kubernetes
  13. kind: Config
  14. preferences: {}
  15. users:
  16. - name: system:bootstrapper

而通常在二进制部署时会出现的问题,例如下列错误

Unable to register node "hostname" with API server: nodes is forbidden: User "system:anonymous" cannot create resource "nodes" in API group "" at the cluster scope

而通常解决方法是执行下列命令,这里就是将 kubeletkube-apiserver 通讯时的用户授权,因为 kubernetes 官方给出的条件是,用户组必须为 system:bootstrappers[10]

$ kubectl create clusterrolebinding kubelet-bootstrap --clusterrole=system:node-bootstrapper --group=system:bootstrappers

生成的 clusterrolebinding 如下

  1. apiVersion: rbac.authorization.k8s.io/v1
  2. kind: ClusterRoleBinding
  3. metadata:
  4.   creationTimestamp: "2022-08-14T22:26:51Z"
  5.   managedFields:
  6.   - apiVersion: rbac.authorization.k8s.io/v1
  7.     fieldsType: FieldsV1
  8.    ...
  9.     time: "2022-08-14T22:26:51Z"
  10.   name: kubelet-bootstrap
  11.   resourceVersion: "158"
  12.   selfLink: /apis/rbac.authorization.k8s.io/v1/clusterrolebindings/kubelet-bootstrap
  13.   uid: b4d70f4f-4ae0-468f-86b7-55e9351e4719
  14. roleRef:
  15.   apiGroup: rbac.authorization.k8s.io
  16.   kind: ClusterRole
  17.   name: system:node-bootstrapper
  18. subjects:
  19. - apiGroup: rbac.authorization.k8s.io
  20.   kind: Group
  21.   name: system:bootstrappers

上述就是 bootstrap token,翻译后就是引导 token,因为其做的工作就是将节点载入 Kubernetes 系统过程提供认证机制的用户。

Notes:这种用户不存在与 kubernetes 内,可以算属于一个外部用户,但认证机制中存在并绑定了最高权限,也可以用来做其他访问时的认证

场景 3:serviceaccount

serviceaccount 通常为 API 自动创建的,但在用户中,实际上认证存在两个方向,一个是 --service-account-key-file 这个参数可以指定多个,指定对应的证书文件公钥或私钥,用以办法 sa 的 token

首先会根据指定的公钥或私钥文件生成 token

  1. if len(config.ServiceAccountKeyFiles) > 0 {
  2.     serviceAccountAuth, err := newLegacyServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.APIAudiences, config.ServiceAccountTokenGetter)
  3.     if err != nil {
  4.         return nilnil, err
  5.     }
  6.     tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
  7. }
  8. if len(config.ServiceAccountIssuers) > 0 {
  9.     serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuers, config.ServiceAccountKeyFiles, config.APIAudiences, config.ServiceAccountTokenGetter)
  10.     if err != nil {
  11.         return nilnil, err
  12.     }
  13.     tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
  14. }

对于  --service-account-key-file  他生成的用户都是 “kubernetes/serviceaccount”  , 而对于 --service-account-issuer 只是对 sa 颁发者提供了一个称号标识是谁,而不是统一的 “kubernetes/serviceaccount” ,这里可以从代码中看到,两者是完全相同的,只是称号不同罢了

  1. // newLegacyServiceAccountAuthenticator returns an authenticator.Token or an error
  2. func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
  3.  allPublicKeys := []interface{}{}
  4.  for _, keyfile := range keyfiles {
  5.   publicKeys, err := keyutil.PublicKeysFromFile(keyfile)
  6.   if err != nil {
  7.    return nil, err
  8.   }
  9.   allPublicKeys = append(allPublicKeys, publicKeys...)
  10.  }
  11. // 唯一的区别 这里使用了常量 serviceaccount.LegacyIssuer
  12.  tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]string{serviceaccount.LegacyIssuer}, allPublicKeys, apiAudiences, serviceaccount.NewLegacyValidator(lookup, serviceAccountGetter))
  13.  return tokenAuthenticator, nil
  14. }
  15. // newServiceAccountAuthenticator returns an authenticator.Token or an error
  16. func newServiceAccountAuthenticator(issuers []string, keyfiles []string, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
  17.  allPublicKeys := []interface{}{}
  18.  for _, keyfile := range keyfiles {
  19.   publicKeys, err := keyutil.PublicKeysFromFile(keyfile)
  20.   if err != nil {
  21.    return nil, err
  22.   }
  23.   allPublicKeys = append(allPublicKeys, publicKeys...)
  24.  }
  25. // 唯一的区别 这里根据kube-apiserver提供的称号指定名称
  26.  tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(issuers, allPublicKeys, apiAudiences, serviceaccount.NewValidator(serviceAccountGetter))
  27.  return tokenAuthenticator, nil
  28. }

最后根据 ServiceAccounts,Secrets 等值签发一个 token,也就是通过下列命令获取的值

$ kubectl get secret multus-token-v6bfg -n kube-system -o jsonpath={".data.token"}

场景 4:openid

OpenID Connect 是 OAuth2 风格,允许用户授权三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容,下面是一张 kubernetes 使用 OID 认证的逻辑图

a3c069f48c818ae6e883f6daf212f812.png
图:Kubernetes OID 认证

场景 5:webhook

webhook 是 kubernetes 提供自定义认证的其中一种,主要是用于认证 “不记名 token“ 的钩子,“不记名 token“ 将 由身份验证服务创建。当用户对 kubernetes 访问时,会触发准入控制,当对 kubernetes 集群注册了 authenticaion webhook 时,将会使用该 webhook 提供的方式进行身份验证时,此时会为您生成一个 token 。

如代码 pkg/kubeapiserver/authenticator.New()[11]  中所示 newWebhookTokenAuthenticator 会通过提供的 config (--authentication-token-webhook-config-file) 来创建出一个 WebhookTokenAuthenticator

  1. if len(config.WebhookTokenAuthnConfigFile) > 0 {
  2.     webhookTokenAuth, err := newWebhookTokenAuthenticator(config)
  3.     if err != nil {
  4.         return nilnil, err
  5.     }
  6.     tokenAuthenticators = append(tokenAuthenticators, webhookTokenAuth)
  7. }

下图是 kubernetes 中 WebhookToken 验证的工作原理

963ad58a9aa8cc18431921365c750cb8.png
图:kubernetes WebhookToken 验证原理

最后由 token 中的 authHandler,循环所有的 Handlers 在运行 AuthenticateToken 去进行获取用户的信息

  1. func (authHandler *unionAuthTokenHandler) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, boolerror) {
  2.    var errlist []error
  3.    for _, currAuthRequestHandler := range authHandler.Handlers {
  4.       info, ok, err := currAuthRequestHandler.AuthenticateToken(ctx, token)
  5.       if err != nil {
  6.          if authHandler.FailOnError {
  7.             return info, ok, err
  8.          }
  9.          errlist = append(errlist, err)
  10.          continue
  11.       }
  12.       if ok {
  13.          return info, ok, err
  14.       }
  15.    }
  16.    return nilfalse, utilerrors.NewAggregate(errlist)
  17. }

而 webhook 插件也实现了这个方法 AuthenticateToken , 这里会通过 POST 请求,调用注入的 webhook,该请求携带一个 JSON 格式的 TokenReview 对象,其中包含要验证的令牌

  1. func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, boolerror) {
  2.     ....
  3.   start := time.Now()
  4.   result, statusCode, tokenReviewErr = w.tokenReview.Create(ctx, r, metav1.CreateOptions{})
  5.   latency := time.Since(start)
  6. ...
  7. }

webhook token 认证服务要返回用户的身份信息[12],就是上面 token 部分提到的数据结构(webhook 来决定接受还是拒绝该用户)

  1. type DefaultInfo struct {
  2.  Name   string
  3.  UID    string
  4.  Groups []string
  5.  Extra  map[string][]string
  6. }

场景 6:代理认证

实验:基于 LDAP 的身份认证

通过上面阐述,大致了解到 kubernetes 认证框架中的用户的分类以及认证的策略由哪些,实验的目的也是为了阐述一个结果,就是使用 OIDC/webhook 是比其他方式更好的保护,管理 kubernetes 集群。首先在安全上,假设网络环境是不安全的,那么任意 node 节点遗漏 bootstrap  token 文件,就意味着拥有了集群中最高权限;其次在管理上,越大的团队,人数越多,不可能每个用户都提供单独的证书或者 token,要知道传统教程中讲到 token 在 kubernetes 集群中是永久有效的,除非你删除了这个 secret/sa;而 Kubernetes 提供的插件就很好的解决了这些问题。

实验环境

  • 一个 kubernetes 集群

  • 一个 openldap 服务,建议可以是集群外部的,因为 webhook 不像 SSSD 有缓存机制,并且集群不可用,那么认证不可用,当认证不可用时会导致集群不可用,这样事故影响的范围可以得到控制,也叫最小化半径

  • 了解 ldap 相关技术,并了解 go ldap 客户端

实验大致分为以下几个步骤

  • 建立一个 HTTP 服务器用于返回给 kubernetes Authenticaion 服务

  • 查询 ldap 该用户是否合法

    • 查询用户是否合法

    • 查询用户所属组是否拥有权限

实验开始

初始化用户数据

首先准备 openldap 初始化数据,创建三个 posixGroup 组,与 5 个用户 admin, admin1, admin11, searchUser, syncUser 密码均为 111,组与用户关联使用的 memberUid

  1. $ cat << EOF | ldapdelete -r  -H ldap://10.0.0.3 -D "cn=admin,dc=test,dc=com" -w 111
  2. dn: dc=test,dc=com
  3. objectClass: top
  4. objectClass: organizationalUnit
  5. objectClass: extensibleObject
  6. description: US Organization
  7. ou: people
  8. dn: ou=tvb,dc=test,dc=com
  9. objectClass: organizationalUnit
  10. description: Television Broadcasts Limited
  11. ou: tvb
  12. dn: cn=admin,ou=tvb,dc=test,dc=com
  13. objectClass: posixGroup
  14. gidNumber: 10000
  15. cn: admin
  16. dn: cn=conf,ou=tvb,dc=test,dc=com
  17. objectClass: posixGroup
  18. gidNumber: 10001
  19. cn: conf
  20. dn: cn=dir,ou=tvb,dc=test,dc=com
  21. objectClass: posixGroup
  22. gidNumber: 10002
  23. cn: dir
  24. dn: uid=syncUser,ou=tvb,dc=test,dc=com
  25. objectClass: inetOrgPerson
  26. objectClass: organizationalPerson
  27. objectClass: person
  28. objectClass: posixAccount
  29. objectClass: shadowAccount
  30. objectClass: pwdPolicy
  31. pwdAttribute: userPassword
  32. uid: syncUser
  33. cn: syncUser
  34. uidNumber: 10006
  35. gidNumber: 10002
  36. homeDirectory: /home/syncUser
  37. loginShell: /bin/bash
  38. sn: syncUser
  39. givenName: syncUser
  40. memberOf: cn=confGroup,ou=tvb,dc=test,dc=com
  41. dn: uid=searchUser,ou=tvb,dc=test,dc=com
  42. objectClass: inetOrgPerson
  43. objectClass: organizationalPerson
  44. objectClass: person
  45. objectClass: posixAccount
  46. objectClass: shadowAccount
  47. objectClass: pwdPolicy
  48. pwdAttribute: userPassword
  49. uid: searchUser
  50. cn: searchUser
  51. uidNumber: 10005
  52. gidNumber: 10001
  53. homeDirectory: /home/searchUser
  54. loginShell: /bin/bash
  55. sn: searchUser
  56. givenName: searchUser
  57. memberOf: cn=dirGroup,ou=tvb,dc=test,dc=com
  58. dn: uid=admin1,ou=tvb,dc=test,dc=com
  59. objectClass: inetOrgPerson
  60. objectClass: organizationalPerson
  61. objectClass: person
  62. objectClass: posixAccount
  63. objectClass: shadowAccount
  64. objectClass: pwdPolicy
  65. pwdAttribute: userPassword
  66. uid: admin1
  67. sn: admin1
  68. cn: admin
  69. uidNumber: 10010
  70. gidNumber: 10000
  71. homeDirectory: /home/admin
  72. loginShell: /bin/bash
  73. givenName: admin
  74. memberOf: cn=adminGroup,ou=tvb,dc=test,dc=com
  75. dn: uid=admin11,ou=tvb,dc=test,dc=com
  76. objectClass: inetOrgPerson
  77. objectClass: organizationalPerson
  78. objectClass: person
  79. objectClass: posixAccount
  80. objectClass: shadowAccount
  81. objectClass: pwdPolicy
  82. sn: admin11
  83. pwdAttribute: userPassword
  84. uid: admin11
  85. cn: admin11
  86. uidNumber: 10011
  87. gidNumber: 10000
  88. homeDirectory: /home/admin
  89. loginShell: /bin/bash
  90. givenName: admin11
  91. memberOf: cn=adminGroup,ou=tvb,dc=test,dc=com
  92. dn: uid=admin,ou=tvb,dc=test,dc=com
  93. objectClass: inetOrgPerson
  94. objectClass: organizationalPerson
  95. objectClass: person
  96. objectClass: posixAccount
  97. objectClass: shadowAccount
  98. objectClass: pwdPolicy
  99. pwdAttribute: userPassword
  100. uid: admin
  101. cn: admin
  102. uidNumber: 10009
  103. gidNumber: 10000
  104. homeDirectory: /home/admin
  105. loginShell: /bin/bash
  106. sn: admin
  107. givenName: admin
  108. memberOf: cn=adminGroup,ou=tvb,dc=test,dc=com
  109. EOF

接下来需要确定如何为认证成功的用户,上面讲到对于 kubernetes 中用户格式为 v1.UserInfo 的格式,即要获得用户,即用户组,假设需要查找的用户为,admin,那么在 openldap 中查询 filter 如下:

"(|(&(objectClass=posixAccount)(uid=admin))(&(objectClass=posixGroup)(memberUid=admin)))"

上面语句意思是,找到 objectClass=posixAccount 并且 uid=admin 或者 objectClass=posixGroup 并且 memberUid=admin 的条目信息,这里使用 ”|“ 与 ”&“ 是为了要拿到这两个结果。

编写 webhook 查询用户部分

这里由于 openldap 配置密码保存格式不是明文的,如果直接使用 ”=“ 来验证是查询不到内容的,故直接多用了一次登录来验证用户是否合法

  1. func ldapSearch(username, password string) (*v1.UserInfo, error) {
  2.  ldapconn, err := ldap.DialURL(ldapURL)
  3.  if err != nil {
  4.   klog.V(3).Info(err)
  5.   return nil, err
  6.  }
  7.  defer ldapconn.Close()
  8.  // Authenticate as LDAP admin user
  9.  err = ldapconn.Bind("uid=searchUser,ou=tvb,dc=test,dc=com""111")
  10.  if err != nil {
  11.   klog.V(3).Info(err)
  12.   return nil, err
  13.  }
  14.  // Execute LDAP Search request
  15.  result, err := ldapconn.Search(ldap.NewSearchRequest(
  16.   "ou=tvb,dc=test,dc=com",
  17.   ldap.ScopeWholeSubtree,
  18.   ldap.NeverDerefAliases,
  19.   0,
  20.   0,
  21.   false,
  22.   fmt.Sprintf("(&(objectClass=posixGroup)(memberUid=%s))", username), // Filter
  23.   nil,
  24.   nil,
  25.  ))
  26.  if err != nil {
  27.   klog.V(3).Info(err)
  28.   return nil, err
  29.  }
  30.  userResult, err := ldapconn.Search(ldap.NewSearchRequest(
  31.   "ou=tvb,dc=test,dc=com",
  32.   ldap.ScopeWholeSubtree,
  33.   ldap.NeverDerefAliases,
  34.   0,
  35.   0,
  36.   false,
  37.   fmt.Sprintf("(&(objectClass=posixAccount)(uid=%s))", username), // Filter
  38.   nil,
  39.   nil,
  40.  ))
  41.  if err != nil {
  42.   klog.V(3).Info(err)
  43.   return nil, err
  44.  }
  45.  if len(result.Entries) == 0 {
  46.   klog.V(3).Info("User does not exist")
  47.   return nil, errors.New("User does not exist")
  48.  } else {
  49.   // 验证用户名密码是否正确
  50.   if err := ldapconn.Bind(userResult.Entries[0].DN, password); err != nil {
  51.    e := fmt.Sprintf("Failed to auth. %s\n", err)
  52.    klog.V(3).Info(e)
  53.    return nil, errors.New(e)
  54.   } else {
  55.    klog.V(3).Info(fmt.Sprintf("User %s Authenticated successfuly!", username))
  56.   }
  57.   // 拼接为kubernetes authentication 的用户格式
  58.   user := new(v1.UserInfo)
  59.   for _, v := range result.Entries {
  60.    attrubute := v.GetAttributeValue("objectClass")
  61.    if strings.Contains(attrubute, "posixGroup") {
  62.     user.Groups = append(user.Groups, v.GetAttributeValue("cn"))
  63.    }
  64.   }
  65.   u := userResult.Entries[0].GetAttributeValue("uid")
  66.   user.UID = u
  67.   user.Username = u
  68.   return user, nil
  69.  }
  70. }
编写 HTTP 部分

这里有几个需要注意的部分,即用户或者理解为要认证的 token 的定义,此处使用了 ”username@password“ 格式作为用户的辨别,即登录 kubernetes 时需要直接输入 ”username@password“ 来作为登录的凭据。

第二个部分为返回值,返回给 Kubernetes 的格式必须为 api/authentication/v1.TokenReview 格式,Status.Authenticated 表示用户身份验证结果,如果该用户合法,则设置 tokenReview.Status.Authenticated = true 反之亦然。如果验证成功还需要 Status.User 这就是在 ldapSearch

  1. func serve(w http.ResponseWriter, r *http.Request) {
  2.  b, err := ioutil.ReadAll(r.Body)
  3.  if err != nil {
  4.   httpError(w, err)
  5.   return
  6.  }
  7.  klog.V(4).Info("Receiving: %s\n"string(b))
  8.  var tokenReview v1.TokenReview
  9.  err = json.Unmarshal(b, &tokenReview)
  10.  if err != nil {
  11.   klog.V(3).Info("Json convert err: ", err)
  12.   httpError(w, err)
  13.   return
  14.  }
  15.  // 提取用户名与密码
  16.  s := strings.SplitN(tokenReview.Spec.Token, "@"2)
  17.  if len(s) != 2 {
  18.   klog.V(3).Info(fmt.Errorf("badly formatted token: %s", tokenReview.Spec.Token))
  19.   httpError(w, fmt.Errorf("badly formatted token: %s", tokenReview.Spec.Token))
  20.   return
  21.  }
  22.  username, password := s[0], s[1]
  23.  // 查询ldap,验证用户是否合法
  24.  userInfo, err := ldapSearch(username, password)
  25.  if err != nil {
  26.   // 这里不打印日志的原因是 ldapSearch 中打印过了
  27.   return
  28.  }
  29.  // 设置返回的tokenReview
  30.  if userInfo == nil {
  31.   tokenReview.Status.Authenticated = false
  32.  } else {
  33.   tokenReview.Status.Authenticated = true
  34.   tokenReview.Status.User = *userInfo
  35.  }
  36.  b, err = json.Marshal(tokenReview)
  37.  if err != nil {
  38.   klog.V(3).Info("Json convert err: ", err)
  39.   httpError(w, err)
  40.   return
  41.  }
  42.  w.Write(b)
  43.  klog.V(3).Info("Returning: "string(b))
  44. }
  45. func httpError(w http.ResponseWriter, err error) {
  46.  err = fmt.Errorf("Error: %v", err)
  47.  w.WriteHeader(http.StatusInternalServerError) // 500
  48.  fmt.Fprintln(w, err)
  49.  klog.V(4).Info("httpcode 500: ", err)
  50. }

下面是完整的代码

  1. package main
  2. import (
  3.  "encoding/json"
  4.  "errors"
  5.  "flag"
  6.  "fmt"
  7.  "io/ioutil"
  8.  "net/http"
  9.  "strings"
  10.  "github.com/go-ldap/ldap"
  11.  "k8s.io/api/authentication/v1"
  12.  "k8s.io/klog/v2"
  13. )
  14. var ldapURL string
  15. func main() {
  16.  klog.InitFlags(nil)
  17.  flag.Parse()
  18.  http.HandleFunc("/authenticate", serve)
  19.  klog.V(4).Info("Listening on port 443 waiting for requests...")
  20.  klog.V(4).Info(http.ListenAndServe(":443"nil))
  21.  ldapURL = "ldap://10.0.0.10:389"
  22.  ldapSearch("admin""1111")
  23. }
  24. func serve(w http.ResponseWriter, r *http.Request) {
  25.  b, err := ioutil.ReadAll(r.Body)
  26.  if err != nil {
  27.   httpError(w, err)
  28.   return
  29.  }
  30.  klog.V(4).Info("Receiving: %s\n"string(b))
  31.  var tokenReview v1.TokenReview
  32.  err = json.Unmarshal(b, &tokenReview)
  33.  if err != nil {
  34.   klog.V(3).Info("Json convert err: ", err)
  35.   httpError(w, err)
  36.   return
  37.  }
  38.  // 提取用户名与密码
  39.  s := strings.SplitN(tokenReview.Spec.Token, "@"2)
  40.  if len(s) != 2 {
  41.   klog.V(3).Info(fmt.Errorf("badly formatted token: %s", tokenReview.Spec.Token))
  42.   httpError(w, fmt.Errorf("badly formatted token: %s", tokenReview.Spec.Token))
  43.   return
  44.  }
  45.  username, password := s[0], s[1]
  46.  // 查询ldap,验证用户是否合法
  47.  userInfo, err := ldapSearch(username, password)
  48.  if err != nil {
  49.   // 这里不打印日志的原因是 ldapSearch 中打印过了
  50.   return
  51.  }
  52.  // 设置返回的tokenReview
  53.  if userInfo == nil {
  54.   tokenReview.Status.Authenticated = false
  55.  } else {
  56.   tokenReview.Status.Authenticated = true
  57.   tokenReview.Status.User = *userInfo
  58.  }
  59.  b, err = json.Marshal(tokenReview)
  60.  if err != nil {
  61.   klog.V(3).Info("Json convert err: ", err)
  62.   httpError(w, err)
  63.   return
  64.  }
  65.  w.Write(b)
  66.  klog.V(3).Info("Returning: "string(b))
  67. }
  68. func httpError(w http.ResponseWriter, err error) {
  69.  err = fmt.Errorf("Error: %v", err)
  70.  w.WriteHeader(http.StatusInternalServerError) // 500
  71.  fmt.Fprintln(w, err)
  72.  klog.V(4).Info("httpcode 500: ", err)
  73. }
  74. func ldapSearch(username, password string) (*v1.UserInfo, error) {
  75.  ldapconn, err := ldap.DialURL(ldapURL)
  76.  if err != nil {
  77.   klog.V(3).Info(err)
  78.   return nil, err
  79.  }
  80.  defer ldapconn.Close()
  81.  // Authenticate as LDAP admin user
  82.  err = ldapconn.Bind("cn=admin,dc=test,dc=com""111")
  83.  if err != nil {
  84.   klog.V(3).Info(err)
  85.   return nil, err
  86.  }
  87.  // Execute LDAP Search request
  88.  result, err := ldapconn.Search(ldap.NewSearchRequest(
  89.   "ou=tvb,dc=test,dc=com",
  90.   ldap.ScopeWholeSubtree,
  91.   ldap.NeverDerefAliases,
  92.   0,
  93.   0,
  94.   false,
  95.   fmt.Sprintf("(&(objectClass=posixGroup)(memberUid=%s))", username), // Filter
  96.   nil,
  97.   nil,
  98.  ))
  99.  if err != nil {
  100.   klog.V(3).Info(err)
  101.   return nil, err
  102.  }
  103.  userResult, err := ldapconn.Search(ldap.NewSearchRequest(
  104.   "ou=tvb,dc=test,dc=com",
  105.   ldap.ScopeWholeSubtree,
  106.   ldap.NeverDerefAliases,
  107.   0,
  108.   0,
  109.   false,
  110.   fmt.Sprintf("(&(objectClass=posixAccount)(uid=%s))", username), // Filter
  111.   nil,
  112.   nil,
  113.  ))
  114.  if err != nil {
  115.   klog.V(3).Info(err)
  116.   return nil, err
  117.  }
  118.  if len(result.Entries) == 0 {
  119.   klog.V(3).Info("User does not exist")
  120.   return nil, errors.New("User does not exist")
  121.  } else {
  122.   // 验证用户名密码是否正确
  123.   if err := ldapconn.Bind(userResult.Entries[0].DN, password); err != nil {
  124.    e := fmt.Sprintf("Failed to auth. %s\n", err)
  125.    klog.V(3).Info(e)
  126.    return nil, errors.New(e)
  127.   } else {
  128.    klog.V(3).Info(fmt.Sprintf("User %s Authenticated successfuly!", username))
  129.   }
  130.   // 拼接为kubernetes authentication 的用户格式
  131.   user := new(v1.UserInfo)
  132.   for _, v := range result.Entries {
  133.    attrubute := v.GetAttributeValue("objectClass")
  134.    if strings.Contains(attrubute, "posixGroup") {
  135.     user.Groups = append(user.Groups, v.GetAttributeValue("cn"))
  136.    }
  137.   }
  138.   u := userResult.Entries[0].GetAttributeValue("uid")
  139.   user.UID = u
  140.   user.Username = u
  141.   return user, nil
  142.  }
  143. }

部署 webhook

kubernetes 官方手册中指出,启用 webhook 认证的标记是在 kube-apiserver 指定参数 --authentication-token-webhook-config-file 。而这个配置文件是一个 kubeconfig 类型的文件格式[13]

下列是部署在 kubernetes 集群外部的配置。

创建一个给 kube-apiserver 使用的配置文件 /etc/kubernetes/auth/authentication-webhook.conf

  1. apiVersion: v1
  2. kind: Config
  3. clusters:
  4. - cluster:
  5.     server: http://10.0.0.1:88/authenticate
  6.   name: authenticator
  7. users:
  8. - name: webhook-authenticator
  9. current-context: webhook-authenticator@authenticator
  10. contexts:
  11. - context:
  12.     cluster: authenticator
  13.     user: webhook-authenticator
  14.   name: webhook-authenticator@authenticator

修改 kube-apiserver 参数

  1. # 指向对应的配置文件
  2. --authentication-token-webhook-config-file=/etc/kubernetes/auth/authentication-webhook.conf
  3. # 这个是token缓存时间,指的是用户在访问API时验证通过后在一定时间内无需在请求webhook进行认证了
  4. --authentication-token-webhook-cache-ttl=30m
  5. # 版本指定为API使用哪个版本?authentication.k8s.io/v1或v1beta1
  6. --authentication-token-webhook-version=v1

启动服务后,创建一个 kubeconfig 中的用户用于验证结果

  1. apiVersion: v1
  2. clusters:
  3. - cluster:
  4.     certificate-authority-data: 
  5.     server: https://10.0.0.4:6443
  6.   name: kubernetes
  7. contexts:
  8. - context:
  9.     cluster: kubernetes
  10.     user: k8s-admin
  11.   name: k8s-admin@kubernetes
  12. current-context: k8s-admin@kubernetes
  13. kind: Config
  14. preferences: {}
  15. users:
  16. - name: admin
  17.   user: 
  18.     token: admin@111

验证结果

当密码不正确时,使用用户 admin 请求集群

  1. $ kubectl get pods --user=admin
  2. error: You must be logged in to the server (Unauthorized)

当密码正确时,使用用户 admin 请求集群

  1. $ kubectl get pods --user=admin
  2. Error from server (Forbidden): pods is forbidden: User "admin" cannot list resource "pods" in API group "" in the namespace "default"

可以看到 admin 用户是一个不存在与集群中的用户,并且提示没有权限操作对应资源,此时将 admin 用户与集群中的 cluster-admin 绑定,测试结果

  1. $ kubectl create clusterrolebinding admin \
  2.  --clusterrole=cluster-admin \
  3.  --group=admin

此时再尝试使用 admin 用户访问集群

  1. $ kubectl get pods --user=admin
  2. NAME                      READY   STATUS    RESTARTS   AGE
  3. netbox-85865d5556-hfg6v   1/1     Running   0          91d
  4. netbox-85865d5556-vlgr4   1/1     Running   0          91d

总结

kubernetes authentication  插件提供的功能可以注入一个认证系统,这样可以完美解决了 kubernetes 中用户的问题,而这些用户并不存在与 kubernetes 中,并且也无需为多个用户准备大量 serviceaccount 或者证书,也可以完成鉴权操作。首先返回值标准如下所示,如果 kubernetes 集群有对在其他用户系统中获得的 Groups 并建立了 clusterrolebindingrolebinding 那么这个组的所有用户都将有这些权限。管理员只需要维护与公司用户系统中组同样多的 clusterrole 与 clusterrolebinding 即可

  1. type DefaultInfo struct {
  2.  Name   string
  3.  UID    string
  4.  Groups []string
  5.  Extra  map[string][]string
  6. }

对于如何将 kubernetes 与其他平台进行融合可以参考 基于 Kubernetes 的 PaaS 平台提供 dashboard 支持的一种方案[14]

引用链接

[1]

kubernetes service account: https://kubernetes.io/docs/concepts/security/controlling-access/

[2]

Kubernetes 给出了多种用户概念: https://kubernetes.io/docs/reference/access-authn-authz/authentication/#users-in-kubernetes

[3]

pkg/kubeapiserver/authenticator: https://www.cnblogs.com/Cylon/p/pkg/kubeapiserver/authenticator

[4]

k8s.io/apiserver/pkg/authentication/request/x509/x509.go: https://github.com/kubernetes/kubernetes/blob/fdc77503e954d1ee641c0e350481f7528e8d068b/staging/src/k8s.io/apiserver/pkg/authentication/request/x509/x509.go#L126-L130

[5]

k8s.io/apiserver/pkg/server/dynamiccertificates/dynamic_cafile_content.go.VerifyOptions: https://github.com/kubernetes/kubernetes/blob/fdc77503e954d1ee641c0e350481f7528e8d068b/staging/src/k8s.io/apiserver/pkg/server/dynamiccertificates/dynamic_cafile_content.go#L253-L261

[6]

k8s.io/apiserver/pkg/authentication/request/x509/x509.go: https://github.com/kubernetes/kubernetes/blob/fdc77503e954d1ee641c0e350481f7528e8d068b/staging/src/k8s.io/apiserver/pkg/authentication/request/x509/x509.go#L248-L258

[7]

RESTStorageProvider: https://github.com/kubernetes/kubernetes/blob/fdc77503e954d1ee641c0e350481f7528e8d068b/pkg/controlplane/instance.go#L561

[8]

pkg/kubeapiserver/authenticator.New(): https://github.com/kubernetes/kubernetes/tree/fdc77503e954d1ee641c0e350481f7528e8d068b/pkg/kubeapiserver/authenticator

[9]

k8s.io/apiserver/pkg/authentication/token/tokenfile/tokenfile.go.NewCSV: https://github.com/kubernetes/kubernetes/blob/fdc77503e954d1ee641c0e350481f7528e8d068b/staging/src/k8s.io/apiserver/pkg/authentication/token/tokenfile/tokenfile.go#L45-L91

[10]

system:bootstrappers: https://kubernetes.io/docs/reference/access-authn-authz/authentication/#bootstrap-tokens

[11]

pkg/kubeapiserver/authenticator.New(): https://github.com/kubernetes/kubernetes/blob/fdc77503e954d1ee641c0e350481f7528e8d068b/pkg/kubeapiserver/authenticator

[12]

用户的身份信息: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#userinfo-v1beta1-authentication-k8s-io

[13]

kubeconfig 类型的文件格式: https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication

[14]

基于 Kubernetes 的 PaaS 平台提供 dashboard 支持的一种方案: https://cylonchau.github.io/kubernetes-dashborad-based.html

04a5f781a4c2d20c3db7f5cc268860c1.gif

ddc496863b73dd1e6fcc5e744eeb4999.png

你可能还喜欢

点击下方图片即可阅读

彻底解决 K8s 节点本地存储被撑爆的问题

2022-11-22

4e4ed7ec0cc4920a9c68903124044f51.jpeg

Karmada 如何跨集群实现完整的自定义资源分发能力?

2022-11-21

f3e40a05c46c95f064d54455f02ab148.jpeg

Cilium 未来数据平面:支撑 100Gbit/s k8s 集群

2022-11-14

dc9079e0fe712e446c076165c3e61a8a.jpeg

Prometheus 官方记录片(中英双语),带你了解 Prometheus 的前世今生

2022-11-11

443fba863822e8b585eed40b8cecd8a0.jpeg

d5ffba4338c6c7578c791e95ab22babf.gif

云原生是一种信仰 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/578814
推荐阅读
相关标签