Углубленный анализ веб-крючков для приема Kubernetes


ИСТОРИЯ

Особенности контроллеров допуска.

  • Настраиваемость: функции приема могут быть адаптированы для различных сценариев.
  • Предотвратимые: в то время как аудит направлен на обнаружение проблем, контроллеры допуска могут предотвратить их возникновение
  • Масштабируемость: добавление дополнительной линии защиты к собственному механизму аутентификации kubernetes компенсирует тот факт, что RBAC может обеспечить гарантии безопасности только для ресурсов.

На приведенной ниже диаграмме, на которой показан процесс манипулирования пользователем ресурсом, видно, что контроллеры допуска выступают в качестве перехватчика до того, как ресурс будет сохранен с помощью аутентификации. Добавление контроллеров допуска добавляет более продвинутую функцию безопасности в kubernetes.

Рисунок: Диаграмма этапов обработки запросов API Kubernetes

Источник: https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/

Вот диаграмма, нарисованная одним крупным блоггером. Эти две диаграммы дают четкое представление о процессе admission webhook, который отличается от официального тем, что в нем kubernetes admission webhook четко расположен в управлении допуском, после RBAC и до push.

Рисунок: Диаграмма этапов обработки запросов к API Kubernetes (подробно)

Источник: https://www.armosec.io/blog/kubernetes-admission-controller/

В чем разница между двумя контроллерами?

Согласно официальному заявлению

Мутирующие контроллеры могут изменять связанные объекты для запросов, которые они принимают; валидирующие контроллеры не могут

Из структурной диаграммы также ясно, что validating находится до персистенции, а Mutating — до структурной валидации. Основываясь на этих характеристиках, мы можем использовать Mutating для изменения содержимого этого ресурсного объекта (например, для добавления валидирующей информации), а в validating<##code ), и подтвердить законность в validating.

Состав контролеров допуска

Контроллеры допуска в kubernetes состоят из двух компонентов.

  • Контроллеры допуска, встроенные во встроенный список APIServer
  • Специальные контроллеры; также встроены в APIServer, но обеспечивают некоторую пользовательскую функциональность
    • MutatingAdmission
    • ValidatingAdmission

Мутирующие контроллеры могут изменять объекты ресурсов, которые они обрабатывают; проверяющие контроллеры — нет. Если любой контроллер в любой фазе отклоняет запрос, весь запрос немедленно отклоняется и возвращается ошибка.

веб-крючок для приема

Поскольку контроллер допуска встроен в kube-apiserver, это ограничивает масштабируемость контроллера допуска. В этом контексте kubernetes предоставляет расширяемый контроллер допуска extensible admission controllers, поведение, называемое Dynamic Admission Control, и Для этого используется admission webhook .

admission webhook широко известен как HTTP callback, где http-сервер определен для приема и обработки запросов на допуск. Пользователи могут обрабатывать пользовательские политики допуска через два типа admission webhook, предоставляемых kubernetes, — validating admission webhook и mutating admission webhook.

Вебхук — это

Примечание: Как видно из приведенной выше блок-схемы, веб-крючок приема также является последовательным. Сначала вызывается мутирующий веб-крючок, а затем проверяющий веб-крючок.

Как использовать контроллер доступа

Условия использования: kubernetes v1.16 с admissionregistration.k8s.io/v1; kubernetes v1.9 с admissionregistration.k8s.io/v1beta1< ##code>.

Как включить контроллер доступа в кластер? : Посмотрите на параметр запуска kube-apiserver -enable-admission-plugins; настройте контроллеры доступа на запуск с этим параметром, например, -enable-admission-plugins= NodeRestriction Множественные контроллеры доступа разделяются по ,, порядок не имеет значения. Вместо этого можно использовать параметр -disable-admission-plugins для отключения соответствующих контроллеров доступа (См. apiserver opts).

Команда kubectl показывает текущую версию контроллера доступа, поддерживаемую кластером kubernetes

$ kubectl api-versions | grep admissionregistration.k8s.io/v1
admissionregistration.k8s.io/v1
admissionregistration.k8s.io/v1beta1
Войдите в полноэкранный режим Выход из полноэкранного режима

Как работает webhook

Как вы узнали выше, два типа веб-крючков работают следующим образом.

Mutating webhook, который перехватывает запросы, соответствующие правилам, определенным в MutatingWebhookConfiguration, перед сохранением, и MutatingAdmissionWebhook, который выполняет аутентификацию путем отправки запроса доступа на сервер mutating webhook.

validaing webhook, который перехватывает запросы, соответствующие правилам, определенным в ValidatingWebhookConfiguration перед сохранением. validatingAdmissionWebhook выполняет проверку, отправляя запрос доступа в validating сервер webhook для выполнения проверки.

Поэтому следующим шагом будет просмотр исходного кода, чтобы понять, что именно делается в этом рабочем процессе.

Типы ресурсов

Для версий после 1.9, т.е. v1, допуск определяется в k8s.ioapiadmissionregistrationv1types.go, который похож, поскольку локально существует только кластер 1.18, так что это объяснение.

Для Validating Webhook реализация в основном находится в вебхуке

