K8s自定义apiserver的认证鉴权流程笔记
概要
K8s自定义apiserver是扩展kube-apiserver的其中一种方式,通过这种方式,可以很轻松的将自定义资源注册到kube-apiserver中,然后就能通过kubectl开始对自定义资源愉快的CRUD了
总体流程
image.png按照K8s官方文档给出的流程图中,可以总结三步:
-
请求首先会在kube-apiserver中进行认证和鉴权
-
kube-apiserver认证和鉴权通过后,使用新的证书对、并将原始请求中的用户和组信息设置到新请求的Header中,然后将请求代理到自定义的apiserver
- 新的证书对、以及设置的Header信息来自kube-apiserver的启动参数
--requestheader-allowed-names='' \ --requestheader-extra-headers-prefix=X-Remote-Extra- \ --requestheader-group-headers=X-Remote-Group \ --requestheader-username-headers=X-Remote-User \ --requestheader-client-ca-file=/etc/kubernetes/ssl/front-proxy-ca.pem --proxy-client-cert-file=/etc/kubernetes/ssl/front-proxy-client.pem \ --proxy-client-key-file=/etc/kubernetes/ssl/front-proxy-client-key.pem \
- 自定义apiserver的请求地址通过ApiService资源定义
apiVersion: apiregistration.k8s.io/v1 kind: APIService metadata: name: v1alpha1.cluster.karmada.io labels: app: karmada-aggregated-apiserver apiserver: "true" spec: insecureSkipTLSVerify: true group: cluster.karmada.io groupPriorityMinimum: 2000 service: name: karmada-aggregated-apiserver namespace: karmada-system version: v1alpha1 versionPriority: 10
- 新的证书对、以及设置的Header信息来自kube-apiserver的启动参数
-
自定义apiserver在对kube-apiserver代理过来的请求进行认证和授权
通过curl来描述这个过程:
- 首先正常通过证书对来访问kube-apiserver:
- curl -k --cert /path/to/cert --key /path/to/key https://2.2.2.2:6443/apis/cluster.karmada.io/v1alhpa1/clusters
- kube-apiserver收到上述请求后,首先会对请求进行认证和授权,
- 通过后,从证书中提取出用户和组信息(证书的CN作为用户名,O作为用户组)
- kube-apiserver发起到自定义apiserver的新请求,
- curl -cacert requestheader-client-ca-file --cert proxy-client-cert-file --key proxy-client-key-file
-H 'X-Remote-Group: Organization' -H 'X-Remote-User: Common Name'
https://karamda-cluster.default.svc.cluster.local:443/apis/cluster.karmada.io/v1alhpa1/clusters
- 自定义apiserver收到该请求后,也需要对该请求进行认证和授权
这里我们主要关注最后一步:自定义apiserver是怎么对kube-apiserver代理过来的请求进行认证和授权?
代码实现
以开源项目karmada为例,它需要往kube-apiserver中注册cluster
这种自定义资源,其GVR用路径表示为cluster.karmada.io/v1alhpa1/clusters
,我们看下它是怎么完成第三步的
自定义apiserver的配置
自定义apiserver的配置在新建时会使用默认的请求处理链DefaultBuildHandlerChain
func NewConfig(codecs serializer.CodecFactory) *Config {
defaultHealthChecks := []healthz.HealthChecker{healthz.PingHealthz, healthz.LogHealthz}
var id string
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) {
hostname, err := os.Hostname()
if err != nil {
klog.Fatalf("error getting hostname for apiserver identity: %v", err)
}
hash := sha256.Sum256([]byte(hostname))
id = "kube-apiserver-" + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:16]))
}
lifecycleSignals := newLifecycleSignals()
return &Config{
Serializer: codecs,
// 默认的请求处理链
BuildHandlerChainFunc: DefaultBuildHandlerChain,
}
}
自定义apiserver请求处理链
在默认的请求处理链中,我们可以看到认证和授权都在其中
func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
...
// 对请求授权
handler = genericapifilters.WithAuthorization(handler, c.Authorization.Authorizer, c.Serializer)
...
// 对请求认证
handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences)
...
return handler
}
认证过程
认证的过程主要是使用认证器对请求进行认证,然后获取到用户和组信息,并将用户和组信息设置到上下文中,用于后续的授权
func withAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences, metrics recordMetrics) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// 使用认证器对请求进行认证,认证成功后会返回认证的用户及其组信息
resp, ok, err := auth.AuthenticateRequest(req)
// 认证完成后将请求Header中的Authorization删除
req.Header.Del("Authorization")
// 将认证完成的用户和组信息设置到上下文中,用于后续的授权
req = req.WithContext(genericapirequest.WithUser(req.Context(), resp.User))
handler.ServeHTTP(w, req)
})
}
认证器的创建
认证器的创建过程在生成配置的流程里
func (c DelegatingAuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDefinitions, error) {
authenticators := []authenticator.Request{}
securityDefinitions := spec.SecurityDefinitions{}
// 创建一个基于请求Header参数的认证器
if c.RequestHeaderConfig != nil {
requestHeaderAuthenticator := headerrequest.NewDynamicVerifyOptionsSecure(
c.RequestHeaderConfig.CAContentProvider.VerifyOptions,
c.RequestHeaderConfig.AllowedClientNames,
c.RequestHeaderConfig.UsernameHeaders,
c.RequestHeaderConfig.GroupHeaders,
c.RequestHeaderConfig.ExtraHeaderPrefixes,
)
authenticators = append(authenticators, requestHeaderAuthenticator)
}
// 创建一个基于证书的认证器
if c.ClientCertificateCAContentProvider != nil {
authenticators = append(authenticators, x509.NewDynamic(c.ClientCertificateCAContentProvider.VerifyOptions, x509.CommonNameUserConversion))
}
// 创建一个基于Token的认证器
if c.TokenAccessReviewClient != nil {
if c.WebhookRetryBackoff == nil {
return nil, nil, errors.New("retry backoff parameters for delegating authentication webhook has not been specified")
}
tokenAuth, err := webhooktoken.NewFromInterface(c.TokenAccessReviewClient, c.APIAudiences, *c.WebhookRetryBackoff, c.TokenAccessReviewTimeout, webhooktoken.AuthenticatorMetrics{
RecordRequestTotal: RecordRequestTotal,
RecordRequestLatency: RecordRequestLatency,
})
if err != nil {
return nil, nil, err
}
cachingTokenAuth := cache.New(tokenAuth, false, c.CacheTTL, c.CacheTTL)
authenticators = append(authenticators, bearertoken.New(cachingTokenAuth), websocket.NewProtocolAuthenticator(cachingTokenAuth))
securityDefinitions["BearerToken"] = &spec.SecurityScheme{
SecuritySchemeProps: spec.SecuritySchemeProps{
Type: "apiKey",
Name: "authorization",
In: "header",
Description: "Bearer Token authentication",
},
}
}
// 将上面的认证器通过数组连接起来,只要有一个认证通过就行了
authenticator := group.NewAuthenticatedGroupAdder(unionauth.New(authenticators...))
return authenticator, &securityDefinitions, nil
}
基于Header的认证器
由于这个认证器在认证器数组的第一个,所以请求会先被这个认证器进行认证
首先对kube-apiserver代理过来的请求证书进行认证,对证书进行认证只需要有根证书就可以了
func (a *Verifier) 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, ok := a.verifyOptionsFn()
// if there are intentionally no verify options, then we cannot authenticate this request
if !ok {
return nil, false, nil
}
if optsCopy.Intermediates == nil && len(req.TLS.PeerCertificates) > 1 {
optsCopy.Intermediates = x509.NewCertPool()
for _, intermediate := range req.TLS.PeerCertificates[1:] {
optsCopy.Intermediates.AddCert(intermediate)
}
}
if _, err := req.TLS.PeerCertificates[0].Verify(optsCopy); err != nil {
return nil, false, err
}
if err := a.verifySubject(req.TLS.PeerCertificates[0].Subject); err != nil {
return nil, false, err
}
return a.auth.AuthenticateRequest(req)
}
然后从请求Header中提取相关的Header信息,从Header中获取到的用户和组信息后返回成功,说明这个认证器已经对请求进行了认证,后续的认证器就不需要再进行认证了
func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
name := headerValue(req.Header, a.nameHeaders.Value())
if len(name) == 0 {
return nil, false, nil
}
groups := allHeaderValues(req.Header, a.groupHeaders.Value())
extra := newExtra(req.Header, a.extraHeaderPrefixes.Value())
// clear headers used for authentication
for _, headerName := range a.nameHeaders.Value() {
req.Header.Del(headerName)
}
for _, headerName := range a.groupHeaders.Value() {
req.Header.Del(headerName)
}
for k := range extra {
for _, prefix := range a.extraHeaderPrefixes.Value() {
req.Header.Del(prefix + k)
}
}
return &authenticator.Response{
User: &user.DefaultInfo{
Name: name,
Groups: groups,
Extra: extra,
},
}, true, nil
}
上面两步中涉及到的一个关键信息是,自定义apiserver是怎么知道根证书和相关Header信息的呢?它的启动参数里是没有指定这些信息的,但是我们可以看到它的启动参数是有指定--authentication-kubeconfig=/etc/kubeconfig
,指定了一个指向kube-apiserver的kubeconfig文件,一般通过挂载方式获取该文件。
再创建认证器的时候,可以看到它通过该kubeconfig文件创建了kube-apiserver的请求客户端,然后查询集群中kube-system这个ns下的extension-apiserver-authentication这个ConfigMap
func (s *DelegatingAuthenticationOptions) ApplyTo(authenticationInfo *server.AuthenticationInfo, servingInfo *server.SecureServingInfo, openAPIConfig *openapicommon.Config) error {
// 由于启动参数里指定了--authentication-kubeconfig=/etc/kubeconfig,因此这里可以直接根据该kubeconfig创建kube-apiserver的客户端
client, err := s.getClient()
if err != nil {
return fmt.Errorf("failed to get delegated authentication kubeconfig: %v", err)
}
// 查询集群中kube-system这个ns下的extension-apiserver-authentication这个ConfigMap中key=client-ca-file的data
// 这个ConfigMap是K8s启动kube-apiserver的时候默认会创建的,里面记录了集群的根证书
clientCAProvider, err = dynamiccertificates.NewDynamicCAFromConfigMapController("client-ca", "kube-system", "extension-apiserver-authentication", "client-ca-file", client)
// 仍然是上面这个ConfigMap,这次读取的是这些key:requestheader-client-ca-file、requestheader-username-headers、
// requestheader-group-headers、requestheader-extra-headers-prefix、requestheader-allowed-names
requestHeaderConfig, err = s.createRequestHeaderConfig(client)
// 新建一个认证器
authenticator, securityDefinitions, err := cfg.New()
return nil
}
这个ConfigMap的内容一般如下,可以看到它记录了根证书、相关Header信息
apiVersion: v1
data:
client-ca-file: |
-----BEGIN CERTIFICATE-----
MIIFzjCCAxxxxxxxxxxxxxxxxxxxJnzbek
bE4=
-----END CERTIFICATE-----
requestheader-allowed-names: '[]'
requestheader-client-ca-file: |
-----BEGIN CERTIFICATE-----
MIIxxxxxxxxxxxxxxxxxxxxxxHn0PI=
-----END CERTIFICATE-----
requestheader-extra-headers-prefix: '["X-Remote-Extra-"]'
requestheader-group-headers: '["X-Remote-Group"]'
requestheader-username-headers: '["X-Remote-User"]'
kind: ConfigMap
metadata:
...
至此,基于Header的认证器的认证流程就比较清晰了,总结一下:
- 根据启动参数
--authentication-kubeconfig=/etc/kubeconfig
创建到kube-apiserver的请求客户端 - 读取集群中kube-system这个ns下的extension-apiserver-authentication这个ConfigMap,获取根证书、相关Header信息
- 使用根证书对kube-apiserver代理过来的请求进行认证
- 再根据相关Header信息提取请求中携带的用户和组信息,完成认证
- 将认证完成的用户和组信息设置到上下文中,用于后续的授权
授权过程
认证请求完成后,成功拿到了请求的用户和组信息,同样的自定义apiserver的启动参数中有--authorization-kubeconfig=/etc/kubeconfig
,指向的仍然是kube-apiserver的kubeconfig文件
func (s *DelegatingAuthorizationOptions) ApplyTo(c *server.AuthorizationInfo) error {
// 由于启动参数里指定了--authorization-kubeconfig=/etc/kubeconfig,因此这里可以直接根据该kubeconfig创建kube-apiserver的客户端
client, err := s.getClient()
// AlwaysAllowGroups对应的用户组为{"system:masters"}的,可以理解为特权用户组,往授权器中添加一个特权授权器
if len(s.AlwaysAllowGroups) > 0 {
authorizers = append(authorizers, authorizerfactory.NewPrivilegedGroups(s.AlwaysAllowGroups...))
}
// AlwaysAllowPaths对应的请求路径是{"/healthz", "/readyz", "/livez"}的,可以理解为这些请求路径是不需要授权的,也添加一个特殊路径授权器
if len(s.AlwaysAllowPaths) > 0 {
a, err := path.NewAuthorizer(s.AlwaysAllowPaths)
if err != nil {
return nil, err
}
authorizers = append(authorizers, a)
}
// 不是特权组,也不是特殊路径的,走正常的授权逻辑,通过调用kube-apiserver的Authorization API
// 发送SubjectAccessReview请求来完成授权,也添加一个授权器
cfg := authorizerfactory.DelegatingAuthorizerConfig{
SubjectAccessReviewClient: client.AuthorizationV1(),
AllowCacheTTL: s.AllowCacheTTL,
DenyCacheTTL: s.DenyCacheTTL,
WebhookRetryBackoff: s.WebhookRetryBackoff,
}
delegatedAuthorizer, err := cfg.New()
if err != nil {
return nil, err
}
authorizers = append(authorizers, delegatedAuthorizer)
// 最终将这些授权器级联起来形成一个数组,从前往后一次进行授权,只要有一个完成了授权就可以了
return union.New(authorizers...), nil
}
特权组的授权方式
这种授权方式就比较简单了,由于认证完成后已经将用户和组信息记录到了请求上下文中,这里只需要从上下文中获取用户组信息,判断其组信息是否为{"system:masters"}即可
func (r *privilegedGroupAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
for _, attr_group := range attr.GetUser().GetGroups() {
for _, priv_group := range r.groups {
if priv_group == attr_group {
return authorizer.DecisionAllow, "", nil
}
}
}
return authorizer.DecisionNoOpinion, "", nil
}
特殊路径的授权方式
这种授权方式也比较简单,只需要判断请求路径是否匹配{"/healthz", "/readyz", "/livez"}之一即可
func NewAuthorizer(alwaysAllowPaths []string) (authorizer.Authorizer, error) {
...
return authorizer.AuthorizerFunc(func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
// 请求路径里是否包括了资源信息,即/api/v1/nodes 或者 /apis/apps/v1/deployments这种
if a.IsResourceRequest() {
return authorizer.DecisionNoOpinion, "", nil
}
pth := strings.TrimPrefix(a.GetPath(), "/")
if paths.Has(pth) {
return authorizer.DecisionAllow, "", nil
}
for _, prefix := range prefixes {
if strings.HasPrefix(pth, prefix) {
return authorizer.DecisionAllow, "", nil
}
}
return authorizer.DecisionNoOpinion, "", nil
}), nil
}
SubjectAccessReview授权方式
这种授权方式需要和kube-apiserver交互,根据用户和组信息、请求路径、请求方法,构建一个SubjectAccessReview资源,由kube-apiserver去权限这个用户和组是否有权限访问这个请求路径
func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
r := &authorizationv1.SubjectAccessReview{}
// 将用户、组信息写到SubjectAccessReview对象中
if user := attr.GetUser(); user != nil {
r.Spec = authorizationv1.SubjectAccessReviewSpec{
User: user.GetName(),
UID: user.GetUID(),
Groups: user.GetGroups(),
Extra: convertToSARExtra(user.GetExtra()),
}
}
// 将请求方法、请求路径、请求资源等写到SubjectAccessReview对象中
if attr.IsResourceRequest() {
r.Spec.ResourceAttributes = &authorizationv1.ResourceAttributes{
Namespace: attr.GetNamespace(),
Verb: attr.GetVerb(),
Group: attr.GetAPIGroup(),
Version: attr.GetAPIVersion(),
Resource: attr.GetResource(),
Subresource: attr.GetSubresource(),
Name: attr.GetName(),
}
} else {
r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
Path: attr.GetPath(),
Verb: attr.GetVerb(),
}
}
key, err := json.Marshal(r.Spec)
// 先尝试从缓存中获取
if entry, ok := w.responseCache.Get(string(key)); ok {
r.Status = entry.(authorizationv1.SubjectAccessReviewStatus)
} else {
// 以失败BackOff的方式提交SubjectAccessReview资源请求,从status中获取授权结果
var result *authorizationv1.SubjectAccessReview
// WithExponentialBackoff will return SAR create error (sarErr) if any.
if err := webhook.WithExponentialBackoff(ctx, w.retryBackoff, func() error {
result, statusCode, sarErr = w.subjectAccessReview.Create(ctx, r, metav1.CreateOptions{})
return sarErr
}, webhook.DefaultShouldRetry); err != nil {
klog.Errorf("Failed to make webhook authorizer request: %v", err)
return w.decisionOnError, "", err
}
r.Status = result.Status
// 将结果缓存起来
if shouldCache(attr) {
if r.Status.Allowed {
w.responseCache.Add(string(key), r.Status, w.authorizedTTL)
} else {
w.responseCache.Add(string(key), r.Status, w.unauthorizedTTL)
}
}
}
// 从Status中获取授权结果
switch {
case r.Status.Denied && r.Status.Allowed:
return authorizer.DecisionDeny, r.Status.Reason, fmt.Errorf("webhook subject access review returned both allow and deny response")
case r.Status.Denied:
return authorizer.DecisionDeny, r.Status.Reason, nil
case r.Status.Allowed:
return authorizer.DecisionAllow, r.Status.Reason, nil
default:
return authorizer.DecisionNoOpinion, r.Status.Reason, nil
}
}