<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入理解 Kubernetes 中的用戶與身份認(rèn)證授權(quán)

          共 40026字,需瀏覽 81分鐘

           ·

          2022-11-24 22:06

          ?

          本文轉(zhuǎn)自 Cylon 的博客,原文:https://www.cnblogs.com/Cylon/p/16905335.html,版權(quán)歸原作者所有。歡迎投稿,投稿請(qǐng)?zhí)砑游⑿藕糜眩?strong style="color: rgb(50, 108, 229);">cloud-native-yang

          本章主要簡(jiǎn)單闡述 kubernetes 認(rèn)證相關(guān)原理,最后以實(shí)驗(yàn)來(lái)闡述 kubernetes 用戶系統(tǒng)的思路。

          主要內(nèi)容

          • 了解 kubernetes 各種認(rèn)證機(jī)制的原理
          • 了解 kubernetes 用戶的概念
          • 了解 kubernetes authentication webhook
          • 完成實(shí)驗(yàn),如何將其他用戶系統(tǒng)接入到 kubernetes 中的一個(gè)思路

          Kubernetes 認(rèn)證

          在 Kubernetes apiserver 對(duì)于認(rèn)證部分所描述的,對(duì)于所有用戶訪問(wèn) Kubernetes API(通過(guò)任何客戶端,客戶端庫(kù),kubectl 等)時(shí)都會(huì)經(jīng)歷 驗(yàn)證 (Authentication) , 授權(quán) (Authorization), 和準(zhǔn)入控制 (Admission control) 三個(gè)階段來(lái)完成對(duì) “用戶” 進(jìn)行授權(quán),整個(gè)流程正如下圖所示:

          圖:Kubernetes API 請(qǐng)求的請(qǐng)求處理步驟圖

          其中在大多數(shù)教程中,在對(duì)這三個(gè)階段所做的工作大致上為:

          • Authentication 階段所指用于確認(rèn)請(qǐng)求訪問(wèn) Kubernetes API 用戶是否為合法用戶
          • Authorization 階段所指的將是這個(gè)用戶是否有對(duì)操作的資源的權(quán)限
          • Admission control 階段所指控制對(duì)請(qǐng)求資源進(jìn)行控制,通俗來(lái)說(shuō),就是一票否決權(quán),即使前兩個(gè)步驟完成

          到這里了解到了 Kubernetes API 實(shí)際上做的工作就是 “人類用戶” 與 kubernetes service account[1];那么就引出了一個(gè)重要概念就是 “用戶” 在 Kubernetes 中是什么,以及用戶在認(rèn)證中的也是本章節(jié)的中心。

          在 Kubernetes 官方手冊(cè)中給出了 ”用戶“  的概念,Kubernetes 集群中存在的用戶包括 ”普通用戶“ 與 “service account” 但是 Kubernetes  沒(méi)有普通用戶的管理方式,只是將使用集群的證書 CA 簽署的有效證書的用戶都被視為合法用戶。

          那么對(duì)于使得 Kubernetes 集群有一個(gè)真正的用戶系統(tǒng),就可以根據(jù)上面給出的概念將 Kubernetes 用戶分為 ”外部用戶“ 與  ”內(nèi)部用戶“。如何理解外部與內(nèi)部用戶呢?實(shí)際上就是有 Kubernetes 管理的用戶,即在 kubernetes 定義用戶的數(shù)據(jù)模型這種為  “內(nèi)部用戶” ,正如 service account;反之,非 Kubernetes 托管的用戶則為 ”外部用戶“  這中概念也更好的對(duì) kubernetes 用戶的闡述。

          對(duì)于外部用戶來(lái)說(shuō),實(shí)際上 Kubernetes 給出了多種用戶概念[2],例如:

          • 擁有 kubernetes 集群證書的用戶
          • 擁有 Kubernetes 集群 token 的用戶(--token-auth-file 指定的靜態(tài) token)
          • 用戶來(lái)自外部用戶系統(tǒng),例如 OpenID,LDAP,QQ connect, google identity platform 等

          向外部用戶授權(quán)集群訪問(wèn)的示例

          場(chǎng)景 1:通過(guò)證書請(qǐng)求 k8s

          該場(chǎng)景中 kubernetes 將使用證書中的 cn 作為用戶,ou 作為組,如果對(duì)應(yīng) rolebinding/clusterrolebinding 給予該用戶權(quán)限,那么請(qǐng)求為合法

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

          接下來(lái)淺析下在代碼中做的事情

          確認(rèn)用戶是 apiserverAuthentication 階段 做的事情,而對(duì)應(yīng)代碼在 pkg/kubeapiserver/authenticator[3] 下,整個(gè)文件就是構(gòu)建了一系列的認(rèn)證器,而 x.509 證書指是其中一個(gè)

          // 創(chuàng)建一個(gè)認(rèn)證器,返回請(qǐng)求或一個(gè)k8s認(rèn)證機(jī)制的標(biāo)準(zhǔn)錯(cuò)誤
          func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, error) {

          ...

           // X509 methods
              // 可以看到這里就是將x509證書解析為user
           if config.ClientCAContentProvider != nil {
            certAuth := x509.NewDynamic(config.ClientCAContentProvider.VerifyOptions, x509.CommonNameUserConversion)
            authenticators = append(authenticators, certAuth)
           }
          ...

          接下來(lái)看實(shí)現(xiàn)原理,NewDynamic 函數(shù)位于代碼 k8s.io/apiserver/pkg/authentication/request/x509/x509.go[4]

          通過(guò)代碼可以看出,是通過(guò)一個(gè)驗(yàn)證函數(shù)與用戶來(lái)解析為一個(gè) Authenticator

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

          驗(yàn)證函數(shù)為 CAContentProvider 的方法,而 x509 部分實(shí)現(xiàn)為 k8s.io/apiserver/pkg/server/dynamiccertificates/dynamic_cafile_content.go.VerifyOptions[5];可以看出返回是一個(gè) x509.VerifyOptions + 與認(rèn)證的狀態(tài)

          // VerifyOptions provides verifyoptions compatible with authenticators
          func (c *DynamicFileCAContent) VerifyOptions() (x509.VerifyOptions, bool) {
           uncastObj := c.caBundle.Load()
           if uncastObj == nil {
            return x509.VerifyOptions{}, false
           }

           return uncastObj.(*caBundleAndVerifier).verifyOptions, true
          }

          而用戶的獲取則位于  k8s.io/apiserver/pkg/authentication/request/x509/x509.go[6];可以看出,用戶正是拿的證書的 CN,而組則是為證書的 OU

          // CommonNameUserConversion builds user info from a certificate chain using the subject's CommonName
          var CommonNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (*authenticator.Response, bool, error) {
           if len(chain[0].Subject.CommonName) == 0 {
            return nilfalsenil
           }
           return &authenticator.Response{
            User: &user.DefaultInfo{
             Name:   chain[0].Subject.CommonName,
             Groups: chain[0].Subject.Organization,
            },
           }, truenil
          })

          由于授權(quán)不在本章范圍內(nèi),直接忽略至入庫(kù)階段,入庫(kù)階段由 RESTStorageProvider[7] 實(shí)現(xiàn) 這里,每一個(gè) Provider 都提供了 Authenticator 這里包含了已經(jīng)允許的請(qǐng)求,將會(huì)被對(duì)應(yīng)的 REST 客戶端寫入到庫(kù)中

          type RESTStorageProvider struct {
           Authenticator authenticator.Request
           APIAudiences  authenticator.Audiences
          }
          // RESTStorageProvider is a factory type for REST storage.
          type RESTStorageProvider interface {
           GroupName() string
           NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, error)
          }

          場(chǎng)景 2:通過(guò) token

          該場(chǎng)景中,當(dāng) kube-apiserver 開啟了 --enable-bootstrap-token-auth 時(shí),就可以使用 Bootstrap Token 進(jìn)行認(rèn)證,通常如下列命令,在請(qǐng)求頭中增加 Authorization: Bearer <token> 標(biāo)識(shí)

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

          接下來(lái)淺析下在代碼中做的事情

          可以看到,在代碼 pkg/kubeapiserver/authenticator.New()[8] 中當(dāng) kube-apiserver 指定了參數(shù) --token-auth-file=/etc/kubernetes/token.csv" 這種認(rèn)證會(huì)被激活

          if len(config.TokenAuthFile) > 0 {
              tokenAuth, err := newAuthenticatorFromTokenFile(config.TokenAuthFile)
              if err != nil {
                  return nilnil, err
              }
              tokenAuthenticators = append(tokenAuthenticators, authenticator.WrapAudienceAgnosticToken(config.APIAudiences, tokenAuth))
          }

          此時(shí)打開 token.csv 查看下 token 長(zhǎng)什么樣

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

          這里回到代碼 k8s.io/apiserver/pkg/authentication/token/tokenfile/tokenfile.go.NewCSV[9] ,這里可以看出,就是讀取 --token-auth-file= 參數(shù)指定的 tokenfile,然后解析為用戶,record[1] 作為用戶名,record[2] 作為 UID

          // NewCSV returns a TokenAuthenticator, populated from a CSV file.
          // The CSV file must contain records in the format "token,username,useruid"
          func NewCSV(path string) (*TokenAuthenticator, error) {
           file, err := os.Open(path)
           if err != nil {
            return nil, err
           }
           defer file.Close()

           recordNum := 0
           tokens := make(map[string]*user.DefaultInfo)
           reader := csv.NewReader(file)
           reader.FieldsPerRecord = -1
           for {
            record, err := reader.Read()
            if err == io.EOF {
             break
            }
            if err != nil {
             return nil, err
            }
            if len(record) < 3 {
             return nil, fmt.Errorf("token file '%s' must have at least 3 columns (token, user name, user uid), found %d", path, len(record))
            }

            recordNum++
            if record[0] == "" {
             klog.Warningf("empty token has been found in token file '%s', record number '%d'", path, recordNum)
             continue
            }

            obj := &user.DefaultInfo{
             Name: record[1],
             UID:  record[2],
            }
            if _, exist := tokens[record[0]]; exist {
             klog.Warningf("duplicate token has been found in token file '%s', record number '%d'", path, recordNum)
            }
            tokens[record[0]] = obj

            if len(record) >= 4 {
             obj.Groups = strings.Split(record[3], ",")
            }
           }

           return &TokenAuthenticator{
            tokens: tokens,
           }, nil
          }

          而 token file 中配置的格式正是以逗號(hào)分隔的一組字符串,

          type DefaultInfo struct {
           Name   string
           UID    string
           Groups []string
           Extra  map[string][]string
          }

          這種用戶最常見的方式就是 kubelet 通常會(huì)以此類用戶向控制平面進(jìn)行身份認(rèn)證,例如下列配置

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

          /etc/kubernetes/auth/bootstrap.conf 內(nèi)容,這里就用到了 kube-apiserver 配置的 --token-auth-file= 用戶名,組必須為 system:bootstrappers

          apiVersion: v1
          clusters:
          - cluster:
              certificate-authority-data: ......
              server: https://10.0.0.4:6443
            name: kubernetes
          contexts:
          - context:
              cluster: kubernetes
              user: system:bootstrapper
            name: system:bootstrapper@kubernetes
          current-context: system:bootstrapper@kubernetes
          kind: Config
          preferences: {}
          users:
          - name: system:bootstrapper

          而通常在二進(jìn)制部署時(shí)會(huì)出現(xiàn)的問(wèn)題,例如下列錯(cuò)誤

          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

          而通常解決方法是執(zhí)行下列命令,這里就是將 kubeletkube-apiserver 通訊時(shí)的用戶授權(quán),因?yàn)?kubernetes 官方給出的條件是,用戶組必須為 system:bootstrappers[10]

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

          生成的 clusterrolebinding 如下

          apiVersion: rbac.authorization.k8s.io/v1
          kind: ClusterRoleBinding
          metadata:
            creationTimestamp: "2022-08-14T22:26:51Z"
            managedFields:
            - apiVersion: rbac.authorization.k8s.io/v1
              fieldsType: FieldsV1
             ...
              time: "2022-08-14T22:26:51Z"
            name: kubelet-bootstrap
            resourceVersion: "158"
            selfLink: /apis/rbac.authorization.k8s.io/v1/clusterrolebindings/kubelet-bootstrap
            uid: b4d70f4f-4ae0-468f-86b7-55e9351e4719
          roleRef:
            apiGroup: rbac.authorization.k8s.io
            kind: ClusterRole
            name: system:node-bootstrapper
          subjects:
          - apiGroup: rbac.authorization.k8s.io
            kind: Group
            name: system:bootstrappers

          上述就是 bootstrap token,翻譯后就是引導(dǎo) token,因?yàn)槠渥龅墓ぷ骶褪菍⒐?jié)點(diǎn)載入 Kubernetes 系統(tǒng)過(guò)程提供認(rèn)證機(jī)制的用戶。

          ?

          Notes:這種用戶不存在與 kubernetes 內(nèi),可以算屬于一個(gè)外部用戶,但認(rèn)證機(jī)制中存在并綁定了最高權(quán)限,也可以用來(lái)做其他訪問(wèn)時(shí)的認(rèn)證

          場(chǎng)景 3:serviceaccount

          serviceaccount 通常為 API 自動(dòng)創(chuàng)建的,但在用戶中,實(shí)際上認(rèn)證存在兩個(gè)方向,一個(gè)是 --service-account-key-file 這個(gè)參數(shù)可以指定多個(gè),指定對(duì)應(yīng)的證書文件公鑰或私鑰,用以辦法 sa 的 token

          首先會(huì)根據(jù)指定的公鑰或私鑰文件生成 token

          if len(config.ServiceAccountKeyFiles) > 0 {
              serviceAccountAuth, err := newLegacyServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.APIAudiences, config.ServiceAccountTokenGetter)
              if err != nil {
                  return nilnil, err
              }
              tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
          }
          if len(config.ServiceAccountIssuers) > 0 {
              serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuers, config.ServiceAccountKeyFiles, config.APIAudiences, config.ServiceAccountTokenGetter)
              if err != nil {
                  return nilnil, err
              }
              tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
          }

          對(duì)于  --service-account-key-file  他生成的用戶都是 “kubernetes/serviceaccount”  , 而對(duì)于 --service-account-issuer 只是對(duì) sa 頒發(fā)者提供了一個(gè)稱號(hào)標(biāo)識(shí)是誰(shuí),而不是統(tǒng)一的 “kubernetes/serviceaccount” ,這里可以從代碼中看到,兩者是完全相同的,只是稱號(hào)不同罷了

          // newLegacyServiceAccountAuthenticator returns an authenticator.Token or an error
          func newLegacyServiceAccountAuthenticator(keyfiles []string, lookup bool, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
           allPublicKeys := []interface{}{}
           for _, keyfile := range keyfiles {
            publicKeys, err := keyutil.PublicKeysFromFile(keyfile)
            if err != nil {
             return nil, err
            }
            allPublicKeys = append(allPublicKeys, publicKeys...)
           }
          // 唯一的區(qū)別 這里使用了常量 serviceaccount.LegacyIssuer
           tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]string{serviceaccount.LegacyIssuer}, allPublicKeys, apiAudiences, serviceaccount.NewLegacyValidator(lookup, serviceAccountGetter))
           return tokenAuthenticator, nil
          }

          // newServiceAccountAuthenticator returns an authenticator.Token or an error
          func newServiceAccountAuthenticator(issuers []string, keyfiles []string, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
           allPublicKeys := []interface{}{}
           for _, keyfile := range keyfiles {
            publicKeys, err := keyutil.PublicKeysFromFile(keyfile)
            if err != nil {
             return nil, err
            }
            allPublicKeys = append(allPublicKeys, publicKeys...)
           }
          // 唯一的區(qū)別 這里根據(jù)kube-apiserver提供的稱號(hào)指定名稱
           tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(issuers, allPublicKeys, apiAudiences, serviceaccount.NewValidator(serviceAccountGetter))
           return tokenAuthenticator, nil
          }

          最后根據(jù) ServiceAccounts,Secrets 等值簽發(fā)一個(gè) token,也就是通過(guò)下列命令獲取的值

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

          場(chǎng)景 4:openid

          OpenID Connect 是 OAuth2 風(fēng)格,允許用戶授權(quán)三方網(wǎng)站訪問(wèn)他們存儲(chǔ)在另外的服務(wù)提供者上的信息,而不需要將用戶名和密碼提供給第三方網(wǎng)站或分享他們數(shù)據(jù)的所有內(nèi)容,下面是一張 kubernetes 使用 OID 認(rèn)證的邏輯圖

          圖:Kubernetes OID 認(rèn)證

          場(chǎng)景 5:webhook

          webhook 是 kubernetes 提供自定義認(rèn)證的其中一種,主要是用于認(rèn)證 “不記名 token“ 的鉤子,“不記名 token“ 將 由身份驗(yàn)證服務(wù)創(chuàng)建。當(dāng)用戶對(duì) kubernetes 訪問(wèn)時(shí),會(huì)觸發(fā)準(zhǔn)入控制,當(dāng)對(duì) kubernetes 集群注冊(cè)了 authenticaion webhook 時(shí),將會(huì)使用該 webhook 提供的方式進(jìn)行身份驗(yàn)證時(shí),此時(shí)會(huì)為您生成一個(gè) token 。

          如代碼 pkg/kubeapiserver/authenticator.New()[11]  中所示 newWebhookTokenAuthenticator 會(huì)通過(guò)提供的 config (--authentication-token-webhook-config-file) 來(lái)創(chuàng)建出一個(gè) WebhookTokenAuthenticator

          if len(config.WebhookTokenAuthnConfigFile) > 0 {
              webhookTokenAuth, err := newWebhookTokenAuthenticator(config)
              if err != nil {
                  return nilnil, err
              }

              tokenAuthenticators = append(tokenAuthenticators, webhookTokenAuth)
          }

          下圖是 kubernetes 中 WebhookToken 驗(yàn)證的工作原理

          圖:kubernetes WebhookToken 驗(yàn)證原理

          最后由 token 中的 authHandler,循環(huán)所有的 Handlers 在運(yùn)行 AuthenticateToken 去進(jìn)行獲取用戶的信息

          func (authHandler *unionAuthTokenHandler) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
             var errlist []error
             for _, currAuthRequestHandler := range authHandler.Handlers {
                info, ok, err := currAuthRequestHandler.AuthenticateToken(ctx, token)
                if err != nil {
                   if authHandler.FailOnError {
                      return info, ok, err
                   }
                   errlist = append(errlist, err)
                   continue
                }

                if ok {
                   return info, ok, err
                }
             }

             return nilfalse, utilerrors.NewAggregate(errlist)
          }

          而 webhook 插件也實(shí)現(xiàn)了這個(gè)方法 AuthenticateToken , 這里會(huì)通過(guò) POST 請(qǐng)求,調(diào)用注入的 webhook,該請(qǐng)求攜帶一個(gè) JSON 格式的 TokenReview 對(duì)象,其中包含要驗(yàn)證的令牌

          func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {

              ....

            start := time.Now()
            result, statusCode, tokenReviewErr = w.tokenReview.Create(ctx, r, metav1.CreateOptions{})
            latency := time.Since(start)
          ...
          }

          webhook token 認(rèn)證服務(wù)要返回用戶的身份信息[12],就是上面 token 部分提到的數(shù)據(jù)結(jié)構(gòu)(webhook 來(lái)決定接受還是拒絕該用戶)

          type DefaultInfo struct {
           Name   string
           UID    string
           Groups []string
           Extra  map[string][]string
          }

          場(chǎng)景 6:代理認(rèn)證

          實(shí)驗(yàn):基于 LDAP 的身份認(rèn)證

          通過(guò)上面闡述,大致了解到 kubernetes 認(rèn)證框架中的用戶的分類以及認(rèn)證的策略由哪些,實(shí)驗(yàn)的目的也是為了闡述一個(gè)結(jié)果,就是使用 OIDC/webhook 是比其他方式更好的保護(hù),管理 kubernetes 集群。首先在安全上,假設(shè)網(wǎng)絡(luò)環(huán)境是不安全的,那么任意 node 節(jié)點(diǎn)遺漏 bootstrap  token 文件,就意味著擁有了集群中最高權(quán)限;其次在管理上,越大的團(tuán)隊(duì),人數(shù)越多,不可能每個(gè)用戶都提供單獨(dú)的證書或者 token,要知道傳統(tǒng)教程中講到 token 在 kubernetes 集群中是永久有效的,除非你刪除了這個(gè) secret/sa;而 Kubernetes 提供的插件就很好的解決了這些問(wèn)題。

          實(shí)驗(yàn)環(huán)境

          • 一個(gè) kubernetes 集群
          • 一個(gè) openldap 服務(wù),建議可以是集群外部的,因?yàn)?webhook 不像 SSSD 有緩存機(jī)制,并且集群不可用,那么認(rèn)證不可用,當(dāng)認(rèn)證不可用時(shí)會(huì)導(dǎo)致集群不可用,這樣事故影響的范圍可以得到控制,也叫最小化半徑
          • 了解 ldap 相關(guān)技術(shù),并了解 go ldap 客戶端

          實(shí)驗(yàn)大致分為以下幾個(gè)步驟

          • 建立一個(gè) HTTP 服務(wù)器用于返回給 kubernetes Authenticaion 服務(wù)
          • 查詢 ldap 該用戶是否合法
            • 查詢用戶是否合法
            • 查詢用戶所屬組是否擁有權(quán)限

          實(shí)驗(yàn)開始

          初始化用戶數(shù)據(jù)

          首先準(zhǔn)備 openldap 初始化數(shù)據(jù),創(chuàng)建三個(gè) posixGroup 組,與 5 個(gè)用戶 admin, admin1, admin11, searchUser, syncUser 密碼均為 111,組與用戶關(guān)聯(lián)使用的 memberUid

          $ cat << EOF | ldapdelete -r  -H ldap://10.0.0.3 -D "cn=admin,dc=test,dc=com" -w 111
          dn: dc=test,dc=com
          objectClass: top
          objectClass: organizationalUnit
          objectClass: extensibleObject
          description: US Organization
          ou: people

          dn: ou=tvb,dc=test,dc=com
          objectClass: organizationalUnit
          description: Television Broadcasts Limited
          ou: tvb

          dn: cn=admin,ou=tvb,dc=test,dc=com
          objectClass: posixGroup
          gidNumber: 10000
          cn: admin

          dn: cn=conf,ou=tvb,dc=test,dc=com
          objectClass: posixGroup
          gidNumber: 10001
          cn: conf

          dn: cn=dir,ou=tvb,dc=test,dc=com
          objectClass: posixGroup
          gidNumber: 10002
          cn: dir

          dn: uid=syncUser,ou=tvb,dc=test,dc=com
          objectClass: inetOrgPerson
          objectClass: organizationalPerson
          objectClass: person
          objectClass: posixAccount
          objectClass: shadowAccount
          objectClass: pwdPolicy
          pwdAttribute: userPassword
          uid: syncUser
          cn: syncUser
          uidNumber: 10006
          gidNumber: 10002
          homeDirectory: /home/syncUser
          loginShell: /bin/bash
          sn: syncUser
          givenName: syncUser
          memberOf: cn=confGroup,ou=tvb,dc=test,dc=com

          dn: uid=searchUser,ou=tvb,dc=test,dc=com
          objectClass: inetOrgPerson
          objectClass: organizationalPerson
          objectClass: person
          objectClass: posixAccount
          objectClass: shadowAccount
          objectClass: pwdPolicy
          pwdAttribute: userPassword
          uid: searchUser
          cn: searchUser
          uidNumber: 10005
          gidNumber: 10001
          homeDirectory: /home/searchUser
          loginShell: /bin/bash
          sn: searchUser
          givenName: searchUser
          memberOf: cn=dirGroup,ou=tvb,dc=test,dc=com

          dn: uid=admin1,ou=tvb,dc=test,dc=com
          objectClass: inetOrgPerson
          objectClass: organizationalPerson
          objectClass: person
          objectClass: posixAccount
          objectClass: shadowAccount
          objectClass: pwdPolicy
          pwdAttribute: userPassword
          uid: admin1
          sn: admin1
          cn: admin
          uidNumber: 10010
          gidNumber: 10000
          homeDirectory: /home/admin
          loginShell: /bin/bash
          givenName: admin
          memberOf: cn=adminGroup,ou=tvb,dc=test,dc=com

          dn: uid=admin11,ou=tvb,dc=test,dc=com
          objectClass: inetOrgPerson
          objectClass: organizationalPerson
          objectClass: person
          objectClass: posixAccount
          objectClass: shadowAccount
          objectClass: pwdPolicy
          sn: admin11
          pwdAttribute: userPassword
          uid: admin11
          cn: admin11
          uidNumber: 10011
          gidNumber: 10000
          homeDirectory: /home/admin
          loginShell: /bin/bash
          givenName: admin11
          memberOf: cn=adminGroup,ou=tvb,dc=test,dc=com

          dn: uid=admin,ou=tvb,dc=test,dc=com
          objectClass: inetOrgPerson
          objectClass: organizationalPerson
          objectClass: person
          objectClass: posixAccount
          objectClass: shadowAccount
          objectClass: pwdPolicy
          pwdAttribute: userPassword
          uid: admin
          cn: admin
          uidNumber: 10009
          gidNumber: 10000
          homeDirectory: /home/admin
          loginShell: /bin/bash
          sn: admin
          givenName: admin
          memberOf: cn=adminGroup,ou=tvb,dc=test,dc=com
          EOF

          接下來(lái)需要確定如何為認(rèn)證成功的用戶,上面講到對(duì)于 kubernetes 中用戶格式為 v1.UserInfo 的格式,即要獲得用戶,即用戶組,假設(shè)需要查找的用戶為,admin,那么在 openldap 中查詢 filter 如下:

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

          上面語(yǔ)句意思是,找到 objectClass=posixAccount 并且 uid=admin 或者 objectClass=posixGroup 并且 memberUid=admin 的條目信息,這里使用 ”|“ 與 ”&“ 是為了要拿到這兩個(gè)結(jié)果。

          編寫 webhook 查詢用戶部分

          這里由于 openldap 配置密碼保存格式不是明文的,如果直接使用 ”=“ 來(lái)驗(yàn)證是查詢不到內(nèi)容的,故直接多用了一次登錄來(lái)驗(yàn)證用戶是否合法

          func ldapSearch(username, password string) (*v1.UserInfo, error) {
           ldapconn, err := ldap.DialURL(ldapURL)
           if err != nil {
            klog.V(3).Info(err)
            return nil, err
           }
           defer ldapconn.Close()

           // Authenticate as LDAP admin user
           err = ldapconn.Bind("uid=searchUser,ou=tvb,dc=test,dc=com""111")
           if err != nil {
            klog.V(3).Info(err)
            return nil, err
           }

           // Execute LDAP Search request
           result, err := ldapconn.Search(ldap.NewSearchRequest(
            "ou=tvb,dc=test,dc=com",
            ldap.ScopeWholeSubtree,
            ldap.NeverDerefAliases,
            0,
            0,
            false,
            fmt.Sprintf("(&(objectClass=posixGroup)(memberUid=%s))", username), // Filter
            nil,
            nil,
           ))

           if err != nil {
            klog.V(3).Info(err)
            return nil, err
           }

           userResult, err := ldapconn.Search(ldap.NewSearchRequest(
            "ou=tvb,dc=test,dc=com",
            ldap.ScopeWholeSubtree,
            ldap.NeverDerefAliases,
            0,
            0,
            false,
            fmt.Sprintf("(&(objectClass=posixAccount)(uid=%s))", username), // Filter
            nil,
            nil,
           ))

           if err != nil {
            klog.V(3).Info(err)
            return nil, err
           }

           if len(result.Entries) == 0 {
            klog.V(3).Info("User does not exist")
            return nil, errors.New("User does not exist")
           } else {
            // 驗(yàn)證用戶名密碼是否正確
            if err := ldapconn.Bind(userResult.Entries[0].DN, password); err != nil {
             e := fmt.Sprintf("Failed to auth. %s\n", err)
             klog.V(3).Info(e)
             return nil, errors.New(e)
            } else {
             klog.V(3).Info(fmt.Sprintf("User %s Authenticated successfuly!", username))
            }
            // 拼接為kubernetes authentication 的用戶格式
            user := new(v1.UserInfo)
            for _, v := range result.Entries {
             attrubute := v.GetAttributeValue("objectClass")
             if strings.Contains(attrubute, "posixGroup") {
              user.Groups = append(user.Groups, v.GetAttributeValue("cn"))
             }
            }

            u := userResult.Entries[0].GetAttributeValue("uid")
            user.UID = u
            user.Username = u
            return user, nil
           }
          }

          編寫 HTTP 部分

          這里有幾個(gè)需要注意的部分,即用戶或者理解為要認(rèn)證的 token 的定義,此處使用了 ”username@password“ 格式作為用戶的辨別,即登錄 kubernetes 時(shí)需要直接輸入 ”username@password“ 來(lái)作為登錄的憑據(jù)。

          第二個(gè)部分為返回值,返回給 Kubernetes 的格式必須為 api/authentication/v1.TokenReview 格式,Status.Authenticated 表示用戶身份驗(yàn)證結(jié)果,如果該用戶合法,則設(shè)置 tokenReview.Status.Authenticated = true 反之亦然。如果驗(yàn)證成功還需要 Status.User 這就是在 ldapSearch

          func serve(w http.ResponseWriter, r *http.Request) {
           b, err := ioutil.ReadAll(r.Body)
           if err != nil {
            httpError(w, err)
            return
           }
           klog.V(4).Info("Receiving: %s\n"string(b))

           var tokenReview v1.TokenReview
           err = json.Unmarshal(b, &tokenReview)
           if err != nil {
            klog.V(3).Info("Json convert err: ", err)
            httpError(w, err)
            return
           }

           // 提取用戶名與密碼
           s := strings.SplitN(tokenReview.Spec.Token, "@"2)
           if len(s) != 2 {
            klog.V(3).Info(fmt.Errorf("badly formatted token: %s", tokenReview.Spec.Token))
            httpError(w, fmt.Errorf("badly formatted token: %s", tokenReview.Spec.Token))
            return
           }
           username, password := s[0], s[1]
           // 查詢ldap,驗(yàn)證用戶是否合法
           userInfo, err := ldapSearch(username, password)
           if err != nil {
            // 這里不打印日志的原因是 ldapSearch 中打印過(guò)了
            return
           }

           // 設(shè)置返回的tokenReview
           if userInfo == nil {
            tokenReview.Status.Authenticated = false
           } else {
            tokenReview.Status.Authenticated = true
            tokenReview.Status.User = *userInfo
           }

           b, err = json.Marshal(tokenReview)
           if err != nil {
            klog.V(3).Info("Json convert err: ", err)
            httpError(w, err)
            return
           }
           w.Write(b)
           klog.V(3).Info("Returning: "string(b))
          }

          func httpError(w http.ResponseWriter, err error) {
           err = fmt.Errorf("Error: %v", err)
           w.WriteHeader(http.StatusInternalServerError) // 500
           fmt.Fprintln(w, err)
           klog.V(4).Info("httpcode 500: ", err)
          }

          下面是完整的代碼

          package main

          import (
           "encoding/json"
           "errors"
           "flag"
           "fmt"
           "io/ioutil"
           "net/http"
           "strings"

           "github.com/go-ldap/ldap"
           "k8s.io/api/authentication/v1"
           "k8s.io/klog/v2"
          )

          var ldapURL string

          func main() {
           klog.InitFlags(nil)
           flag.Parse()
           http.HandleFunc("/authenticate", serve)
           klog.V(4).Info("Listening on port 443 waiting for requests...")
           klog.V(4).Info(http.ListenAndServe(":443"nil))
           ldapURL = "ldap://10.0.0.10:389"
           ldapSearch("admin""1111")
          }

          func serve(w http.ResponseWriter, r *http.Request) {
           b, err := ioutil.ReadAll(r.Body)
           if err != nil {
            httpError(w, err)
            return
           }
           klog.V(4).Info("Receiving: %s\n"string(b))

           var tokenReview v1.TokenReview
           err = json.Unmarshal(b, &tokenReview)
           if err != nil {
            klog.V(3).Info("Json convert err: ", err)
            httpError(w, err)
            return
           }

           // 提取用戶名與密碼
           s := strings.SplitN(tokenReview.Spec.Token, "@"2)
           if len(s) != 2 {
            klog.V(3).Info(fmt.Errorf("badly formatted token: %s", tokenReview.Spec.Token))
            httpError(w, fmt.Errorf("badly formatted token: %s", tokenReview.Spec.Token))
            return
           }
           username, password := s[0], s[1]
           // 查詢ldap,驗(yàn)證用戶是否合法
           userInfo, err := ldapSearch(username, password)
           if err != nil {
            // 這里不打印日志的原因是 ldapSearch 中打印過(guò)了
            return
           }

           // 設(shè)置返回的tokenReview
           if userInfo == nil {
            tokenReview.Status.Authenticated = false
           } else {
            tokenReview.Status.Authenticated = true
            tokenReview.Status.User = *userInfo
           }

           b, err = json.Marshal(tokenReview)
           if err != nil {
            klog.V(3).Info("Json convert err: ", err)
            httpError(w, err)
            return
           }
           w.Write(b)
           klog.V(3).Info("Returning: "string(b))
          }

          func httpError(w http.ResponseWriter, err error) {
           err = fmt.Errorf("Error: %v", err)
           w.WriteHeader(http.StatusInternalServerError) // 500
           fmt.Fprintln(w, err)
           klog.V(4).Info("httpcode 500: ", err)
          }

          func ldapSearch(username, password string) (*v1.UserInfo, error) {

           ldapconn, err := ldap.DialURL(ldapURL)
           if err != nil {
            klog.V(3).Info(err)
            return nil, err
           }
           defer ldapconn.Close()

           // Authenticate as LDAP admin user
           err = ldapconn.Bind("cn=admin,dc=test,dc=com""111")
           if err != nil {
            klog.V(3).Info(err)
            return nil, err
           }

           // Execute LDAP Search request
           result, err := ldapconn.Search(ldap.NewSearchRequest(
            "ou=tvb,dc=test,dc=com",
            ldap.ScopeWholeSubtree,
            ldap.NeverDerefAliases,
            0,
            0,
            false,
            fmt.Sprintf("(&(objectClass=posixGroup)(memberUid=%s))", username), // Filter
            nil,
            nil,
           ))

           if err != nil {
            klog.V(3).Info(err)
            return nil, err
           }

           userResult, err := ldapconn.Search(ldap.NewSearchRequest(
            "ou=tvb,dc=test,dc=com",
            ldap.ScopeWholeSubtree,
            ldap.NeverDerefAliases,
            0,
            0,
            false,
            fmt.Sprintf("(&(objectClass=posixAccount)(uid=%s))", username), // Filter
            nil,
            nil,
           ))

           if err != nil {
            klog.V(3).Info(err)
            return nil, err
           }

           if len(result.Entries) == 0 {
            klog.V(3).Info("User does not exist")
            return nil, errors.New("User does not exist")
           } else {
            // 驗(yàn)證用戶名密碼是否正確
            if err := ldapconn.Bind(userResult.Entries[0].DN, password); err != nil {
             e := fmt.Sprintf("Failed to auth. %s\n", err)
             klog.V(3).Info(e)
             return nil, errors.New(e)
            } else {
             klog.V(3).Info(fmt.Sprintf("User %s Authenticated successfuly!", username))
            }
            // 拼接為kubernetes authentication 的用戶格式
            user := new(v1.UserInfo)
            for _, v := range result.Entries {
             attrubute := v.GetAttributeValue("objectClass")
             if strings.Contains(attrubute, "posixGroup") {
              user.Groups = append(user.Groups, v.GetAttributeValue("cn"))
             }
            }

            u := userResult.Entries[0].GetAttributeValue("uid")
            user.UID = u
            user.Username = u
            return user, nil
           }
          }

          部署 webhook

          kubernetes 官方手冊(cè)中指出,啟用 webhook 認(rèn)證的標(biāo)記是在 kube-apiserver 指定參數(shù) --authentication-token-webhook-config-file 。而這個(gè)配置文件是一個(gè) kubeconfig 類型的文件格式[13]

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

          創(chuàng)建一個(gè)給 kube-apiserver 使用的配置文件 /etc/kubernetes/auth/authentication-webhook.conf

          apiVersion: v1
          kind: Config
          clusters:
          - cluster:
              server: http://10.0.0.1:88/authenticate
            name: authenticator
          users:
          - name: webhook-authenticator
          current-context: webhook-authenticator@authenticator
          contexts:
          - context:
              cluster: authenticator
              user: webhook-authenticator
            name: webhook-authenticator@authenticator

          修改 kube-apiserver 參數(shù)

          # 指向?qū)?yīng)的配置文件
          --authentication-token-webhook-config-file=/etc/kubernetes/auth/authentication-webhook.conf
          # 這個(gè)是token緩存時(shí)間,指的是用戶在訪問(wèn)API時(shí)驗(yàn)證通過(guò)后在一定時(shí)間內(nèi)無(wú)需在請(qǐng)求webhook進(jìn)行認(rèn)證了
          --authentication-token-webhook-cache-ttl=30m
          # 版本指定為API使用哪個(gè)版本?authentication.k8s.io/v1或v1beta1
          --authentication-token-webhook-version=v1

          啟動(dòng)服務(wù)后,創(chuàng)建一個(gè) kubeconfig 中的用戶用于驗(yàn)證結(jié)果

          apiVersion: v1
          clusters:
          - cluster:
              certificate-authority-data: 
              server: https://10.0.0.4:6443
            name: kubernetes
          contexts:
          - context:
              cluster: kubernetes
              user: k8s-admin
            name: k8s-admin@kubernetes
          current-context: k8s-admin@kubernetes
          kind: Config
          preferences: {}
          users:
          - name: admin
            user: 
              token: admin@111

          驗(yàn)證結(jié)果

          當(dāng)密碼不正確時(shí),使用用戶 admin 請(qǐng)求集群

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

          當(dāng)密碼正確時(shí),使用用戶 admin 請(qǐng)求集群

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

          可以看到 admin 用戶是一個(gè)不存在與集群中的用戶,并且提示沒(méi)有權(quán)限操作對(duì)應(yīng)資源,此時(shí)將 admin 用戶與集群中的 cluster-admin 綁定,測(cè)試結(jié)果

          $ kubectl create clusterrolebinding admin \
           --clusterrole=cluster-admin \
           --group=admin

          此時(shí)再嘗試使用 admin 用戶訪問(wèn)集群

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

          總結(jié)

          kubernetes authentication  插件提供的功能可以注入一個(gè)認(rèn)證系統(tǒng),這樣可以完美解決了 kubernetes 中用戶的問(wèn)題,而這些用戶并不存在與 kubernetes 中,并且也無(wú)需為多個(gè)用戶準(zhǔn)備大量 serviceaccount 或者證書,也可以完成鑒權(quán)操作。首先返回值標(biāo)準(zhǔn)如下所示,如果 kubernetes 集群有對(duì)在其他用戶系統(tǒng)中獲得的 Groups 并建立了 clusterrolebindingrolebinding 那么這個(gè)組的所有用戶都將有這些權(quán)限。管理員只需要維護(hù)與公司用戶系統(tǒng)中組同樣多的 clusterrole 與 clusterrolebinding 即可

          type DefaultInfo struct {
           Name   string
           UID    string
           Groups []string
           Extra  map[string][]string
          }

          對(duì)于如何將 kubernetes 與其他平臺(tái)進(jìn)行融合可以參考 基于 Kubernetes 的 PaaS 平臺(tái)提供 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 平臺(tái)提供 dashboard 支持的一種方案: https://cylonchau.github.io/kubernetes-dashborad-based.html




          你可能還喜歡

          點(diǎn)擊下方圖片即可閱讀

          徹底解決 K8s 節(jié)點(diǎn)本地存儲(chǔ)被撐爆的問(wèn)題

          2022-11-22

          Karmada 如何跨集群實(shí)現(xiàn)完整的自定義資源分發(fā)能力?

          2022-11-21

          Cilium 未來(lái)數(shù)據(jù)平面:支撐 100Gbit/s k8s 集群

          2022-11-14

          Prometheus 官方記錄片(中英雙語(yǔ)),帶你了解 Prometheus 的前世今生

          2022-11-11


          云原生是一種信仰 ??

          關(guān)注公眾號(hào)

          后臺(tái)回復(fù)?k8s?獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!



          點(diǎn)擊 "閱讀原文" 獲取更好的閱讀體驗(yàn)!


          發(fā)現(xiàn)朋友圈變“安靜”了嗎?

          瀏覽 47
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  粉嫩逼逼| 婷婷爱爱 | 中文字幕在线成人 | 中文国产字幕 | 亚洲欧美日本一区二区三区 |