type ValidatingWebhookConfiguration struct {
    // 每个api必须包含下列的metadata,这个是kubernetes规范,可以在注释中的url看到相关文档
    metav1.TypeMeta `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
    // Webhooks在这里被表示为[]ValidatingWebhook,表示我们可以注册多个
    // +optional
    // +patchMergeKey=name
    // +patchStrategy=merge
    Webhooks []ValidatingWebhook `json:"webhooks,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=Webhooks"`
}
Войдите в полноэкранный режим Выход из полноэкранного режима

веб-крючок, операции, ресурсы и т.д., предоставляемые этим типом веб-крючка. Эта часть не слишком аннотирована, поскольку это ресурс API kubernetes, а подробные примеры и объяснения есть на официальном сайте. Для получения дополнительной информации о значении полей здесь, пожалуйста, обратитесь к официальному документу

type ValidatingWebhook struct {
    //  admission webhook的名词,Required
    Name string `json:"name" protobuf:"bytes,1,opt,name=name"`

    // ClientConfig 定义了与webhook通讯的方式 Required
    ClientConfig WebhookClientConfig `json:"clientConfig" protobuf:"bytes,2,opt,name=clientConfig"`

    // rule表示了webhook对于哪些资源及子资源的操作进行关注
    Rules []RuleWithOperations `json:"rules,omitempty" protobuf:"bytes,3,rep,name=rules"`

    // FailurePolicy 对于无法识别的value将如何处理,allowed/Ignore optional
    FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,4,opt,name=failurePolicy,casttype=FailurePolicyType"`

    // matchPolicy 定义了如何使用“rules”列表来匹配传入的请求。
    MatchPolicy *MatchPolicyType `json:"matchPolicy,omitempty" protobuf:"bytes,9,opt,name=matchPolicy,casttype=MatchPolicyType"`
    NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty" protobuf:"bytes,5,opt,name=namespaceSelector"`
    SideEffects *SideEffectClass `json:"sideEffects" protobuf:"bytes,6,opt,name=sideEffects,casttype=SideEffectClass"`
    AdmissionReviewVersions []string `json:"admissionReviewVersions" protobuf:"bytes,8,rep,name=admissionReviewVersions"`
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Это определение ресурса webhook, но как он используется? Найдите k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go с помощью Find Usages. Здесь нет комментариев, но структура показывает, что она состоит из клиента с селекторами

type mutatingWebhookAccessor struct {
    *v1.MutatingWebhook
    uid               string
    configurationName string

    initObjectSelector sync.Once
    objectSelector     labels.Selector
    objectSelectorErr  error

    initNamespaceSelector sync.Once
    namespaceSelector     labels.Selector
    namespaceSelectorErr  error

    initClient sync.Once
    client     *rest.RESTClient
    clientErr  error
}
Войдите в полноэкранный режим Выход из полноэкранного режима

accessor, потому что он содержит некоторые действия, определенные всем webhookconfig (лично я так думаю).

Ниже accessor.go есть метод GetRESTClient, и, как вы можете видеть, здесь делается то, что используется клиент, построенный из accessor.

func (m *mutatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error) {
    m.initClient.Do(func() {
        m.client, m.clientErr = clientManager.HookClient(hookClientConfigForWebhook(m))
    })
    return m.client, m.clientErr
}
Войдите в полноэкранный режим Выход из полноэкранного режима

На этом этапе нет необходимости читать дальше, поскольку мы уже знаем, что это шаг перед запросом веб-крючка, так что вот когда его запрашивать.

k8s.ioapiserverpkgadmissionpluginwebhookvalidatingdispatcher.go Есть два метода диспетчеризации для запроса нашего собственного определенного вебхука

func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
    var relevantHooks []*generic.WebhookInvocation
    // Construct all the versions we need to call our webhooks
    versionedAttrs := map[schema.GroupVersionKind]*generic.VersionedAttributes{}
    for _, hook := range hooks {
        invocation, statusError := d.plugin.ShouldCallHook(hook, attr, o)
        if statusError != nil {
            return statusError
        }
        if invocation == nil {
            continue
        }
        relevantHooks = append(relevantHooks, invocation)
        // If we already have this version, continue
        if _, ok := versionedAttrs[invocation.Kind]; ok {
            continue
        }
        versionedAttr, err := generic.NewVersionedAttributes(attr, invocation.Kind, o)
        if err != nil {
            return apierrors.NewInternalError(err)
        }
        versionedAttrs[invocation.Kind] = versionedAttr
    }

    if len(relevantHooks) == 0 {
        // no matching hooks
        return nil
    }

    // Check if the request has already timed out before spawning remote calls
    select {
    case <-ctx.Done():
        // parent context is canceled or timed out, no point in continuing
        return apierrors.NewTimeoutError("request did not complete within requested timeout", 0)
    default:
    }

    wg := sync.WaitGroup{}
    errCh := make(chan error, len(relevantHooks))
    wg.Add(len(relevantHooks))
    // 循环所有相关的注册的hook
    for i := range relevantHooks {
        go func(invocation *generic.WebhookInvocation) {
            defer wg.Done()
            // invacation 中有一个 Accessor,Accessor注册了一个相关的webhookconfig
            // 也就是我们 kubectl -f 注册进来的那个webhook的相关配置
            hook, ok := invocation.Webhook.GetValidatingWebhook()
            if !ok {
                utilruntime.HandleError(fmt.Errorf("validating webhook dispatch requires v1.ValidatingWebhook, but got %T", hook))
                return
            }
            versionedAttr := versionedAttrs[invocation.Kind]
            t := time.Now()
            // 调用了callHook去请求我们自定义的webhook
            err := d.callHook(ctx, hook, invocation, versionedAttr)
            ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1.Ignore
            rejected := false
            if err != nil {
                switch err := err.(type) {
                case *webhookutil.ErrCallingWebhook:
                    if !ignoreClientCallFailures {
                        rejected = true
                        admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionCallingWebhookError, 0)
                    }
                case *webhookutil.ErrWebhookRejection:
                    rejected = true
                    admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionNoError, int(err.Status.ErrStatus.Code))
                default:
                    rejected = true
                    admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionAPIServerInternalError, 0)
                }
            }
            admissionmetrics.Metrics.ObserveWebhook(time.Since(t), rejected, versionedAttr.Attributes, "validating", hook.Name)
            if err == nil {
                return
            }

            if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok {
                if ignoreClientCallFailures {
                    klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
                    utilruntime.HandleError(callErr)
                    return
                }

                klog.Warningf("Failed calling webhook, failing closed %v: %v", hook.Name, err)
                errCh <- apierrors.NewInternalError(err)
                return
            }

            if rejectionErr, ok := err.(*webhookutil.ErrWebhookRejection); ok {
                err = rejectionErr.Status
            }
            klog.Warningf("rejected by webhook %q: %#v", hook.Name, err)
            errCh <- err
        }(relevantHooks[i])
    }
    wg.Wait()
    close(errCh)

    var errs []error
    for e := range errCh {
        errs = append(errs, e)
    }
    if len(errs) == 0 {
        return nil
    }
    if len(errs) > 1 {
        for i := 1; i < len(errs); i++ {
            // TODO: merge status errors; until then, just return the first one.
            utilruntime.HandleError(errs[i])
        }
    }
    return errs[0]
}

