作者:乔克
公众号:运维开发故事
知乎:乔克叔叔
博客:https://www.coolops.cn
大家好,我是乔克。
在 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 list 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】是比较不错的产品,可以直接使用。
链接
【1】http://www.ldap.org.cn/286.html
【2】https://github.com/dexidp/dex
【3】项目地址:https://gitee.com/coolops/kubernetes-auth-webhook.git