kubernetes kube-apiserver源码阅读3之认证

文章目录

kubernetes代码版本: v1.20.2

从前文我们知道,认证,鉴权,审计等操作的并没有集成在GenericServer里面,所以本文看看这个请求链是怎么工作的。

DefaultBuildHandlerChain

说实话这个BuildHandlerChainFunc的构造藏的还挺深,如果不了解k8s的一些惯用模式还是很难找的.

最简单的办法其实就是暴力搜素BuildHandlerChainFunc关键字, 因为笔者已经找过了,所以就直接贴初始化过程吧。

func CreateServerChain(completedOptions completedServerRunOptions, stopCh <-chan struct{}) (*aggregatorapiserver.APIAggregator, error) {
	kubeAPIServerConfig, serviceResolver, pluginInitializer, err := CreateKubeAPIServerConfig(completedOptions, nodeTunneler, proxyTransport)
}

func CreateKubeAPIServerConfig()  {
	genericConfig, versionedInformers, serviceResolver, pluginInitializers, admissionPostStartHook, storageFactory, err := buildGenericConfig(s.ServerRunOptions, proxyTransport)
}

func buildGenericConfig() {
	genericConfig = genericapiserver.NewConfig(legacyscheme.Codecs)
}

func NewConfig(codecs serializer.CodecFactory) *Config {
	return &Config{
		Serializer:                  codecs,
		BuildHandlerChainFunc:       DefaultBuildHandlerChain,
    }
}

说实话这个调用链还是比较深的,在CreateServerChain函数体的CreateKubeAPIServerConfig中会构建一个kubeAPIServerConfig, 这个对象会创建一个k8s.io/apiserver/pkg/server.Config对象。

总的来说我们知道BuildHandlerChainFunc的值是DefaultBuildHandlerChain 现在看看他的业务逻辑。

func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
	handler := filterlatency.TrackCompleted(apiHandler)
    // 鉴权的处理函数
	handler = genericapifilters.WithAuthorization(handler, c.Authorization.Authorizer, c.Serializer)
	handler = filterlatency.TrackStarted(handler, "authorization")

	if c.FlowControl != nil {
		handler = filterlatency.TrackCompleted(handler)
		handler = genericfilters.WithPriorityAndFairness(handler, c.LongRunningFunc, c.FlowControl)
		handler = filterlatency.TrackStarted(handler, "priorityandfairness")
	} else {
		handler = genericfilters.WithMaxInFlightLimit(handler, c.MaxRequestsInFlight, c.MaxMutatingRequestsInFlight, c.LongRunningFunc)
	}

	handler = filterlatency.TrackCompleted(handler)
	handler = genericapifilters.WithImpersonation(handler, c.Authorization.Authorizer, c.Serializer)
	handler = filterlatency.TrackStarted(handler, "impersonation")

	handler = filterlatency.TrackCompleted(handler)
	handler = genericapifilters.WithAudit(handler, c.AuditBackend, c.AuditPolicyChecker, c.LongRunningFunc)
	handler = filterlatency.TrackStarted(handler, "audit")

	failedHandler := genericapifilters.Unauthorized(c.Serializer)
	failedHandler = genericapifilters.WithFailedAuthenticationAudit(failedHandler, c.AuditBackend, c.AuditPolicyChecker)

	failedHandler = filterlatency.TrackCompleted(failedHandler)
	handler = filterlatency.TrackCompleted(handler)
    // 认证的处理函数
	handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences)
	handler = filterlatency.TrackStarted(handler, "authentication")

	handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true")
	handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, c.LongRunningFunc, c.RequestTimeout)
	handler = genericfilters.WithWaitGroup(handler, c.LongRunningFunc, c.HandlerChainWaitGroup)
    // 构造RequestInfo对象,后续的处理还是都会使用
	handler = genericapifilters.WithRequestInfo(handler, c.RequestInfoResolver)
	if c.SecureServing != nil && !c.SecureServing.DisableHTTP2 && c.GoawayChance > 0 {
		handler = genericfilters.WithProbabilisticGoaway(handler, c.GoawayChance)
	}
	handler = genericapifilters.WithAuditAnnotations(handler, c.AuditBackend, c.AuditPolicyChecker)
	handler = genericapifilters.WithWarningRecorder(handler)
	handler = genericapifilters.WithCacheControl(handler)
	handler = genericapifilters.WithRequestReceivedTimestamp(handler)
	handler = genericfilters.WithPanicRecovery(handler, c.RequestInfoResolver)
	return handler
}