Войдите в полноэкранный режим Выход из полноэкранного режима

callHook может быть интерпретирован как фактический запрос нашей пользовательской службы webhook

func (d *validatingDispatcher) callHook(ctx context.Context, h *v1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error {
   if attr.Attributes.IsDryRun() {
      if h.SideEffects == nil {
         return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
      }
      if !(*h.SideEffects == v1.SideEffectClassNone || *h.SideEffects == v1.SideEffectClassNoneOnDryRun) {
         return webhookerrors.NewDryRunUnsupportedErr(h.Name)
      }
   }

   uid, request, response, err := webhookrequest.CreateAdmissionObjects(attr, invocation)
   if err != nil {
      return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
   }
   // 发生请求,可以看到,这里从上面的讲到的地方获取了一个客户端
   client, err := invocation.Webhook.GetRESTClient(d.cm)
   if err != nil {
      return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
   }
   trace := utiltrace.New("Call validating webhook",
      utiltrace.Field{"configuration", invocation.Webhook.GetConfigurationName()},
      utiltrace.Field{"webhook", h.Name},
      utiltrace.Field{"resource", attr.GetResource()},
      utiltrace.Field{"subresource", attr.GetSubresource()},
      utiltrace.Field{"operation", attr.GetOperation()},
      utiltrace.Field{"UID", uid})
   defer trace.LogIfLong(500 * time.Millisecond)

   // 这里设置超时,超时时长就是在yaml资源清单中设置的那个值
   if h.TimeoutSeconds != nil {
      var cancel context.CancelFunc
      ctx, cancel = context.WithTimeout(ctx, time.Duration(*h.TimeoutSeconds)*time.Second)
      defer cancel()
   }
   // 直接用post请求我们自己定义的webhook接口
   r := client.Post().Body(request)

   // if the context has a deadline, set it as a parameter to inform the backend
   if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
      // compute the timeout
      if timeout := time.Until(deadline); timeout > 0 {
         // if it's not an even number of seconds, round up to the nearest second
         if truncated := timeout.Truncate(time.Second); truncated != timeout {
            timeout = truncated + time.Second
         }
         // set the timeout
         r.Timeout(timeout)
      }
   }

   if err := r.Do(ctx).Into(response); err != nil {
      return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
   }
   trace.Step("Request completed")

   result, err := webhookrequest.VerifyAdmissionResponse(uid, false, response)
   if err != nil {
      return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
   }

   for k, v := range result.AuditAnnotations {
      key := h.Name + "/" + k
      if err := attr.Attributes.AddAnnotation(key, v); err != nil {
         klog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, v, h.Name, err)
      }
   }
   if result.Allowed {
      return nil
   }
   return &webhookutil.ErrWebhookRejection{Status: webhookerrors.ToStatusErr(h.Name, result.Result)}
}
Войдите в полноэкранный режим Выход из полноэкранного режима

На данный момент мы имеем общее представление о admission webhook и знаем, что это делается apiserver. Вот как на самом деле сделать пользовательский webhook.

