This chapter briefly describes the principles related to kubernetes authentication, and ends with an experiment to illustrate the idea of implementing the kubernetes user system.
The main content is as follows.
- Understanding the principles of various kubernetes authentication mechanisms
- Understanding the concept of kubernetes users
- Understanding kubernetes authentication webhook
- Complete experiments with an idea of how to get other user systems into kubernetes
Kubernetes Authentication
As described in the Kubernetes apiserver for authentication section, all users accessing the Kubernetes API (via any client, client library, kubectl
, etc.) go through three stages of Authentication
, Authorization
, and Admission control
to complete the authorization of the “user”, as shown in the following diagram.

In most tutorials, the work done on these three phases is roughly as follows.
- Authentication phase refers to confirming that the user requesting access to the Kubernetes API is a legitimate user
- Authorization phase will refer to whether or not the user has permission to operate on the resource
- Admission control phase controls the requested resource, which is commonly known as a veto, even if the first two steps are completed.
Having learned here that the Kubernetes API actually does the work of a “human user” with a kubernetes service account; this leads to the important concept of what a “user” is in Kubernetes, and what a user is in authentication, which is also the center of this chapter.
The concept of “user” is given in the official Kubernetes manual, and the users present in a Kubernetes cluster include “normal user” and “service account”, but Kubernetes does not manage normal users in the same way as normal users, except that users who use a valid certificate signed by the cluster’s certificate CA are considered legitimate users.
Then for making a Kubernetes cluster have a real user system, it is possible to divide Kubernetes users into “external users” and “internal users” based on the concept given above. How do you understand external vs. internal users? In fact, users that are managed by Kubernetes, i.e., in the data model that defines users in kubernetes, are “internal users”, as in service account; conversely, users that are not hosted by Kubernetes are “external users”. This concept is also a better articulation of kubernetes users.
For external users, Kubernetes actually gives a variety of user concepts, such as
- Users with kubernetes cluster credentials
- Users with Kubernetes cluster token (static token specified by
--token-auth-file
)
- users from external user systems, such as OpenID, LDAP, QQ connect, google identity platform, etc.
Example of granting access to a cluster to an external user
Scenario 1: Requesting k8s via certificate
In this scenario kubernetes will use cn
as the user and ou
as the group in the certificate, and the request is legitimate if the corresponding rolebinding/clusterrolebinding
gives permissions to the user.
1
2
3
4
|
$ curl https://hostname:6443/api/v1/pods \
--cert ./client.pem \
--key ./client-key.pem \
--cacert ./ca.pem
|
Next, we analyze the related source code.
Authenticating users is what apiserver
does in the Authentication
phase, and the corresponding code is under pkg/kubeapiserver/authenticator
, the whole file is a series of authenticators, and the x.509 certificate is one of them.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Create an authenticator that returns a request or a standard error
func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, error) {
...
// X509 methods
// You can see that this is where the x509 certificate is parsed into the user
if config.ClientCAContentProvider != nil {
certAuth := x509.NewDynamic(config.ClientCAContentProvider.VerifyOptions, x509.CommonNameUserConversion)
authenticators = append(authenticators, certAuth)
}
...
|
Next, see the implementation principle, the NewDynamic function is located in the code http://k8s.io/apiserver/pkg/authentication/request/x509/x509.go
As you can see by the code, it is an authentication function with the user to resolve to an Authenticator
1
2
3
4
5
|
// 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}
}
|
The verification function is a method of CAContentProvider, and the x509 part is implemented as http://k8s.io/apiserver/pkg/server/dynamiccertificates/dynamic_cafile_content.go.VerifyOptions
.
You can see that the return is an x509.VerifyOptions
and an authenticated status
1
2
3
4
5
6
7
8
9
|
// 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
}
|
And the user’s get is located at http://k8s.io/apiserver/pkg/authentication/request/x509/x509.go
; you can see that the user is exactly the CN of the certificate, while the group is the OU of the certificate
1
2
3
4
5
6
7
8
9
10
11
12
|
// 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
})
|
Since authorization is out of the scope of this chapter, it is directly ignored until the inbound phase. The inbound phase is implemented by RESTStorageProvider
, where each Provider provides an Authenticator which contains the allowed requests that will be written to the repository by the corresponding REST client.
1
2
3
4
5
6
7
8
9
|
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)
}
|
Scenario 2: via token
In this scenario, when kube-apiserver has --enable-bootstrap-token-auth
enabled, you can use Bootstrap Token for authentication, usually with the following command, adding Authorization: Bearer <token>
to the request header.
1
2
3
|
$ curl https://hostname:6443/api/v1/pods \
--cacert ${CACERT} \
--header "Authorization: Bearer <token>" \
|
As you can see, in the code pkg/kubeapiserver/authenticator.New()
when kube-apiserver
specifies the parameter --token-auth-file=/etc/kubernetes/token.csv
this authentication will be activated.
1
2
3
4
5
6
7
|
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))
}
|
At this point, open token.csv to see what the token looks like.
1
2
|
$ cat /etc/kubernetes/token.csv
12ba4f.d82a57a4433b2359,"system:bootstrapper",10001,"system:bootstrappers"
|
Back to the code here http://k8s.io/apiserver/pkg/authentication/token/tokenfile/tokenfile.go.NewCSV
, where you can see that it is reading the --token-auth-file=
parameter specified by the tokenfile, and then parses it to the user, record[1]
as the username, and record[2]
as the UID.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
// 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
}
|
The format configured in the token file is a comma-separated set of strings.
1
2
3
4
5
6
|
type DefaultInfo struct {
Name string
UID string
Groups []string
Extra map[string][]string
}
|
The most common way such a user is kubelet will usually authenticate to the control plane as such a user, such as the following configuration.
1
2
3
4
5
6
7
|
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"
|
The contents of /etc/kubernetes/auth/bootstrap.conf
, where the kube-apiserver configuration of --token-auth-file=
username and the group must be system:bootstrappers
, are used.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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
|
The problems that usually occur in binary deployments, such as the following errors.
1
|
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
|
And the usual solution is to execute the following command, here is the user authorization when communicating kubelet with kube-apiserver, because the official condition given by kubernetes is that the user group must be system:bootstrappers.
1
|
$ kubectl create clusterrolebinding kubelet-bootstrap --clusterrole=system:node-bootstrapper --group=system:bootstrappers
|
The generated clusterrolebinding is as follows
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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
|
Such a user does not exist within kubernetes and can be considered an external user, but is present in the authentication mechanism and bound to the highest privilege, which can also be used for authentication for other accesses.
Scenario 3: serviceaccount
serviceaccount is usually created automatically for the API, but in the user, there are actually two directions for authentication, one is --service-account-key-file
This parameter can specify more than one, specifying the corresponding certificate file public or private key for issuing the token of the sa.
First, a token is generated based on the specified public or private key file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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)
}
|
For --service-account-key-file
he generates the user as “kubernetes/serviceaccount” , while for --service-account-issuer
just provides a title for the sa issuer to identify who it is instead of the uniform “kubernetes/serviceaccount” , here you can see from the code that the two are exactly the same, just with different titles.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// 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...)
}
// 唯一的区别 这里使用了常量 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...)
}
// 唯一的区别 这里根据kube-apiserver提供的称号指定名称
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(issuers, allPublicKeys, apiAudiences, serviceaccount.NewValidator(serviceAccountGetter))
return tokenAuthenticator, nil
}
|
Finally, a token is issued based on the values of ServiceAccounts, Secrets, etc., which is the value obtained by the following command.
1
|
$ kubectl get secret multus-token-v6bfg -n kube-system -o jsonpath={".data.token"}
|
Scenario 4: openid
OpenID Connect is OAuth2 style and allows users to authorize a three-party site to access their information stored on another service provider without giving the username and password to the third-party site or sharing all the contents of their data, here is a logical diagram of kubernetes using OID authentication.

