K8S自定义Webhook实现认证管理

运维 系统运维
在Kubernetes中,APIServer是整个集群的中枢神经,它不仅连接了各个模块,更是为整个集群提供了访问控制能力。这篇文章主要和大家讨论认证环节。

[[439204]]

大家好,我是乔克。

在Kubernetes中,APIServer是整个集群的中枢神经,它不仅连接了各个模块,更是为整个集群提供了访问控制能力。

Kubernetes API的每个请求都要经过多阶段的访问控制才会被接受,包括认证、授权、准入,如下所示。

客户端(普通账户、ServiceAccount等)想要访问Kubernetes中的资源,需要通过经过APIServer的三大步骤才能正常访问,三大步骤如下:

  1. Authentication 认证阶段:判断请求用户是否为能够访问集群的合法用户。如果用户是个非法用户,那 apiserver会返回一个 401 的状态码,并终止该请求;
  2. 如果用户合法的话,我们的 apiserver 会进入到访问控制的第二阶段 Authorization:授权阶段。在该阶段中apiserver 会判断用户是否有权限进行请求中的操作。如果无权进行操作,apiserver 会返回 403的状态码,并同样终止该请求;
  3. 如果用户有权进行该操作的话,访问控制会进入到第三个阶段: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开发

开发环境

构建符合规范的Webhook

在开发Webhook的时候,需要符合Kubernetes的规范,具体如下:

  • URL:https://auth.example.com/auth
  • Method:POST
  • Input参数
  1.   "apiVersion""authentication.k8s.io/v1beta1"
  2.   "kind""TokenReview"
  3.   "spec": { 
  4.     "token""<持有者令牌>" 
  5.   } 
  • Output参数

如果成功会返回:

  1.   "apiVersion""authentication.k8s.io/v1beta1"
  2.   "kind""TokenReview"
  3.   "status": { 
  4.     "authenticated"true
  5.     "user": { 
  6.       "username""janedoe@example.com"
  7.       "uid""42"
  8.       "groups": [ 
  9.         "developers"
  10.         "qa" 
  11.       ], 
  12.       "extra": { 
  13.         "extrafield1": [ 
  14.           "extravalue1"
  15.           "extravalue2" 
  16.         ] 
  17.       } 
  18.     } 
  19.   } 

如果不成功,会返回:

  1.   "apiVersion""authentication.k8s.io/v1beta1"
  2.   "kind""TokenReview"
  3.   "status": { 
  4.     "authenticated"false 
  5.   } 

远程服务应该会填充请求的 status 字段,以标明登录操作是否成功。

开发认证服务

(1)创建项目并初始化go mod

  1. # mkdir kubernetes-auth-webhook 
  2. # cd kubernetes-auth-webhook 
  3. # go mod init 

