Kube-apiserver 认证鉴权插件Authenticator和Authorizer
原文链接:https://note.youdao.com/ynoteshare1/index.html?id=9d0b804336ce5f4009d35848bc3acded&type=note
cmd/kube-apiserver/app/server.go中的BuildAuthenticator函数初始化kube-apiserver的认证插件,这个函数又调用了pkg/kubeapiserver/authenticator/Config.New()添加了很多authenticator。
cmd/kube-apiserver/app/server.go中的BuildAuthorizer函数初始化kube-apiserver的鉴权插件,这个函数又调用了pkg/kubeapiserver/authorizer/Config.New()初始化各种authorizers
请求头authenticator,实际是上是x509.Verifier(staging/src/k8s.io/apiserver/pkg/authentication/request/x509/x509.go),用CA证书进行认证。用户信息是从请求头指定的Key中获取的,如X-Remote-User,X-Remote-Group
basicAuthFile里保存的用户名密码,请求头中 Authorization:Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== 获取用户名密码进行对比
staging/src/k8s.io/apiserver/pkg/authentication/request/x509/x509.go.Authenticator, 使用ClientCAFile校验证书,用户信息是x509.CommonNameUserConversion函数从客户端证书中获取的(User:CommonName,Groups:Organization)
包含多种token authenticator的数组:
a. TokenAuthFile:本地token file保存的token
b. LegacyServiceAccountAuthenticator: jwt格式token,JWT payload中的private claim是 pkg/serviceaccount/legacy.go.legacyPrivateClaims 结构。jwt token格式为 header.payload.signature,header和 payload都是base64编码,signature是把前两部分用privateKey加密的,服务端解析时用publicKey解密
c. ServiceAccountAuthenticator:也是jwt token, JWT payload中的private claim是pkg/serviceaccount/claims.go.validator字段
d. BootstrapTokenAuthenticator:格式为(token-id).(token-secret),用token-secret和bootstrap-token-<token-id> secret中保存的token对比
e. oidcAuth: 也是一种jwt格式token,可以从issuerUrl获取token_endpoint、jwks_uri,keyfile可以从jwks_uri获取
f. 调用webhook插件解析token
对来自kubelet的请求鉴权
// NodeAuthorizer authorizes requests from kubelets, with the following logic:
// 1. If a request is not from a node (NodeIdentity() returns isNode=false), reject
// 2. If a specific node cannot be identified (NodeIdentity() returns nodeName=""), reject
// 3. If a request is for a secret, configmap, persistent volume or persistent volume claim, reject unless the verb is get, and the requested object is related to the requesting node:
// node <- configmap
// node <- pod
// node <- pod <- secret
// node <- pod <- configmap
// node <- pod <- pvc
// node <- pod <- pvc <- pv
// node <- pod <- pvc <- pv <- secret
// 4. For other resources, authorize all nodes uniformly using statically defined rules
基于属性的鉴权,--authorization-policy-file=SOME_FILENAME 指定策略文件
举例:Kubelet可以读取任何pod:
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "kubelet", "namespace": "*", "resource": "pods", "readonly": true}}
鉴权就是遍历每一个Policy,执行下面的matches函数,先检查user和group是否匹配,再检查verb动作是否匹配,最后检查资源是否匹配。
// pkg/auth/authorizer/abac/abac.go
func matches(p abac.Policy, a authorizer.Attributes) bool {
if subjectMatches(p, a.GetUser()) {
if verbMatches(p, a) {
// Resource and non-resource requests are mutually exclusive, at most one will match a policy
if resourceMatches(p, a) {
return true
}
if nonResourceMatches(p, a) {
return true
}
}
}
return false
}
更多示例见:http://docs.kubernetes.org.cn/87.html
基于角色的鉴权,通过ClusterRoleBinding、RoleBinding找到和该用户(或者用户组)绑定的ClusterRole、Role,ClusterRole和Role就是用户拥有的角色,在ClusterRole和Role中找到具体的rules,这些rules就是用户可以通过鉴权的白名单。然后调用visitor函数检查该rule和请求的Verb、APIGroup、Resource等是否匹配。
// pkg/registry/rbac/validation/rule.go
func (r *DefaultRuleResolver) VisitRulesFor(user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) {
// 查询所有的clusterRoleBindings
if clusterRoleBindings, err := r.clusterRoleBindingLister.ListClusterRoleBindings(); err != nil {
if !visitor(nil, nil, err) {
return
}
} else {
sourceDescriber := &clusterRoleBindingDescriber{}
for _, clusterRoleBinding := range clusterRoleBindings {
// 遍历clusterRoleBindings,检查是否该用户或者用户组有绑定关系,没有的跳过
subjectIndex, applies := appliesTo(user, clusterRoleBinding.Subjects, "")
if !applies {
continue
}
// 找到clusterRoleBinding绑定的clusterRole,从clusterRole中获取该角色拥有的权限规则rules
rules, err := r.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "")
if err != nil {
if !visitor(nil, nil, err) {
return
}
continue
}
sourceDescriber.binding = clusterRoleBinding
sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex]
// 调用visitor函数检查每个rule和请求的Verb、APIGroup、Resource等是否匹配。
for i := range rules {
if !visitor(sourceDescriber, &rules[i], nil) {
return
}
}
}
}
// roleBinding是和namespace关联的,请求中带有namespace时,查询所有的roleBindings,
// 逻辑和clusterRoleBindings类似,找到roleBinding对应的Role,获取Role中的鉴权规则rules,调用visitor函数做校验
if len(namespace) > 0 {
if roleBindings, err := r.roleBindingLister.ListRoleBindings(namespace); err != nil {
if !visitor(nil, nil, err) {
return
}
} else {
sourceDescriber := &roleBindingDescriber{}
for _, roleBinding := range roleBindings {
subjectIndex, applies := appliesTo(user, roleBinding.Subjects, namespace)
if !applies {
continue
}
rules, err := r.GetRoleReferenceRules(roleBinding.RoleRef, namespace)
if err != nil {
if !visitor(nil, nil, err) {
return
}
continue
}
sourceDescriber.binding = roleBinding
sourceDescriber.subject = &roleBinding.Subjects[subjectIndex]
for i := range rules {
if !visitor(sourceDescriber, &rules[i], nil) {
return
}
}
}
}
}
}
visit函数是authorizingVisitor.visit,authorizingVisitor初始的时候保存了请求相关的属性requestAttributes,调用RuleAllows检查rule是否允许requestAttributes,RuleAllows中就是分别检查 Verb、APIGroup、Resource、ResourceName是否match。
// plugin/pkg/auth/authorizer/rbac/rbac.go
func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool {
if rule != nil && RuleAllows(v.requestAttributes, rule) {
v.allowed = true
v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String())
return false
}
if err != nil {
v.errors = append(v.errors, err)
}
return true
}
func RuleAllows(requestAttributes authorizer.Attributes, rule *rbacv1.PolicyRule) bool {
if requestAttributes.IsResourceRequest() { //是关于Resource的请求
combinedResource := requestAttributes.GetResource()
if len(requestAttributes.GetSubresource()) > 0 {
combinedResource = requestAttributes.GetResource() + "/" + requestAttributes.GetSubresource()
}
// 分别检查 Verb、APIGroup、Resource、ResourceName是否match
return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) &&
rbacv1helpers.APIGroupMatches(rule, requestAttributes.GetAPIGroup()) &&
rbacv1helpers.ResourceMatches(rule, combinedResource, requestAttributes.GetSubresource()) &&
rbacv1helpers.ResourceNameMatches(rule, requestAttributes.GetName())
}
return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) &&
rbacv1helpers.NonResourceURLMatches(rule, requestAttributes.GetPath())
}
调用webhook插件进行鉴权,把请求相关的所有信息封装到SubjectAccessReview对象中,作为请求体调用webhook插件。
代码逻辑可以看下面的代码注释,注释中包含了调用webhook的请求体和返回体样例。逻辑也比较简单,先构造请求体,填充User和Resource信息,然后看是否有缓存,又缓存就取缓存结果,否则就调用subjectAccessReview.Create函数,subjectAccessReview.Create函数就是调用Post请求把SubjectAccessReview作为请求体请求webhook插件。从请求结果中获取鉴权结果。
// Authorize makes a REST request to the remote service describing the attempted action as a JSON
// serialized api.authorization.v1beta1.SubjectAccessReview object. An example request body is
// provided below.
// 调用webhook 请求体样例
// {
// "apiVersion": "authorization.k8s.io/v1beta1",
// "kind": "SubjectAccessReview",
// "spec": {
// "resourceAttributes": {
// "namespace": "kittensandponies",
// "verb": "GET",
// "group": "group3",
// "resource": "pods"
// },
// "user": "jane",
// "group": [
// "group1",
// "group2"
// ]
// }
// }
//
// The remote service is expected to fill the SubjectAccessReviewStatus field to either allow or
// disallow access. A permissive response would return:
// webhook返回成功样例
// {
// "apiVersion": "authorization.k8s.io/v1beta1",
// "kind": "SubjectAccessReview",
// "status": {
// "allowed": true
// }
// }
//
// To disallow access, the remote service would return:
// webhook返回失败样例
// {
// "apiVersion": "authorization.k8s.io/v1beta1",
// "kind": "SubjectAccessReview",
// "status": {
// "allowed": false,
// "reason": "user does not have read access to the namespace"
// }
// }
//
// TODO(mikedanese): We should eventually support failing closed when we
// encounter an error. We are failing open now to preserve backwards compatible
// behavior.
func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
r := &authorization.SubjectAccessReview{}
// 填充请求体User信息
if user := attr.GetUser(); user != nil {
r.Spec = authorization.SubjectAccessReviewSpec{
User: user.GetName(),
UID: user.GetUID(),
Groups: user.GetGroups(),
Extra: convertToSARExtra(user.GetExtra()),
}
}
// 填充请求体Resource信息
if attr.IsResourceRequest() {
r.Spec.ResourceAttributes = &authorization.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 = &authorization.NonResourceAttributes{
Path: attr.GetPath(),
Verb: attr.GetVerb(),
}
}
key, err := json.Marshal(r.Spec)
if err != nil {
return w.decisionOnError, "", err
}
// 如果有缓存就取缓存结果
if entry, ok := w.responseCache.Get(string(key)); ok {
r.Status = entry.(authorization.SubjectAccessReviewStatus)
} else {
var (
result *authorization.SubjectAccessReview
err error
)
webhook.WithExponentialBackoff(w.initialBackoff, func() error {
// 没有缓存就调用webhook插件获取鉴权结果,见下面的func (t *subjectAccessReviewClient) Create()函数
result, err = w.subjectAccessReview.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 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)
}
}
}
// webhook插件返回的鉴权结果
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
}
}
// 就是一个Post请求,把SubjectAccessReview作为请求体,调用webhook插件
func (t *subjectAccessReviewClient) Create(subjectAccessReview *authorization.SubjectAccessReview) (*authorization.SubjectAccessReview, error) {
result := &authorization.SubjectAccessReview{}
err := t.w.RestClient.Post().Body(subjectAccessReview).Do().Into(result)
return result, err
}