kubernetes apiserver源码分析——api请求的认证过程

文 王哲

解决什么问题

笔者之前希望全面分析一下k8apiserver的源码,后来发现这样并不十分有效,其一没有针对性,其二由于代码本身比较复杂,涉及到的功能较多,面面俱到也不太现实。

于是我们就回到最初的需求,到底需要解决什么问题,第一个问题就是,apiserver启动的时候,使用secure模式,参数应该如何设置,相关的机制又是怎样?

这一部分的issue很多,如果不从源码来分析的话,就只能黑盒化的去尝试各种参数搭配,费时费力,也不确定是否正确,之前就是这样,这几个issue可以供参考:

https://github.com/GoogleCloudPlatform/kubernetes/issues/10159#issuecomment-113955582

https://github.com/GoogleCloudPlatform/kubernetes/issues/11000

apiserver启动过程的代码概览

apiserver相当于是k8集群的一个入口,不论通过kubectl还是使用remote api 直接控制,都要经过apiserver。apiserver说白了就是一个server负责监听指定的端口,之后处理不同的请求,只不过加上的很多控制,k8s项目由那么多大牛构建,作为参考学习,看一下各个组件的源码,想必也是很有帮助的。

这里分析的是k8s v1.0.0 版本的代码,commit id 为 cd821444dcf3。

main函数的代码位于./kubernetes/cmd/kube-apiserver:

这一部分主要是进行一些初始化的设置,启动一个apiserver实例,再将其run起来。初始化的各种细节参数暂时不做重点分析,主要关注一下,app.NewAPIServer()以及s.AddFlags(pflag.CommandLine) 两个函数,New一个apiserver的时候会放入许多默认的初始化参数:

可以看到insecure的端口,以及一些默认的监听端口,还有默认的证书存放位置等等,都是一些比较重要的信息。

启动时候的全部参数通过s.AddFlags(pflag.CommandLine)这个函数传入,里面包括了apiserver启动时候的全部参数,这个使用的是"github.com/spf13/pflag"这个库,可以具体查看每个相关参数的含义以及初始值。把这些参数的含义弄清,启动的时候把对应的合适的值填进去,作为apiserver的基本使用就基本没问题了。

之后负责启动的操作都是在run函数中执行,前面初始化的具体细节暂不做分析,这里着重关注一下两部分,一个是master实例的生成:

这个是主要是生成master实例对象,各种api请求最后都是通过master对象来处理的。

还有一个是server启动的时候:

大致看一下这部分代码,首先是生成一个http.Server对象secureServer,设置好相关的启动参数,之后会新启一个goroutine,如果有ca文件,说明要使用https的方式,就把ca文件也一并加载进来,之后在新的goroutine中通过secureServer.ListenAndServeTLS启动secureserver,使用https的方式来监听指定的secure端口。

之后还会生成一个http.Server实例 http,这个就是采用insecure的方式,最后通过http.ListenAndServe()来启动。

对比两种启动方式,可以看到,它们加载的handler都来自与之前生成的master实例m,一个是m.Handler,另一个是m.InsecureHandler。采用m.Handler的时候会多一些额外的处理,这个暂不分析,总是这个Handler中存放的就是这个server去进行处理的各种路由和对应的实现方式。

api认证部分的实现

在上面提到的run函数中,可以找到认证组件的实现:

之后在生成master实例的时候,这个认证器authenticator会作为Master实例的初始参数传入:

这几个参数的使用也是之前issue里提到的问题比较多的地方,下面大致了解一下生成认证器的这几个参数,具体的使用在后面再进行具体的说明。

  • s.BasicAuthFile:指定basicauthfile文件所在的位置,当这个参数不为空的时候,会开启basicauth的认证方式,这是一个.csv文件,三列分别是password,username,useruid。

  • s.ClientCAFile:用于给客户端签名的根证书,当这个参数不为空的时候,会启动https的认证方式,会通过这个根证书对客户端的证书进行身份认证。

  • s.TokenAuthFile:用于指定token文件所在的位置,当这个参数不为空的时候,会采用token的认证方式,token文件也是csv的格式,三列分别是”token,username,useruid”。

  • s.ServiceAccountKeyFile:当不为空的时候,采用ServiceAccount的认证方式,这个其实是一个公钥密钥。注释里说要包含:PEM-encoded x509 RSA private or public key,发送过来的信息是在客户端使用对应的私钥加密过的,服务端使用指定的公钥来解密信息。

  • s.ServiceAccountLookup:这个参数值一个bool值,默认为false,如果为true的话,就会从etcd中取出对应的ServiceAccount与传过来的信息进行对比验证,反之则不会。

  • helper:这是一个用于与etcd交互的客户端实例,具体生成过程这里不进行具体分析。