Здесь есть еще два понятия, параметр запроса AdmissionRequest и соответствующий параметр AdmissionResponse, которые можно увидеть в callHook, которые определены в k8s.ioapiadmissionv1types.go; эти два параметра мы будем использовать для настройки webhook. Эти два параметра также являются структурой тела, которую мы должны обработать при настройке webhook, а также структурой данных содержимого нашего ответа.

Как написать пользовательский веб-крючок для поступления

Как мы узнали выше, пользовательский webhook - это промежуточный webhook допуска, который служит промежуточным звеном для двух типов контроллеров допуска, которые kubernetes предоставляет пользователям для проверки пользовательских сервисов. Пользователь может сделать это на любом языке. Конечно, если вам необходимо манипулировать ресурсами кластера kubernetes, рекомендуется использовать официальный kubernetes SDK для выполнения пользовательского webhook.

Для создания пользовательского веб-крючка приема необходимо выполнить два шага.

  • Зарегистрируйте соответствующий конфиг webhook в kubernetes, т.е. дайте kubernetes знать о вашем webhook
  • Подготовьте http-сервер для обработки сообщений аутентификации, отправляемых apiserver

Примечание: Здесь вы используете пакет go net/http, который сам по себе не различает методы для обработки HTTP-запросов. Если вы используете другую реализацию фреймворка, например, django, вам нужно указать, что соответствующий метод должен быть POST

Регистрация объектов webhook в kubernetes

Два типа настраиваемых контроллеров доступа, предоставляемых kubernetes, как и другие ресурсы, могут быть использованы для динамической настройки этих ресурсов для обработки веб-крюком adminssion, используя список ресурсов. kubernetes абстрагирует эту форму на два ресурса.

  • ValidatingWebhookConfiguration

  • MutatingWebhookConfiguration

ValidatingAdmission

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: "pod-policy.example.com"
webhooks:
- name: "pod-policy.example.com"
  rules:
  - apiGroups:   [""] # 拦截资源的Group "" 表示 core。"*" 表示所有。
    apiVersions: ["v1"] # 拦截资源的版本
    operations:  ["CREATE"] # 什么请求下拦截
    resources:   ["pods"]  # 拦截什么资源
    scope:       "Namespaced" # 生效的范围,cluster还是namespace "*"表示没有范围限制。
  clientConfig: # 我们部署的webhook服务,
    service: # service是在cluster-in模式下
      namespace: "example-namespace"
      name: "example-service"
      port: 443 # 服务的端口
      path: "/validate" # path是对应用于验证的接口
    # caBundle是提供给 admission webhook CA证书  
    caBundle: "Ci0tLS0tQk...<base64-encoded PEM bundle containing the CA that signed the webhook's serving certificate>...tLS0K"
  admissionReviewVersions: ["v1", "v1beta1"]
  sideEffects: None
  timeoutSeconds: 5 # 1-30s直接,表示请求api的超时时间
Войдите в полноэкранный режим Выход из полноэкранного режима

MutatingAdmission

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: "valipod-policy.example.com"
webhooks:
- name: "valipod-policy.example.com"
  rules:
    - apiGroups:   ["apps"] # 拦截资源的Group "" 表示 core。"*" 表示所有。
      apiVersions: ["v1"] # 拦截资源的版本
      operations:  ["CREATE"] # 什么请求下拦截
      resources:   ["deployments"]  # 拦截什么资源
      scope:       "Namespaced" # 生效的范围,cluster还是namespace "*"表示没有范围限制。
  clientConfig: # 我们部署的webhook服务,
    url: "https://10.0.0.1:81/validate" # 这里是外部模式
    #      service: # service是在cluster-in模式下
    #        namespace: "default"
    #        name: "admission-webhook"
    #        port: 81 # 服务的端口
    #        path: "/mutate" # path是对应用于验证的接口
    # caBundle是提供给 admission webhook CA证书
    caBundle: "Ci0tLS0tQk...<base64-encoded PEM bundle containing the CA that signed the webhook's serving certificate>...tLS0K"
  admissionReviewVersions: ["v1"]
  sideEffects: None
  timeoutSeconds: 5 # 1-30s直接,表示请求api的超时时间
Войдите в полноэкранный режим Выход из полноэкранного режима

Примечание: Для webhook также могут быть внедрены внешние службы, которые не обязательно развертывать внутри кластера.

Для внешних служб, service в clientConfig, замените на url; внешняя служба может быть введена с помощью параметра url.

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
...
webhooks:
- name: my-webhook.example.com
  clientConfig:
    url: "https://my-webhook.example.com:9443/my-webhook-path"
  ...
Войдите в полноэкранный режим Выход из полноэкранного режима

Примечание: Правило url здесь должно соответствовать следующей форме.

  • scheme://host:port/path
  • Если используется url, его не следует заполнять для служб внутри кластера
  • Параметр ?xx=xx также не разрешен при настройке (официально, но я узнал через исходный код, что параметр не нужен, потому что отправляется определенное тело запроса).

Для получения дополнительной информации о конфигурации см. официальный документ kubernetes doc

Подготовка веб-крючка

