# Kubernetes Basics ## Kubernetes Basics {{#include ../../banners/hacktricks-training.md}} **Оригінальний автор цієї сторінки** [**Хорхе**](https://www.linkedin.com/in/jorge-belmonte-a924b616b/) **(читайте його оригінальну публікацію** [**тут**](https://sickrov.github.io)**)** ## Архітектура та основи ### Що робить Kubernetes? - Дозволяє запускати контейнери в контейнерному двигуні. - Планування дозволяє ефективно виконувати місії контейнерів. - Підтримує життєздатність контейнерів. - Дозволяє комунікацію між контейнерами. - Дозволяє техніки розгортання. - Обробляє обсяги інформації. ### Архітектура ![](https://sickrov.github.io/media/Screenshot-68.jpg) - **Вузол**: операційна система з подом або подами. - **Под**: обгортка навколо контейнера або кількох контейнерів. Под повинен містити лише один додаток (тому зазвичай под запускає лише 1 контейнер). Под є способом, яким Kubernetes абстрагує технологію контейнерів. - **Сервіс**: Кожен под має 1 внутрішню **IP-адресу** з внутрішнього діапазону вузла. Однак його також можна відкрити через сервіс. **Сервіс також має IP-адресу** і його мета - підтримувати зв'язок між подами, тому якщо один з них зламається, **новий замінник** (з іншою внутрішньою IP) **буде доступний** через **ту ж IP-адресу сервісу**. Його можна налаштувати як внутрішній або зовнішній. Сервіс також діє як **балансувальник навантаження, коли 2 поди підключені** до одного сервісу.\ Коли **сервіс** створено, ви можете знайти кінцеві точки кожного сервісу, запустивши `kubectl get endpoints` - **Kubelet**: Основний агент вузла. Компонент, який встановлює зв'язок між вузлом і kubectl, і може запускати лише поди (через API-сервер). Kubelet не керує контейнерами, які не були створені Kubernetes. - **Kube-proxy**: це сервіс, відповідальний за комунікацію (сервіси) між apiserver і вузлом. Основою є IPtables для вузлів. Найбільш досвідчені користувачі можуть встановлювати інші kube-proxies від інших постачальників. - **Контейнер Sidecar**: Контейнери Sidecar - це контейнери, які повинні працювати разом з основним контейнером у поді. Цей шаблон Sidecar розширює та покращує функціональність поточних контейнерів без їх зміни. Сьогодні ми знаємо, що використовуємо технологію контейнерів, щоб обернути всі залежності для запуску додатка в будь-якому місці. Контейнер виконує лише одну задачу і робить це дуже добре. - **Головний процес:** - **Api Server:** Це спосіб, яким користувачі та поди спілкуються з головним процесом. Дозволяються лише автентифіковані запити. - **Планувальник**: Планування стосується забезпечення того, щоб поди відповідали вузлам, щоб Kubelet міг їх запускати. Він має достатньо інтелекту, щоб вирішити, який вузол має більше доступних ресурсів, і призначити новий под. Зверніть увагу, що планувальник не запускає нові поди, він лише спілкується з процесом Kubelet, що працює всередині вузла, який запустить новий под. - **Kube Controller manager**: Він перевіряє ресурси, такі як набори реплік або розгортання, щоб перевірити, чи, наприклад, працює правильна кількість подів або вузлів. У разі відсутності пода він зв'яжеться з планувальником, щоб запустити новий. Він контролює реплікацію, токени та облікові сервіси для API. - **etcd**: Сховище даних, постійне, узгоджене та розподілене. Це база даних Kubernetes і сховище ключ-значення, де зберігається повний стан кластерів (кожна зміна тут реєструється). Компоненти, такі як планувальник або менеджер контролера, залежать від цих даних, щоб знати, які зміни відбулися (доступні ресурси вузлів, кількість запущених подів...) - **Cloud controller manager**: Це специфічний контролер для управління потоками та додатками, тобто: якщо у вас є кластери в AWS або OpenStack. Зверніть увагу, що оскільки може бути кілька вузлів (які запускають кілька подів), також може бути кілька головних процесів, доступ до яких до API-сервера збалансовано, а їх etcd синхронізовано. **Томи:** Коли под створює дані, які не повинні бути втрачені, коли под зникає, їх слід зберігати в фізичному томі. **Kubernetes дозволяє прикріпити том до пода для збереження даних**. Том може бути на локальному комп'ютері або в **віддаленому сховищі**. Якщо ви запускаєте поди на різних фізичних вузлах, вам слід використовувати віддалене сховище, щоб усі поди могли до нього отримати доступ. **Інші конфігурації:** - **ConfigMap**: Ви можете налаштувати **URL-адреси** для доступу до сервісів. Под отримає дані звідси, щоб знати, як спілкуватися з іншими сервісами (поди). Зверніть увагу, що це не рекомендоване місце для збереження облікових даних! - **Secret**: Це місце для **зберігання секретних даних**, таких як паролі, API-ключі... закодованих у B64. Под зможе отримати доступ до цих даних, щоб використовувати необхідні облікові дані. - **Deployments**: Це місце, де вказуються компоненти, які будуть запущені Kubernetes. Користувач зазвичай не працює безпосередньо з подами, поди абстраговані в **ReplicaSets** (кількість однакових подів, що реплікуються), які запускаються через розгортання. Зверніть увагу, що розгортання призначені для **безстанційних** додатків. Мінімальна конфігурація для розгортання - це ім'я та образ для запуску. - **StatefulSet**: Цей компонент призначений спеціально для додатків, таких як **бази даних**, які потребують **доступу до одного й того ж сховища**. - **Ingress**: Це конфігурація, яка використовується для **публічного відкриття додатка з URL-адресою**. Зверніть увагу, що це також можна зробити за допомогою зовнішніх сервісів, але це правильний спосіб відкрити додаток. - Якщо ви реалізуєте Ingress, вам потрібно буде створити **Ingress Controllers**. Ingress Controller - це **под**, який буде кінцевою точкою, що отримуватиме запити, перевірятиме їх і балансуватиме навантаження на сервіси. Ingress controller **надсилатиме запит на основі налаштованих правил ingress**. Зверніть увагу, що правила ingress можуть вказувати на різні шляхи або навіть піддомени для різних внутрішніх сервісів Kubernetes. - Кращою практикою безпеки буде використання хмарного балансувальника навантаження або проксі-сервера як точки входу, щоб жодна частина кластера Kubernetes не була відкрита. - Коли отримується запит, який не відповідає жодному правилу ingress, контролер ingress направить його на "**Default backend**". Ви можете `describe` контролер ingress, щоб отримати адресу цього параметра. - `minikube addons enable ingress` ### Інфраструктура PKI - Центр сертифікації CA: ![](https://sickrov.github.io/media/Screenshot-66.jpg) - CA є надійним коренем для всіх сертифікатів у кластері. - Дозволяє компонентам перевіряти один одного. - Усі сертифікати кластера підписані CA. - ETCd має свій власний сертифікат. - типи: - сертифікат apiserver. - сертифікат kubelet. - сертифікат планувальника. ## Основні дії ### Minikube **Minikube** можна використовувати для виконання деяких **швидких тестів** на Kubernetes без необхідності розгортання цілого середовища Kubernetes. Він запустить **головні та вузлові процеси на одному комп'ютері**. Minikube використовуватиме virtualbox для запуску вузла. Дивіться [**тут, як його встановити**](https://minikube.sigs.k8s.io/docs/start/). ``` $ minikube start 😄 minikube v1.19.0 on Ubuntu 20.04 ✨ Automatically selected the virtualbox driver. Other choices: none, ssh 💿 Downloading VM boot image ... > minikube-v1.19.0.iso.sha256: 65 B / 65 B [-------------] 100.00% ? p/s 0s > minikube-v1.19.0.iso: 244.49 MiB / 244.49 MiB 100.00% 1.78 MiB p/s 2m17. 👍 Starting control plane node minikube in cluster minikube 💾 Downloading Kubernetes v1.20.2 preload ... > preloaded-images-k8s-v10-v1...: 491.71 MiB / 491.71 MiB 100.00% 2.59 MiB 🔥 Creating virtualbox VM (CPUs=2, Memory=3900MB, Disk=20000MB) ... 🐳 Preparing Kubernetes v1.20.2 on Docker 20.10.4 ... ▪ Generating certificates and keys ... ▪ Booting up control plane ... ▪ Configuring RBAC rules ... 🔎 Verifying Kubernetes components... ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5 🌟 Enabled addons: storage-provisioner, default-storageclass 🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by defaul $ minikube status host: Running kubelet: Running apiserver: Running kubeconfig: Configured ---- ONCE YOU HAVE A K8 SERVICE RUNNING WITH AN EXTERNAL SERVICE ----- $ minikube service mongo-express-service (This will open your browser to access the service exposed port) $ minikube delete 🔥 Deleting "minikube" in virtualbox ... 💀 Removed all traces of the "minikube" cluster ``` ### Kubectl Основи **`Kubectl`** - це інструмент командного рядка для кластерів kubernetes. Він спілкується з Api сервером головного процесу для виконання дій у kubernetes або для запиту даних. ```bash kubectl version #Get client and server version kubectl get pod kubectl get services kubectl get deployment kubectl get replicaset kubectl get secret kubectl get all kubectl get ingress kubectl get endpoints #kubectl create deployment --image= kubectl create deployment nginx-deployment --image=nginx #Access the configuration of the deployment and modify it #kubectl edit deployment kubectl edit deployment nginx-deployment #Get the logs of the pod for debbugging (the output of the docker container running) #kubectl logs kubectl logs nginx-deployment-84cd76b964 #kubectl describe pod kubectl describe pod mongo-depl-5fd6b7d4b4-kkt9q #kubectl exec -it -- bash kubectl exec -it mongo-depl-5fd6b7d4b4-kkt9q -- bash #kubectl describe service kubectl describe service mongodb-service #kubectl delete deployment kubectl delete deployment mongo-depl #Deploy from config file kubectl apply -f deployment.yml ``` ### Minikube Dashboard Дашборд дозволяє вам легше бачити, що виконує minikube, ви можете знайти URL для доступу до нього в: ``` minikube dashboard --url 🔌 Enabling dashboard ... ▪ Using image kubernetesui/dashboard:v2.3.1 ▪ Using image kubernetesui/metrics-scraper:v1.0.7 🤔 Verifying dashboard health ... 🚀 Launching proxy ... 🤔 Verifying proxy health ... http://127.0.0.1:50034/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ ``` ### YAML конфігураційні файли приклади Кожен конфігураційний файл має 3 частини: **метадані**, **специфікація** (що потрібно запустити), **статус** (бажаний стан).\ Всередині специфікації файлу конфігурації розгортання ви можете знайти шаблон, визначений з новою структурою конфігурації, що визначає образ для запуску: **Приклад розгортання + служби, оголошених в одному конфігураційному файлі (з** [**тут**](https://gitlab.com/nanuchi/youtube-tutorial-series/-/blob/master/demo-kubernetes-components/mongo.yaml)**)** Оскільки служба зазвичай пов'язана з одним розгортанням, можливо оголосити обидва в одному конфігураційному файлі (служба, оголошена в цій конфігурації, доступна лише внутрішньо): ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: mongodb-deployment labels: app: mongodb spec: replicas: 1 selector: matchLabels: app: mongodb template: metadata: labels: app: mongodb spec: containers: - name: mongodb image: mongo ports: - containerPort: 27017 env: - name: MONGO_INITDB_ROOT_USERNAME valueFrom: secretKeyRef: name: mongodb-secret key: mongo-root-username - name: MONGO_INITDB_ROOT_PASSWORD valueFrom: secretKeyRef: name: mongodb-secret key: mongo-root-password --- apiVersion: v1 kind: Service metadata: name: mongodb-service spec: selector: app: mongodb ports: - protocol: TCP port: 27017 targetPort: 27017 ``` **Приклад конфігурації зовнішнього сервісу** Цей сервіс буде доступний зовні (перевірте атрибути `nodePort` та `type: LoadBlancer`): ```yaml --- apiVersion: v1 kind: Service metadata: name: mongo-express-service spec: selector: app: mongo-express type: LoadBalancer ports: - protocol: TCP port: 8081 targetPort: 8081 nodePort: 30000 ``` > [!NOTE] > Це корисно для тестування, але для продуктивного середовища у вас повинні бути лише внутрішні сервіси та Ingress для відкриття програми. **Приклад файлу конфігурації Ingress** Це відкриє програму за адресою `http://dashboard.com`. ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: dashboard-ingress namespace: kubernetes-dashboard spec: rules: - host: dashboard.com http: paths: - backend: serviceName: kubernetes-dashboard servicePort: 80 ``` **Приклад конфігураційного файлу секретів** Зверніть увагу, що паролі закодовані в B64 (що не є безпечним!) ```yaml apiVersion: v1 kind: Secret metadata: name: mongodb-secret type: Opaque data: mongo-root-username: dXNlcm5hbWU= mongo-root-password: cGFzc3dvcmQ= ``` **Приклад ConfigMap** **ConfigMap** - це конфігурація, яка надається подам, щоб вони знали, як знаходити та отримувати доступ до інших сервісів. У цьому випадку кожен под буде знати, що ім'я `mongodb-service` є адресою пода, з яким вони можуть спілкуватися (цей под буде виконувати mongodb): ```yaml apiVersion: v1 kind: ConfigMap metadata: name: mongodb-configmap data: database_url: mongodb-service ``` Тоді, всередині **deployment config** ця адреса може бути вказана наступним чином, щоб вона завантажувалася в середовище pod: ```yaml [...] spec: [...] template: [...] spec: containers: - name: mongo-express image: mongo-express ports: - containerPort: 8081 env: - name: ME_CONFIG_MONGODB_SERVER valueFrom: configMapKeyRef: name: mongodb-configmap key: database_url [...] ``` **Приклад конфігурації обсягу** Ви можете знайти різні приклади файлів конфігурації зберігання yaml за адресою [https://gitlab.com/nanuchi/youtube-tutorial-series/-/tree/master/kubernetes-volumes](https://gitlab.com/nanuchi/youtube-tutorial-series/-/tree/master/kubernetes-volumes).\ **Зверніть увагу, що обсяги не знаходяться всередині просторів імен** ### Простори імен Kubernetes підтримує **декілька віртуальних кластерів**, які базуються на одному фізичному кластері. Ці віртуальні кластери називаються **просторами імен**. Вони призначені для використання в середовищах з багатьма користувачами, розподіленими по кількох командах або проектах. Для кластерів з кількома до десятків користувачів вам не потрібно створювати або думати про простори імен. Ви повинні почати використовувати простори імен, щоб мати кращий контроль і організацію кожної частини програми, розгорнутої в kubernetes. Простори імен забезпечують область для імен. Імена ресурсів повинні бути унікальними в межах простору імен, але не між просторами імен. Простори імен не можуть бути вкладеними один в одного, і **кожен** ресурс Kubernetes може бути **тільки в** **одному** **просторі імен**. За замовчуванням є 4 простори імен, якщо ви використовуєте minikube: ``` kubectl get namespace NAME STATUS AGE default Active 1d kube-node-lease Active 1d kube-public Active 1d kube-system Active 1d ``` - **kube-system**: Це не призначено для використання користувачами, і вам не слід його чіпати. Це для процесів master і kubectl. - **kube-public**: Публічно доступні дані. Містить configmap, який містить інформацію про кластер. - **kube-node-lease**: Визначає доступність вузла. - **default**: Простір імен, який користувач буде використовувати для створення ресурсів. ```bash #Create namespace kubectl create namespace my-namespace ``` > [!NOTE] > Зверніть увагу, що більшість ресурсів Kubernetes (наприклад, pods, services, replication controllers та інші) знаходяться в деяких просторах імен. Однак інші ресурси, такі як ресурси простору імен та низькорівневі ресурси, такі як nodes і persistenVolumes, не знаходяться в просторі імен. Щоб побачити, які ресурси Kubernetes є в просторі імен, а які ні: > > ```bash > kubectl api-resources --namespaced=true #В просторі імен > kubectl api-resources --namespaced=false #Не в просторі імен > ``` Ви можете зберегти простір імен для всіх наступних команд kubectl у цьому контексті. ```bash kubectl config set-context --current --namespace= ``` ### Helm Helm є **менеджером пакетів** для Kubernetes. Він дозволяє упаковувати YAML файли та розповсюджувати їх у публічних і приватних репозиторіях. Ці пакети називаються **Helm Charts**. ``` helm search ``` Helm також є шаблонним двигуном, який дозволяє генерувати конфігураційні файли з змінними: ## Kubernetes секрети **Секрет** - це об'єкт, який **містить чутливі дані**, такі як пароль, токен або ключ. Таку інформацію інакше можна було б помістити в специфікацію Pod або в образ. Користувачі можуть створювати Секрети, а система також створює Секрети. Ім'я об'єкта Секрету повинно бути дійсним **DNS піддоменом**. Читайте тут [офіційну документацію](https://kubernetes.io/docs/concepts/configuration/secret/). Секрети можуть бути такими, як: - API, SSH ключі. - OAuth токени. - Облікові дані, паролі (в чистому вигляді або b64 + шифрування). - Інформація або коментарі. - Код підключення до бази даних, рядки… . Існують різні типи секретів у Kubernetes | Вбудований тип | Використання | | ------------------------------------ | ------------------------------------------ | | **Opaque** | **произвольні дані, визначені користувачем (за замовчуванням)** | | kubernetes.io/service-account-token | токен облікового запису служби | | kubernetes.io/dockercfg | серіалізований файл \~/.dockercfg | | kubernetes.io/dockerconfigjson | серіалізований файл \~/.docker/config.json | | kubernetes.io/basic-auth | облікові дані для базової аутентифікації | | kubernetes.io/ssh-auth | облікові дані для SSH аутентифікації | | kubernetes.io/tls | дані для TLS клієнта або сервера | | bootstrap.kubernetes.io/token | дані токена для завантаження | > [!NOTE] > **Тип Opaque є типом за замовчуванням, типовою парою ключ-значення, визначеною користувачами.** **Як працюють секрети:** ![](https://sickrov.github.io/media/Screenshot-164.jpg) Наступний конфігураційний файл визначає **секрет** під назвою `mysecret` з 2 парами ключ-значення `username: YWRtaW4=` та `password: MWYyZDFlMmU2N2Rm`. Він також визначає **pod** під назвою `secretpod`, який матиме `username` та `password`, визначені в `mysecret`, доступними в **змінних середовища** `SECRET_USERNAME` \_\_ та \_\_ `SECRET_PASSWOR`. Він також **монтує** секрет `username` всередині `mysecret` за шляхом `/etc/foo/my-group/my-username` з правами `0640`. ```yaml:secretpod.yaml apiVersion: v1 kind: Secret metadata: name: mysecret type: Opaque data: username: YWRtaW4= password: MWYyZDFlMmU2N2Rm --- apiVersion: v1 kind: Pod metadata: name: secretpod spec: containers: - name: secretpod image: nginx env: - name: SECRET_USERNAME valueFrom: secretKeyRef: name: mysecret key: username - name: SECRET_PASSWORD valueFrom: secretKeyRef: name: mysecret key: password volumeMounts: - name: foo mountPath: "/etc/foo" restartPolicy: Never volumes: - name: foo secret: secretName: mysecret items: - key: username path: my-group/my-username mode: 0640 ``` ```bash kubectl apply -f kubectl get pods #Wait until the pod secretpod is running kubectl exec -it secretpod -- bash env | grep SECRET && cat /etc/foo/my-group/my-username && echo ``` ### Secrets in etcd **etcd** - це послідовне та високо доступне **сховище ключ-значення**, яке використовується як основне сховище Kubernetes для всіх даних кластера. Давайте отримати доступ до секретів, збережених в etcd: ```bash cat /etc/kubernetes/manifests/kube-apiserver.yaml | grep etcd ``` Ви побачите, де сертифікати, ключі та URL-адреси розташовані у файловій системі. Як тільки ви їх отримаєте, ви зможете підключитися до etcd. ```bash #ETCDCTL_API=3 etcdctl --cert --key --cacert endpoint=[] health ETCDCTL_API=3 etcdctl --cert /etc/kubernetes/pki/apiserver-etcd-client.crt --key /etc/kubernetes/pki/apiserver-etcd-client.key --cacert /etc/kubernetes/pki/etcd/etcd/ca.cert endpoint=[127.0.0.1:1234] health ``` Якщо ви зможете встановити зв'язок, ви зможете отримати секрети: ```bash #ETCDCTL_API=3 etcdctl --cert --key --cacert endpoint=[] get ETCDCTL_API=3 etcdctl --cert /etc/kubernetes/pki/apiserver-etcd-client.crt --key /etc/kubernetes/pki/apiserver-etcd-client.key --cacert /etc/kubernetes/pki/etcd/etcd/ca.cert endpoint=[127.0.0.1:1234] get /registry/secrets/default/secret_02 ``` **Додавання шифрування до ETCD** За замовчуванням всі секрети **зберігаються у відкритому** тексті всередині etcd, якщо ви не застосуєте шар шифрування. Наступний приклад базується на [https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/) ```yaml:encryption.yaml apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets providers: - aescbc: keys: - name: key1 secret: cjjPMcWpTPKhAdieVtd+KhG4NN+N6e3NmBPMXJvbfrY= #Any random key - identity: {} ``` Після цього вам потрібно встановити прапорець `--encryption-provider-config` на `kube-apiserver`, щоб вказати на місце розташування створеного конфігураційного файлу. Ви можете змінити `/etc/kubernetes/manifest/kube-apiserver.yaml` і додати наступні рядки: ```yaml containers: - command: - kube-apiserver - --encriyption-provider-config=/etc/kubernetes/etcd/ ``` Прокрутіть униз у volumeMounts: ```yaml - mountPath: /etc/kubernetes/etcd name: etcd readOnly: true ``` Прокрутіть униз у volumeMounts до hostPath: ```yaml - hostPath: path: /etc/kubernetes/etcd type: DirectoryOrCreate name: etcd ``` **Перевірка, що дані зашифровані** Дані зашифровані при запису в etcd. Після перезапуску вашого `kube-apiserver`, будь-який новостворений або оновлений секрет повинен бути зашифрований при зберіганні. Щоб перевірити, ви можете використовувати програму командного рядка `etcdctl`, щоб отримати вміст вашого секрету. 1. Створіть новий секрет під назвою `secret1` у просторі імен `default`: ``` kubectl create secret generic secret1 -n default --from-literal=mykey=mydata ``` 2. Використовуючи командний рядок etcdctl, прочитайте цей секрет з etcd: `ETCDCTL_API=3 etcdctl get /registry/secrets/default/secret1 [...] | hexdump -C` де `[...]` повинні бути додаткові аргументи для підключення до сервера etcd. 3. Перевірте, що збережений секрет має префікс `k8s:enc:aescbc:v1:`, що вказує на те, що провайдер `aescbc` зашифрував отримані дані. 4. Перевірте, що секрет правильно розшифрований при отриманні через API: ``` kubectl describe secret secret1 -n default ``` повинен відповідати `mykey: bXlkYXRh`, mydata закодовано, перевірте [декодування секрету](https://kubernetes.io/docs/concepts/configuration/secret#decoding-a-secret), щоб повністю декодувати секрет. **Оскільки секрети зашифровані при запису, виконання оновлення секрету зашифрує цей вміст:** ``` kubectl get secrets --all-namespaces -o json | kubectl replace -f - ``` **Останні поради:** - Намагайтеся не зберігати секрети у файловій системі, отримуйте їх з інших місць. - Перегляньте [https://www.vaultproject.io/](https://www.vaultproject.io) для додаткового захисту ваших секретів. - [https://kubernetes.io/docs/concepts/configuration/secret/#risks](https://kubernetes.io/docs/concepts/configuration/secret/#risks) - [https://docs.cyberark.com/Product-Doc/OnlineHelp/AAM-DAP/11.2/en/Content/Integrations/Kubernetes_deployApplicationsConjur-k8s-Secrets.htm](https://docs.cyberark.com/Product-Doc/OnlineHelp/AAM-DAP/11.2/en/Content/Integrations/Kubernetes_deployApplicationsConjur-k8s-Secrets.htm) ## Посилання {{#ref}} https://sickrov.github.io/ {{#endref}} {{#ref}} https://www.youtube.com/watch?v=X48VuDVv0do {{#endref}} {{#include ../../banners/hacktricks-training.md}}