下面结合认证器的具体生成过程对这些参数的使用进行具体分析,先总体看一下认证器部分的代码结构:

结合上面的分析,这部分的代码结构就比较清楚了,返回的结果是一个authenticator.Request对象数组,每一个元素都是一个认证器,根据传入的参数是否为空来判断最后要生成多少个认证器,最后的union.New函数实际上返回的就是一个authenticator.Request数组:

我们可以看一下authenticator.Request接口的实现:

其中的方法 AuthenticateRequest的主要功能就是把userinfo从request中提取出来,并返回是否认证成功,以及对应的错误信息。

生成带有认证器的handler

下面我们直接跳到对于api请求的认证部分,看一下当某个请求过来的时候,apiserver是如何对其进行认证的,具体代码在/pkg/master/master.go的func (m *Master) init(c *Config)函数中:

实现细节暂不讨论,从功能上讲,这一段就是对handler进行一层包装,生成一个带有认证器的handler。 其中handlers.Unauthorized(c.SupportsBasicAuth)函数是一个返回Unauthorized信息的函数,如果认证失败,这个函数就会被调用。

我们大致看一下NewRequestAuthenticator函数:

可以看到HandleFunc中调用的函数,就是要调用我们之前提到的AuthenticateRequest函数,使用其提取用户信息,判断验证是否成功,如果有错误或者认证失败,返回Unauthorized新的的函数就会被调用。结合之前的分析,我们只要把每种认证器的AuthenticateRequest函数分析一下,就可以了解认证操作的具体实现过程了。

每种认证操作的具体实现过程

结合上面的NewAuthenticator源码可以知道,最多一共有五种authenticators:即basicAuthcertAuthtokenAuthserviceAccountAuth,还有通过Union.New生成的unionAuthRequestHandler,下面我们结合每个认证器的生成过程具体看一下每个authenticatorsAuthenticateRequest函数:

  • unionAuthRequestHandler实例

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