上面的代码有点类似装饰器,一层包一层,最终返回一个反方向的调用链,即最下面的处理函数会最先执行

在处理链的处理过程中个人认为最重要的两个部分就是认证鉴权,所以这里主要讲解这两个处理函数。

WithRequestInfo

不过在此之前需要先看看整个处理链中比较通用的部分,这个处理函数会为后续的处理函数提供一个通用上下文对象,即RequestInfo

func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
	handler = genericapifilters.WithRequestInfo(handler, c.RequestInfoResolver)
}

func WithRequestInfo(handler http.Handler, resolver request.RequestInfoResolver) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		ctx := req.Context()
        // 1.
		info, err := resolver.NewRequestInfo(req)
        // 2.
		req = req.WithContext(request.WithRequestInfo(ctx, info))
		handler.ServeHTTP(w, req)
	})
}

func WithRequestInfo(parent context.Context, info *RequestInfo) context.Context {
    // 3.
	return WithValue(parent, requestInfoKey, info)
}
// 4.
func WithValue(parent context.Context, key interface{}, val interface{}) context.Context {
	return context.WithValue(parent, key, val)
}

代码分解如下:

  1. 使用resolver将http.Request解析成一个RequestInfo对象
  2. RequestInfo对象通过http.Request中的context对象保存数据,以备后续处理函数直接使用
  3. 上一步的具体实现
  4. 上一步的具体实现

从上面的代码可以了解到,重点应该是resolver, 而这个resolver其实在Config对象的Complete阶段才初始化。

func (c *Config) Complete(informers informers.SharedInformerFactory) CompletedConfig {
    if c.RequestInfoResolver == nil {
		c.RequestInfoResolver = NewRequestInfoResolver(c)
	}
}

func NewRequestInfoResolver(c *Config) *apirequest.RequestInfoFactory {
	return &apirequest.RequestInfoFactory{
		APIPrefixes:          apiPrefixes,
		GrouplessAPIPrefixes: legacyAPIPrefixes,
	}
}

func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, error) {
	requestInfo := RequestInfo{
		IsResourceRequest: false,
		Path:              req.URL.Path,
		Verb:              strings.ToLower(req.Method),
	}

	currentParts := splitPath(req.URL.Path)
    // 检查是否有/api /apis等前缀
	if !r.APIPrefixes.Has(currentParts[0]) {
		// return a non-resource request
		return &requestInfo, nil
	}
	requestInfo.APIPrefix = currentParts[0]
	requestInfo.IsResourceRequest = true
	requestInfo.APIVersion = currentParts[0]
	currentParts = currentParts[1:]

    // 处理的特别的url显示  /{specialVerb}/* 没遇见过
	if specialVerbs.Has(currentParts[0]) {
		if len(currentParts) < 2 {
			return &requestInfo, fmt.Errorf("unable to determine kind and namespace from url, %v", req.URL)
		}

		requestInfo.Verb = currentParts[0]
		currentParts = currentParts[1:]

	} else {
        // 将HTTP方法转换
		switch req.Method {
		case "POST":
			requestInfo.Verb = "create"
		case "GET", "HEAD":
			requestInfo.Verb = "get"
		case "PUT":
			requestInfo.Verb = "update"
		case "PATCH":
			requestInfo.Verb = "patch"
		case "DELETE":
			requestInfo.Verb = "delete"
		default:
			requestInfo.Verb = ""
		}
	}

	// 检查请求的资源是否是namespace scoped
	if currentParts[0] == "namespaces" {
		if len(currentParts) > 1 {
			requestInfo.Namespace = currentParts[1]
			if len(currentParts) > 2 && !namespaceSubresources.Has(currentParts[2]) {
				currentParts = currentParts[2:]
			}
		}
	} else {
		requestInfo.Namespace = metav1.NamespaceNone
	}

	requestInfo.Parts = currentParts

	// 检查是否是子资源
	switch {
	case len(requestInfo.Parts) >= 3 && !specialVerbsNoSubresources.Has(requestInfo.Verb):
		requestInfo.Subresource = requestInfo.Parts[2]
		fallthrough
	case len(requestInfo.Parts) >= 2:
		requestInfo.Name = requestInfo.Parts[1]
		fallthrough
	case len(requestInfo.Parts) >= 1:
		requestInfo.Resource = requestInfo.Parts[0]
	}

	return &requestInfo, nil
}

