大家好,我是乔克。
在Kubernetes中,APIServer是整个集群的中枢神经,它不仅连接了各个模块,更是为整个集群提供了访问控制能力。
Kubernetes API的每个请求都要经过多阶段的访问控制才会被接受,包括认证、授权、准入,如下所示。
客户端(普通账户、ServiceAccount等)想要访问Kubernetes中的资源,需要通过经过APIServer的三大步骤才能正常访问,三大步骤如下:
- Authentication 认证阶段:判断请求用户是否为能够访问集群的合法用户。如果用户是个非法用户,那 apiserver会返回一个 401 的状态码,并终止该请求;
- 如果用户合法的话,我们的 apiserver 会进入到访问控制的第二阶段 Authorization:授权阶段。在该阶段中apiserver 会判断用户是否有权限进行请求中的操作。如果无权进行操作,apiserver 会返回 403的状态码,并同样终止该请求;
- 如果用户有权进行该操作的话,访问控制会进入到第三个阶段:AdmissionControl。在该阶段中 apiserver 的admission controller 会判断请求是否是一个安全合规的请求。如果最终验证通过的话,访问控制流程才会结束。
这篇文章主要和大家讨论认证环节。
认证
Kubernetes中支持多种认证机制,也支持多种认证插件,在认证过程中,只要一个通过则表示认证通过。
常用的认证插件有:
- X509证书
- 静态Token
- ServiceAccount
- OpenID
- Webhook
- .....
这里不会把每种认证插件都介绍一下,主要讲讲Webhook的使用场景。
在企业中,大部分都会有自己的账户中心,用于管理员工的账户以及权限,而在K8s集群中,也需要进行账户管理,如果能直接使用现有的账户系统是不是会方便很多?
K8s的Webhook就可以实现这种需求,Webhook是一个HTTP回调,通过一个条件触发HTTP POST请求发送到Webhook 服务端,服务端根据请求数据进行处理。
下面就带大家从0到1开发一个认证服务。
开发Webhook
简介
WebHook的功能主要是接收APIServer的认证请求,然后调用不同的认证服务进行认证,如下所示。
这里只是做一个Webhook的例子,目前主要实现了Github
和LDAP
认证,当然,认证部分的功能比较单一,没有考虑复杂的场景。
Webhook开发
开发环境
软件 | 版本 |
Go | 1.17.3 |
Kubernetes | v1.22.3 |
System | CentOS7.6 |
构建符合规范的Webhook
在开发Webhook的时候,需要符合Kubernetes的规范,具体如下:
- URL:https://auth.example.com/auth
- Method:POST
- Input参数
{ "apiVersion": "authentication.k8s.io/v1beta1", "kind": "TokenReview", "spec": { "token": "<持有者令牌>" } }
- Output参数
如果成功会返回:
{ "apiVersion": "authentication.k8s.io/v1beta1", "kind": "TokenReview", "status": { "authenticated": true, "user": { "username": "janedoe@example.com", "uid": "42", "groups": [ "developers", "qa" ], "extra": { "extrafield1": [ "extravalue1", "extravalue2" ] } } } }
如果不成功,会返回:
{ "apiVersion": "authentication.k8s.io/v1beta1", "kind": "TokenReview", "status": { "authenticated": false } }
远程服务应该会填充请求的 status 字段,以标明登录操作是否成功。
开发认证服务
(1)创建项目并初始化go mod
# mkdir kubernetes-auth-webhook # cd kubernetes-auth-webhook # go mod init
(2)在项目根目录下创建webhook.go,写入如下内容
package main import ( "encoding/json" "github.com/golang/glog" authentication "k8s.io/api/authentication/v1beta1" "k8s.io/klog/v2" "net/http" "strings" ) type WebHookServer struct { server *http.Server } func (ctx *WebHookServer) serve(w http.ResponseWriter, r *http.Request) { // 从APIServer中取出body // 将body进行拆分, 取出type // 根据type, 取出不同的认证数据 var req authentication.TokenReview decoder := json.NewDecoder(r.Body) err := decoder.Decode(&req) if err != nil { klog.Error(err, "decoder request body error.") req.Status = authentication.TokenReviewStatus{Authenticated: false} w.WriteHeader(http.StatusUnauthorized) _ = json.NewEncoder(w).Encode(req) return } // 判断token是否包含':' // 如果不包含,则返回认证失败 if !(strings.Contains(req.Spec.Token, ":")) { klog.Error(err, "token invalied.") req.Status = authentication.TokenReviewStatus{Authenticated: false} //req.Status = map[string]interface{}{"authenticated": false} w.WriteHeader(http.StatusUnauthorized) _ = json.NewEncoder(w).Encode(req) return } // split token, 获取type tokenSlice := strings.SplitN(req.Spec.Token, ":", -1) glog.Infof("tokenSlice: ", tokenSlice) hookType := tokenSlice[0] switch hookType { case "github": githubToken := tokenSlice[1] err := authByGithub(githubToken) if err != nil { klog.Error(err, "auth by github error") req.Status = authentication.TokenReviewStatus{Authenticated: false} w.WriteHeader(http.StatusUnauthorized) _ = json.NewEncoder(w).Encode(req) return } klog.Info("auth by github success") req.Status = authentication.TokenReviewStatus{Authenticated: true} w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(req) return case "ldap": username := tokenSlice[1] password := tokenSlice[2] err := authByLdap(username, password) if err != nil { klog.Error(err, "auth by ldap error") req.Status = authentication.TokenReviewStatus{Authenticated: false} //req.Status = map[string]interface{}{"authenticated": false} w.WriteHeader(http.StatusUnauthorized) _ = json.NewEncoder(w).Encode(req) return } klog.Info("auth by ldap success") req.Status = authentication.TokenReviewStatus{Authenticated: true} //req.Status = map[string]interface{}{"authenticated": true} w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(req) return } }
主要是解析认证的请求Token,然后将Token进行拆分判断是需要什么认证,Token的样例如下:
- Github认证:github:
- LDAP认证:ldap::
这样就可以获取到用户想用哪种认证,再掉具体的认证服务进行处理。
(3)创建github.go,提供github认证方法
package main import ( "context" "github.com/golang/glog" "github.com/google/go-github/github" "golang.org/x/oauth2" ) func authByGithub(token string) (err error) { glog.V(2).Info("start auth by github......") tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) tokenClient := oauth2.NewClient(context.Background(), tokenSource) githubClient := github.NewClient(tokenClient) _, _, err = githubClient.Users.Get(context.Background(), "") if err != nil { return err } return nil }
可以看到,这里仅仅做了一个简单的Token认证,认证的结果比较粗暴,如果err=nil
,则表示认证成功。
(4)创建ldap.go,提供ldap认证
package main import ( "crypto/tls" "errors" "fmt" "github.com/go-ldap/ldap/v3" "github.com/golang/glog" "k8s.io/klog/v2" "strings" ) var ( ldapUrl = "ldap://" + "192.168.100.179:389" ) func authByLdap(username, password string) error { groups, err := getLdapGroups(username, password) if err != nil { return err } if len(groups) > 0 { return nil } return fmt.Errorf("No matching group or user attribute. Authentication rejected, Username: %s", username) } // 获取user的groups func getLdapGroups(username, password string) ([]string, error) { glog.Info("username:password", username, ":", password) var groups []string config := &tls.Config{InsecureSkipVerify: true} ldapConn, err := ldap.DialURL(ldapUrl, ldap.DialWithTLSConfig(config)) if err != nil { glog.V(4).Info("dial ldap failed, err: ", err) return groups, err } defer ldapConn.Close() binduser := fmt.Sprintf("CN=%s,ou=People,dc=demo,dc=com", username) err = ldapConn.Bind(binduser, password) if err != nil { klog.V(4).ErrorS(err, "bind user to ldap error") return groups, err } // 查询用户成员 searchString := fmt.Sprintf("(&(objectClass=person)(cn=%s))", username) memberSearchAttribute := "memberOf" searchRequest := ldap.NewSearchRequest( "dc=demo,dc=com", ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, searchString, []string{memberSearchAttribute}, nil, ) searchResult, err := ldapConn.Search(searchRequest) if err != nil { klog.V(4).ErrorS(err, "search user properties error") return groups, err } // 如果没有查到结果,返回失败 if len(searchResult.Entries[0].Attributes) < 1 { return groups, errors.New("no user in ldap") } entry := searchResult.Entries[0] for _, e := range entry.Attributes { for _, attr := range e.Values { groupList := strings.Split(attr, ",") for _, g := range groupList { if strings.HasPrefix(g, "cn=") { group := strings.Split(g, "=") groups = append(groups, group[1]) } } } } return groups, nil }
这里的用户名是固定了的,所以不适合其他场景。
(5)创建main.go入口函数
package main import ( "context" "flag" "fmt" "github.com/golang/glog" "net/http" "os" "os/signal" "syscall" ) var port string func main() { flag.StringVar(&port, "port", "9999", "http server port") flag.Parse() // 启动httpserver wbsrv := WebHookServer{server: &http.Server{ Addr: fmt.Sprintf(":%v", port), }} mux := http.NewServeMux() mux.HandleFunc("/auth", wbsrv.serve) wbsrv.server.Handler = mux // 启动协程来处理 go func() { if err := wbsrv.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { glog.Errorf("Failed to listen and serve webhook server: %v", err) } }() glog.Info("Server started") // 优雅退出 signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) <-signalChan glog.Infof("Got OS shutdown signal, shutting down webhook server gracefully...") _ = wbsrv.server.Shutdown(context.Background()) }
到此整个认证服务就开发完毕了,是不是很简单?
Webhook测试
APIServer添加认证服务
使用Webhook进行认证,需要在kube-apiserver里开启,参数如下:
- --authentication-token-webhook-config-file 指向一个配置文件,其中描述 如何访问远程的 Webhook 服务
- --authentication-token-webhook-config-file 指向一个配置文件,其中描述 如何访问远程的 Webhook 服务
配置文件使用 kubeconfig 文件的格式。文件中,clusters 指代远程服务,users 指代远程 API 服务 Webhook。配置如下:
(1)、将配置文件放到相应的目录
# mkdir /etc/kubernetes/webhook # cat >> webhook-config.json <EOF { "kind": "Config", "apiVersion": "v1", "preferences": {}, "clusters": [ { "name": "github-authn", "cluster": { "server": "http://10.0.4.9:9999/auth" } } ], "users": [ { "name": "authn-apiserver", "user": { "token": "secret" } } ], "contexts": [ { "name": "webhook", "context": { "cluster": "github-authn", "user": "authn-apiserver" } } ], "current-context": "webhook" } EOF
(2)在kube-apiserver中添加配置参数
# mkdir /etc/kubernetes/backup # cp /etc/kubernetes/manifests/kube-apiserver.yaml /etc/kubernetes/backup/kube-apiserver.yaml # cd /etc/kubernetes/manifests/ # cat kube-apiserver.yaml apiVersion: v1 kind: Pod metadata: annotations: kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 10.0.4.9:6443 creationTimestamp: null labels: component: kube-apiserver tier: control-plane name: kube-apiserver namespace: kube-system spec: containers: - command: - kube-apiserver - ...... - --authentication-token-webhook-config-file=/etc/config/webhook-config.json image: registry.cn-hangzhou.aliyuncs.com/google_containers/kube-apiserver:v1.22.0 imagePullPolicy: IfNotPresent ...... volumeMounts: ...... - name: webhook-config mountPath: /etc/config readOnly: true hostNetwork: true priorityClassName: system-node-critical securityContext: seccompProfile: type: RuntimeDefault volumes: ...... - hostPath: path: /etc/kubernetes/webhook type: DirectoryOrCreate name: webhook-config status: {}
ps: 为了节约篇幅,上面省略了部分配置。
当修改完过后,kube-apiserver会自动重启。
测试Github认证
(1)在github上获取Token,操作如图所示
(2)配置kubeconfig,添加user
# cat ~/.kube/config apiVersion: v1 ...... users: - name: joker user: token: github:ghp_jevHquU4g43m46nczWS0ojxxxxxxxxx
(3)用Joker用户进行访问
返回结果如下,至于报错是因为用户的权限不足。
# kubectl get po --user=joker Error from server (Forbidden): pods is forbidden: User "" cannot list resource "pods" in API group "" in the namespace "default"
可以在webhook上看到日志信息,如下:
# ./kubernetes-auth-webhook I1207 15:37:29.531502 21959 webhook.go:55] auth by github success
从日志和结果可以看到,使用Github认证是OK的。
测试LDAP认证
LDAP简介
LDAP是协议,不是软件。
LDAP
是轻量目录访问协议,英文全称是Lightweight Directory Access Protocol
,一般都简称为LDAP。按照我们对文件目录的理解,ldap可以看成一个文件系统,类似目录和文件树。
OpenLDAP是常用的服务之一,也是我们本次测试的认证服务。
安装OpenLDAP
OpenLDAP
的安装方式有很多,可以使用容器部署,也可以直接安装在裸机上,这里采用后者。
# yum install -y openldap openldap-clients openldap-servers # systemctl start slapd # systemctl enable slapd
默认配置文件,位于/etc/openldap/slapd.d
, 文件格式为LDAP Input Format (LDIF)
, ldap目录特定的格式。这里不对配置文件做太多的介绍,有兴趣可以自己去学习学习【1】。
在LDAP上配置用户
(1)导入模板
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/cosine.ldif ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/nis.ldif ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/inetorgperson.ldif
(2)创建base组织
# cat base.ldif dn: dc=demo,dc=com objectClass: top objectClass: dcObject objectClass: organization o: ldap测试组织 dc: demo dn: cn=Manager,dc=demo,dc=com objectClass: organizationalRole cn: Manager description: 组织管理人 dn: ou=People,dc=demo,dc=com objectClass: organizationalUnit ou: People dn: ou=Group,dc=demo,dc=com objectClass: organizationalUnit ou: Group
使用ldapadd
添加base。
ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f base.ldif
(3)添加成员
# cat adduser.ldif dn: cn=jack,ou=People,dc=demo,dc=com changetype: add objectClass: inetOrgPerson cn: jack departmentNumber: 1 title: 大牛 userPassword: 123456 sn: Bai mail: jack@demo.com displayName: 中文名
使用ldapadd执行添加。
ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f adduser.ldif
(4)将用户添加到组
# cat add_member_group.ldif dn: cn=g-admin,ou=Group,dc=demo,dc=com changetype: modify add: member member: cn=jack,ou=People,dc=demo,dc=com
使用ldapadd执行添加。
ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f add_member_group.ldif
配置kubeconfig,进行ldap认证测试
(1)修改~/.kube/config
配置文件
# cat ~/.kube/config apiVersion: v1 ...... users: - name: joker user: token: github:ghp_jevHquU4g43m46nczWS0oxxxxxxxx - name: jack user: token: ldap:jack:123456
(2)使用kubectl进行测试
# kubectl get po --user=jack Error from server (Forbidden): pods is forbidden: User "" cannot lis t resource "pods" in API group "" in the namespace "default"
webhook服务日志如下:
# ./kubernetes-auth-webhook I1207 16:09:09.292067 7605 webhook.go:72] auth by ldap success
通过测试结果可以看到使用LDAP认证测试成功。
总结
使用Webhook可以很灵活的将K8S的租户和企业内部账户系统进行打通,这样可以方便管理用户账户。
不过上面开发的Webhook只是一个简单的例子,验证方式和手法都比较粗暴,CoreOS
开源的Dex
【2】是比较不错的产品,可以直接使用。