Давайте напишем наш сервер webhook, который создаст два хука, /mutate и /validate.

  • /validate добавит allow:true к /mutate, затем продолжит, иначе отклонит.

Все это написано здесь для удобства и на самом деле не соответствует дизайну программы. Сервер webhook также предоставляется в кодовой базе kubernetes, поэтому вы можете обратиться к нему, чтобы узнать, что нужно делать

package main

import (
    "context"
    "crypto/tls"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "os/signal"
    "strings"
    "syscall"

    v1admission "k8s.io/api/admission/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/serializer"

    appv1 "k8s.io/api/apps/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/klog"
)

type patch struct {
    Op    string            `json:"op"`
    Path  string            `json:"path"`
    Value map[string]string `json:"value"`
}

func serve(w http.ResponseWriter, r *http.Request) {

    var body []byte
    if data, err := ioutil.ReadAll(r.Body); err == nil {
        body = data
    }
    klog.Infof(fmt.Sprintf("receive request: %v....", string(body)[:130]))
    if len(body) == 0 {
        klog.Error(fmt.Sprintf("admission request body is empty"))
        http.Error(w, fmt.Errorf("admission request body is empty").Error(), http.StatusBadRequest)
        return
    }
    var admission v1admission.AdmissionReview
    codefc := serializer.NewCodecFactory(runtime.NewScheme())
    decoder := codefc.UniversalDeserializer()
    _, _, err := decoder.Decode(body, nil, &admission)

    if err != nil {
        msg := fmt.Sprintf("Request could not be decoded: %v", err)
        klog.Error(msg)
        http.Error(w, msg, http.StatusBadRequest)
        return
    }

    if admission.Request == nil {
        klog.Error(fmt.Sprintf("admission review can't be used: Request field is nil"))
        http.Error(w, fmt.Errorf("admission review can't be used: Request field is nil").Error(), http.StatusBadRequest)
        return
    }

    switch strings.Split(r.RequestURI, "?")[0] {
    case "/mutate":
        req := admission.Request
        var admissionResp v1admission.AdmissionReview
        admissionResp.APIVersion = admission.APIVersion
        admissionResp.Kind = admission.Kind
        klog.Infof("AdmissionReview for Kind=%v, Namespace=%v Name=%v UID=%v Operation=%v",
            req.Kind.Kind, req.Namespace, req.Name, req.UID, req.Operation)
        switch req.Kind.Kind {
        case "Deployment":
            var (
                respstr []byte
                err     error
                deploy  appv1.Deployment
            )
            if err = json.Unmarshal(req.Object.Raw, &deploy); err != nil {
                respStructure := v1admission.AdmissionResponse{Result: &metav1.Status{
                    Message: fmt.Sprintf("could not unmarshal resouces review request: %v", err),
                    Code:    http.StatusInternalServerError,
                }}
                klog.Error(fmt.Sprintf("could not unmarshal resouces review request: %v", err))
                if respstr, err = json.Marshal(respStructure); err != nil {
                    klog.Error(fmt.Errorf("could not unmarshal resouces review response: %v", err))
                    http.Error(w, fmt.Errorf("could not unmarshal resouces review response: %v", err).Error(), http.StatusInternalServerError)
                    return
                }
                http.Error(w, string(respstr), http.StatusBadRequest)
                return
            }

            current_annotations := deploy.GetAnnotations()
            pl := []patch{}
            for k, v := range current_annotations {
                pl = append(pl, patch{
                    Op:   "add",
                    Path: "/metadata/annotations",
                    Value: map[string]string{
                        k: v,
                    },
                })
            }
            pl = append(pl, patch{
                Op:   "add",
                Path: "/metadata/annotations",
                Value: map[string]string{
                    deploy.Name + "/Allow": "true",
                },
            })

            annotationbyte, err := json.Marshal(pl)

            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            respStructure := &v1admission.AdmissionResponse{
                UID:     req.UID,
                Allowed: true,
                Patch:   annotationbyte,
                PatchType: func() *v1admission.PatchType {
                    t := v1admission.PatchTypeJSONPatch
                    return &t
                }(),
                Result: &metav1.Status{
                    Message: fmt.Sprintf("could not unmarshal resouces review request: %v", err),
                    Code:    http.StatusOK,
                },
            }
            admissionResp.Response = respStructure

            klog.Infof("sending response: %s....", admissionResp.Response.String()[:130])
            respByte, err := json.Marshal(admissionResp)
            if err != nil {
                klog.Errorf("Can't encode response messages: %v", err)
                http.Error(w, err.Error(), http.StatusInternalServerError)
            }
            klog.Infof("prepare to write response...")
            w.Header().Set("Content-Type", "application/json")
            if _, err := w.Write(respByte); err != nil {
                klog.Errorf("Can't write response: %v", err)
                http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
            }

        default:
            klog.Error(fmt.Sprintf("unsupport resouces review request type"))
            http.Error(w, "unsupport resouces review request type", http.StatusBadRequest)
        }

    case "/validate":
        req := admission.Request
        var admissionResp v1admission.AdmissionReview
        admissionResp.APIVersion = admission.APIVersion
        admissionResp.Kind = admission.Kind
        klog.Infof("AdmissionReview for Kind=%v, Namespace=%v Name=%v UID=%v Operation=%v",
            req.Kind.Kind, req.Namespace, req.Name, req.UID, req.Operation)
        var (
            deploy  appv1.Deployment
            respstr []byte
        )
        switch req.Kind.Kind {
        case "Deployment":
            if err = json.Unmarshal(req.Object.Raw, &deploy); err != nil {
                respStructure := v1admission.AdmissionResponse{Result: &metav1.Status{
                    Message: fmt.Sprintf("could not unmarshal resouces review request: %v", err),
                    Code:    http.StatusInternalServerError,
                }}
                klog.Error(fmt.Sprintf("could not unmarshal resouces review request: %v", err))
                if respstr, err = json.Marshal(respStructure); err != nil {
                    klog.Error(fmt.Errorf("could not unmarshal resouces review response: %v", err))
                    http.Error(w, fmt.Errorf("could not unmarshal resouces review response: %v", err).Error(), http.StatusInternalServerError)
                    return
                }
                http.Error(w, string(respstr), http.StatusBadRequest)
                return
            }
        }
        al := deploy.GetAnnotations()
        respStructure := v1admission.AdmissionResponse{
            UID: req.UID,
        }
        if al[fmt.Sprintf("%s/Allow", deploy.Name)] == "true" {
            respStructure.Allowed = true
            respStructure.Result = &metav1.Status{
                Code: http.StatusOK,
            }
        } else {
            respStructure.Allowed = false
            respStructure.Result = &metav1.Status{
                Code: http.StatusForbidden,
                Reason: func() metav1.StatusReason {
                    return metav1.StatusReasonForbidden
                }(),
                Message: fmt.Sprintf("the resource %s couldn't to allow entry.", deploy.Kind),
            }
        }

        admissionResp.Response = &respStructure

        klog.Infof("sending response: %s....", admissionResp.Response.String()[:130])
        respByte, err := json.Marshal(admissionResp)
        if err != nil {
            klog.Errorf("Can't encode response messages: %v", err)
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
        klog.Infof("prepare to write response...")
        w.Header().Set("Content-Type", "application/json")
        if _, err := w.Write(respByte); err != nil {
            klog.Errorf("Can't write response: %v", err)
            http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError)
        }
    }
}

