Автор: Мартон Натко, Інженер з інфраструктури, Adyen
З переходом до контейнеризованих навантажень в Adyen ми значно збільшили швидкість виконання задач. Ми також прийняли одну з наших основних формул — запускати швидко та ітераційно — оскільки контейнери надають оптимальний спосіб для швидких циклів випуску. Однак ми розуміємо, що швидкість не повинна ставати на заваді дотриманню регуляцій.
Ми обрали Kubernetes як оркестратор контейнерів для багатьох наших проектів з нуля і, щоб забезпечити належну інженерну автономію, надаємо нашим інженерам доступ до прямої взаємодії з ним. Автономія особливо корисна для налагодження, оскільки інженерні команди володіють власними застосунками.
Вступ до kubectl exec
Якщо ви знайомі з екосистемою Kubernetes, то команда kubectl exec
спадає на думку, коли потрібно налагоджувати працюючий застосунок в Kubernetes.
Якщо ви не знайомі з нею, то коротке пояснення: ця команда дозволяє користувачеві виконувати команди всередині контейнера, що працює в Kubernetes, або навіть, можна отримати TTY
(teletype) в цьому контейнері.
У нашій галузі важливо мати аудиторські журнали таких подій. Проблема в тому, що не так багато рішень пропонують цю функцію.
Зануримося глибше
Щоб вирішити цю проблему, давайте детальніше розглянемо потік даних. Ми охопимо лише частину між користувачем та apiserver
, яка є найзручнішою для розробки рішення.
Коли користувач виконує команду kubectl exec
, ми можемо легко перехопити параметр команди з запиту. Але припустімо, що користувач також запитує TTY
і передає свій STDIN
до контейнера, він отримає TTY
всередині контейнера, і будь-яка команда, яка буде виконана, пройде через оновлений протокол. Для версій Kubernetes нижче v1.30 це був SPDY, а у v1.30 Websocket став стандартом. На цьому етапі все ускладнюється, оскільки аудиторський запис того, що виконується всередині контейнера через ці сесії, стає менш зручним для спостереження.
Наше рішення — kubectl-rexec
Ми вирішили зберегти всі можливості, які надає kubectl exec
, при розгляді можливих рішень. Тому ми сформулювали наступну задачу: що, якщо ми будемо мати ту саму команду як плагін для kubectl, але викликатимемо інший API-ендпоінт, який потрапляє до компонента, яким ми керуємо?
Kubernetes має ресурс APIService, який дозволяє нам розширювати сервер API Kubernetes новими ендпоінтами.
Ми впровадили APIService, що показаний у коді нижче. Він дозволяє запити з шляхом /apis/audit.adyen.internal/v1beta1/…
перенаправляти до служби під назвою rexec
в просторі імен kube-system
.
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1beta1.audit.adyen.internal
spec:
group: audit.adyen.internal
groupPriorityMinimum: 100
caBundle: caCertAsBase64…
service:
name: rexec
namespace: kube-system
port: 8443
version: v1beta1
versionPriority: 100
Ми створили плагін kubectl, який реалізує все те, що робить exec
, за винятком одного: замість виклику api/v1/namespaces/{{ namespace }}/pods/{{ pod }}/exec
, він викликає apis/audit.adyen.internal/v1beta1/namespaces/{{ namespace }}/pods/{{ pod }}/exec
. Цей запит потрапляє — через APIService — до компонента, яким ми керуємо; давайте назвемо його "proxy rexec".
В rexec
проксі ми робимо дві речі: по-перше, переписуємо шлях на рідний шлях exec, а потім проксіруємо його назад до API сервера Kubernetes. Ми повинні зазначити, що це проксирування відбувається через імперсонацію, оскільки на цьому рівні ми вже не маємо оригінальних даних користувача для автентифікації; ми маємо лише користувача та групи, які можемо передати через імперсонацію.
Проксирування може відбуватись двома способами:
1.
1. Коли користувач не запитує TTY
, ми реєструємо параметри команди з запиту як аудиторську подію та проксируємо запит безпосередньо до API сервера Kubernetes.
2. Коли запитується TTY
, ситуація ускладнюється. У цьому випадку проксі не перенаправляє запит безпосередньо до API сервера Kubernetes. Замість цього він проксирує запит на себе через TCP
слухач на Unix-сокеті, який запускається для кожного запиту exec
. Кожен з цих TCP
слухачів діє як TCP
проксі, проксируючи запити до API сервера Kubernetes. Маючи TCP
проксі, ми можемо досліджувати сирий трафік TCP
, аналізувати фрейми Websocket і захоплювати кожен натискання клавіші, яке термінал користувача відправляє до контейнера в TTY
. Додатково, наявність TCP
сесії для кожного користувача — це простий спосіб відслідковувати ідентичність користувача та сесію, залишаючись на рівні TCP
.
Тепер, коли ми вирішили половину проблеми — ми маємо аудиторське логування — нам все ще потрібно забезпечити, щоб користувачі не використовували рідну команду. Для цього ми додали ValidatingWebhookConfiguration
.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: deny-pod-exec
webhooks:
- name: deny-pod-exec.k8s.io
clientConfig:
service:
name: rexec
namespace: kube-system
port: 8443
path: /validate-exec
caBundle: caCertAsBase64...
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CONNECT"]
resources: ["pods/exec"]
admissionReviewVersions: ["v1"]
sideEffects: None
failurePolicy: Fail
Ця конфігурація робить одну річ: кожного разу, коли API сервер Kubernetes отримує запит, націлений на api/v1/namespaces/{{ namespace }}/pods/{{ pod }}/exec
, API викликає проксі rexec
з шляхом /validate-exec
.
Тепер це означає, що ми також маємо спосіб контролювати запити до рідної команди exec; нам потрібно лише знайти спосіб відмовити всьому, що не проходить через сам проксі rexec
. Для цього, на запитах exec, що проходять через наш проксі, ми додаємо заголовок з значенням, яке діє як спільний ключ між двома ендпоінтами, які ми надаємо. Якщо заголовок присутній і значення однакове, ми можемо дозволити запит рухатись далі.
Висновок
З цим мінімалістичним застосунком ми можемо легко аудіювати команди exec
, і нам потрібно лише встановити кілька манифестів на стороні Kubernetes, при цьому розповсюджуючи наш плагін серед наших інженерів.
Якщо вам цікаво спробувати, ознайомтесь з ним на Github: https://github.com/adyen/kubectl-rexec.
Перекладено з: Kubectl-r[exe]c: A kubectl plugin for auditing kubectl exec commands