K8S自定义webhook实现认证管理

本文涉及的产品
访问控制,不限时长
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
简介: K8S自定义webhook实现认证管理

大家好,我是乔克。


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


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

640.png

客户端(普通账户、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的认证请求,然后调用不同的认证服务进行认证,如下所示。


640.png


这里只是做一个Webhook的例子,目前主要实现了GithubLDAP认证,当然,认证部分的功能比较单一,没有考虑复杂的场景。


Webhook开发


开发环境


软件 版本
Go 1.17.3
Kubernetes v1.22.3
System CentOS7.6


构建符合规范的Webhook


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



{
  "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,操作如图所示


640.png


(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】是比较不错的产品,可以直接使用。

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
6月前
|
Kubernetes 网络协议 网络安全
提升你的云技能:深入了解CKA认证之k8s升级秘籍!
提升你的云技能:深入了解CKA认证之k8s升级秘籍!
101 0
|
6月前
|
Kubernetes 监控 数据安全/隐私保护
K8s好看的管理页面Rancher管理K8S
K8s好看的管理页面Rancher管理K8S
88 4
|
6月前
|
Kubernetes 开发工具 Docker
微服务实践k8s与dapr开发部署实验(2)状态管理
微服务实践k8s与dapr开发部署实验(2)状态管理
107 3
微服务实践k8s与dapr开发部署实验(2)状态管理
|
6月前
|
存储 Kubernetes API
使用Kubernetes管理容器化应用的深度解析
【5月更文挑战第20天】本文深度解析Kubernetes在管理容器化应用中的作用。Kubernetes是一个开源平台,用于自动化部署、扩展和管理容器,提供API对象描述应用资源并维持其期望状态。核心组件包括负责集群控制的Master节点(含API Server、Scheduler、Controller Manager和Etcd)和运行Pod的工作节点Node(含Kubelet、Kube-Proxy和容器运行时环境)。
|
2月前
|
Kubernetes 容器
k8s基于secretRef认证对接rbd块设备
文章介绍了如何在Kubernetes集群中通过secretRef认证方式接入Ceph的RBD块设备,并提供了详细的步骤和配置文件示例。
40 7
|
2月前
|
Kubernetes 容器 Perl
k8s基于keyring文件认证对接rbd块设备
文章介绍了如何在Kubernetes集群中使用Ceph的keyring文件进行认证,并对接RBD块设备,包括使用admin用户和自定义用户两种方式的详细步骤和注意事项。
32 3
|
2月前
|
Kubernetes 安全 API
Kubernetes系统安全-认证(Authentication)
文章主要介绍了Kubernetes系统中的安全认证机制,包括API服务器的访问控制、认证、授权策略和准入控制,以及如何使用kubeconfig文件和创建自定义用户与服务账号。
128 0
Kubernetes系统安全-认证(Authentication)
|
4月前
|
存储 Kubernetes 容器
k8s卷管理-2
k8s卷管理-2
25 2
|
4月前
|
存储 Kubernetes 调度
k8s卷管理-1
k8s卷管理-1
21 2
|
4月前
|
Kubernetes 持续交付 Python
Kubernetes(通常简称为K8s)是一个开源的容器编排系统,用于自动化部署、扩展和管理容器化应用程序。
Kubernetes(通常简称为K8s)是一个开源的容器编排系统,用于自动化部署、扩展和管理容器化应用程序。