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)
}
代码分解如下:
- 使用resolver将http.Request解析成一个
RequestInfo
对象 - 将
RequestInfo
对象通过http.Request
中的context
对象保存数据,以备后续处理函数直接使用 - 上一步的具体实现
- 上一步的具体实现
从上面的代码可以了解到,重点应该是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)
})
}
代码分解如下:
- 尝试认证并返回结果
- 如果认证失败则直接用失败的handler返回
- 删除
Authorization
头并在http.Request
里存储User
这里的逻辑并不是太复杂,复杂的是认证的逻辑和认证的配置过程。
Authentication初始化
熟悉的k8s代码的人应该知道k8s的配置大致有两种对象,Options
和Config
, 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)
}
代码分解如下
- 解析命令行传递参数并加载对应的配置对象
k8s.io/kubernetes/pkg/kubeapiserver/authenticator.Config
,比如加载证书认证(WithClientCert)的根证书, 这个Config对象的数据会依次赋值给genericapiserver.AuthenticationInfo
,这个对象最终会赋值到c.Authentication
- 校验证书认证(
WithClientCert
) - 创建获取serviceaccount的
getter
。 serviceaccount等信息都在secrets等对象中 - 创建一个
Authenticator
, 它实现了AuthenticateRequest
, 用于在请求链中认证。 - 增加证书认证(
WithClientCert
)的authenticator
- 增加
WithServiceAccounts
的authenticator
- 虽然
WithServiceAccounts
,WithBootstrapToken
,WithTokenFile
等认证方法的具体实现不同,但是都是Token认证,也就是使用HTTP请求头Authorization: "Bearer <Token>"
这样的认证方式,所以这里聚合在一起。 - 将所有
authenticator
聚合在一起,认证的时候就会依次从前到后依次调用authenticator
的AuthenticateRequest
方法,成功了就返回,失败了就交个下一个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
})
代码分解如下:
- 判断客户端是否携带TLS证书
- 验证客户端的TLS证书是否是自己配置的根证书颁发的
- 获取客户端证书的Subject中配置的用户名, 用户组,比如
Subject: CN=kubernetes-admin,O=system:masters
- 获取证书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
}
代码分解如下:
- 创建基于ServiceAccount认证的认证器(Authenticator)
- 因为token认证用的都是同一个HTTP头, 所以将所有token认证都放在
tokenAuthenticators
里 - 将所有token验证器组合在一起,然后追加到authenticators里面
- 创建
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
}
代码分解如下:
- 获取HTTP请求头的Authorization,并验证是一个合法的
Authorization: Bearer <Token>
- 获取Token数据
- 验证token数据中的iss字段是否正确,即
kubernetes/serviceaccount
- 解析token
- 验证是否合法,并找到其对应的私钥和公钥
- 用token对应的secrets再验证一遍是否合法。
总结
文章篇幅足够长了,所以鉴权的逻辑放在下一篇文章。