func main() {
    var (
        cert, key string
    )

    if cert = os.Getenv("TLS_CERT"); len(cert) == 0 {
        cert = "./tls/tls.crt"
    }

    if key = os.Getenv("TLS_KEY"); len(key) == 0 {
        key = "./tls/tls.key"
    }

    ca, err := tls.LoadX509KeyPair(cert, key)
    if err != nil {
        klog.Error(err.Error())
        return
    }

    server := &http.Server{
        Addr: ":81",
        TLSConfig: &tls.Config{
            Certificates: []tls.Certificate{
                ca,
            },
        },
    }

    httpserver := http.NewServeMux()

    httpserver.HandleFunc("/validate", serve)
    httpserver.HandleFunc("/mutate", serve)
    httpserver.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        klog.Info(fmt.Sprintf("%s %s", r.RequestURI, "pong"))
        fmt.Fprint(w, "pong")
    })
    server.Handler = httpserver

    go func() {
        if err := server.ListenAndServeTLS("", ""); err != nil {
            klog.Errorf("Failed to listen and serve webhook server: %v", err)
        }
    }()

    klog.Info("starting serve.")
    signalChan := make(chan os.Signal, 1)
    signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
    <-signalChan

    klog.Infof("Got shut signal, shutting...")
    if err := server.Shutdown(context.Background()); err != nil {
        klog.Errorf("HTTP server Shutdown: %v", err)
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Соответствующий Dockerfile

FROM golang:alpine AS builder
MAINTAINER cylon
WORKDIR /admission
COPY ./ /admission
ENV GOPROXY https://goproxy.cn,direct
RUN 
    sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && 
    apk add upx  && 
    GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o webhook main.go && 
    upx -1 webhook && 
    chmod +x webhook

FROM alpine AS runner
WORKDIR /go/admission
COPY --from=builder /admission/webhook .
VOLUME ["/admission"]
Войдите в полноэкранный режим Выход из полноэкранного режима

Список ресурсов, необходимых для развертывания в кластере

apiVersion: v1
kind: Service
metadata:
  name: admission-webhook
  labels:
    app: admission-webhook
spec:
  ports:
    - port: 81
      targetPort: 81
  selector:
    app: simple-webhook
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: simple-webhook
  name: simple-webhook
spec:
  replicas: 1
  selector:
    matchLabels:
      app: simple-webhook
  template:
    metadata:
      labels:
        app: simple-webhook
    spec:
      containers:
        - image: cylonchau/simple-webhook:v0.0.2
          imagePullPolicy: IfNotPresent
          name: webhook
          command: ["./webhook"]
          env:
            - name: "TLS_CERT"
              value: "./tls/tls.crt"
            - name: "TLS_KEY"
              value: "./tls/tls.key"
            - name: NS_NAME
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.namespace
          ports:
            - containerPort: 81
          volumeMounts:
            - name: tlsdir
              mountPath: /go/admission/tls
              readOnly: true
      volumes:
        - name: tlsdir
          secret:
            secretName: webhook
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: "pod-policy.example.com"
webhooks:
  - name: "pod-policy.example.com"
    rules:
      - apiGroups:   ["apps"] # 拦截资源的Group "" 表示 core。"*" 表示所有。
        apiVersions: ["v1"] # 拦截资源的版本
        operations:  ["CREATE"] # 什么请求下拦截
        resources:   ["deployments"]  # 拦截什么资源
        scope:       "Namespaced" # 生效的范围,cluster还是namespace "*"表示没有范围限制。
    clientConfig: # 我们部署的webhook服务,
      url: "https://10.0.0.1:81/mutate"
#      service: # service是在cluster-in模式下
#        namespace: "default"
#        name: "admission-webhook"
#        port: 81 # 服务的端口
#        path: "/mutate" # path是对应用于验证的接口
      # caBundle是提供给 admission webhook CA证书
      caBundle: Put you CA (base64 encode) in here
    admissionReviewVersions: ["v1"]
    sideEffects: None
    timeoutSeconds: 5 # 1-30s直接,表示请求api的超时时间
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: "valipod-policy.example.com"
webhooks:
- name: "valipod-policy.example.com"
  rules:
    - apiGroups:   ["apps"] # 拦截资源的Group "" 表示 core。"*" 表示所有。
      apiVersions: ["v1"] # 拦截资源的版本
      operations:  ["CREATE"] # 什么请求下拦截
      resources:   ["deployments"]  # 拦截什么资源
      scope:       "Namespaced" # 生效的范围,cluster还是namespace "*"表示没有范围限制。
  clientConfig: # 我们部署的webhook服务,
    #      service: # service是在cluster-in模式下
    #        namespace: "default"
    #        name: "admission-webhook"
    #        port: 81 # 服务的端口
    #        path: "/mutate" # path是对应用于验证的接口
    # caBundle是提供给 admission webhook CA证书
    caBundle: Put you CA (base64 encode) in here
  admissionReviewVersions: ["v1"]
  sideEffects: None
  timeoutSeconds: 5 # 1-30s直接,表示请求api的超时时间
Войдите в полноэкранный режим Выход из полноэкранного режима

Вот вопросы, которые необходимо решить

Вопросы сертификации

Если вам нужен cluster-in, то вам нужно настроить service для соответствующего ресурса webhookconfig; если вы используете внешнее развертывание, то вам нужно настроить соответствующий адрес доступа, например, "https://xxxx:port/method".

Сертификат для обоих методов требует соответствующего subjectAltName, а режим cluster-in требует соответствующего имени службы, например, хотя бы одного доменного имени, содержащего serviceName.NS.svc.

Ниже приведена ошибка, связанная с проблемой типа сертификата

Failed calling webhook, failing closed pod-policy.example.com: failed calling webhook "pod-policy.example.com": Post https://admission-webhook.default.svc:81/mutate?timeout=5s: x509: certificate signed by unknown authority (possibly because of "crypto/rsa: verification error" while trying to verify candidate authority certificate "admission-webhook-ca")
Войдите в полноэкранный режим Выход из полноэкранного режима

Соответствующая информационная проблема

APIServer, как мы видели выше, посылает v1admission.AdmissionReview, который является типом запроса и ответа, поэтому, чтобы было понятнее, в чем проблема, необходимо изменить Reason и Message в формате ответа. code> и Message, которое является сообщением об ошибке, которое мы видим на стороне клиента.

&metav1.Status{
    Code: http.StatusForbidden,
    Reason: func() metav1.StatusReason {
        return metav1.StatusReasonForbidden
    }(),
    Message: fmt.Sprintf("the resource %s couldn't to allow entry.", deploy.Kind),
}
Войдите в полноэкранный режим Выход из полноэкранного режима

При указанных выше настройках пользователь увидит следующие ошибки

$ kubectl apply -f nginx.yaml 
Error from server (Forbidden): error when creating "nginx.yaml": admission webhook "valipod-policy.example.com" denied the request: the resource Deployment couldn't to allow entry.
Войдите в полноэкранный режим Выход из полноэкранного режима

Примечание: Необходимые параметры также включены, UID, разрешенные, оба из которых являются обязательными, вышеприведенное является лишь удобным для пользователя сообщением.

Следующая ошибка является ошибкой при установке соответствующего формата

Error from server (InternalError): error when creating "nginx.yaml": Internal error occurred: failed calling webhook "pod-policy.example.com": the server rejected our request for an unknown reason
Войдите в полноэкранный режим Выход из полноэкранного режима

Соответствующие проблемы с версией сообщения

В соответствующем сообщении также необходимо указать версию, которая может быть взята из структуры запроса

admissionResp.APIVersion = admission.APIVersion
admissionResp.Kind = admission.Kind
Войдите в полноэкранный режим Выход из полноэкранного режима

Следующая ошибка возникает, если значение KV не настроено для соответствующего сообщения

Error from server (InternalError): error when creating "nginx.yaml": Internal error occurred: failed calling webhook "pod-policy.example.com": expected webhook response of admission.k8s.io/v1, Kind=AdmissionReview, got /, Kind=
Войдите в полноэкранный режим Выход из полноэкранного режима

О заплате

Патчи в kubernetes используют определенную спецификацию, например jsonpatch.

Единственный patchType, который в настоящее время поддерживается kubernetes, это JSONPatch. Для более подробной информации смотрите раздел JSON patch

Чтобы jsonpatch был фиксированным типом, его структура должна быть определена в go

{
  "op": "add", // 做什么操作
  "path": "/spec/replicas", // 操作的路径
  "value": 3 // 对应添加的key value
}

Ниже приведена ошибка, возникающая при установке строкового типа в boolean

Error from server (InternalError): error when creating "nginx.yaml": Internal error occurred: v1.Deployment.ObjectMeta: v1.ObjectMeta.Annotations: ReadString: expects " or n, but found t, error found in #10 byte of ...|t/Allow":true},"crea|..., bigger context ...|tadata":{"annotations":{"nginx-deployment/Allow":true},"creationTimestamp":null,"managedFields":[{"m|..
Войдите в полноэкранный режим Выход из полноэкранного режима

Подготовка сертификата

Ubuntu

touch ./demoCAindex.txt
touch ./demoCA/serial 
touch ./demoCA/crlnumber
echo 01 > ./demoCA/serial
mkdir ./demoCA/newcerts

openssl genrsa -out cakey.pem 2048

openssl req -new 
    -x509 
    -key cakey.pem 
    -out cacert.pem 
    -days 3650 
    -subj "/CN=admission webhook ca"

openssl genrsa -out tls.key 2048

openssl req -new 
    -key tls.key 
    -subj "/CN=admission webhook client" 
    -reqexts webhook 
    -config <(cat /etc/ssl/openssl.cnf 
    <(printf "[webhook]nsubjectAltName=DNS: admission-webhook, DNS: admission-webhook.default.svc, DNS: admission-webhook.default.svc.cluster.local, IP:10.0.0.1,  IP:10.0.0.4")) 
    -out tls.csr

sed -i 's/= match/= optional/g' /etc/ssl/openssl.cnf

openssl ca 
    -in tls.csr 
    -cert cacert.pem 
    -keyfile cakey.pem 
    -out tls.crt 
    -days 300 
    -extensions webhook 
    -extfile <(cat /etc/ssl/openssl.cnf 
    <(printf "[webhook]nsubjectAltName=DNS: admission-webhook, DNS: admission-webhook.default.svc, DNS: admission-webhook.default.svc.cluster.local, IP:10.0.0.1,  IP:10.0.0.4"))
Войдите в полноэкранный режим Выход из полноэкранного режима

CentOS

touch /etc/pki/CA/index.txt
touch /etc/pki/CA/serial # 下一个要颁发的编号 16进制
touch /etc/pki/CA/crlnumber
echo 01 > /etc/pki/CA/serial

openssl req -new 
    -x509 
    -key cakey.pem 
    -out cacert.pem 
    -days 3650 
    -subj "/CN=admission webhook ca"

openssl genrsa -out tls.key 2048

openssl req -new 
    -key tls.key 
    -subj "/CN=admission webhook client" 
    -reqexts webhook 
    -config <(cat /etc/pki/tls/openssl.cnf 
    <(printf "[webhook]nsubjectAltName=DNS: admission-webhook, DNS: admission-webhook.default.svc, DNS: admission-webhook.default.svc.cluster.local, IP:10.0.0.1,  IP:10.0.0.4")) 
    -out tls.csr

sed -i 's/= match/= optional/g' /etc/ssl/openssl.cnf

openssl ca 
    -in tls.csr 
    -cert cacert.pem 
    -keyfile cakey.pem 
    -out tls.crt 
    -days 300 
    -extensions webhook 
    -extfile <(cat /etc/pki/tls/openssl.cnf 
    <(printf "[webhook]nsubjectAltName=DNS: admission-webhook, DNS: admission-webhook.default.svc, DNS: admission-webhook.default.svc.cluster.local, IP:10.0.0.1,  IP:10.0.0.4"))
Войдите в полноэкранный режим Выход из полноэкранного режима

Проверка результатов путем развертывания

Вы можете видеть, что мы ввели нашу собственную аннотацию nginx-deployment/Allow: true, в данном примере это демонстрационный процесс, а не реальная политика, вы можете настроить свою собственную политику в реальной среде.

Как вы видите, когда mutating не проходит, т.е. тег аннотации отсутствует, то validating не разрешит доступ

$ kubectl describe deploy nginx-deployment
Name:                   nginx-deployment
Namespace:              default
CreationTimestamp:      Mon, 11 Jul 2022 20:25:16 +0800
Labels:                 <none>
Annotations:            deployment.kubernetes.io/revision: 1
                        nginx-deployment/Allow: true
Selector:               app=nginx
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:  app=nginx
  Containers:
   nginx:
    Image:        nginx:1.14.2
Войдите в полноэкранный режим Выход из полноэкранного режима

Ссылка

расширяемые контроллеры допуска

Пример патча K8S client-go Patch

ответ контроллеров допуска

руководство по контроллерам допуска kubernetes

Оцените статью
devanswers.ru
Добавить комментарий