深入理解 Kubernetes 中的用戶與身份認(rèn)證授權(quán)
?本文轉(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è)流程正如下圖所示:

其中在大多數(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)用戶是 apiserver 在 Authentication 階段 做的事情,而對(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 nil, false, nil
}
return &authenticator.Response{
User: &user.DefaultInfo{
Name: chain[0].Subject.CommonName,
Groups: chain[0].Subject.Organization,
},
}, true, nil
})
由于授權(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 nil, nil, 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í)行下列命令,這里就是將 kubelet 與 kube-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 nil, nil, 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 nil, nil, 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)證的邏輯圖

場(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 nil, nil, err
}
tokenAuthenticators = append(tokenAuthenticators, webhookTokenAuth)
}
下圖是 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 nil, false, 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 并建立了 clusterrolebinding 或 rolebinding 那么這個(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]。
引用鏈接
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)擊下方圖片即可閱讀
2022-11-22
2022-11-21
2022-11-14
2022-11-11

云原生是一種信仰 ??
關(guān)注公眾號(hào)
后臺(tái)回復(fù)?k8s?獲取史上最方便快捷的 Kubernetes 高可用部署工具,只需一條命令,連 ssh 都不需要!


點(diǎn)擊 "閱讀原文" 獲取更好的閱讀體驗(yàn)!
發(fā)現(xiàn)朋友圈變“安靜”了嗎?