Scenario 5: webhook
The webhook is one of the custom authentication options provided by kubernetes, and is primarily a hook for authenticating “bearer tokens” that will be created by the authentication service. Access control is triggered when a user accesses kubernetes, and when the authenticaion webhook is registered for a kubernetes cluster, a token is generated for you when authenticating using the method provided by the webhook.
As shown in the code pkg/kubeapiserver/authenticator.New()
the newWebhookTokenAuthenticator
is created with the provided config (--authentication-token-webhook-config-file
) create a WebhookTokenAuthenticator
1
2
3
4
5
6
7
8
|
if len(config.WebhookTokenAuthnConfigFile) > 0 {
webhookTokenAuth, err := newWebhookTokenAuthenticator(config)
if err != nil {
return nil, nil, err
}
tokenAuthenticators = append(tokenAuthenticators, webhookTokenAuth)
}
|
The following diagram shows how WebhookToken authentication works in kubernetes.

Finally, the authHandler in the token loops through all the Handlers and then runs the AuthenticateToken to get the user’s information.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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)
}
|
The webhook plugin also implements this method AuthenticateToken
, where the injected webhook is called via a POST request that carries a JSON
formatted TokenReview
object containing the token to be authenticated.
1
2
3
4
5
6
7
8
9
|
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)
...
}
|
The webhook token authentication service returns the user’s identity, which is the data structure mentioned in the token section above (webhook to decide whether to accept or reject the user).
1
2
3
4
5
6
|
type DefaultInfo struct {
Name string
UID string
Groups []string
Extra map[string][]string
}
|
Scenario 6: Proxy authentication
Experiment: LDAP-based Authentication
The above exposition gives a general idea of how users are classified in the kubernetes authentication framework and what the authentication policies consist of. The purpose of the experiment is also to illustrate the result that using OIDC/webhook is a better way to protect and manage kubernetes clusters than other ways. First, in terms of security, assuming that the network environment is insecure, then any node node that misses the bootstrap token file means that it has the highest privileges in the cluster; second, in terms of management, the larger the team, the more people, it is impossible to provide a separate certificate or token for each user, knowing that the traditional tutorials talk about token in kubernetes The token is permanently valid in the kubernetes cluster unless you delete the secret/sa, and the plugins provided by Kubernetes solve these problems very well.
Experimental environment
- A kubernetes cluster
- An openldap service, it is recommended that it can be external to the cluster, because webhook does not have a caching mechanism like SSSD, and the cluster is not available, then authentication is not available, and when authentication is not available will cause the cluster to be unavailable, so that the scope of the impact of the accident can be controlled, also called the minimization radius.
- Understanding of ldap related technologies and go ldap client
The experiment is roughly divided into the following steps.
Start of experiment
Initialize user data
First prepare the openldap initialization data, create three posixGroup groups with 5 users admin, admin1, admin11, searchUser, syncUser all with password 111, the group is associated with the user using memberUid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
$ 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
|
Next, you need to determine how to authenticate successful users, as mentioned above for kubernetes in the user format of v1.UserInfo, that is, to get the user, that is, the user group, assuming that the need to find the user for admin, then the query filter in openldap as follows.
1
|
"(|(&(objectClass=posixAccount)(uid=admin))(&(objectClass=posixGroup)(memberUid=admin)))"
|
The above statement means to find the information of entries with objectClass=posixAccount
and uid=admin or objectClass=posixGroup
and memberUid=admin
, where “|
” and “&
” are used to get these two results.
Write webhook to query user part
Here because the openldap configuration password format is not explicit, if you use “=” directly to verify the content is not queried, so directly use an additional login to verify whether the user is legitimate.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
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 {
// Verify that the username and password are correct
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))
}
//The user format for splicing into 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
}
}
|
Writing the HTTP part
There are a few things to note here, namely the definition of the user or token to be authenticated, here the username@password
format is used as the user identification, i.e. when logging into kubernetes you need to directly enter username@password
as the login credentials.
The second part is the return value, which must be returned to Kubernetes in the api/authentication/v1.TokenReview
format, with Status.Authenticated
indicating the user authentication result, and if the user is legitimate, then set tokenReview. Authenticated = true
and vice versa. If authentication is successful you also need Status.User
which is in ldapSearch
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
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
}
// Extract username and password
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]
// Query ldap to verify that the user is legitimate
userInfo, err := ldapSearch(username, password)
if err != nil {
// The reason the log is not printed here is that it was printed in ldapSearch
return
}
// Set the returned 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)
}
|
Here is the complete code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
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]
userInfo, err := ldapSearch(username, password)
if err != nil {
return
}
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 {
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))
}
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
}
}
|
Deploying webhook
The official kubernetes manual states that the flag to enable webhook authentication is to specify the parameter --authentication-token-webhook-config-file
in kube-apiserver . And this configuration file is a kubeconfig type file format.
The following is the configuration for deploying outside of a kubernetes cluster.
Create a configuration file /etc/kubernetes/auth/authentication-webhook.conf
for use by kube-apiserver
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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
|
Modify the kube-apiserver parameter.
1
2
3
4
5
6
|
# Points to the corresponding profile
--authentication-token-webhook-config-file=/etc/kubernetes/auth/authentication-webhook.conf
# This is the token cache time, which means that the user does not need to request the webhook for authentication for a certain period of time after the authentication is passed when accessing the API.
--authentication-token-webhook-cache-ttl=30m
# Version specifies which version of the API to use, authentication.k8s.io/v1 or v1beta1
--authentication-token-webhook-version=v1
|
After starting the service, create a user in kubeconfig to verify the results.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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
|
Authentication results
When the password is incorrect, use the user admin to request the cluster.
1
2
|
$ kubectl get pods --user=admin
error: You must be logged in to the server (Unauthorized)
|
When the password is correct, use the user admin to request the cluster.
1
2
|
$ 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"
|
You can see that the admin user is a non-existent user in the cluster, and prompted no permission to operate the corresponding resources, at this time the admin user and the cluster-admin in the cluster binding, test results:
1
2
3
|
$ kubectl create clusterrolebinding admin \
--clusterrole=cluster-admin \
--group=admin
|
At this point, try again to access the cluster using the admin user.
1
2
3
4
|
$ 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
|
Summary
The kubernetes authentication plugin provides the ability to inject an authentication system that perfectly solves the problem of users in kubernetes who do not exist in kubernetes and can be authenticated without having to prepare a large number of serviceaccounts or certificates for multiple users. The The first return value criterion is as follows: if the kubernetes cluster has Groups
that are available on other user systems and clusterrolebinding
or rolebinding
is established, then all users in the group will have those privileges. The administrator only needs to maintain as many clusterrole and clusterrolebinding as there are groups in the company user system
1
2
3
4
5
6
|
type DefaultInfo struct {
Name string
UID string
Groups []string
Extra map[string][]string
}
|