(2)在项目根目录下创建webhook.go,写入如下内容

  1. package main 
  2.  
  3. import ( 
  4.  "encoding/json" 
  5.  "github.com/golang/glog" 
  6.  authentication "k8s.io/api/authentication/v1beta1" 
  7.  "k8s.io/klog/v2" 
  8.  "net/http" 
  9.  "strings" 
  10.  
  11. type WebHookServer struct { 
  12.  server *http.Server 
  13.  
  14. func (ctx *WebHookServer) serve(w http.ResponseWriter, r *http.Request) { 
  15.  // 从APIServer中取出body 
  16.  // 将body进行拆分, 取出type 
  17.  // 根据type, 取出不同的认证数据 
  18.  var req authentication.TokenReview 
  19.  decoder := json.NewDecoder(r.Body) 
  20.  err := decoder.Decode(&req) 
  21.  if err != nil { 
  22.   klog.Error(err, "decoder request body error."
  23.   req.Status = authentication.TokenReviewStatus{Authenticated: false
  24.   w.WriteHeader(http.StatusUnauthorized) 
  25.   _ = json.NewEncoder(w).Encode(req) 
  26.   return 
  27.  } 
  28.  // 判断token是否包含':' 
  29.  // 如果不包含,则返回认证失败 
  30.  if !(strings.Contains(req.Spec.Token, ":")) { 
  31.   klog.Error(err, "token invalied."
  32.   req.Status = authentication.TokenReviewStatus{Authenticated: false
  33.   //req.Status = map[string]interface{}{"authenticated"false
  34.   w.WriteHeader(http.StatusUnauthorized) 
  35.   _ = json.NewEncoder(w).Encode(req) 
  36.   return 
  37.  } 
  38.  // split token, 获取type 
  39.  tokenSlice := strings.SplitN(req.Spec.Token, ":", -1) 
  40.  glog.Infof("tokenSlice: ", tokenSlice) 
  41.  hookType := tokenSlice[0] 
  42.  switch hookType { 
  43.  case "github"
  44.   githubToken := tokenSlice[1] 
  45.   err := authByGithub(githubToken) 
  46.   if err != nil { 
  47.    klog.Error(err, "auth by github error"
  48.    req.Status = authentication.TokenReviewStatus{Authenticated: false
  49.    w.WriteHeader(http.StatusUnauthorized) 
  50.    _ = json.NewEncoder(w).Encode(req) 
  51.    return 
  52.   } 
  53.   klog.Info("auth by github success"
  54.   req.Status = authentication.TokenReviewStatus{Authenticated: true
  55.   w.WriteHeader(http.StatusOK) 
  56.   _ = json.NewEncoder(w).Encode(req) 
  57.   return 
  58.  case "ldap"
  59.   username := tokenSlice[1] 
  60.   password := tokenSlice[2] 
  61.   err := authByLdap(username, password
  62.   if err != nil { 
  63.    klog.Error(err, "auth by ldap error"
  64.    req.Status = authentication.TokenReviewStatus{Authenticated: false
  65.    //req.Status = map[string]interface{}{"authenticated"false
  66.    w.WriteHeader(http.StatusUnauthorized) 
  67.    _ = json.NewEncoder(w).Encode(req) 
  68.    return 
  69.   } 
  70.   klog.Info("auth by ldap success"
  71.   req.Status = authentication.TokenReviewStatus{Authenticated: true
  72.   //req.Status = map[string]interface{}{"authenticated"true
  73.   w.WriteHeader(http.StatusOK) 
  74.   _ = json.NewEncoder(w).Encode(req) 
  75.   return 
  76.  } 

主要是解析认证的请求Token,然后将Token进行拆分判断是需要什么认证,Token的样例如下:

  • Github认证:github:
  • LDAP认证:ldap::

这样就可以获取到用户想用哪种认证,再掉具体的认证服务进行处理。

(3)创建github.go,提供github认证方法

  1. package main 
  2.  
  3. import ( 
  4.  "context" 
  5.  "github.com/golang/glog" 
  6.  "github.com/google/go-github/github" 
  7.  "golang.org/x/oauth2" 
  8.  
  9. func authByGithub(token string) (err error) { 
  10.  glog.V(2).Info("start auth by github......"
  11.  tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) 
  12.  tokenClient := oauth2.NewClient(context.Background(), tokenSource) 
  13.  githubClient := github.NewClient(tokenClient) 
  14.  _, _, err = githubClient.Users.Get(context.Background(), ""
  15.  if err != nil { 
  16.   return err 
  17.  } 
  18.  return nil 

可以看到,这里仅仅做了一个简单的Token认证,认证的结果比较粗暴,如果err=nil,则表示认证成功。

(4)创建ldap.go,提供ldap认证

  1. package main 
  2.  
  3. import ( 
  4.  "crypto/tls" 
  5.  "errors" 
  6.  "fmt" 
  7.  "github.com/go-ldap/ldap/v3" 
  8.  "github.com/golang/glog" 
  9.  "k8s.io/klog/v2" 
  10.  "strings" 
  11.  
  12. var ( 
  13.  ldapUrl = "ldap://" + "192.168.100.179:389" 
  14.  
  15. func authByLdap(username, password string) error { 
  16.  groups, err := getLdapGroups(username, password
  17.  if err != nil { 
  18.   return err 
  19.  } 
  20.  if len(groups) > 0 { 
  21.   return nil 
  22.  } 
  23.  
  24.  return fmt.Errorf("No matching group or user attribute. Authentication rejected, Username: %s", username) 
  25.  
  26. // 获取user的groups 
  27. func getLdapGroups(username, password string) ([]string, error) { 
  28.  glog.Info("username:password", username, ":"password
  29.  var groups []string 
  30.  
  31.  config := &tls.Config{InsecureSkipVerify: true
  32.  ldapConn, err := ldap.DialURL(ldapUrl, ldap.DialWithTLSConfig(config)) 
  33.  if err != nil { 
  34.   glog.V(4).Info("dial ldap failed, err: ", err) 
  35.   return groups, err 
  36.  } 
  37.  defer ldapConn.Close() 
  38.  
  39.  binduser := fmt.Sprintf("CN=%s,ou=People,dc=demo,dc=com", username) 
  40.  
  41.  err = ldapConn.Bind(binduser, password
  42.  if err != nil { 
  43.   klog.V(4).ErrorS(err, "bind user to ldap error"
  44.   return groups, err 
  45.  } 
  46.  
  47.  // 查询用户成员 
  48.  searchString := fmt.Sprintf("(&(objectClass=person)(cn=%s))", username) 
  49.  memberSearchAttribute := "memberOf" 
  50.  searchRequest := ldap.NewSearchRequest( 
  51.   "dc=demo,dc=com"
  52.   ldap.ScopeWholeSubtree, 
  53.   ldap.NeverDerefAliases, 
  54.   0, 
  55.   0, 
  56.   false
  57.   searchString, 
  58.   []string{memberSearchAttribute}, 
  59.   nil, 
  60.  ) 
  61.  searchResult, err := ldapConn.Search(searchRequest) 
  62.  if err != nil { 
  63.   klog.V(4).ErrorS(err, "search user properties error"
  64.   return groups, err 
  65.  } 
  66.  // 如果没有查到结果,返回失败 
  67.  if len(searchResult.Entries[0].Attributes) < 1 { 
  68.   return groups, errors.New("no user in ldap"
  69.  } 
  70.  entry := searchResult.Entries[0] 
  71.  for _, e := range entry.Attributes { 
  72.   for _, attr := range e.Values { 
  73.    groupList := strings.Split(attr, ","
  74.    for _, g := range groupList { 
  75.     if strings.HasPrefix(g, "cn=") { 
  76.      group := strings.Split(g, "="
  77.      groups = append(groups, group[1]) 
  78.     } 
  79.    } 
  80.   } 
  81.  } 
  82.  return groups, nil 

这里的用户名是固定了的,所以不适合其他场景。

(5)创建main.go入口函数

  1. package main 
  2.  
  3. import ( 
  4.  "context" 
  5.  "flag" 
  6.  "fmt" 
  7.  "github.com/golang/glog" 
  8.  "net/http" 
  9.  "os" 
  10.  "os/signal" 
  11.  "syscall" 
  12.  
  13. var port string 
  14.  
  15. func main() { 
  16.  flag.StringVar(&port, "port""9999""http server port"
  17.  flag.Parse() 
  18.  // 启动httpserver 
  19.  wbsrv := WebHookServer{server: &http.Server{ 
  20.   Addr: fmt.Sprintf(":%v", port), 
  21.  }} 
  22.  mux := http.NewServeMux() 
  23.  mux.HandleFunc("/auth", wbsrv.serve) 
  24.  wbsrv.server.Handler = mux 
  25.  
  26.  // 启动协程来处理 
  27.  go func() { 
  28.   if err := wbsrv.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 
  29.    glog.Errorf("Failed to listen and serve webhook server: %v", err) 
  30.   } 
  31.  }() 
  32.  
  33.  glog.Info("Server started"
  34.  
  35.  // 优雅退出 
  36.  signalChan := make(chan os.Signal, 1) 
  37.  signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 
  38.  <-signalChan 
  39.  
  40.  glog.Infof("Got OS shutdown signal, shutting down webhook server gracefully..."
  41.  _ = 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)、将配置文件放到相应的目录

  1. # mkdir /etc/kubernetes/webhook 
  2. # cat >> webhook-config.json <EOF 
  3.   "kind""Config"
  4.   "apiVersion""v1"
  5.   "preferences": {}, 
  6.   "clusters": [ 
  7.     { 
  8.       "name""github-authn"
  9.       "cluster": { 
  10.         "server""http://10.0.4.9:9999/auth" 
  11.       } 
  12.     } 
  13.   ], 
  14.   "users": [ 
  15.     { 
  16.       "name""authn-apiserver"
  17.       "user": { 
  18.         "token""secret" 
  19.       } 
  20.     } 
  21.   ], 
  22.   "contexts": [ 
  23.     { 
  24.       "name""webhook"
  25.       "context": { 
  26.         "cluster""github-authn"
  27.         "user""authn-apiserver" 
  28.       } 
  29.     } 
  30.   ], 
  31.   "current-context""webhook" 
  32. EOF 

 (2)在kube-apiserver中添加配置参数

  1. # mkdir /etc/kubernetes/backup 
  2. # cp /etc/kubernetes/manifests/kube-apiserver.yaml /etc/kubernetes/backup/kube-apiserver.yaml 
  3. # cd /etc/kubernetes/manifests/ 
  4. # cat kube-apiserver.yaml 
  5. apiVersion: v1 
  6. kind: Pod 
  7. metadata: 
  8.   annotations: 
  9.     kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 10.0.4.9:6443 
  10.   creationTimestamp: null 
  11.   labels: 
  12.     component: kube-apiserver 
  13.     tier: control-plane 
  14.   name: kube-apiserver 
  15.   namespace: kube-system 
  16. spec: 
  17.   containers: 
  18.   - command: 
  19.     - kube-apiserver 
  20.     - ...... 
  21.     - --authentication-token-webhook-config-file=/etc/config/webhook-config.json 
  22.     image: registry.cn-hangzhou.aliyuncs.com/google_containers/kube-apiserver:v1.22.0 
  23.     imagePullPolicy: IfNotPresent 
  24.     ...... 
  25.     volumeMounts: 
  26.     ...... 
  27.     - name: webhook-config 
  28.       mountPath: /etc/config 
  29.       readOnly: true 
  30.   hostNetwork: true 
  31.   priorityClassName: system-node-critical 
  32.   securityContext: 
  33.     seccompProfile: 
  34.       type: RuntimeDefault 
  35.   volumes: 
  36.   ...... 
  37.   - hostPath: 
  38.       path: /etc/kubernetes/webhook 
  39.       type: DirectoryOrCreate 
  40.     name: webhook-config 
  41. status: {} 

ps: 为了节约篇幅,上面省略了部分配置。

当修改完过后,kube-apiserver会自动重启。

测试Github认证

(1)在github上获取Token,操作如图所示

(2)配置kubeconfig,添加user

  1. # cat ~/.kube/config  
  2. apiVersion: v1 
  3. ...... 
  4. users: 
  5. name: joker 
  6.   user
  7.     token: github:ghp_jevHquU4g43m46nczWS0ojxxxxxxxxx 

(3)用Joker用户进行访问

返回结果如下,至于报错是因为用户的权限不足。

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

可以在webhook上看到日志信息,如下:

  1. # ./kubernetes-auth-webhook  
  2. 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的安装方式有很多,可以使用容器部署,也可以直接安装在裸机上,这里采用后者。

  1. # yum install -y openldap openldap-clients openldap-servers  
  2. # systemctl start slapd 
  3. # systemctl enable slapd 

默认配置文件,位于/etc/openldap/slapd.d, 文件格式为LDAP Input Format (LDIF), ldap目录特定的格式。这里不对配置文件做太多的介绍,有兴趣可以自己去学习学习【1】。

在LDAP上配置用户

(1)导入模板

  1. ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/cosine.ldif  
  2. ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/nis.ldif  
  3. ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/inetorgperson.ldif  

(2)创建base组织

  1. # cat base.ldif 
  2. dn: dc=demo,dc=com 
  3. objectClass: top 
  4. objectClass: dcObject 
  5. objectClass: organization 
  6. o: ldap测试组织 
  7. dc: demo 
  8.  
  9. dn: cn=Manager,dc=demo,dc=com 
  10. objectClass: organizationalRole 
  11. cn: Manager 
  12. description: 组织管理人 
  13.  
  14. dn: ou=People,dc=demo,dc=com 
  15. objectClass: organizationalUnit 
  16. ou: People 
  17.  
  18. dn: ou=Group,dc=demo,dc=com 
  19. objectClass: organizationalUnit 
  20. ou: Group 

使用ldapadd添加base。

  1. ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f base.ldif  

(3)添加成员

  1. # cat adduser.ldif 
  2. dn: cn=jack,ou=People,dc=demo,dc=com 
  3. changetype: add 
  4. objectClass: inetOrgPerson 
  5. cn: jack 
  6. departmentNumber: 1 
  7. title: 大牛 
  8. userPassword: 123456 
  9. sn: Bai 
  10. mail: jack@demo.com 
  11. displayName: 中文名 

使用ldapadd执行添加。

  1. ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f adduser.ldif 

(4)将用户添加到组

  1. # cat add_member_group.ldif  
  2. dn: cn=g-admin,ou=Group,dc=demo,dc=com 
  3. changetype: modify 
  4. add: member 
  5. member: cn=jack,ou=People,dc=demo,dc=com 

使用ldapadd执行添加。

  1. ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f add_member_group.ldif 

配置kubeconfig,进行ldap认证测试

(1)修改~/.kube/config配置文件

  1. # cat ~/.kube/config  
  2. apiVersion: v1 
  3. ...... 
  4. users: 
  5. name: joker 
  6.   user
  7.     token: github:ghp_jevHquU4g43m46nczWS0oxxxxxxxx 
  8. name: jack 
  9.   user
  10.     token: ldap:jack:123456 

(2)使用kubectl进行测试

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

webhook服务日志如下:

  1. # ./kubernetes-auth-webhook  
  2. I1207 16:09:09.292067    7605 webhook.go:72] auth by ldap success 

通过测试结果可以看到使用LDAP认证测试成功。

总结

使用Webhook可以很灵活的将K8S的租户和企业内部账户系统进行打通,这样可以方便管理用户账户。

不过上面开发的Webhook只是一个简单的例子,验证方式和手法都比较粗暴,CoreOS开源的Dex【2】是比较不错的产品,可以直接使用。

本文转载自微信公众号「运维开发故事」

 

责任编辑:姜华 来源: 运维开发故事
相关推荐

2015-02-12 15:33:43

微信SDK

2022-04-22 13:32:01

K8s容器引擎架构

2023-03-31 07:17:16

2015-02-12 15:38:26

微信SDK

2023-11-06 07:16:22

WasmK8s模块

2023-09-06 08:12:04

k8s云原生

2022-04-29 10:40:38

技术服务端K8s

2021-02-03 14:04:52

k8spermissionm管理工具

2023-01-04 17:42:22

KubernetesK8s

2009-09-07 22:00:15

LINQ自定义

2022-05-18 07:44:13

自定义菜单前端

2022-09-07 15:57:41

KubernetesCRD

2024-01-26 14:35:03

鉴权K8sNode

2023-09-11 14:21:00

2023-05-25 21:38:30

2023-08-04 08:19:02

2023-08-03 08:36:30

Service服务架构

2023-09-15 07:34:15

AIOps云原生项目

2024-03-04 18:42:11

网络策略k8s网络通信
点赞
收藏

51CTO技术栈公众号