结合之前的NewAuthenticator可以看到,当authenticators数目大于1的时候,会生成unionAuthRequestHandler实例,之后会遍历其中的元素,调用每一个元素的AuthenticateReques方法,只要其中有一种认证方式成功,最后认证就会返回true

  • basicAuth:bacisAuth的认证比较直接,就是把信息从.csv文件中读取出来,返回一个PasswordAuthenticator结构,其中包含一个map:users map[string]*userPasswordInfo,具体验证的时候,就从map中读取已有信息,比较用户名和密码,可以看出这种认证方式确实比较基础,仅仅做了基本的认证。

    basicAuthenticator, err := passwordfile.NewCSV(basicAuthFile)
    if err != nil { return nil, err }
    return basicauth.New(basicAuthenticator), nil}

    首先会生成一个basicAuthenticator实例(PasswordAuthenticator对象),之后会将这个对象转化为Authenticator实例,里面包含authenticator.Password接口,PasswordAuthenticator实现了这个接口。

    具体的AuthenticateRequest的调用代码比较简单:

    auth := strings.TrimSpace(req.Header.Get(“Authorization”)) if auth == “” { return nil, false, nil } parts := strings.Split(auth, ” “) if len(parts) < 2 || strings.ToLower(parts[0]) != “basic” { return nil, false, nil }

    主要就是从request的Header中提取出Authorization字段的信息,用basic作为分隔,之后根据:作为分隔,提取出用户名和密码,调用AuthenticatePassword进行检验。这里的a.auth是之前传过来的PasswordAuthenticator对象,可以看下具体的这对象的AuthenticatePassword的实现:

    就是根据username把info从map中提取出来进行检验,比较简单。

  • certAuth:这里首先要声明一点, https仅仅是认证方式的一种,secureport可以是https的也可以不是https的,不要把这两个弄混。关于golang中https的使用的基本内容以及相关证书的生成可以参考之前这个文章

    ca文件指定了之后,说明要使用https的方式,这里是cafile是给客户端证书签名的根证书,用于https握手的时候对客户端进行身份认证。正常情况下还要指定服务端的.key和.crt文件,这里默认的就是使用双向认证的方式,在服务端启动的时候,要把对应的证书也加进去,分别用到的是tls-cert-file以及tls-private-key-file这个两个参数,如果这两个参数没指定的话,证书就会使用自签名的方式被自动生成,放在CertDirectory: "/var/run/kubernetes"目录下。

    这里具体验证的操作使用的是包含x509验证对象的AuthenticateRequest函数(./plugin/pkg/auth/authenticator/request/x509/x509.go),遵循的也是通常的https认证原理,具体细节不在此讨论。

    使用ca认证的时候,只要是ca签名过的证书都可以通过验证,这个时候ca的安全性就比较重要了,在某些使用场景中,证书应该如何分发问题可能是需要考虑的。

  • tokenAuth:是用token的方式,具体代码的结构与basic auth file的方式比较类似,代码不再赘述,主要功能是先从指定的.csv文件中把信息加载进来,存在服务端TokenAuthenticator实例的一个tokens的map中tokens map[string]*user.DefaultInfo,之后用户信息发送过来,会从Authorization中提取出携带token值,只不过这里标记token的关键字使用的是”bearer”,把token值提取出来之后,进行对比,看是否ok。

  • serviceAccountAuth:saAuth实际上是token auth的变形,这里用到的是jwt(json web token)来进行具体的操作,具体的功能可以参考这个文章,本质上来说,saAuth也是一个token认证,只不过这个token是把一些信息加密(签名)之后生成的。大致介绍一下jwt,具体格式可以参考这个文章这里从实现的角度进行一些分析:

    可以看到,首先将publickey提取出来,之后如果lookup参数为true,会根据etcdhelper生成一个serviceAccountGetter,否则使用默认的serviceAccountGetter,用来从etcd中取具体的sa和secret,secret可以理解为某些敏感信息:

    我们先看最后一部分bearertoken.New(tokenAuthenticator),返回的Authenticator结构的AuthenticateRequest方法就和tokenauth中的一样,从Authorization字段中提取出bearer token,之后使用接口中的方法a.auth.AuthenticateToken(token)进行验证,这里实际执行AuthenticateToken方法的是tokenAuthenticator对象(jwtTokenAuthenticator实例),func (j *jwtTokenAuthenticator) AuthenticateToken(token string)函数代码较长,就不在赘述,其主要的功能是使用之前提取出来的公钥密钥对信息进行解密,得到parsedToken:

    之后提取出其中的Claims信息并进行检验,看是否符合要求,如果look up字段为true的话,就会根据标记在Claims中的namespace ,secretName ,serviceAccountName , 利用之前生成的ServiceAccountTokenGetter从etcd中取出已设置好的serviceaccount以及secret来进行身份验证,验证通过之后会返回user信息。

    ServiceAccount的相关部分代码还在不断完善中,这里只分析了大概逻辑,相比其他方式serviceaccount还是挺好使用的,相当于是token的升级版,由于信息加密的原因,比仅仅使用token安全了不少,下面是参考k8相关代码生成的使用serviceacount方式发送api的方式,实际使用中,后面的发送api的部分直接使用源码中自己的kubectl,设置好对应的BearerToken字段即可:

总结

通过上面的分析,相信对kube-apiserver启动时候的身份验证部分的参数已经可以做到“心中有数”,即使有不清楚的地方,至少也可以做到按图索骥,从源码的角度分析参数应该如何设置,在实际使用中可以根据不同的场景使用合适的方式进行认证。

浙江大学SEL实验室是本网站上所有页面设计、页面内容的著作权人,对该网站所载的作品,包括但不限于网站所载的文字、数据、图形、照片、有声文件、动画文件、音视频资料等拥有完整的版权,受著作权法保护。严禁任何媒体、网站、个人或组织以任何形式或出于任何目的在未经本实验室书面授权的情況下抄袭、转载、摘编、修改本网站內容,或链接、转帖或以其他方式复制用于商业目的或发行,或稍作修改后在其它网站上使用,前述行为均将构成对本网站版权之侵犯,本网站將依法追究其法律责任。
本网站与他人另有协议授权下载的或法律另有规定的,在下载使用时必须注明“稿件来源:浙江大学”。

  1. 2 thoughts on “kubernetes apiserver源码分析——api请求的认证过程

    你们是不是在维护k8s ubuntu的demo啊,我严格按照你们的文档来操作的,结果在执行kube-up.sh之后环境不对,etcd在init.d里面的配置竟然写到/opt/bin/etcd里面去了,怎噩梦回事啊

    • 2 thoughts on “kubernetes apiserver源码分析——api请求的认证过程

      抱歉,现在才看到您的回复。
      etcd的配置信息应该会写入/etc/default/etcd, 您确认没有改动过其它地方么?
      ps: 如果有support方面的问题,在google container的group里或者stackoverflow上提问会更快地得到解答噢:)

发表评论

电子邮件地址不会被公开。 必填项已用*标注

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">