Перейти до основного контенту

Admission Webhook for K8s

1. Огляд та документація для Casbin K8s-Gatekeeper

Casbin K8s-GateKeeper - це вебхук для прийому в Kubernetes, який інтегрує Casbin як інструмент контролю доступу. Використовуючи Casbin K8s-GateKeeper, ви можете створювати гнучкі правила для авторизації або перехоплення будь-якої операції з ресурсами K8s, НЕ пишучи жодного рядка коду, а лише кілька рядків декларативних конфігурацій моделей та політик Casbin, які є частиною мови ACL (список контролю доступу) Casbin.

Casbin K8s-GateKeeper розроблений та підтримується спільнотою Casbin. Репозиторій цього проекту доступний тут: https://github.com/casbin/k8s-gatekeeper

0.1 Простий приклад

Наприклад, вам не потрібно писати жодного коду, а використовувати наступні рядки конфігурації для досягнення цієї функції: "Заборонити використання зображень з деякими вказаними тегами в будь-яких розгортаннях":

Model:

[request_definition]
r = obj

[policy_definition]
p = obj,eft

[policy_effect]
e = !some(where (p.eft == deny))

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
contain(split(accessWithWildcard(${OBJECT}.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj)

And Policy:

p, "1.14.1",deny

Це звичайна мова ACL Casbin. Припустимо, ви вже прочитали розділи про них, тоді буде дуже легко зрозуміти.

Casbin K8s-Gatekeeper має наступні переваги:

  • Легко використовувати. Написання кількох рядків ACL набагато краще, ніж написання багатьох рядків коду.
  • Дозволяє гаряче оновлення конфігурацій. Вам не потрібно вимикати весь плагін, щоб змінити конфігурації.
  • Це гнучко. Можна створити довільні правила на будь-який ресурс K8s, який можна дослідити за допомогою kubectl gatekeeper.
  • Це спрощує реалізацію вебхука прийому K8s, який є дуже складним. Вам не потрібно знати, що таке вебхук прийому K8s або як писати код для нього. Все, що вам потрібно зробити, це знати ресурс, на якому ви хочете встановити обмеження, а потім написати Casbin ACL. Всі знають, що K8s складний, але використовуючи Casbin K8s-Gatekeeper, ви можете заощадити час.
  • Це підтримується спільнотою Casbin. Не соромтеся звертатися до нас, якщо щось у цьому плагіні вас бентежить або якщо ви зіткнулися з будь-якими проблемами під час спроби цього.

1.1 Як працює Casbin K8s-Gatekeeper?

K8s-Gatekeeper - це вебхук для прийому в K8s, який використовує Casbin для застосування довільних правил контролю доступу, визначених користувачем, щоб допомогти запобігти будь-якій операції в K8s, яку адміністратор не хоче.

Casbin - це потужна та ефективна бібліотека контролю доступу з відкритим вихідним кодом. Вона надає підтримку для забезпечення авторизації на основі різних моделей контролю доступу. Для отримання додаткової інформації про Casbin, дивіться Огляд.

Вебхуки прийому в K8s - це HTTP-зворотні виклики, які отримують 'запити на прийом' і роблять з ними щось. Зокрема, K8s-Gatekeeper - це спеціальний тип вебхука прийому: 'ValidatingAdmissionWebhook', який може вирішити, чи приймати цей запит на прийом, чи ні. Що стосується запитів на прийом, то це HTTP-запити, які описують операцію над вказаними ресурсами K8s (наприклад, створення/видалення розгортання). Для отримання додаткової інформації про вебхуки прийому, дивіться офіційну документацію K8s.

1.2 Приклад, що ілюструє, як це працює

Наприклад, коли хтось хоче створити розгортання, що містить под з запущеним nginx (використовуючи kubectl або клієнти K8s), K8s згенерує запит на прийом, який (якщо перекласти у формат YAML) може бути таким:

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 1
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.1
ports:
- containerPort: 80

Цей запит пройде через процес всього проміжного програмного забезпечення, показаного на малюнку, включаючи наш K8s-Gatekeeper. K8s-Gatekeeper може виявити всі Casbin примусові пристрої, збережені в etcd K8s, які створені та підтримуються користувачем (через kubectl або Go клієнт, який ми надаємо). Кожен примусовий пристрій містить модель Casbin та політику Casbin. Запит на прийом буде оброблений кожним примусовим пристроєм, по черзі, і лише пройшовши всі примусові пристрої, запит може бути прийнятий цим K8s-Gatekeeper.

(Якщо ви не розумієте, що таке примусовий пристрій Casbin, модель або політика, дивіться цей документ: Початок роботи).

Наприклад, з якоїсь причини адміністратор хоче заборонити використання зображення 'nginx:1.14.1', дозволяючи при цьому 'nginx:1.3.1'. Можна створити примусовий пристрій, що містить наступне правило та політику (Ми пояснимо, як створити примусовий пристрій, що таке ці моделі та політики, і як їх писати в наступних розділах).

Model:

[request_definition]
r = obj

[policy_definition]
p = obj,eft

[policy_effect]
e = !some(where (p.eft == deny))

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") == p.obj

Policy:

p, "nginx:1.13.1",allow
p, "nginx:1.14.1",deny

Створивши примусовий пристрій, що містить вищезазначену модель та політику, попередній запит на прийом буде відхилений цим примусовим пристроєм, що означає, що K8s не створить це розгортання.

2 Встановлення K8s-gatekeeper

Існує три методи встановлення K8s-gatekeeper: зовнішній вебхук, внутрішній вебхук та Helm.

примітка

Примітка: Ці методи призначені лише для користувачів, які хочуть спробувати K8s-gatekeeper, і не є безпечними. Якщо ви хочете використовувати його в продуктивному середовищі, будь ласка, переконайтеся, що ви прочитали Розділ 5. Розширені налаштування і зробили необхідні зміни перед встановленням. 2.1 Внутрішній вебхук

2.1.1 Крок 1: Побудова зображення

2.1.1 Step 1: Build the image

Для внутрішнього методу webhook сам webhook буде реалізований як сервіс у Kubernetes. Щоб створити необхідний сервіс та розгортання, вам потрібно побудувати образ K8s-gatekeeper. Ви можете побудувати власний образ, виконавши наступну команду:

docker build --target webhook -t k8s-gatekeeper .

Ця команда створить локальний образ під назвою 'k8s-gatekeeper:latest'.

примітка

Примітка: Якщо ви використовуєте minikube, будь ласка, виконайте eval $(minikube -p minikube docker-env) перед запуском 'docker build'.

2.1.2 Крок 2: Налаштування сервісів та розгортань для K8s-gatekeeper

Виконайте наступні команди:

kubectl apply -f config/rbac.yaml
kubectl apply -f config/webhook_deployment.yaml
kubectl apply -f config/webhook_internal.yaml

Це запустить K8s-gatekeeper, і ви можете перевірити це, виконавши kubectl get pods.

2.1.3 Крок 3: Встановлення CRD ресурсів для K8s-gatekeeper

Виконайте наступні команди:

kubectl apply -f config/auth.casbin.org_casbinmodels.yaml 
kubectl apply -f config/auth.casbin.org_casbinpolicies.yaml

2.2 Зовнішній webhook

Для методу зовнішнього webhook K8s-gatekeeper буде працювати поза Kubernetes, і Kubernetes буде доступатися до K8s-gatekeeper, як до звичайного веб-сайту. Kubernetes має обов'язкову вимогу, щоб admission webhook був HTTPS. Для тестування K8s-gatekeeper ми надали набір сертифікатів та приватний ключ (хоча це не є безпечним). Якщо ви віддаєте перевагу використанню власного сертифікату, будь ласка, зверніться до Розділу 5. Розширені налаштування для інструкцій щодо налаштування сертифікату та приватного ключа. Сертифікат, який ми надаємо, виданий для 'webhook.domain.local'.

Отже, змініть хост (наприклад, /etc/hosts) та спрямуйте 'webhook.domain.local' на IP-адресу, на якій працює K8s-gatekeeper. Потім виконайте наступну команду:

2.3 Встановлення K8s-gatekeeper через Helm

go mod tidy
go mod vendor
go run cmd/webhook/main.go
kubectl apply -f config/auth.casbin.org_casbinmodels.yaml
kubectl apply -f config/auth.casbin.org_casbinpolicies.yaml
kubectl apply -f config/webhook_external.yaml

2.3.1 Крок 1: Побудова образу

Будь ласка, зверніться до Розділу 2.1.1.

2.3.2 Встановлення через Helm

Виконайте команду helm install k8sgatekeeper ./k8sgatekeeper.

Спробуйте K8s-gatekeeper 3.1 Створення моделі та політики Casbin

У вас є два способи створення моделі та політики: через kubectl або через go-клієнт, який ми надаємо.

3.1.1 Створення/Оновлення моделі та політики Casbin через kubectl

У K8s-gatekeeper модель Casbin зберігається у ресурсі CRD під назвою 'CasbinModel'.

Її визначення розташоване в config/auth.casbin.org_casbinmodels.yaml. Приклади є в example/allowed_repo/model.yaml.

Зверніть увагу на наступні поля: metadata.name: назва моделі.

  • Ця назва ПОВИННА бути такою ж, як назва об'єкта CasbinPolicy, пов'язаного з цією моделлю, щоб K8s-gatekeeper міг їх спарувати та створити виконавця. spec.enable: якщо це поле встановлено в 'false', ця модель (а також об'єкт CasbinPolicy, пов'язаний з цією моделлю) буде ігноруватися.
  • spec.modelText: рядок, який містить текст моделі Casbin.
  • Політика Casbin зберігається в іншому ресурсі CRD під назвою 'CasbinPolicy', визначення якого можна знайти в config/auth.casbin.org_casbinpolicies.yaml.

Приклади є в example/allowed_repo/policy.yaml.

Зверніть увагу на наступні поля: metadata.name: назва політики.

  • Ця назва ПОВИННА бути такою ж, як назва об'єкта CasbinModel, пов'язаного з цією політикою, щоб K8s-gatekeeper міг їх спарувати та створити виконавця. spec.policyItem: рядок, який містить текст політики моделі Casbin.
  • Після створення власних файлів CasbinModel та CasbinPolicy використовуйте наступну команду для їх застосування:

Як тільки пара CasbinModel та CasbinPolicy буде створена, K8s-gatekeeper зможе її виявити протягом 5 секунд.

kubectl apply -f <filename>

3.1.2 Створення/Оновлення моделі та політики Casbin через go-клієнт, який ми надаємо

Ми розуміємо, що можуть бути ситуації, коли не зручно використовувати оболонку для виконання команд безпосередньо на вузлі кластера K8s, наприклад, коли ви будуєте автоматичну хмарну платформу для вашої корпорації.

Тому ми розробили go-клієнт для створення та підтримки CasbinModel та CasbinPolicy. Бібліотека go-клієнта розташована в pkg/client.

У client.go ми надаємо функцію для створення клієнта.

In client.go, we provide a function to create a client.

func NewK8sGateKeeperClient(externalClient bool) (*K8sGateKeeperClient, error) 

The externalClient parameter determines whether K8s-gatekeeper is running inside the K8s cluster or not.

In model.go, we provide various functions to create, delete, and modify CasbinModel. You can find out how to use these interfaces in model_test.go.

In policy.go, we provide various functions to create, delete, and modify CasbiPolicy. You can find out how to use these interfaces in policy_test.go.

3.1.2 Try Whether K8s-gatekeeper Works

Suppose you have already created the exact model and policy in example/allowed_repo. Now, try the following command:

kubectl apply -f example/allowed_repo/testcase/reject_1.yaml

You should find that K8s will reject this request and mention that the webhook was the reason why this request is rejected. However, when you try to apply example/allowed_repo/testcase/approve_2.yaml, it will be accepted.

4. How to Write Model and Policy with K8s-gatekeeper

First of all, make sure you are familiar with the basic grammar of Casbin Models and Policies. If you are not, please read the Get Started section first. In this chapter, we assume that you already understand what Casbin Models and Policies are.

4.1 Request Definition of Model

When K8s-gatekeeper is authorizing a request, the input is always an object: the Go object of the Admission Request. This means that the enforcer will always be used like this:

ok, err := enforcer.Enforce(admission)

where admission is an AdmissionReview object defined by K8s's official go api "k8s.io/api/admission/v1". You can find the definition of this struct in this repository: https://github.com/kubernetes/api/blob/master/admission/v1/types.go. For more information, you can also refer to https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-request-and-response.

Therefore, for any model used by K8s-gatekeeper, the definition of the request_definition should always be like this:

    [request_definition]
r = obj

The name 'obj' is not mandatory, as long as the name is consistent with the name used in the [matchers] part.

4.2 Matchers of Model

You are supposed to use the ABAC feature of Casbin to write your rules. However, the expression evaluator integrated in Casbin does not support indexing in maps or arrays(slices), nor the expansion of arrays. Therefore, K8s-gatekeeper provides various 'Casbin functions' as extensions to implement these features. If you still find that your demand cannot be fulfilled by these extensions, feel free to start an issue, or create a pull request.

If you are not familiar with Casbin functions, you can refer to Function for more information.

Here are the extension functions:

4.2.1 Extension functions

4.2.1.1 access

Access is used to solve the problem that Casbin does not support indexing in maps or arrays. The example example/allowed_repo/model.yaml demonstrates the usage of this function:

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") == p.obj

In this matcher, access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Image") is equivalent to r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Image, where r.obj.Request.Object.Object.Spec.Template.Spec.Containers is a slice.

Access can also call simple functions that have no parameters and return a single value. The example example/container_resource_limit/model.yaml demonstrates this:

[matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
parseFloat(access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value")) >= parseFloat(p.cpu) && \
parseFloat(access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","memory","Value")) >= parseFloat(p.memory)

In this matcher, access(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , 0, "Resources","Limits","cpu","Value") is equivalent to r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits["cpu"].Value(), where r.obj.Request.Object.Object.Spec.Template.Spec.Containers[0].Resources.Limits is a map, and Value() is a simple function that has no parameters and returns a single value.

4.2.1.2 accessWithWildcard

Sometimes, you may have a demand like this: all elements in an array must have a prefix "aaa". However, Casbin does not support for loops. With accessWithWildcard and the "map/slice expansion" feature, you can easily implement such a demand.

For example, suppose a.b.c is an array [aaa,bbb,ccc,ddd,eee], then the result of accessWithWildcard(a,"b","c","*") will be a slice [aaa,bbb,ccc,ddd,eee]. By using the wildcard *, the slice is expanded.

Similarly, the wildcard can be used more than once. For example, the result of accessWithWildcard(a,"b","c","*","*") will be [a.b.c[0][0], a.b.c[0][1], ..., a.b.c[1][0], a.b.c[1][1], ...].

4.2.1.3 Functions Supporting Variable-length Arguments

In the expression evaluator of Casbin, when a parameter is an array, it will be automatically expanded as a variable-length argument. Використовуючи цю функцію для підтримки розширення масиву/зрізу/мапи, ми також інтегрували кілька функцій, які приймають масив/зріз як параметр:

  • contain(): приймає кілька параметрів і повертає, чи будь-який параметр (крім останнього) дорівнює останньому параметру.
  • split(a,b,c...,sep,index): повертає фрагмент, який містить [splits(a,sep)[index], splits(b,sep)[index], splits(a,sep)[index], ...].
  • len(): повертає довжину аргументу змінної довжини.
  • matchRegex(a,b,c...,regex): повертає, чи всі дані параметри (a, b, c, ...) відповідають заданому regex. відповідають заданому regex.

Ось приклад у example/disallowed_tag/model.yaml:

    [matchers]
m = r.obj.Request.Namespace == "default" && r.obj.Request.Resource.Resource =="deployments" && \
contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj)

Припускаючи, що accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image") повертає ['a:b', 'c:d', 'e:f', 'g:h'], оскільки splits підтримує аргументи змінної довжини та виконує операцію розбиття для кожного елемента, елемент з індексом 1 буде вибраний та повернений. Тому, split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) повертає ['b','d','f','h']. І contain(split(accessWithWildcard(r.obj.Request.Object.Object.Spec.Template.Spec.Containers , "*", "Image"),":",1) , p.obj) повертає, чи p.obj міститься в ['b','d','f','h'].

4.2.1.2 Функції конвертації типів

  • ParseFloat(): Перетворює ціле число в число з плаваючою комою (це необхідно, оскільки будь-яке число, що використовується у порівнянні, має бути перетворене у число з плаваючою комою).
  • ToString(): Перетворює об'єкт у рядок. Цей об'єкт має мати базовий тип рядка (наприклад, об'єкт типу XXX, коли є висловлювання type XXX string).
  • IsNil(): Повертає, чи параметр є nil.

5. Розширені налаштування

5.1 Про сертифікати

У Kubernetes (k8s) обов'язково, щоб webhook використовував HTTPS. Існує два підходи для досягнення цього:

  • Використання самопідписаних сертифікатів (приклади у цьому репозиторії використовують цей метод)
  • Використання звичайного сертифіката

5.1.1 Самопідписані сертифікати

Використання самопідписаного сертифіката означає, що Центр сертифікації (CA), який видає сертифікат, не є одним з відомих CA. Тому, ви повинні повідомити k8s про цей CA.

Наразі, приклад у цьому репозиторії використовує самостійно створений CA, приватний ключ та сертифікат якого зберігаються у config/certificate/ca.crt та config/certificate/ca.key відповідно. Сертифікат для webhook є config/certificate/server.crt, який випущений самостійно створеним CA. Домени цього сертифіката - "webhook.domain.local" (для зовнішнього webhook) та "casbin-webhook-svc.default.svc" (для внутрішнього webhook).

Інформація про CA передається до k8s через конфігураційні файли webhook. Обидва config/webhook_external.yaml та config/webhook_internal.yaml мають поле під назвою "CABundle", яке містить base64 закодований рядок сертифіката CA.

У випадку, якщо вам потрібно змінити сертифікат/домен (наприклад, якщо ви хочете помістити цей webhook в інший простір імен k8s, використовуючи внутрішній webhook, або якщо ви хочете змінити домен, використовуючи зовнішній webhook), слід дотримуватися наступних процедур:

  1. Створіть новий CA:

    • Створіть приватний ключ для фейкового CA:

      openssl genrsa -des3 -out ca.key 2048
    • Видаліть захист паролем приватного ключа:

      openssl rsa -in ca.key -out ca.key
  2. Створіть приватний ключ для сервера webhook:

    openssl genrsa -des3 -out server.key 2048
    openssl rsa -in server.key -out server.key
  3. Використовуйте самостійно створений CA для підписання сертифіката для webhook:

    • Скопіюйте конфігураційний файл openssl вашої системи для тимчасового використання. Ви можете дізнатися місцезнаходження конфігураційного файлу, запустивши openssl version -a, зазвичай називається openssl.cnf.

    • У конфігураційному файлі:

      • Знайдіть параграф [req] та додайте наступний рядок: req_extensions = v3_req

      • Знайдіть параграф [v3_req] та додайте наступний рядок: subjectAltName = @alt_names

      • Додайте наступні рядки до файлу:

        [alt_names]
        DNS.2=<The domain you want>

        Примітка: Замініть 'casbin-webhook-svc.default.svc' на справжнє ім'я сервісу вашого сервісу, якщо ви вирішите змінити ім'я сервісу.

    • Використовуйте змінений конфігураційний файл для генерації файлу запиту сертифіката:

      openssl req -new -nodes -keyout server.key -out server.csr -config openssl.cnf
    • Використовуйте самостійно створений CA для відповіді на запит та підписання сертифіката:

      openssl x509 -req -days 3650 -in server.csr -out server.crt -CA ca.crt -CAkey ca.key -CAcreateserial -extensions v3_req -extensions SAN -extfile openssl.cnf
  4. Замініть поле 'CABundle': Обидва config/webhook_external.yaml та config/webhook_internal.yaml мають поле під назвою "CABundle", яке містить base64 закодований рядок сертифіката CA.

  5. Оновіть це поле новим сертифікатом.

Якщо ви використовуєте helm, подібні зміни потрібно застосувати до helm charts.

5.1.2 Легальні сертифікати Якщо ви використовуєте легальні сертифікати, вам не потрібно проходити всі ці процедури.