这部分代码并不复杂,所以就贴上来了,而注释足够清晰了就不分解了。

总的来说,requestInfo是对http.Request的一个抽象,将HTTP请求转换成了k8s可以更好的理解的形式,比如将HTTP方法生成对应的Verb, 如create, update, watch等, 但是最重要的是将url里的资源名,命名空间,子资源等数据解析出来,这对后面的路由注册很重要。

认证

值得注意的是,认证不是授权,如果没有携带任何认证信息自然会被认证成Anonymous, 虽然被认证了,但是并不代表Anonymous有权限访问,是否有权限访问由鉴权的处理逻辑决定。

下面是处理函数的处理逻辑。

func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
	handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences)
}

func WithAuthentication() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		// 1.
		resp, ok, err := auth.AuthenticateRequest(req)
		defer recordAuthMetrics(resp, ok, err, apiAuds, authenticationStart)
        // 2.
		if err != nil || !ok {
			failed.ServeHTTP(w, req)
			return
		}
		if !audiencesAreAcceptable(apiAuds, resp.Audiences) {
			err = fmt.Errorf("unable to match the audience: %v , accepted: %v", resp.Audiences, apiAuds)
			failed.ServeHTTP(w, req)
			return
		}

		// 3.
		req.Header.Del("Authorization")
		req = req.WithContext(genericapirequest.WithUser(req.Context(), resp.User))
		handler.ServeHTTP(w, req)
	})
}

代码分解如下:

  1. 尝试认证并返回结果
  2. 如果认证失败则直接用失败的handler返回
  3. 删除Authorization头并在http.Request里存储User

这里的逻辑并不是太复杂,复杂的是认证的逻辑和认证的配置过程。

Authentication初始化

熟悉的k8s代码的人应该知道k8s的配置大致有两种对象,OptionsConfig, Options跟命令行参数比较近,而后者里核心组件比较近,然后两者一般在Complete方法中转换,众所周知, k8s可以通过命令行参数配置认证方法,所以可以先去Options对象中找找绑定的Authentication相关参数,然后再到Complete方法中找找是否有将Options对象参数复制到Config对象的逻辑。

func NewAPIServerCommand() *cobra.Command {
	s := options.NewServerRunOptions()
}

func NewServerRunOptions() *ServerRunOptions {
	s := ServerRunOptions{
		Authentication:          kubeoptions.NewBuiltInAuthenticationOptions().WithAll(),
    }
}

func NewBuiltInAuthenticationOptions() *BuiltInAuthenticationOptions {
	return &BuiltInAuthenticationOptions{
		TokenSuccessCacheTTL: 10 * time.Second,
		TokenFailureCacheTTL: 0 * time.Second,
	}
}

// pkg\kubeapiserver\options\authentication.go
func (o *BuiltInAuthenticationOptions) WithAll() *BuiltInAuthenticationOptions {
	return o.
		WithAnonymous().
		WithBootstrapToken().
		WithClientCert().
		WithOIDC().
		WithRequestHeader().
		WithServiceAccounts().
		WithTokenFile().
		WithWebHook()
}

