Kubernetes 是一个很棒的容器编排工具,它提供了许多自定义选项。您可以轻松地扩展/替换它的许多…
译自 Kubernetes Beyond RBAC - Make Your Own Authorization via Webhook,作者 Emre Savcı。
Kubernetes 是一款出色的容器编排工具,提供了大量的自定义选项。您可以轻松扩展/替换许多组件,例如 CNI、CSI、调度器,甚至授权组件。
在本文中,您将了解如何编写自己的授权 Webhook,该 Webhook 可在 Kubernetes 上运行以扩展 RBAC 功能或完全移除 RBAC。
我们将探讨以下主题:
- Kubernetes 授权流程
- 为授权 Webhook 配置 Kubernetes API 服务器
- 授权请求的结构
- 编写授权 Webhook
- 生成自签名证书
- Kubectl 身份验证怎么办
- 展示时间 - 全部运行
- 使用场景
- 参考文献
Kubernetes 授权流程
首先,让我们解释一下 Kubernetes 的内部授权流程。
https://kubernetes.io/docs/concepts/security/controlling-access/
到达 API 服务器的请求将经过上图所示的流程。
每个发送到 Kubernetes 集群的请求都由 API 服务器进行身份验证,然后启动多个授权流程。在该授权流程之后,API 服务器调用准入控制 Webhook。最后,如果一切顺利,将通过查询或修改 etcd 的状态来完成请求。
由于 Kubernetes 具有可扩展的架构,我们可以扩展上述每个步骤。我们可以集成自定义身份验证解决方案。我们可以编写自己的授权服务器。或者,我们可以干预每个资源的创建或修改。
如果您想了解如何在 Kubernetes 中使用 RBAC 进行授权,请参阅我之前关于配置 RBAC 的文章。
为授权 Webhook 配置 Kubernetes API 服务器
您需要配置 API 服务器以指定授权 Webhook 地址。
就个人而言,我使用 Kind 在本地测试 Kubernetes。以下配置为 Kubernetes 的 API 服务器启用了 Webhook 授权。让我们将此配置放在名为“kind-cp.yaml”的文件中。
代码语言:javascript复制kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraMounts:
- hostPath: /Users/emre.savci/Desktop/kube-authz
containerPath: /files
kubeadmConfigPatches:
- |
kind: ClusterConfiguration
apiServer:
extraArgs:
enable-admission-plugins: NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook
authorization-mode: Webhook,RBAC
authorization-webhook-version: v1
authorization-webhook-config-file: /files/authz-webhook.yaml
authorization-webhook-cache-authorized-ttl: 120s
authorization-webhook-cache-unauthorized-ttl: 30s
extraVolumes:
- name: api-server-basic-auth-files
hostPath: "/files"
mountPath: "/files"
readOnly: true
如果仔细查看配置文件,您会发现与授权相关的参数。
以下行指定我们的授权模式同时使用原生 RBAC 和我们自定义编写的授权 Webhook:
代码语言:javascript复制authorization-mode: Webhook, RBAC
以下行指定授权 Webhook 的配置文件:
代码语言:javascript复制authorization-webhook-config-file: /files/authz-webhook.yaml
以下是我们的授权 Webhook 配置文件:
代码语言:javascript复制clusters:
- name: my-cluster
cluster:
certificate-authority: /files/webhook.crt
server: https://authz-webhook/authorize
users:
- name: api-server
user:
token: test-token
current-context: my-cluster
contexts:
- context:
cluster: my-cluster
user: api-server
name: my-cluster
现在,我们可以使用这些配置创建集群。
代码语言:javascript复制kind create cluster --retain --config kind-cp.yaml
授权请求的结构
在编写自定义授权 Webhook 之前,让我们看一下 Kubernetes 发送的授权请求。
您始终可以为传入请求定义自定义类型,但由于 Kubernetes api,我们拥有适用于 Golang 的请求类型。
我们可以通过以下命令安装 Kubernetes api 包:
代码语言:javascript复制go get "k8s.io/api/authorization/v1"
之后,我们就拥有了授权请求对象:SubjectAccessReview
。
// SubjectAccessReview checks whether or not a user or group can perform an action.
type SubjectAccessReview struct {
metav1.TypeMeta `json:",inline"`
// Standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Spec holds information about the request being evaluated
Spec SubjectAccessReviewSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"`
// Status is filled in by the server and indicates whether the request is allowed or not
// optional
Status SubjectAccessReviewStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}
在这个结构体中,我们有两个重要的字段:
-
SubjectAccessReviewSpec
:它包含请求详细信息,如资源属性和用户组信息。
在这种类型中,有两个重要的字段:ResourceAttributes 和 NonResourceAttributes。
ResourceAttributes: 当请求访问 Kubernetes 资源(如 pod、服务等)时,此字段不为空。
NonResourceAttributes: 当您尝试通过 kubectl auth can-i 检查权限时,此字段不为空。
代码语言:javascript复制// SubjectAccessReviewSpec is a description of the access request. Exactly one of ResourceAuthorizationAttributes
// and NonResourceAuthorizationAttributes must be set
type SubjectAccessReviewSpec struct {
// ResourceAuthorizationAttributes describes information for a resource access request
// optional
ResourceAttributes *ResourceAttributes `json:"resourceAttributes,omitempty" protobuf:"bytes,1,opt,name=resourceAttributes"`
// NonResourceAttributes describes information for a non-resource access request
// optional
NonResourceAttributes *NonResourceAttributes `json:"nonResourceAttributes,omitempty" protobuf:"bytes,2,opt,name=nonResourceAttributes"`
// User is the user you're testing for.
// If you specify "User" but not "Groups", then is it interpreted as "What if User were not a member of any groups
// optional
User string `json:"user,omitempty" protobuf:"bytes,3,opt,name=user"`
// Groups is the groups you're testing for.
// optional
Groups []string `json:"groups,omitempty" protobuf:"bytes,4,rep,name=groups"`
// Extra corresponds to the user.Info.GetExtra() method from the authenticator. Since that is input to the authorizer
// it needs a reflection here.
// optional
Extra map[string]ExtraValue `json:"extra,omitempty" protobuf:"bytes,5,rep,name=extra"`
// UID information about the requesting user.
// optional
UID string `json:"uid,omitempty" protobuf:"bytes,6,opt,name=uid"`
}
代码语言:javascript复制// ResourceAttributes includes the authorization attributes available for resource requests to the Authorizer interface
type ResourceAttributes struct {
// Namespace is the namespace of the action being requested. Currently, there is no distinction between no namespace and all namespaces
// "" (empty) is defaulted for LocalSubjectAccessReviews
// "" (empty) is empty for cluster-scoped resources
// "" (empty) means "all" for namespace scoped resources from a SubjectAccessReview or SelfSubjectAccessReview
// optional
Namespace string `json:"namespace,omitempty" protobuf:"bytes,1,opt,name=namespace"`
// Verb is a kubernetes resource API verb, like: get, list, watch, create, update, delete, proxy. "*" means all.
// optional
Verb string `json:"verb,omitempty" protobuf:"bytes,2,opt,name=verb"`
// Group is the API Group of the Resource. "*" means all.
// optional
Group string `json:"group,omitempty" protobuf:"bytes,3,opt,name=group"`
// Version is the API Version of the Resource. "*" means all.
// optional
Version string `json:"version,omitempty" protobuf:"bytes,4,opt,name=version"`
// Resource is one of the existing resource types. "*" means all.
// optional
Resource string `json:"resource,omitempty" protobuf:"bytes,5,opt,name=resource"`
// Subresource is one of the existing resource types. "" means none.
// optional
Subresource string `json:"subresource,omitempty" protobuf:"bytes,6,opt,name=subresource"`
// Name is the name of the resource being requested for a "get" or deleted for a "delete". "" (empty) means all.
// optional
Name string `json:"name,omitempty" protobuf:"bytes,7,opt,name=name"`
}
-
SubjectAccessReviewStatus
:此字段包含针对请求的授权响应,表示是允许还是拒绝。
// SubjectAccessReviewStatus
type SubjectAccessReviewStatus struct {
// Allowed is required. True if the action would be allowed, false otherwise.
Allowed bool `json:"allowed" protobuf:"varint,1,opt,name=allowed"`
// Denied is optional. True if the action would be denied, otherwise
// false. If both allowed is false and denied is false, then the
// authorizer has no opinion on whether to authorize the action. Denied
// may not be true if Allowed is true.
// optional
Denied bool `json:"denied,omitempty" protobuf:"varint,4,opt,name=denied"`
// Reason is optional. It indicates why a request was allowed or denied.
// optional
Reason string `json:"reason,omitempty" protobuf:"bytes,2,opt,name=reason"`
// EvaluationError is an indication that some error occurred during the authorization check.
// It is entirely possible to get an error and be able to continue determine authorization status in spite of it.
// For instance, RBAC can be missing a role, but enough roles are still present and bound to reason about the request.
// optional
EvaluationError string `json:"evaluationError,omitempty" protobuf:"bytes,3,opt,name=evaluationError"`
}
有关更详细的说明,您可以查看 Kubernetes Subject Access Review。
编写授权 Webhook
不要被标题吓到,创建授权 webhook 是一件非常简单的事情。实际上,webhook 就是一个简单的 HTTP 服务器。
以下是一个简单的授权 webhook,它允许名为“test-user”的服务帐户执行 list 和 get 操作,但禁止 delete 操作:
代码语言:javascript复制package main
import (
"fmt"
"github.com/gofiber/fiber/v2"
authorizationv1 "k8s.io/api/authorization/v1"
)
func main() {
app := fiber.New()
app.Post("/authorize", func(ctx *fiber.Ctx) error {
var req authorizationv1.SubjectAccessReview
ctx.BodyParser(&req)
req.Status.Allowed = true
if req.Spec.User == "system:serviceaccount:default:test-user" {
if req.Spec.ResourceAttributes != nil {
if req.Spec.ResourceAttributes.Verb == "get" || req.Spec.ResourceAttributes.Verb == "list" {
req.Status.Allowed = true
}
if req.Spec.ResourceAttributes.Verb == "delete" {
req.Status.Allowed = false
}
}
if req.Spec.NonResourceAttributes != nil {
if req.Spec.NonResourceAttributes.Verb == "get" || req.Spec.NonResourceAttributes.Verb == "list" {
req.Status.Allowed = true
}
if req.Spec.NonResourceAttributes.Verb == "delete" {
req.Status.Allowed = false
}
}
}
return ctx.JSON(req)
})
app.Get("/healthz", func(ctx *fiber.Ctx) error {
fmt.Println("healthz")
return ctx.SendStatus(200)
})
if err := app.ListenTLS(":443", "/app/webhook.crt", "/app/webhook.key"); err != nil {
fmt.Println(err)
}
}
以下配置适用于我们的授权 webhook。它指定了我们的 webhook 服务器地址和证书颁发机构。
代码语言:javascript复制clusters:
- name: devx-webhooks
cluster:
certificate-authority: /files/webhook.crt
server: https://devx-webhooks/authorize
users:
- name: api-server
user:
token: test-token
current-context: devx-webhooks
contexts:
- context:
cluster: devx-webhooks
user: api-server
name: devx-webhooks
让我们运行我们的 webhook。请记住,我们通过 kind 运行 Kubernetes 集群,我们将在 kind 网络中使用 Docker 运行 webhook。
代码语言:javascript复制docker build -t go-kube-authz .
docker run -it -d --name devx-webhooks --network kind -p 443:443 go-kube-authz
Webhook 自签名证书
我们需要创建一个自签名证书,以便 api-server 与我们的 webhook 安全通信。我们将在授权 webhook 服务器中使用生成的 webhook.cert 和 webhook.key。我们还将把 webhook.cert 传递给 webhook 配置文件中的 Kubernetes api 服务器。
代码语言:javascript复制openssl genrsa -out webhook.key 2048
让我们创建一个名为 webook.csr.cnf 的文件,并将以下配置放入其中:
代码语言:javascript复制[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[dn]
CN = devx-webhooks
[req_ext]
subjectAltName = @alt_names
[alt_names]
DNS.1 = devx-webhooks
代码语言:javascript复制openssl req -new -key webhook.key -out webhook.csr -config webhook.csr.cnf
现在创建另一个文件并放入以下几行
代码语言:javascript复制authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = devx-webhooks
代码语言:javascript复制openssl x509 -req -in webhook.csr -signkey webhook.key -out webhook.crt -days 365 -extfile webhook.ext
我们的 webhook.key 和 webhook.cert 文件现在可以使用了