diff --git a/.DS_Store b/.DS_Store index 883c1a8..04a0783 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/apiServer/Kubernetes源码学习-APIServer-P2-启动流程.md b/apiServer/Kubernetes源码学习-APIServer-P2-启动流程.md index deb1fab..453f95a 100644 --- a/apiServer/Kubernetes源码学习-APIServer-P2-启动流程.md +++ b/apiServer/Kubernetes源码学习-APIServer-P2-启动流程.md @@ -484,7 +484,7 @@ func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, er 这个New()方法依次生成每一种认证方式的Authenticator,New()方法的流程图如下: - +![](https://mycloudn.upweto.top/20210202145027.png) `union.New(authenticators...)`方法集合所有的认证器 diff --git a/apiServer/Kubernetes源码学习-APIServer-P3-APIServer的认证机制.md b/apiServer/Kubernetes源码学习-APIServer-P3-APIServer的认证机制.md new file mode 100644 index 0000000..265dc86 --- /dev/null +++ b/apiServer/Kubernetes源码学习-APIServer-P3-APIServer的认证机制.md @@ -0,0 +1,791 @@ +## 前言 + +在上一篇[**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.upweto.top/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.upweto.top/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相关定义代码大多集中在这个目录里: + + + +那么依次逐个来看看每一种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.upweto.top/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.upweto.top/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 + }) +} +``` + + + + + +## 总结 + +认证机制有许多种,每种都有不同的适用场景,一般情况下只需要了解认证流程和其中常见的几种认证器即可,不必过分深挖。认证流程通过之后,下一篇进入授权环节 \ No newline at end of file