通过上面的代码我们可以知道,k8s的认证方式支持8种。虽然支持八种,但是大多数人只会使用其中的5种。

  • WithAnonymous
  • WithBootstrapToken
  • WithClientCert
  • WithRequestHeader
  • WithServiceAccounts

比如这样的启动命令

# 为了方便查看,每个参数都换行了
kube-apiserver
# WithBootstrapToken
--enable-bootstrap-token-auth=true
# WithClientCert
--client-ca-file=/etc/kubernetes/pki/ca.crt
# WithRequestHeader
--requestheader-allowed-names=front-proxy-client
--requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
--requestheader-extra-headers-prefix=X-Remote-Extra-
--requestheader-group-headers=X-Remote-Group
--requestheader-username-headers=X-Remote-User
# WithServiceAccounts
--service-account-issuer=https://kubernetes.default.svc.cluster.local
--service-account-key-file=/etc/kubernetes/pki/sa.pub
--service-account-signing-key-file=/etc/kubernetes/pki/sa.key

虽然一般会启用五种认证方式,但是日常使用的时候就两种

  • WithClientCert 使用kubectl就是用这种方式
  • WithServiceAccounts pod内部的/var/run/secrets/kubernetes.io/serviceaccount/token就是这种认证方式。

下面我们着重看这两种认证方式。

让我们看看Options对象是怎么将值赋值到Config对象的。

// cmd\kube-apiserver\app\server.go
func buildGenericConfig() {
    	if lastErr = s.Authentication.ApplyTo(&genericConfig.Authentication, genericConfig.SecureServing, genericConfig.EgressSelector, genericConfig.OpenAPIConfig, clientgoExternalClient, versionedInformers); lastErr != nil {
		return
	}
}

func (o *BuiltInAuthenticationOptions) ApplyTo() error {
	// 1.
	authenticatorConfig, err := o.ToAuthenticationConfig()
	// 2.
	if authenticatorConfig.ClientCAContentProvider != nil {
		if err = authInfo.ApplyClientCert(authenticatorConfig.ClientCAContentProvider, secureServing); err != nil {
			return fmt.Errorf("unable to load client CA file: %v", err)
		}
	}
	
    // 不太懂这个audiences的作用
	authInfo.APIAudiences = o.APIAudiences
	if o.ServiceAccounts != nil && o.ServiceAccounts.Issuer != "" && len(o.APIAudiences) == 0 {
		authInfo.APIAudiences = authenticator.Audiences{o.ServiceAccounts.Issuer}
	}

    // 3.
	authenticatorConfig.ServiceAccountTokenGetter = serviceaccountcontroller.NewGetterFromClient(
		extclient,
		versionedInformer.Core().V1().Secrets().Lister(),
		versionedInformer.Core().V1().ServiceAccounts().Lister(),
		versionedInformer.Core().V1().Pods().Lister(),
	)
	// 4.
	authInfo.Authenticator, openAPIConfig.SecurityDefinitions, err = authenticatorConfig.New()

	return nil
}

func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, error) {
	var authenticators []authenticator.Request
	var tokenAuthenticators []authenticator.Token
	securityDefinitions := spec.SecurityDefinitions{}

	// X509 methods
    // 5.
	if config.ClientCAContentProvider != nil {
		certAuth := x509.NewDynamic(config.ClientCAContentProvider.VerifyOptions, x509.CommonNameUserConversion)
		authenticators = append(authenticators, certAuth)
	}

	// 6.
	if config.ServiceAccountIssuer != "" {
		serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuer, config.ServiceAccountKeyFiles, config.APIAudiences, config.ServiceAccountTokenGetter)
		tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
	}
	
    // 7.
	if len(tokenAuthenticators) > 0 {
		// Union the token authenticators
		tokenAuth := tokenunion.New(tokenAuthenticators...)
		// Optionally cache authentication results
		if config.TokenSuccessCacheTTL > 0 || config.TokenFailureCacheTTL > 0 {
			tokenAuth = tokencache.New(tokenAuth, true, config.TokenSuccessCacheTTL, config.TokenFailureCacheTTL)
		}
		authenticators = append(authenticators, bearertoken.New(tokenAuth), websocket.NewProtocolAuthenticator(tokenAuth))
		securityDefinitions["BearerToken"] = &spec.SecurityScheme{
			SecuritySchemeProps: spec.SecuritySchemeProps{
				Type:        "apiKey",
				Name:        "authorization",
				In:          "header",
				Description: "Bearer Token authentication",
			},
		}
	}

	// 8.
	authenticator := union.New(authenticators...)
	authenticator = group.NewAuthenticatedGroupAdder(authenticator)

	return authenticator, &securityDefinitions, nil
}


