|
|
|
|
## 前言
|
|
|
|
|
|
|
|
|
|
在上一篇[**APIServer-P2-启动流程**](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-APIServer-P2-%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B.md)中,把APIServer预启动、加载配置、启动 的流程大致过了一遍,其中也有提到,APIServer的认证机制分为多种,以组合的形式遍历认证,任一机制认证成功,则判定请求认证成功,顺利进入下一个授权判定的环节。那么在本篇,就来详细看看这些认证机制。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Kubernetes用户
|
|
|
|
|
|
|
|
|
|
所有 Kubernetes 集群都有两类用户:由 Kubernetes 管理的**服务账号**和**普通用户**。
|
|
|
|
|
|
|
|
|
|
其中服务账号(ServiceAccount)是提供给集群中的程序使用,以Secret资源保存凭据,挂载到pod中,从而允许集群内的服务调用k8s API。
|
|
|
|
|
|
|
|
|
|
而普通用户,尚不支持使用API创建,一般由证书创建,Kubernetes 使用证书中的 'subject' 的通用名称(Common Name)字段(例如,"/CN=bob")来 确定用户名。
|
|
|
|
|
|
|
|
|
|
> 以上摘自官方文档
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 身份认证策略
|
|
|
|
|
|
|
|
|
|
Kubernetes 使用身份认证插件利用客户端证书、持有者令牌(Bearer Token)、身份认证代理(Proxy) 或者 HTTP 基本认证机制来认证 API 请求的身份。HTTP 请求发给 API 服务器时, 插件会将以下属性关联到请求本身:
|
|
|
|
|
|
|
|
|
|
- 用户名:用来辩识最终用户的字符串。常见的值可以是 `kube-admin` 或 `jane@example.com`。
|
|
|
|
|
- 用户 ID:用来辩识最终用户的字符串,旨在比用户名有更好的一致性和唯一性。
|
|
|
|
|
- 用户组:取值为一组字符串,其中各个字符串用来标明用户是某个命名的用户逻辑集合的成员。 常见的值可能是 `system:masters` 或者 `devops-team` 等。
|
|
|
|
|
- 附加字段:一组额外的键-值映射,键是字符串,值是一组字符串;用来保存一些鉴权组件可能 觉得有用的额外信息。
|
|
|
|
|
|
|
|
|
|
与其它身份认证协议(LDAP、SAML、Kerberos、X509 的替代模式等等)都可以通过 使用一个[身份认证代理](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#authenticating-proxy)或 [身份认证 Webhoook](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#webhook-token-authentication)来实现。
|
|
|
|
|
|
|
|
|
|
> 以上摘自官方文档
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 认证机制
|
|
|
|
|
|
|
|
|
|
### 流程图
|
|
|
|
|
|
|
|
|
|
首先为了加深印象,再贴一下上一章列出的认证流程图:
|
|
|
|
|
|
|
|
|
|
![](https://mycloudn.wqyin.cn/20210203112246.png)
|
|
|
|
|
|
|
|
|
|
> 注:认证器的执行顺序为随机的,图中流向不代表固定顺序
|
|
|
|
|
|
|
|
|
|
当客户端发送请求到达APIServer端,请求首先进入认证环节,对应处理认证的是Authentication Handler方法,
|
|
|
|
|
|
|
|
|
|
Authentication Handler方法中,遍历每一个已启用的认证器,仅需某一个认证器返回true,则认证成功结束遍历,若所有认证器全部返回false,则认证失败。
|
|
|
|
|
|
|
|
|
|
代码路径:
|
|
|
|
|
|
|
|
|
|
`staging/src/k8s.io/apiserver/pkg/server/config.go:550`
|
|
|
|
|
|
|
|
|
|
--> `vendor/k8s.io/apiserver/pkg/endpoints/filters/authentication.go:53`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
func WithAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences) http.Handler {
|
|
|
|
|
// 关闭认证则直接进入下一个环节
|
|
|
|
|
if auth == nil {
|
|
|
|
|
klog.Warningf("Authentication is disabled")
|
|
|
|
|
return handler
|
|
|
|
|
}
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
|
|
|
if len(apiAuds) > 0 {
|
|
|
|
|
req = req.WithContext(authenticator.WithAudiences(req.Context(), apiAuds))
|
|
|
|
|
}
|
|
|
|
|
// 这里进行认证
|
|
|
|
|
resp, ok, err := auth.AuthenticateRequest(req)
|
|
|
|
|
if err != nil || !ok {
|
|
|
|
|
if err != nil {
|
|
|
|
|
klog.Errorf("Unable to authenticate the request due to an error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
failed.ServeHTTP(w, req)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO(mikedanese): verify the response audience matches one of apiAuds if
|
|
|
|
|
// non-empty
|
|
|
|
|
|
|
|
|
|
// authorization header is not required anymore in case of a successful authentication.
|
|
|
|
|
req.Header.Del("Authorization")
|
|
|
|
|
|
|
|
|
|
// 请求上下文携带上认证后的用户的信息
|
|
|
|
|
req = req.WithContext(genericapirequest.WithUser(req.Context(), resp.User))
|
|
|
|
|
// 计数器
|
|
|
|
|
authenticatedUserCounter.WithLabelValues(compressUsername(resp.User.GetName())).Inc()
|
|
|
|
|
// 处理请求
|
|
|
|
|
handler.ServeHTTP(w, req)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`auth.AuthenticateRequest`是一个接口方法,看看它的引用:
|
|
|
|
|
|
|
|
|
|
`vendor/k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go:35`
|
|
|
|
|
|
|
|
|
|
![](https://mycloudn.wqyin.cn/20210202115500.png)
|
|
|
|
|
|
|
|
|
|
引用中的union.go中,聚合了所有的Authenticator链,进去看看
|
|
|
|
|
|
|
|
|
|
`staging/src/k8s.io/apiserver/pkg/authentication/request/union/union.go:53`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// AuthenticateRequest authenticates the request using a chain of authenticator.Request objects.
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
apiserver的各种request相关定义代码大多集中在这个目录里:
|
|
|
|
|
|
|
|
|
|
<img src="https://mycloudn.wqyin.cn/20210202145632.png" style="zoom:70%;" />
|
|
|
|
|
|
|
|
|
|
那么依次逐个来看看每一种Authenticator对应的AuthenticateRequest方法。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### RequestHeader认证
|
|
|
|
|
|
|
|
|
|
是一种代理认证方式,需要再apiserver启动时以参数形式配置,来看看官方的介绍:
|
|
|
|
|
|
|
|
|
|
API 服务器可以配置成从请求的头部字段值(如 `X-Remote-User`)中辩识用户。 这一设计是用来与某身份认证代理一起使用 API 服务器,代理负责设置请求的头部字段值。
|
|
|
|
|
|
|
|
|
|
- `--requestheader-username-headers` 必需字段,大小写不敏感。用来设置要获得用户身份所要检查的头部字段名称列表(有序)。第一个包含数值的字段会被用来提取用户名。
|
|
|
|
|
- `--requestheader-group-headers` 可选字段,在 Kubernetes 1.6 版本以后支持,大小写不敏感。 建议设置为 "X-Remote-Group"。用来指定一组头部字段名称列表,以供检查用户所属的组名称。 所找到的全部头部字段的取值都会被用作用户组名。
|
|
|
|
|
- `--requestheader-extra-headers-prefix` 可选字段,在 Kubernetes 1.6 版本以后支持,大小写不敏感。 建议设置为 "X-Remote-Extra-"。用来设置一个头部字段的前缀字符串,API 服务器会基于所给 前缀来查找与用户有关的一些额外信息。这些额外信息通常用于所配置的鉴权插件。 API 服务器会将与所给前缀匹配的头部字段过滤出来,去掉其前缀部分,将剩余部分 转换为小写字符串并在必要时执行[百分号解码](https://tools.ietf.org/html/rfc3986#section-2.1) 后,构造新的附加信息字段键名。原来的头部字段值直接作为附加信息字段的值。
|
|
|
|
|
|
|
|
|
|
例如,使用下面的配置:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
--requestheader-username-headers=X-Remote-User
|
|
|
|
|
--requestheader-group-headers=X-Remote-Group
|
|
|
|
|
--requestheader-extra-headers-prefix=X-Remote-Extra-
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
针对所收到的如下请求:
|
|
|
|
|
|
|
|
|
|
```http
|
|
|
|
|
GET / HTTP/1.1
|
|
|
|
|
X-Remote-User: fido
|
|
|
|
|
X-Remote-Group: dogs
|
|
|
|
|
X-Remote-Group: dachshunds
|
|
|
|
|
X-Remote-Extra-Acme.com%2Fproject: some-project
|
|
|
|
|
X-Remote-Extra-Scopes: openid
|
|
|
|
|
X-Remote-Extra-Scopes: profile
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
会生成下面的用户信息:
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
name: fido
|
|
|
|
|
groups:
|
|
|
|
|
- dogs
|
|
|
|
|
- dachshunds
|
|
|
|
|
extra:
|
|
|
|
|
acme.com/project:
|
|
|
|
|
- some-project
|
|
|
|
|
scopes:
|
|
|
|
|
- openid
|
|
|
|
|
- profile
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
为了防范头部信息侦听,在请求中的头部字段被检视之前, 身份认证代理需要向 API 服务器提供一份合法的客户端证书, 供后者使用所给的 CA 来执行验证。 警告:*不要* 在不同的上下文中复用 CA 证书,除非你清楚这样做的风险是什么以及 应如何保护 CA 用法的机制。
|
|
|
|
|
|
|
|
|
|
- `--requestheader-client-ca-file` 必需字段,给出 PEM 编码的证书包。 在检查请求的头部字段以提取用户名信息之前,必须提供一个合法的客户端证书, 且该证书要能够被所给文件中的机构所验证。
|
|
|
|
|
- `--requestheader-allowed-names` 可选字段,用来给出一组公共名称(CN)。 如果此标志被设置,则在检视请求中的头部以提取用户信息之前,必须提供 包含此列表中所给的 CN 名的、合法的客户端证书。
|
|
|
|
|
|
|
|
|
|
> 以上摘自官方文档[身份认证代理](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#authenticating-proxy)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
简而言之,就是在apiserver之前有一个代理服务器,通过代理服务器的名义,可以将相关的认证信息在请求头透传给apiserver,以通过apiserver的认证。代理服务的相关信息需要预先配置。
|
|
|
|
|
|
|
|
|
|
使用kubeadm默认部署的集群,就启用了requestheader的配置:
|
|
|
|
|
|
|
|
|
|
![](https://mycloudn.wqyin.cn/20210202163330.png)
|
|
|
|
|
|
|
|
|
|
来看看AuthenticateRequest方法的代码:
|
|
|
|
|
|
|
|
|
|
`vendor/k8s.io/apiserver/pkg/authentication/request/headerrequest/requestheader.go:108`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
|
|
|
|
|
// 取到用户名
|
|
|
|
|
name := headerValue(req.Header, a.nameHeaders)
|
|
|
|
|
if len(name) == 0 {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
// 取到用户group
|
|
|
|
|
groups := allHeaderValues(req.Header, a.groupHeaders)
|
|
|
|
|
// 取到其他信息
|
|
|
|
|
extra := newExtra(req.Header, a.extraHeaderPrefixes)
|
|
|
|
|
|
|
|
|
|
// clear headers used for authentication
|
|
|
|
|
// 请求的header信息去除
|
|
|
|
|
for _, headerName := range a.nameHeaders {
|
|
|
|
|
req.Header.Del(headerName)
|
|
|
|
|
}
|
|
|
|
|
for _, headerName := range a.groupHeaders {
|
|
|
|
|
req.Header.Del(headerName)
|
|
|
|
|
}
|
|
|
|
|
for k := range extra {
|
|
|
|
|
for _, prefix := range a.extraHeaderPrefixes {
|
|
|
|
|
req.Header.Del(prefix + k)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 返回认证成功信息
|
|
|
|
|
return &authenticator.Response{
|
|
|
|
|
User: &user.DefaultInfo{
|
|
|
|
|
Name: name,
|
|
|
|
|
Groups: groups,
|
|
|
|
|
Extra: extra,
|
|
|
|
|
},
|
|
|
|
|
}, true, nil
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
参考代码片中注释
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### BasicAuth认证
|
|
|
|
|
|
|
|
|
|
BasicAuth是一种简单的基础http认证,用户名、密码写入http请求头中,用base64编码,防君子不防小人,安全性较低,因此很少使用。快速略过
|
|
|
|
|
|
|
|
|
|
启动apiserver时,使用--basic-auth-file参数指定csv文件,csv里面以逗号切割,存放用户名、密码、uid。
|
|
|
|
|
|
|
|
|
|
看看AuthenticateRequest方法:
|
|
|
|
|
|
|
|
|
|
`vendor/k8s.io/apiserver/plugin/pkg/authenticator/request/basicauth/basicauth.go:38`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// AuthenticateRequest authenticates the request using the "Authorization: Basic" header in the request
|
|
|
|
|
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
|
|
|
|
|
username, password, found := req.BasicAuth()
|
|
|
|
|
if !found {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
// 简单的账号密码比对,没有什么值得深入的
|
|
|
|
|
resp, ok, err := a.auth.AuthenticatePassword(req.Context(), username, password)
|
|
|
|
|
|
|
|
|
|
// If the password authenticator didn't error, provide a default error
|
|
|
|
|
if !ok && err == nil {
|
|
|
|
|
err = errInvalidAuth
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return resp, ok, err
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### x509 CA认证
|
|
|
|
|
|
|
|
|
|
又称TLS双向认证,APIServer启动时使用--client-ca-file指定客户端的证书文件,用作请求的认证。
|
|
|
|
|
|
|
|
|
|
`vendor/k8s.io/apiserver/pkg/authentication/request/x509/x509.go:88`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// AuthenticateRequest authenticates the request using presented client certificates
|
|
|
|
|
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
|
|
|
|
|
if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use intermediates, if provided
|
|
|
|
|
optsCopy := a.opts
|
|
|
|
|
if optsCopy.Intermediates == nil && len(req.TLS.PeerCertificates) > 1 {
|
|
|
|
|
optsCopy.Intermediates = x509.NewCertPool()
|
|
|
|
|
for _, intermediate := range req.TLS.PeerCertificates[1:] {
|
|
|
|
|
optsCopy.Intermediates.AddCert(intermediate)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
remaining := req.TLS.PeerCertificates[0].NotAfter.Sub(time.Now())
|
|
|
|
|
clientCertificateExpirationHistogram.Observe(remaining.Seconds())
|
|
|
|
|
// 校验客户端request携带的证书,如果是ca签名的证书,且在有效期内,则视为有效,返回对应的证书chain
|
|
|
|
|
chains, err := req.TLS.PeerCertificates[0].Verify(optsCopy)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
--> `vendor/k8s.io/apiserver/pkg/authentication/request/x509/x509.go:72`
|
|
|
|
|
|
|
|
|
|
--> `vendor/k8s.io/apiserver/pkg/authentication/request/x509/x509.go:185`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// CommonNameUserConversion builds user info from a certificate chain using the subject's CommonName
|
|
|
|
|
var CommonNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (*authenticator.Response, bool, error) {
|
|
|
|
|
if len(chain[0].Subject.CommonName) == 0 {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
// 取得证书chain中的CommonName/Organization作为用户和组
|
|
|
|
|
return &authenticator.Response{
|
|
|
|
|
User: &user.DefaultInfo{
|
|
|
|
|
Name: chain[0].Subject.CommonName,
|
|
|
|
|
Groups: chain[0].Subject.Organization,
|
|
|
|
|
},
|
|
|
|
|
}, true, nil
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### BearerToken认证
|
|
|
|
|
|
|
|
|
|
这种认证方式是专为k8s节点准备的,避免每个节点都要手动配置TLS证书,在apiserver启动时指定`--enable-bootstrap-token-auth`参数来启用这种认证方式,详细说明见官方文档:
|
|
|
|
|
|
|
|
|
|
> [启动引导令牌](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#bootstrap-tokens)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bearertoken的认证方式是在请求头里放入bearer令牌,令牌的格式为 `[a-z0-9]{6}.[a-z0-9]{16}`。第一个部分是令牌的 ID;第二个部分 是令牌的 Secret。对应http请求头的格式是:
|
|
|
|
|
|
|
|
|
|
`Authorization: Bearer xxxxxx.xxxxxxxxxxxxxxxx`
|
|
|
|
|
|
|
|
|
|
代码路径:
|
|
|
|
|
|
|
|
|
|
`vendor/k8s.io/apiserver/pkg/authentication/request/bearertoken/bearertoken.go:37`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
|
|
|
|
|
// Authorization header
|
|
|
|
|
auth := strings.TrimSpace(req.Header.Get("Authorization"))
|
|
|
|
|
if auth == "" {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
// 切割分开,第二部分就是token
|
|
|
|
|
parts := strings.Split(auth, " ")
|
|
|
|
|
if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
token := parts[1]
|
|
|
|
|
|
|
|
|
|
// Empty bearer tokens aren't valid
|
|
|
|
|
if len(token) == 0 {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
// 校验token
|
|
|
|
|
resp, ok, err := a.auth.AuthenticateToken(req.Context(), token)
|
|
|
|
|
// if we authenticated successfully, go ahead and remove the bearer token so that no one
|
|
|
|
|
// is ever tempted to use it inside of the API server
|
|
|
|
|
if ok {
|
|
|
|
|
req.Header.Del("Authorization")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the token authenticator didn't error, provide a default error
|
|
|
|
|
if !ok && err == nil {
|
|
|
|
|
err = invalidToken
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return resp, ok, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`AuthenticateToken`是一个接口方法:
|
|
|
|
|
|
|
|
|
|
`vendor/k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go:28`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
type Token interface {
|
|
|
|
|
AuthenticateToken(ctx context.Context, token string) (*Response, bool, error)
|
|
|
|
|
}
|
|
|
|
|
// TokenFunc is a function that implements the Token interface.
|
|
|
|
|
type TokenFunc func(ctx context.Context, token string) (*Response, bool, error)
|
|
|
|
|
|
|
|
|
|
// AuthenticateToken implements authenticator.Token.
|
|
|
|
|
func (f TokenFunc) AuthenticateToken(ctx context.Context, token string) (*Response, bool, error) {
|
|
|
|
|
return f(ctx, token)
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
接口的实现方法在:
|
|
|
|
|
|
|
|
|
|
`plugin/pkg/auth/authenticator/token/bootstrap/bootstrap.go:94`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
func (t *TokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
|
|
|
|
// 把token用'.'切割分为id和密文两段
|
|
|
|
|
tokenID, tokenSecret, err := parseToken(token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// Token isn't of the correct form, ignore it.
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
// 从APIServer取到对应的secret资源对象,跟这个token进行信息比对
|
|
|
|
|
secretName := bootstrapapi.BootstrapTokenSecretPrefix + tokenID
|
|
|
|
|
secret, err := t.lister.Get(secretName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.IsNotFound(err) {
|
|
|
|
|
klog.V(3).Infof("No secret of name %s to match bootstrap bearer token", secretName)
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if secret.DeletionTimestamp != nil {
|
|
|
|
|
tokenErrorf(secret, "is deleted and awaiting removal")
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if string(secret.Type) != string(bootstrapapi.SecretTypeBootstrapToken) || secret.Data == nil {
|
|
|
|
|
tokenErrorf(secret, "has invalid type, expected %s.", bootstrapapi.SecretTypeBootstrapToken)
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ts := getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey)
|
|
|
|
|
if subtle.ConstantTimeCompare([]byte(ts), []byte(tokenSecret)) != 1 {
|
|
|
|
|
tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenSecretKey, tokenSecret)
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
id := getSecretString(secret, bootstrapapi.BootstrapTokenIDKey)
|
|
|
|
|
if id != tokenID {
|
|
|
|
|
tokenErrorf(secret, "has invalid value for key %s, expected %s.", bootstrapapi.BootstrapTokenIDKey, tokenID)
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isSecretExpired(secret) {
|
|
|
|
|
// logging done in isSecretExpired method.
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if getSecretString(secret, bootstrapapi.BootstrapTokenUsageAuthentication) != "true" {
|
|
|
|
|
tokenErrorf(secret, "not marked %s=true.", bootstrapapi.BootstrapTokenUsageAuthentication)
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
groups, err := getGroups(secret)
|
|
|
|
|
if err != nil {
|
|
|
|
|
tokenErrorf(secret, "has invalid value for key %s: %v.", bootstrapapi.BootstrapTokenExtraGroupsKey, err)
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &authenticator.Response{
|
|
|
|
|
User: &user.DefaultInfo{
|
|
|
|
|
Name: bootstrapapi.BootstrapUserPrefix + string(id),
|
|
|
|
|
Groups: groups,
|
|
|
|
|
},
|
|
|
|
|
}, true, nil
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### ServiceAccount(SA)认证
|
|
|
|
|
|
|
|
|
|
启用方式为APIServer命令使用`--service-account-key-file`参数指定一个为token签名的PEM秘钥文件。
|
|
|
|
|
|
|
|
|
|
SA认证是jwt形式的认证,使用方式与bearer token类似,也是放在请求头里,内容为Base64编码,header格式为:
|
|
|
|
|
|
|
|
|
|
`Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo0NTk0LCJ1c2VybmFtZSI6InlpbndlbnFpbiIsImV4cCI6MTU3MDY3NDEyNywiZW1haWwiOiIifQ.djC2w5l3IiXYv7slZtGzlMzLc3_oPuR1M0dM9FwoaUU`
|
|
|
|
|
|
|
|
|
|
token哪里来呢?答案就是ServiceAccount。
|
|
|
|
|
|
|
|
|
|
SA是一种面向集群内部应用需要调用APIServer的场景所设计的认证方式。在创建ServiceAccount资源时,可以显示地设置标签将ServiceAccount绑定给某Deploy/sts/pod,也可以在Deploy/sts/pod的声明文件里显示指定ServiceAccount。ServiceAccount会自动创建Secret资源,token秘钥存放其中。
|
|
|
|
|
|
|
|
|
|
在相应的容器层面,token信息会被挂载进容器中,包含3个文件:
|
|
|
|
|
|
|
|
|
|
- namespace文件:指明命名空间
|
|
|
|
|
- ca.crt文件:APIServer的公钥证书,容器用来校验APIServer
|
|
|
|
|
- token文件: 存放在Secret里的JWT token
|
|
|
|
|
|
|
|
|
|
在集群上找一个例子,以kube-proxy举例:
|
|
|
|
|
|
|
|
|
|
![](https://mycloudn.wqyin.cn/20210203103806.png)
|
|
|
|
|
|
|
|
|
|
看看代码层面:
|
|
|
|
|
|
|
|
|
|
`pkg/serviceaccount/jwt.go:145`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
func (j *jwtTokenAuthenticator) AuthenticateToken(ctx context.Context, tokenData string) (*authenticator.Response, bool, error) {
|
|
|
|
|
if !j.hasCorrectIssuer(tokenData) {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
// 拿到token
|
|
|
|
|
tok, err := jwt.ParseSigned(tokenData)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !found {
|
|
|
|
|
return nil, false, utilerrors.NewAggregate(errlist)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tokenAudiences := authenticator.Audiences(public.Audience)
|
|
|
|
|
if len(tokenAudiences) == 0 {
|
|
|
|
|
// only apiserver audiences are allowed for legacy tokens
|
|
|
|
|
tokenAudiences = j.implicitAuds
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestedAudiences, ok := authenticator.AudiencesFrom(ctx)
|
|
|
|
|
if !ok {
|
|
|
|
|
// default to apiserver audiences
|
|
|
|
|
requestedAudiences = j.implicitAuds
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auds := authenticator.Audiences(tokenAudiences).Intersect(requestedAudiences)
|
|
|
|
|
if len(auds) == 0 && len(j.implicitAuds) != 0 {
|
|
|
|
|
return nil, false, fmt.Errorf("token audiences %q is invalid for the target audiences %q", tokenAudiences, requestedAudiences)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we get here, we have a token with a recognized signature and
|
|
|
|
|
// issuer string.
|
|
|
|
|
// 校验token并拿到SA信息
|
|
|
|
|
sa, err := j.validator.Validate(tokenData, public, private)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &authenticator.Response{
|
|
|
|
|
User: sa.UserInfo(),
|
|
|
|
|
Audiences: auds,
|
|
|
|
|
}, true, nil
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### WebhookToken认证
|
|
|
|
|
|
|
|
|
|
Webhook 身份认证是一种用来验证持有者令牌的回调机制。
|
|
|
|
|
|
|
|
|
|
- `--authentication-token-webhook-config-file` 指向一个配置文件,其中描述 如何访问远程的 Webhook 服务。
|
|
|
|
|
- `--authentication-token-webhook-cache-ttl` 用来设定身份认证决定的缓存时间。 默认时长为 2 分钟。
|
|
|
|
|
|
|
|
|
|
配置文件使用 [kubeconfig](https://kubernetes.io/zh/docs/concepts/configuration/organize-cluster-access-kubeconfig/) 文件的格式。文件中,`clusters` 指代远程服务,`users` 指代远程 API 服务 Webhook。下面是一个例子:
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
# Kubernetes API 版本
|
|
|
|
|
apiVersion: v1
|
|
|
|
|
# API 对象类别
|
|
|
|
|
kind: Config
|
|
|
|
|
# clusters 指代远程服务
|
|
|
|
|
clusters:
|
|
|
|
|
- name: name-of-remote-authn-service
|
|
|
|
|
cluster:
|
|
|
|
|
certificate-authority: /path/to/ca.pem # 用来验证远程服务的 CA
|
|
|
|
|
server: https://authn.example.com/authenticate # 要查询的远程服务 URL。必须使用 'https'。
|
|
|
|
|
|
|
|
|
|
# users 指代 API 服务的 Webhook 配置
|
|
|
|
|
users:
|
|
|
|
|
- name: name-of-api-server
|
|
|
|
|
user:
|
|
|
|
|
client-certificate: /path/to/cert.pem # Webhook 插件要使用的证书
|
|
|
|
|
client-key: /path/to/key.pem # 与证书匹配的密钥
|
|
|
|
|
|
|
|
|
|
# kubeconfig 文件需要一个上下文(Context),此上下文用于本 API 服务器
|
|
|
|
|
current-context: webhook
|
|
|
|
|
contexts:
|
|
|
|
|
- context:
|
|
|
|
|
cluster: name-of-remote-authn-service
|
|
|
|
|
user: name-of-api-sever
|
|
|
|
|
name: webhook
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
当客户端尝试在 API 服务器上使用持有者令牌完成身份认证( 如[前](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#putting-a-bearer-token-in-a-request)所述)时, 身份认证 Webhook 会用 POST 请求发送一个 JSON 序列化的对象到远程服务。 该对象是 `authentication.k8s.io/v1beta1` 组的 `TokenReview` 对象, 其中包含持有者令牌。 Kubernetes 不会强制请求提供此 HTTP 头部。
|
|
|
|
|
|
|
|
|
|
要注意的是,Webhook API 对象和其他 Kubernetes API 对象一样,也要受到同一 [版本兼容规则](https://kubernetes.io/zh/docs/concepts/overview/kubernetes-api/)约束。 实现者要了解对 Beta 阶段对象的兼容性承诺,并检查请求的 `apiVersion` 字段, 以确保数据结构能够正常反序列化解析。此外,API 服务器必须启用 `authentication.k8s.io/v1beta1` API 扩展组 (`--runtime-config=authentication.k8s.io/v1beta1=true`)。
|
|
|
|
|
|
|
|
|
|
POST 请求的 Body 部分将是如下格式:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"apiVersion": "authentication.k8s.io/v1beta1",
|
|
|
|
|
"kind": "TokenReview",
|
|
|
|
|
"spec": {
|
|
|
|
|
"token": "<持有者令牌>"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
远程服务应该会填充请求的 `status` 字段,以标明登录操作是否成功。 响应的 Body 中的 `spec` 字段会被忽略,因此可以省略。 如果持有者令牌验证成功,应该返回如下所示的响应:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"apiVersion": "authentication.k8s.io/v1beta1",
|
|
|
|
|
"kind": "TokenReview",
|
|
|
|
|
"status": {
|
|
|
|
|
"authenticated": true,
|
|
|
|
|
"user": {
|
|
|
|
|
"username": "janedoe@example.com",
|
|
|
|
|
"uid": "42",
|
|
|
|
|
"groups": [
|
|
|
|
|
"developers",
|
|
|
|
|
"qa"
|
|
|
|
|
],
|
|
|
|
|
"extra": {
|
|
|
|
|
"extrafield1": [
|
|
|
|
|
"extravalue1",
|
|
|
|
|
"extravalue2"
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
而不成功的请求会返回:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"apiVersion": "authentication.k8s.io/v1beta1",
|
|
|
|
|
"kind": "TokenReview",
|
|
|
|
|
"status": {
|
|
|
|
|
"authenticated": false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
HTTP 状态码可用来提供进一步的错误语境信息。
|
|
|
|
|
|
|
|
|
|
> 以上摘自官方文档[Webhook 令牌身份认证](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#webhook-token-authentication)
|
|
|
|
|
|
|
|
|
|
代码:
|
|
|
|
|
|
|
|
|
|
`vendor/k8s.io/apiserver/plugin/pkg/authenticator/token/webhook/webhook.go:77`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// AuthenticateToken implements the authenticator.Token interface.
|
|
|
|
|
func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
|
|
|
|
// We take implicit audiences of the API server at WebhookTokenAuthenticator
|
|
|
|
|
// construction time. The outline of how we validate audience here is:
|
|
|
|
|
//
|
|
|
|
|
// * if the ctx is not audience limited, don't do any audience validation.
|
|
|
|
|
// * if ctx is audience-limited, add the audiences to the tokenreview spec
|
|
|
|
|
// * if the tokenreview returns with audiences in the status that intersect
|
|
|
|
|
// with the audiences in the ctx, copy into the response and return success
|
|
|
|
|
// * if the tokenreview returns without an audience in the status, ensure
|
|
|
|
|
// the ctx audiences intersect with the implicit audiences, and set the
|
|
|
|
|
// intersection in the response.
|
|
|
|
|
// * otherwise return unauthenticated.
|
|
|
|
|
wantAuds, checkAuds := authenticator.AudiencesFrom(ctx)
|
|
|
|
|
r := &authentication.TokenReview{
|
|
|
|
|
Spec: authentication.TokenReviewSpec{
|
|
|
|
|
Token: token,
|
|
|
|
|
Audiences: wantAuds,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
var (
|
|
|
|
|
result *authentication.TokenReview
|
|
|
|
|
err error
|
|
|
|
|
auds authenticator.Audiences
|
|
|
|
|
)
|
|
|
|
|
webhook.WithExponentialBackoff(w.initialBackoff, func() error {
|
|
|
|
|
// webhook插件向远端认证服务器发起post请求
|
|
|
|
|
result, err = w.tokenReview.Create(r)
|
|
|
|
|
return err
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
// An error here indicates bad configuration or an outage. Log for debugging.
|
|
|
|
|
klog.Errorf("Failed to make webhook authenticator request: %v", err)
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if checkAuds {
|
|
|
|
|
gotAuds := w.implicitAuds
|
|
|
|
|
if len(result.Status.Audiences) > 0 {
|
|
|
|
|
gotAuds = result.Status.Audiences
|
|
|
|
|
}
|
|
|
|
|
auds = wantAuds.Intersect(gotAuds)
|
|
|
|
|
if len(auds) == 0 {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r.Status = result.Status
|
|
|
|
|
if !r.Status.Authenticated {
|
|
|
|
|
var err error
|
|
|
|
|
if len(r.Status.Error) != 0 {
|
|
|
|
|
err = errors.New(r.Status.Error)
|
|
|
|
|
}
|
|
|
|
|
return nil, false, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var extra map[string][]string
|
|
|
|
|
if r.Status.User.Extra != nil {
|
|
|
|
|
extra = map[string][]string{}
|
|
|
|
|
for k, v := range r.Status.User.Extra {
|
|
|
|
|
extra[k] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 根据远端认证服务器返回的status字段,填充请求的相关用户信息(name/uid/group等)
|
|
|
|
|
return &authenticator.Response{
|
|
|
|
|
User: &user.DefaultInfo{
|
|
|
|
|
Name: r.Status.User.Username,
|
|
|
|
|
UID: r.Status.User.UID,
|
|
|
|
|
Groups: r.Status.User.Groups,
|
|
|
|
|
Extra: extra,
|
|
|
|
|
},
|
|
|
|
|
Audiences: auds,
|
|
|
|
|
}, true, nil
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Anonymous认证
|
|
|
|
|
|
|
|
|
|
启用匿名请求支持之后,如果请求没有被已配置的其他身份认证方法拒绝,则被视作 匿名请求(Anonymous Requests)。这类请求获得用户名 `system:anonymous` 和 对应的用户组 `system:unauthenticated`。
|
|
|
|
|
|
|
|
|
|
例如,在一个配置了令牌身份认证且启用了匿名访问的服务器上,如果请求提供了非法的 持有者令牌,则会返回 `401 Unauthorized` 错误。 如果请求没有提供持有者令牌,则被视为匿名请求。
|
|
|
|
|
|
|
|
|
|
在 1.5.1-1.5.x 版本中,匿名访问默认情况下是被禁用的,可以通过为 API 服务器设定 `--anonymous-auth=true` 来启用。
|
|
|
|
|
|
|
|
|
|
在 1.6 及之后版本中,如果所使用的鉴权模式不是 `AlwaysAllow`,则匿名访问默认是被启用的。 从 1.6 版本开始,ABAC 和 RBAC 鉴权模块要求对 `system:anonymous` 用户或者 `system:unauthenticated` 用户组执行显式的权限判定,所以之前的为 `*` 用户或 `*` 用户组赋予访问权限的策略规则都不再包含匿名用户。
|
|
|
|
|
|
|
|
|
|
> 以上摘自官方文档[匿名请求](https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/#anonymous-requests)
|
|
|
|
|
|
|
|
|
|
代码:
|
|
|
|
|
|
|
|
|
|
`vendor/k8s.io/apiserver/pkg/authentication/request/anonymous/anonymous.go:32`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
func NewAuthenticator() authenticator.Request {
|
|
|
|
|
return authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
|
|
|
|
|
auds, _ := authenticator.AudiencesFrom(req.Context())
|
|
|
|
|
return &authenticator.Response{
|
|
|
|
|
User: &user.DefaultInfo{
|
|
|
|
|
Name: anonymousUser,
|
|
|
|
|
Groups: []string{unauthenticatedGroup},
|
|
|
|
|
},
|
|
|
|
|
Audiences: auds,
|
|
|
|
|
}, true, nil
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 总结
|
|
|
|
|
|
|
|
|
|
认证机制有许多种,每种都有不同的适用场景,一般情况下只需要了解认证流程和其中常见的几种认证器即可,不必过分深挖。认证流程通过之后,下一篇进入授权环节
|