Kubernetes超越RBAC – 通过Webhook自定义授权

2024-09-12 11:15:39 浏览数 (2)

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

代码语言:javascript复制
// 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:它包含请求详细信息,如资源属性和用户组信息。

在这种类型中,有两个重要的字段:ResourceAttributesNonResourceAttributes

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:此字段包含针对请求的授权响应,表示是允许还是拒绝。
代码语言:javascript复制
// 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”的服务帐户执行 listget 操作,但禁止 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 文件现在可以使用了

0 人点赞