// union.New的具体实现
func New(authRequestHandlers ...authenticator.Request) authenticator.Request {
	if len(authRequestHandlers) == 1 {
		return authRequestHandlers[0]
	}
	return &unionAuthRequestHandler{Handlers: authRequestHandlers, FailOnError: false}
}
func (authHandler *unionAuthRequestHandler) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
	var errlist []error
	for _, currAuthRequestHandler := range authHandler.Handlers {
		resp, ok, err := currAuthRequestHandler.AuthenticateRequest(req)
		if err != nil {
			if authHandler.FailOnError {
				return resp, ok, err
			}
			errlist = append(errlist, err)
			continue
		}

		if ok {
			return resp, ok, err
		}
	}

	return nil, false, utilerrors.NewAggregate(errlist)
}

代码分解如下

  1. 解析命令行传递参数并加载对应的配置对象k8s.io/kubernetes/pkg/kubeapiserver/authenticator.Config,比如加载证书认证(WithClientCert)的根证书, 这个Config对象的数据会依次赋值给genericapiserver.AuthenticationInfo,这个对象最终会赋值到c.Authentication
  2. 校验证书认证(WithClientCert)
  3. 创建获取serviceaccount的getter。 serviceaccount等信息都在secrets等对象中
  4. 创建一个Authenticator, 它实现了AuthenticateRequest, 用于在请求链中认证。
  5. 增加证书认证(WithClientCert)的authenticator
  6. 增加WithServiceAccountsauthenticator
  7. 虽然WithServiceAccounts, WithBootstrapToken,WithTokenFile等认证方法的具体实现不同,但是都是Token认证,也就是使用HTTP请求头Authorization: "Bearer <Token>"这样的认证方式,所以这里聚合在一起。
  8. 将所有authenticator聚合在一起,认证的时候就会依次从前到后依次调用authenticatorAuthenticateRequest方法,成功了就返回,失败了就交个下一个authenticator来验证

WithClientCert

从上面我们看到了证书认证的构造过程,现在我们看看它的实现代码

func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
    // 1.
	if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 {
		return nil, false, nil
	}

	// 2.
	chains, err := req.TLS.PeerCertificates[0].Verify(optsCopy)


    // 3. 
	var errlist []error
	for _, chain := range chains {
		user, ok, err := a.user.User(chain)
		if err != nil {
			errlist = append(errlist, err)
			continue
		}

		if ok {
			return user, ok, err
		}
	}
	return nil, false, utilerrors.NewAggregate(errlist)
}

var CommonNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (*authenticator.Response, bool, error) {
	if len(chain[0].Subject.CommonName) == 0 {
		return nil, false, nil
	}
    // 4.
	return &authenticator.Response{
		User: &user.DefaultInfo{
			Name:   chain[0].Subject.CommonName,
			Groups: chain[0].Subject.Organization,
		},
	}, true, nil
})

代码分解如下:

  1. 判断客户端是否携带TLS证书
  2. 验证客户端的TLS证书是否是自己配置的根证书颁发的
  3. 获取客户端证书的Subject中配置的用户名, 用户组,比如Subject: CN=kubernetes-admin,O=system:masters
  4. 获取证书Subject字段的CN和O字段的值用以填充UserInfo

关于证书的信息大家可以使用certinfo这个工具来查看,这里展示一下一个kubeconfig里面根证书和客户端证书的信息

# /etc/kubernetes/pki/ca.crt就是kubeconfig里的certificate-authority-data
certinfo /etc/kubernetes/pki/ca.crt
Version: 3
Serial Number: 0
Signature Algorithm: SHA256-RSA
Type: root
Issuer: CN=kubernetes
Validity
    Not Before: Aug 11 19:21:39 2023 UTC
    Not After : Aug 11 19:21:39 2032 UTC
Subject: CN=kubernetes
DNS Names: 
IP Addresses: 
Key Usage: Digital Signature, Key Encipherment, Cert Sign
Ext Key Usage: 
CA: true

然后我们在看看客户端的证书client-certificate-data, 它是一段base64编码的数据,需要自行解码

certinfo client.pem
Version: 3
Serial Number: 218981394846439475
Signature Algorithm: SHA256-RSA
Type: end-entity
# 颁布者指向ca证书
Issuer: CN=kubernetes
Validity
    Not Before: Aug 22 09:06:39 2022 UTC
    Not After : Aug 22 09:06:43 2023 UTC
# Subject标识用户信息
# kubernetes-admin是用户名
# system:masters是用户组名
Subject: CN=kubernetes-adminshi,O=system:masters
DNS Names: 
IP Addresses: 
Key Usage: Digital Signature, Key Encipherment
Ext Key Usage: Client Auth
CA: false

至此kube-apiserver通过客户端的证书认证了用户的请求,并从请求中获取了用户名等相关信息。

WithServiceAccounts

现在继续来看看ServiceAccount怎么认证

首先来看看一个default命名空间里的pod挂载的token是怎么样的。

 cat /var/run/secrets/kubernetes.io/serviceaccount/token 
eyJhbGciOiJSUzI1NiIsImtpZCI6Ikw1TWtwS2FWeDZTdVJJMEtCbHN6M2V6QnVDMXRMVkZVa2o1UkQwM2xfNVkifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tMmZoOHIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjE1YzEzNThkLWU5ZDktNDFkZC05NDlkLTY3YWIwYjk4YjZlYSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.pWJaQEUPkcLNorIZhrqG3qSkxpvZd_JUagVQL1B96dOoGM63q4f_fpuPPWtDSr9K5pG0Ba6FNTxLVivvyDmMb2PY1m9T4SL56OgKDsE19-sXs1CL6AYjb1B3gnV5FonDRE83meK-6Q_ootYhe_DE4duEyoqpSCE7vCRv1JozSeWgybWBWiGWcsvMcVmc28rqTDvf1gpGHbGep8iEAosyOnq5byzkGGrTho2iT7XeQLKq3hPbzUvb3OFljqxylSssbFHX725YcizDLi1YB3Glgw_OBmQwLWqYlLTxrFvxvPjqsEDcvrAPX6ODOjL3Bq7kMic5fMQenUWF3Y64ju32sw

这些数据可以通过https://jwt.io/来解码

jwt是一种开源的标准,数据由三部分组成header, payload, 签名, 下面看看前两者的解码数据

header

{
  "alg": "RS256",
  "kid": "L5MkpKaVx6SuRI0KBlsz3ezBuC1tLVFUkj5RD03l_5Y"
}

payload

{
  "iss": "kubernetes/serviceaccount",
  "kubernetes.io/serviceaccount/namespace": "default",
  "kubernetes.io/serviceaccount/secret.name": "default-token-2fh8r",
  "kubernetes.io/serviceaccount/service-account.name": "default",
  "kubernetes.io/serviceaccount/service-account.uid": "15c1358d-e9d9-41dd-949d-67ab0b98b6ea",
  "sub": "system:serviceaccount:default:default"
}

通过上面的数据我们能够很容易知道,这个token所携带的各种用户信息,其中用于验证的信息主要是namespace, uid, name。

初始化

然后我们回到serviceAccount的初始化和认证

// pkg\kubeapiserver\authenticator\config.go
func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, error) {
    if len(config.ServiceAccountKeyFiles) > 0 {
        // 1.
		serviceAccountAuth, err := newLegacyServiceAccountAuthenticator(config.ServiceAccountKeyFiles, config.ServiceAccountLookup, config.APIAudiences, config.ServiceAccountTokenGetter)
        // 2.
		tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
	}
    
    if len(tokenAuthenticators) > 0 {
		// 3.
		tokenAuth := tokenunion.New(tokenAuthenticators...)
		authenticators = append(authenticators, bearertoken.New(tokenAuth), websocket.NewProtocolAuthenticator(tokenAuth))
		securityDefinitions["BearerToken"] = &spec.SecurityScheme{
			SecuritySchemeProps: spec.SecuritySchemeProps{
				Type:        "apiKey",
				Name:        "authorization",
				In:          "header",
				Description: "Bearer Token authentication",
			},
		}
	}
}

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)
		allPublicKeys = append(allPublicKeys, publicKeys...)
	}
	// 4.
	tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(serviceaccount.LegacyIssuer, allPublicKeys, apiAudiences, serviceaccount.NewLegacyValidator(lookup, serviceAccountGetter))
	return tokenAuthenticator, nil
}

代码分解如下:

  1. 创建基于ServiceAccount认证的认证器(Authenticator)
  2. 因为token认证用的都是同一个HTTP头, 所以将所有token认证都放在tokenAuthenticators
  3. 将所有token验证器组合在一起,然后追加到authenticators里面
  4. 创建JWTTokenAuthenticator

这里简单的介绍一下JWT的认证方式,JWT基于非对称加密, 用私钥和公钥对元数据header(比如使用的算法), 数据(Payload), 生成一个签名,最后将三者组合在一起作为一个Token。

JWT认证的时候会秘钥验证签名的合法性,如果合法就说明是一个合法的token,那么携带的数据(Payload)就可信。

JWT详细介绍: https://jwt.io/introduction

认证

// vendor\k8s.io\apiserver\pkg\authentication\request\bearertoken\bearertoken.go
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
    // 1.
	auth := strings.TrimSpace(req.Header.Get("Authorization"))
	parts := strings.SplitN(auth, " ", 3)
	if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" {
		return nil, false, nil
	}
    // 2.
	token := parts[1]
	resp, ok, err := a.auth.AuthenticateToken(req.Context(), token)
	return resp, ok, err
}
// pkg\serviceaccount\jwt.go
func (j *jwtTokenAuthenticator) AuthenticateToken(ctx context.Context, tokenData string) (*authenticator.Response, bool, error) {
    //3.
	if !j.hasCorrectIssuer(tokenData) {
		return nil, false, nil
	}
	
    // 4.
	tok, err := jwt.ParseSigned(tokenData)

    // 5.
	public := &jwt.Claims{}
	private := j.validator.NewPrivateClaims()
	var (
		found   bool
		errlist []error
	)
	for _, key := range j.keys {
		if err := tok.Claims(key, public, private); err != nil {
			errlist = append(errlist, err)
			continue
		}
		found = true
		break
	}
	
    // 6.
	sa, err := j.validator.Validate(ctx, tokenData, public, private)
	return &authenticator.Response{
		User:      sa.UserInfo(),
		Audiences: auds,
	}, true, nil
}

func (sa *ServiceAccountInfo) UserInfo() user.Info {
	info := &user.DefaultInfo{
        // ServiceAccountUsernamePrefix + namespace + ServiceAccountUsernameSeparator + name
        // system:serviceaccount:default:default
		Name:   MakeUsername(sa.Namespace, sa.Name),
		UID:    sa.UID,
        // system:serviceaccounts
        // system:serviceaccounts:default
		Groups: MakeGroupNames(sa.Namespace),
	}
	return info
}

代码分解如下:

  1. 获取HTTP请求头的Authorization,并验证是一个合法的Authorization: Bearer <Token>
  2. 获取Token数据
  3. 验证token数据中的iss字段是否正确,即kubernetes/serviceaccount
  4. 解析token
  5. 验证是否合法,并找到其对应的私钥和公钥
  6. 用token对应的secrets再验证一遍是否合法。

总结

文章篇幅足够长了,所以鉴权的逻辑放在下一篇文章。

参考链接