This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
|
управление_доступом_в_kubernetes [2025/12/09 10:37] val [Перезапуск вебинара] |
управление_доступом_в_kubernetes [2025/12/19 09:44] (current) val [Шаг 7. Использование OpenID Connect] |
||
|---|---|---|---|
| Line 15: | Line 15: | ||
| * [[https://medium.com/@subhampradhan966/implementing-and-verifying-kubernetes-service-accounts-a-step-by-step-guide-c43b727260b2|Implementing and Verifying Kubernetes Service Accounts: A Step-by-Step Guide]] | * [[https://medium.com/@subhampradhan966/implementing-and-verifying-kubernetes-service-accounts-a-step-by-step-guide-c43b727260b2|Implementing and Verifying Kubernetes Service Accounts: A Step-by-Step Guide]] | ||
| - | + | * [[https://medium.com/@amirhosseineidy/kubernetes-authentication-with-keycloak-oidc-63571eaeed61|Kubernetes authentication with keycloak oidc]] | |
| + | * [[https://vlasov.pro/ru/p/kubernetes-oidc/|Kubernetes авторизация через OIDC]] | ||
| + | * [[https://timeweb.cloud/docs/k8s/connect-oidc-provider-to-cluster|Подключение OIDC-провайдера к кластеру]] | ||
| + | * [[https://www.talkingquickly.co.uk/setting-up-oidc-login-kubernetes-kubectl-with-keycloak|OIDC Login to Kubernetes and Kubectl with Keycloak]] | ||
| + | * [[https://github.com/int128/kubelogin|kubelogin - This is a kubectl plugin for Kubernetes OpenID Connect]] | ||
| ===== Реклама ===== | ===== Реклама ===== | ||
| Line 49: | Line 52: | ||
| ==== Вариант 2.1 Использование сертификатов ==== | ==== Вариант 2.1 Использование сертификатов ==== | ||
| - | * [[Пакет OpenSSL#Создание приватного ключа пользователя]] | + | <code> |
| - | * [[Пакет OpenSSL#Создание запроса на сертификат]] | + | kube1:~# cat ~/.kube/config |
| - | <code> | + | kube1:~# echo LS0tLS1CR...LS0tLS0K | base64 -d |
| - | user1@client1:~$ cat user1.req | base64 -w0 | + | |
| </code> | </code> | ||
| - | * [[https://stackoverflow.com/questions/75735249/what-do-the-values-in-certificatesigningrequest-spec-usages-mean|What do the values in CertificateSigningRequest.spec.usages mean?]] | ||
| - | <code> | ||
| - | kube1:~/users# kubectl explain csr.spec.usages | ||
| - | kube1:~/users# cat user1.req.yaml | + | * [[https://www.sslshopper.com/certificate-decoder.html|Certificate Decoder]] |
| - | </code><code> | + | |
| - | apiVersion: certificates.k8s.io/v1 | + | |
| - | kind: CertificateSigningRequest | + | |
| - | metadata: | + | |
| - | name: user1 | + | |
| - | spec: | + | |
| - | request: LS0t...S0tCg== | + | |
| - | signerName: kubernetes.io/kube-apiserver-client | + | |
| - | expirationSeconds: 8640000 # 100 * one day | + | |
| - | usages: | + | |
| - | # - digital signature | + | |
| - | # - key encipherment | + | |
| - | - client auth | + | |
| - | </code><code> | + | |
| - | kube1:~/users# kubectl apply -f user1.req.yaml | + | |
| - | kube1:~/users# kubectl describe csr/user1 | + | <code> |
| + | kube1:~# kubectl auth whoami | ||
| + | </code> | ||
| - | kube1:~/users# kubectl certificate approve user1 | + | * [[Система Kubernetes#Использование сертификатов]] |
| - | + | ||
| - | kube1:~/users# kubectl get csr | + | |
| - | + | ||
| - | kube1:~/users# kubectl get csr/user1 -o yaml | + | |
| - | + | ||
| - | kube1:~/users# kubectl get csr/user1 -o jsonpath="{.status.certificate}" | base64 -d | tee user1.crt | + | |
| - | + | ||
| - | user1@client1:~$ scp root@kube1:users/user1.crt . | + | |
| - | + | ||
| - | kube1:~/users# kubectl delete csr user1 | + | |
| - | </code> | + | |
| ==== Вариант 2.2 Использование ServiceAccount ==== | ==== Вариант 2.2 Использование ServiceAccount ==== | ||
| Line 105: | Line 80: | ||
| ===== Шаг 3. Использование Role и RoleBinding ===== | ===== Шаг 3. Использование Role и RoleBinding ===== | ||
| - | ==== Предоставление доступа к services/proxy в Namespace ==== | + | * [[Система Kubernetes#Предоставление доступа к services/proxy в Namespace]] |
| - | * Cloud native distributed block storage for Kubernetes [[Система Kubernetes#longhorn]] | + | * [[Система Kubernetes#Предоставление полного доступа к Namespace]] |
| - | + | ||
| - | <code> | + | |
| - | kube1:~# kubectl api-resources -o wide | less | + | |
| - | APIVERSION = <group> + "/" + <version of the API> | + | |
| - | + | ||
| - | kube1:~/users# cat lh-svc-proxy-role.yaml | + | |
| - | </code><code> | + | |
| - | apiVersion: rbac.authorization.k8s.io/v1 | + | |
| - | kind: Role | + | |
| - | metadata: | + | |
| - | namespace: longhorn-system | + | |
| - | name: lh-svc-proxy-role | + | |
| - | rules: | + | |
| - | - apiGroups: [""] | + | |
| - | resources: ["services/proxy"] | + | |
| - | verbs: ["get"] | + | |
| - | </code><code> | + | |
| - | kube1:~/users# cat user1-lh-svc-proxy-rolebinding.yaml | + | |
| - | </code><code> | + | |
| - | apiVersion: rbac.authorization.k8s.io/v1 | + | |
| - | kind: RoleBinding | + | |
| - | metadata: | + | |
| - | name: user1-lh-svc-proxy-rolebinding | + | |
| - | namespace: longhorn-system | + | |
| - | subjects: | + | |
| - | - kind: User | + | |
| - | name: user1 | + | |
| - | apiGroup: rbac.authorization.k8s.io | + | |
| - | roleRef: | + | |
| - | kind: Role | + | |
| - | name: lh-svc-proxy-role | + | |
| - | apiGroup: rbac.authorization.k8s.io | + | |
| - | </code><code> | + | |
| - | kube1:~/users# kubectl apply -f lh-svc-proxy-role.yaml,user1-lh-svc-proxy-rolebinding.yaml | + | |
| - | + | ||
| - | student@client1:~$ kubectl proxy | + | |
| - | + | ||
| - | student@client1:~$ curl http://localhost:8001/api/v1/namespaces/longhorn-system/services/longhorn-frontend:80/proxy/ | + | |
| - | + | ||
| - | student@client1:~$ curl http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ | + | |
| - | + | ||
| - | kube1:~/users# kubectl delete -f lh-svc-proxy-role.yaml,user1-lh-svc-proxy-rolebinding.yaml | + | |
| - | </code> | + | |
| - | ==== Предоставление полного доступа к Namespace ==== | + | |
| - | + | ||
| - | <code> | + | |
| - | kube1:~/users# cat ns-full-access.yaml | + | |
| - | </code><code> | + | |
| - | --- | + | |
| - | kind: Role | + | |
| - | apiVersion: rbac.authorization.k8s.io/v1 | + | |
| - | metadata: | + | |
| - | name: ns-full-access | + | |
| - | namespace: my-ns | + | |
| - | rules: | + | |
| - | - apiGroups: ["*"] | + | |
| - | resources: ["*"] | + | |
| - | verbs: ["*"] | + | |
| - | --- | + | |
| - | kind: RoleBinding | + | |
| - | apiVersion: rbac.authorization.k8s.io/v1 | + | |
| - | metadata: | + | |
| - | name: ns-full-access-rolebinding | + | |
| - | namespace: my-ns | + | |
| - | subjects: | + | |
| - | - apiGroup: rbac.authorization.k8s.io | + | |
| - | kind: Group | + | |
| - | name: cko | + | |
| - | #kind: User | + | |
| - | #name: user1 | + | |
| - | roleRef: | + | |
| - | kind: Role | + | |
| - | name: ns-full-access | + | |
| - | apiGroup: rbac.authorization.k8s.io | + | |
| - | #roleRef: | + | |
| - | #apiGroup: rbac.authorization.k8s.io | + | |
| - | #kind: ClusterRole | + | |
| - | #name: admin | + | |
| - | </code><code> | + | |
| - | kube1:~/users# kubectl apply -f ns-full-access.yaml | + | |
| - | </code> | + | |
| * Запускаем pod my-debian ([[Система Kubernetes#Базовые объекты k8s]]) в my-ns (создать, если нет) | * Запускаем pod my-debian ([[Система Kubernetes#Базовые объекты k8s]]) в my-ns (создать, если нет) | ||
| + | * [[Система Kubernetes#Поиск предоставленных ролей для учетной записи]] | ||
| - | ==== Поиск предоставленных ролей для учетной записи ==== | ||
| - | <code> | ||
| - | kube1:~/users# kubectl get rolebindings --all-namespaces -o=json | jq '.items[] | select(.subjects[]?.name == "user1")' | ||
| - | |||
| - | kube1:~/users# kubectl get rolebindings --all-namespaces -o=json | jq '.items[] | select(.subjects[]?.name == "cko")' | ||
| - | |||
| - | kube1:~/users# kubectl delete -f ns-full-access.yaml | ||
| - | ИЛИ | ||
| - | kube1:~/users# kubectl -n my-ns delete rolebindings ns-full-access-rolebinding | ||
| - | kube1:~/users# kubectl -n my-ns delete role ns-full-access | ||
| - | </code> | ||
| ===== Шаг 4. Использование ClusterRole и ClusterRoleBinding ===== | ===== Шаг 4. Использование ClusterRole и ClusterRoleBinding ===== | ||
| - | ==== Предоставление доступа к services/port-forward в Cluster ==== | + | * [[Система Kubernetes#Предоставление доступа к services/port-forward в Cluster]] |
| - | <code> | + | * [[Система Kubernetes#Предоставление полного доступа к Kubernetes Cluster]] |
| - | kube1:~/users# cat svc-pfw-role.yaml | + | |
| - | </code><code> | + | |
| - | apiVersion: rbac.authorization.k8s.io/v1 | + | |
| - | kind: ClusterRole | + | |
| - | #kind: Role | + | |
| - | metadata: | + | |
| - | name: svc-pfw-role | + | |
| - | # namespace: my-pgcluster-ns | + | |
| - | rules: | + | |
| - | - apiGroups: [""] | + | |
| - | resources: ["services"] | + | |
| - | verbs: ["get"] | + | |
| - | - apiGroups: [""] | + | |
| - | resources: ["pods"] | + | |
| - | verbs: ["get", "list"] | + | |
| - | - apiGroups: [""] | + | |
| - | resources: ["pods/portforward"] | + | |
| - | verbs: ["create"] | + | |
| - | </code><code> | + | |
| - | kube1:~/users# cat user1-svc-pfw-rolebinding.yaml | + | |
| - | </code><code> | + | |
| - | apiVersion: rbac.authorization.k8s.io/v1 | + | |
| - | kind: ClusterRoleBinding | + | |
| - | #kind: RoleBinding | + | |
| - | metadata: | + | |
| - | name: user1-svc-pfw-rolebinding | + | |
| - | # namespace: my-pgcluster-ns | + | |
| - | subjects: | + | |
| - | - kind: User | + | |
| - | name: user1 | + | |
| - | apiGroup: rbac.authorization.k8s.io | + | |
| - | roleRef: | + | |
| - | kind: ClusterRole | + | |
| - | # kind: Role | + | |
| - | name: svc-pfw-role | + | |
| - | apiGroup: rbac.authorization.k8s.io | + | |
| - | </code><code> | + | |
| - | kube1:~/users# kubectl apply -f svc-pfw-role.yaml,user1-svc-pfw-rolebinding.yaml | + | |
| - | + | ||
| - | student@client1:~$ kubectl port-forward -n my-pgcluster-ns services/my-pgcluster-rw 5432:5432 | + | |
| - | + | ||
| - | student@client1:~$ psql postgres://keycloak:strongpassword@127.0.0.1:5432/keycloak | + | |
| - | </code> | + | |
| - | + | ||
| - | * Доступ через proxy к [[Система Kubernetes#Kubernetes Dashboard]] | + | |
| - | + | ||
| - | <code> | + | |
| - | kube1:~/users# kubectl delete -f svc-pfw-role.yaml,user1-svc-pfw-rolebinding.yaml | + | |
| - | </code> | + | |
| - | ==== Предоставление полного доступа к Kubernetes Cluster ==== | + | |
| - | + | ||
| - | <code> | + | |
| - | kube1:~/users# kubectl get clusterroles | less | + | |
| - | + | ||
| - | kube1:~/users# kubectl get clusterrole cluster-admin -o yaml | + | |
| - | + | ||
| - | kube1:~/users# kubectl get clusterrolebindings | less | + | |
| - | + | ||
| - | kube1:~/users# kubectl get clusterrolebindings cluster-admin -o yaml | + | |
| - | + | ||
| - | kube1:~/users# cat user1-cluster-admin.yaml | + | |
| - | </code><code> | + | |
| - | apiVersion: rbac.authorization.k8s.io/v1 | + | |
| - | kind: ClusterRoleBinding | + | |
| - | metadata: | + | |
| - | name: user1-cluster-admin | + | |
| - | subjects: | + | |
| - | - kind: User | + | |
| - | name: user1 | + | |
| - | # name: user1@corp13.un | + | |
| - | apiGroup: rbac.authorization.k8s.io | + | |
| - | roleRef: | + | |
| - | kind: ClusterRole | + | |
| - | name: cluster-admin | + | |
| - | apiGroup: rbac.authorization.k8s.io | + | |
| - | </code><code> | + | |
| - | kube1:~/users# kubectl apply -f user1-cluster-admin.yaml | + | |
| - | + | ||
| - | student@client1:~$ kubectl get nodes | + | |
| - | </code> | + | |
| - | + | ||
| - | ==== Поиск предоставленных кластерных ролей для учетной записи ==== | + | |
| - | <code> | + | |
| - | kube1:~/users# kubectl get clusterrolebindings -o=json | jq '.items[] | select(.subjects[]?.name == "kubeadm:cluster-admins")' | + | |
| - | kube1:~/users# kubectl get clusterrolebindings -o=json | jq '.items[] | select(.subjects[]?.name == "user1")' | + | * [[Система Kubernetes#Поиск предоставленных кластерных ролей для учетной записи]] |
| - | + | ||
| - | kube1:~/users# kubectl get clusterrolebindings -o=json | jq '.items[] | select(.subjects[]?.name == "default")' | + | |
| - | + | ||
| - | kube1:~/users# kubectl delete -f user1-cluster-admin.yaml | + | |
| - | ИЛИ | + | |
| - | kube1:~/users# kubectl delete clusterrolebindings user1-cluster-admin | + | |
| - | </code> | + | |
| ===== Шаг 5. Использование JSON Web Token (JWT) для доступа в Kubernetes ===== | ===== Шаг 5. Использование JSON Web Token (JWT) для доступа в Kubernetes ===== | ||
| Line 308: | Line 101: | ||
| <code> | <code> | ||
| + | kube1:~/users# kubectl delete clusterrolebindings admin-user | ||
| + | |||
| kube1:~/users# kubectl delete serviceaccounts admin-user | kube1:~/users# kubectl delete serviceaccounts admin-user | ||
| + | |||
| + | user1@client1:~$ rm -rf .kube/ | ||
| </code> | </code> | ||
| ===== Шаг 6. Использование Service Accounts в приложениях ===== | ===== Шаг 6. Использование Service Accounts в приложениях ===== | ||
| Line 353: | Line 150: | ||
| </code> | </code> | ||
| - | ===== Вопросы? ===== | + | ===== Шаг 7. Использование OpenID Connect ===== |
| - | ===== Домашнее задание ===== | + | * Сервис Keycloak [[Сервис Keycloak#Аутентификация пользователей WEB приложения]] |
| - | - Куда и через сколько исчезает "kubectl get csr" после "approve" ? | ||
| - | |||
| - | ===== Перезапуск вебинара ===== | ||
| <code> | <code> | ||
| - | user1@client1:~$ rm -v user1* | ||
| - | |||
| - | user1@client1:~$ rm -rfv .kube/ | ||
| - | |||
| - | kube1:~/users# kubectl delete -f ns-full-access.yaml | ||
| - | |||
| - | kube1:~# rm -rfv users/ | ||
| - | </code> | ||
| - | |||
| - | ===== Дополнительные материалы ===== | ||
| - | |||
| - | ==== oidc ==== | ||
| - | <code> | ||
| - | https://medium.com/@amirhosseineidy/kubernetes-authentication-with-keycloak-oidc-63571eaeed61 | ||
| - | |||
| - | https://vlasov.pro/ru/p/kubernetes-oidc/ | ||
| - | |||
| - | https://github.com/int128/kubelogin | ||
| - | |||
| - | https://timeweb.cloud/docs/k8s/connect-oidc-provider-to-cluster | ||
| - | |||
| - | ? https://www.talkingquickly.co.uk/setting-up-oidc-login-kubernetes-kubectl-with-keycloak | ||
| - | |||
| - | Email verified | ||
| - | |||
| kube1:~/users# vim /etc/kubernetes/manifests/kube-apiserver.yaml | kube1:~/users# vim /etc/kubernetes/manifests/kube-apiserver.yaml | ||
| + | </code><code> | ||
| ... | ... | ||
| spec: | spec: | ||
| Line 392: | Line 162: | ||
| - command: | - command: | ||
| - kube-apiserver | - kube-apiserver | ||
| - | - --oidc-issuer-url=https://keycloak.corp13.un/realms/corp13 | + | - --oidc-issuer-url=https://keycloak.corpX.un/realms/corpX |
| #- --oidc-client-id=account | #- --oidc-client-id=account | ||
| - --oidc-client-id=any-client | - --oidc-client-id=any-client | ||
| Line 399: | Line 169: | ||
| - --oidc-groups-claim=groups | - --oidc-groups-claim=groups | ||
| ... | ... | ||
| + | </code><code> | ||
| + | kube1:~# ps ax | grep kube-apiserver | ||
| kube1:~/users# kubectl -n kube-system logs Pod/kube-apiserver-kube1 | kube1:~/users# kubectl -n kube-system logs Pod/kube-apiserver-kube1 | ||
| Line 404: | Line 176: | ||
| E1203 05:22:46.412571 1 authentication.go:73] "Unable to authenticate the request" err="[invalid bearer token, oidc: verify token: oidc: expected audience \"any-client\" got [\"account\"]]" | E1203 05:22:46.412571 1 authentication.go:73] "Unable to authenticate the request" err="[invalid bearer token, oidc: verify token: oidc: expected audience \"any-client\" got [\"account\"]]" | ||
| ... | ... | ||
| + | E1218 10:36:21.105422 1 authentication.go:75] "Unable to authenticate the request" err="[invalid bearer token, oidc: email not verified]" | ||
| + | ... | ||
| + | </code> | ||
| + | |||
| + | * [[Сервис Keycloak#Проверка получения токена]] Keycloak | ||
| + | * [[Система Kubernetes#Создание файла конфигурации kubectl]] c полученным токеном | ||
| + | |||
| + | <code> | ||
| + | client1:~# wget https://github.com/int128/kubelogin/releases/download/v1.35.0/kubelogin_linux_amd64.zip | ||
| + | |||
| + | client1:~# unzip kubelogin_linux_amd64.zip | ||
| + | |||
| + | client1:~# mv kubelogin /usr/local/bin/ | ||
| user1@client1:~$ cat .kube/config | user1@client1:~$ cat .kube/config | ||
| - | apiVersion: v1 | + | </code><code> |
| - | clusters: | + | ... |
| - | - cluster: | + | |
| - | certificate-authority-data: ... | + | |
| - | server: https://192.168.13.221:6443 | + | |
| - | name: cluster.local | + | |
| - | contexts: | + | |
| - | - context: | + | |
| - | cluster: cluster.local | + | |
| - | user: user1 | + | |
| - | name: default-context | + | |
| - | current-context: default-context | + | |
| - | kind: Config | + | |
| - | preferences: {} | + | |
| users: | users: | ||
| - name: user1 | - name: user1 | ||
| Line 432: | Line 205: | ||
| #refresh-token: | #refresh-token: | ||
| name: oidc | name: oidc | ||
| + | </code><code> | ||
| + | user1@client1:~$ kubelogin | ||
| + | user1@client1:~$ kubectl auth whoami | ||
| </code> | </code> | ||
| - | ==== Auditing ==== | + | |
| + | * [[Система Kubernetes#Предоставление полного доступа к Kubernetes Cluster]] | ||
| + | |||
| + | ===== Вопросы? ===== | ||
| + | |||
| + | ===== Домашнее задание ===== | ||
| + | |||
| + | - Куда и через сколько исчезает "kubectl get csr" после "approve" ? | ||
| + | |||
| + | ===== Перезапуск вебинара ===== | ||
| + | <code> | ||
| + | user1@client1:~$ rm -v user1* | ||
| + | |||
| + | user1@client1:~$ rm -rfv .kube/ | ||
| + | |||
| + | kube1:~# rm -rfv users/ | ||
| + | </code> | ||
| + | |||
| + | ===== Дополнительные материалы ===== | ||
| + | |||
| + | ==== Черновик Auditing ==== | ||
| <code> | <code> | ||
| https://habr.com/ru/companies/slurm/articles/711868/|Журналы аудита Kubernetes: лучшие практики и настройка | https://habr.com/ru/companies/slurm/articles/711868/|Журналы аудита Kubernetes: лучшие практики и настройка | ||
| Line 486: | Line 282: | ||
| user1@client1:~$ kubectl -n my-apwebd-ns delete pod my-webd-<TAB> | user1@client1:~$ kubectl -n my-apwebd-ns delete pod my-webd-<TAB> | ||
| + | </code> | ||
| + | |||
| + | ==== Черновик DEX ==== | ||
| + | |||
| + | === Описание === | ||
| + | <code> | ||
| + | Создаём группы и пользователей под Kubernetes | ||
| + | У нас уже есть настроенная Freeipa | ||
| + | FreeIPA — это серверная система для централизованного управления пользователями, группами, доступами, сертификатами и DNS в инфраструктуре. | ||
| + | Заходим на freeipa, через web интерфейс или же можно воспользоваться консолью | ||
| + | https://ipa-server-1.teach.local/ | ||
| + | ssh root@192.168.100.252 | ||
| + | Мы воспользуемся консолью чтобы сэкономить время | ||
| + | Но предварительно на web проверим что нет групп для кубернетис | ||
| + | Используем команду kinit чтобы получить билет Kerberos и введем команды для создания | ||
| + | |||
| + | ipa group-add k8s-cluster-admins \ | ||
| + | --desc="Kubernetes cluster admins" | ||
| + | |||
| + | ipa group-add k8s-ns-core-test-view \ | ||
| + | --desc="View access to core-test namespace" | ||
| + | |||
| + | ipa group-add k8s-ns-core-test-admin \ | ||
| + | --desc="Admin access to core-test namespace" | ||
| + | ipa group-add k8s-ns \ | ||
| + | --desc="Access only view all ns" | ||
| + | |||
| + | Теперь обновим страницу с группами и увидим созданные нами группы | ||
| + | Далее Создаём тестового пользователя | ||
| + | ipa user-add ivan \ | ||
| + | --first=Ivan \ | ||
| + | --last=Petrov \ | ||
| + | --password | ||
| + | |||
| + | ipa user-add sergey \ | ||
| + | --first=Sergey \ | ||
| + | --last=Admin \ | ||
| + | --password | ||
| + | |||
| + | |||
| + | ipa user-add kirill \ | ||
| + | --first=Kirill \ | ||
| + | --last=Viewer \ | ||
| + | --password | ||
| + | ipa user-add ira \ | ||
| + | --first=Irina\ | ||
| + | --last=Viewer \ | ||
| + | --password | ||
| + | |||
| + | |||
| + | Далее попросят ввести пароль | ||
| + | Вводим пароль 123456 | ||
| + | И подтверждаем | ||
| + | Конечно такой пароль только для демонстрации а не для продового использования | ||
| + | Добавляем пользователя в группу | ||
| + | |||
| + | ipa group-add-member k8s-ns-core-test-view --users=ivan | ||
| + | ipa group-add-member k8s-ns-core-test-admin --users=kirill | ||
| + | ipa group-add-member k8s-ns --users=ira | ||
| + | ipa group-add-member k8s-cluster-admins --users=sergey | ||
| + | |||
| + | Далее Аккаунт для Dex в FreeIPA (LDAP bind user) | ||
| + | Dex должен логиниться в LDAP FreeIPA с каким-то пользователем. | ||
| + | |||
| + | ipa user-add dex-bind \ | ||
| + | --first=Dex \ | ||
| + | --last=Bind \ | ||
| + | --password | ||
| + | |||
| + | Проверяем LDAP-доступ к FreeIPA | ||
| + | Чтобы убедится что dex сможет залогинется | ||
| + | |||
| + | |||
| + | Зайдем на сервер c которого будем управлять k8s и выполнил поиск нашего пользователя | ||
| + | ssh root@192.168.100.249 | ||
| + | |||
| + | ldapsearch -x -H ldap://192.168.100.252:389 -D "uid=dex-bind,cn=users,cn=accounts,dc=teach,dc=local" -W -b "cn=users,cn=accounts,dc=teach | ||
| + | ,dc=local" "(uid=ivan)" | ||
| + | |||
| + | Подключимся пользователем dex-bind | ||
| + | И поищем созданного нами пользователя ivan | ||
| + | Видим что все ок данные получаем | ||
| + | |||
| + | Далее настроим установим и настроим dex | ||
| + | Для этого будем использовать helm | ||
| + | Helm — это менеджер пакетов для Kubernetes, грубо говоря: | ||
| + | «apt/yum для Kubernetes | ||
| + | |||
| + | |||
| + | Нам понадобится серт мэнеджер для выпуска сетификатов и ингресс контроллер | ||
| + | |||
| + | |||
| + | helm repo add jetstack https://charts.jetstack.io | ||
| + | helm repo update | ||
| + | |||
| + | helm install cert-manager jetstack/cert-manager \ | ||
| + | --namespace cert-manager \ | ||
| + | --create-namespace \ | ||
| + | --set crds.enabled=true | ||
| + | |||
| + | |||
| + | helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx | ||
| + | helm repo update | ||
| + | |||
| + | helm install ingress-nginx ingress-nginx/ingress-nginx \ | ||
| + | -n ingress-nginx --create-namespace \ | ||
| + | --set controller.ingressClassResource.name=nginx \ | ||
| + | --set controller.ingressClassByName=true \ | ||
| + | --set controller.hostNetwork=true \ | ||
| + | --set controller.dnsPolicy=ClusterFirstWithHostNet \ | ||
| + | --set controller.kind=DaemonSet \ | ||
| + | --set controller.service.type=NodePort | ||
| + | |||
| + | |||
| + | Перезагружаем сервера | ||
| + | |||
| + | |||
| + | |||
| + | Начиная с новых версий FreeIPA, в нём есть ACME-сервер | ||
| + | ACME-server — это “серверная сторона автоматики сертификатов”. | ||
| + | ACME (Automatic Certificate Management Environment) — это протокол, по которому: | ||
| + | клиент (cert-manager, acme.sh, lego, certbot и т.п.) | ||
| + | общается с CA (центром сертификации) через ACME-server | ||
| + | чтобы автоматически выпускать и обновлять TLS-сертификаты. | ||
| + | Примеры ACME-серверов: | ||
| + | Let’s Encrypt (самый известный; их сервер — Boulder) | ||
| + | |||
| + | |||
| + | |||
| + | ipa-acme-manage enable | ||
| + | ipa-acme-manage status | ||
| + | |||
| + | |||
| + | появится эндпоинт | ||
| + | |||
| + | https://ipa-server-1.teach.local/acme/directory | ||
| + | |||
| + | curl -vk https://ipa-server-1.teach.local/acme/directory | ||
| + | |||
| + | далее получаем сертифкат freeipa корневой | ||
| + | идем на сервер freeipa | ||
| + | |||
| + | sudo cat /etc/ipa/ca.crt > ipa-ca.crt | ||
| + | далее переводим в base64 | ||
| + | base64 -w0 ipa-ca.crt > ipa-ca.b64 | ||
| + | |||
| + | затем нам нужен ClusterIssuer | ||
| + | |||
| + | ClusterIssuer — это кластерный “поставщик сертификатов” в cert-manager, который виден во всех неймспейсах Kubernetes. | ||
| + | Говоря по-человечески: | ||
| + | cert-manager — это оператор, который умеет автоматически выпускать и обновлять сертификаты. | ||
| + | ClusterIssuer/Issuer — это “настройка, куда идти за сертификатом и как его получать”. | ||
| + | |||
| + | Файл issuser.txt | ||
| + | И caBundle ПОДСТАВЬ_ЗДЕСЬ_СТРОКУ_ИЗ_ipa-ca.b64 | ||
| + | |||
| + | |||
| + | apiVersion: cert-manager.io/v1 | ||
| + | kind: ClusterIssuer | ||
| + | metadata: | ||
| + | name: freeipa-acme | ||
| + | spec: | ||
| + | acme: | ||
| + | email: admin@teach.local | ||
| + | server: https://ipa-server-1.teach.local/acme/directory | ||
| + | privateKeySecretRef: | ||
| + | name: freeipa-acme-account-key | ||
| + | |||
| + | # ВАЖНО: одна длинная строка из ipa-ca.b64 | ||
| + | caBundle: ПОДСТАВЬ_ЗДЕСЬ_СТРОКУ_ИЗ_ipa-ca.b64 | ||
| + | |||
| + | solvers: | ||
| + | - http01: | ||
| + | ingress: | ||
| + | ingressClassName: nginx | ||
| + | |||
| + | |||
| + | 1. Выпускаем сертификат для Dex | ||
| + | Сделаем Certificate, который: | ||
| + | использует твой ClusterIssuer freeipa-acme; | ||
| + | кладёт сертификат в Secret dex-tls в namespace dex. | ||
| + | |||
| + | |||
| + | Для начала нам нужна dns запись | ||
| + | Посмотрим какие ip у серверов k8s | ||
| + | kubectl get nodes -o wide | ||
| + | В freeipa | ||
| + | |||
| + | ipa dnsrecord-add teach.local dex --a-rec=192.168.100.229 | ||
| + | это адрес одной из наших нод | ||
| + | |||
| + | Создаем нейаспейс | ||
| + | kubectl create ns dex | ||
| + | |||
| + | затем применяем файл с запросом сетификата | ||
| + | файл certificate.txt | ||
| + | |||
| + | apiVersion: cert-manager.io/v1 | ||
| + | kind: Certificate | ||
| + | metadata: | ||
| + | name: dex-cert | ||
| + | namespace: dex | ||
| + | spec: | ||
| + | secretName: dex-tls | ||
| + | dnsNames: | ||
| + | - dex.teach.local | ||
| + | issuerRef: | ||
| + | name: freeipa-acme | ||
| + | kind: ClusterIssuer | ||
| + | |||
| + | Добавляем репозиторий с dex | ||
| + | |||
| + | |||
| + | helm repo add dex https://charts.dexidp.io | ||
| + | helm repo update | ||
| + | |||
| + | Качаем локально хелм чарт | ||
| + | helm pull dex/dex | ||
| + | |||
| + | У нас скачивается архив с helm чартом | ||
| + | ls | ||
| + | далее распаковываем архив | ||
| + | tar -xvf dex-0.24.0.tgz | ||
| + | переходим в каталог | ||
| + | cd dex | ||
| + | |||
| + | |||
| + | и тут нас интересует файл values.yaml | ||
| + | vi values.yml | ||
| + | открываем файл и редактируем берем пример из файла config.txt | ||
| + | нужно сгенерить секрет | ||
| + | openssl rand -hex 32 | ||
| + | и заменить SUPER-SECRET-STRING на полученный секрет | ||
| + | Создаем секрет | ||
| + | |||
| + | kubectl -n dex create secret generic dex-ldap-bind \ | ||
| + | --from-literal=DEX_LDAP_BIND_DN='uid=dex-bind,cn=users,cn=accounts,dc=teach,dc=local' \ | ||
| + | --from-literal=DEX_LDAP_BIND_PW='123456' | ||
| + | |||
| + | |||
| + | сохраняем и применяем | ||
| + | helm upgrade --install dex . -n dex -f values.yaml | ||
| + | Тест с любой машины, где dex.teach.local резолвится в IP ноды: | ||
| + | curl -vk https://dex.teach.local/dex/.well-known/openid-configuration | ||
| + | Кладём CA FreeIPA на мастер | ||
| + | Переходим на мастер ноду | ||
| + | ssh root@192.168.100.230 | ||
| + | scp root@ipa-server-1.teach.local:/etc/ipa/ca.crt /root/ipa-ca.crt | ||
| + | sudo cp /root/ipa-ca.crt /etc/kubernetes/ssl/dex-ca.crt | ||
| + | |||
| + | Затем правим настройки kubeapi | ||
| + | |||
| + | vi /etc/kubernetes/manifests/kube-apiserver.yaml | ||
| + | |||
| + | Добавляем | ||
| + | |||
| + | - --oidc-issuer-url=https://dex.teach.local/dex | ||
| + | - --oidc-client-id=dex-k8s-authenticator | ||
| + | - --oidc-username-claim=email | ||
| + | - --oidc-groups-claim=groups | ||
| + | - --oidc-ca-file=/etc/kubernetes/ssl/dex-ca.crt | ||
| + | |||
| + | Kubeapi сам перезагрузится | ||
| + | |||
| + | Если все ок то в браузере перейдя по ссылке увидим ответ от dex | ||
| + | https://dex.teach.local/dex/.well-known/openid-configuration | ||
| + | |||
| + | отлично dex установлен но Dex сам по себе — только (OIDC-провайдер). | ||
| + | Страничку, где после логина рисуется готовый kubeconfig, делает другой сервис — dex-k8s-authenticator. | ||
| + | |||
| + | |||
| + | kubectl create namespace dex-auth | ||
| + | |||
| + | |||
| + | Добавляем на freeipa dns запись | ||
| + | |||
| + | ipa dnsrecord-add teach.local auth --a-ip-address=192.168.100.229 | ||
| + | ipa dnsrecord-show teach.local auth | ||
| + | выпускаем сертификат для web интерфейса | ||
| + | kubectl apply -f ../certificate-auth.yaml | ||
| + | |||
| + | Ставим dex-k8s-authenticator | ||
| + | |||
| + | helm repo add wiremind https://wiremind.github.io/wiremind-helm-charts | ||
| + | helm repo update | ||
| + | helm pull wiremind/dex-k8s-authenticator | ||
| + | |||
| + | tar -xvf dex-k8s-authenticator-1.7.0.tgz | ||
| + | |||
| + | cd dex-k8s-authenticator | ||
| + | |||
| + | vi values.yaml | ||
| + | |||
| + | в вэлюсах указываем наши данные | ||
| + | вначале копируем содержимое файла certificate-auth.txt | ||
| + | |||
| + | secret: тут должен совпадать с секретов в values dex | ||
| + | далее добавить CA Freeipa в файл конфига | ||
| + | caCerts: | ||
| + | enabled: true | ||
| + | secrets: | ||
| + | - name: ipa-ca | ||
| + | filename: ipa-ca.crt | ||
| + | value: |- | ||
| + | |||
| + | |||
| + | идем на сервер Freeipa | ||
| + | выполняем команду чтобы получить серт в base64 | ||
| + | cat /etc/ipa/ca.crt | base64 -w0 > ipa-ca.b64 | ||
| + | |||
| + | |||
| + | в k8s_ca_pem: | ||
| + | указываем CA кубернетис | ||
| + | идем на ноду мастера k8s | ||
| + | |||
| + | cat /etc/kubernetes/ssl/ca.crt | ||
| + | |||
| + | helm upgrade --install dex-auth . -n dex-auth -f values.yaml | ||
| + | |||
| + | Теперь нас необходимо связать группы в k8s и freeipa | ||
| + | Применяем манифесты для добавления кластер ролей | ||
| + | |||
| + | kubectl create ns core-test-admin | ||
| + | kubectl create ns core-test-view | ||
| + | |||
| + | проверяем заходим на https://auth.teach.local/ | ||
| + | |||
| + | пробуем залогинится под польз кто админ | ||
| + | |||
| + | Заходим на другой сервер и проверяем команды которые нам дал dex | ||
| + | |||
| + | ssh root@192.168.100.227 | ||
| + | |||
| + | Вводим команды из web интерфейса | ||
| + | |||
| + | И пробуем подключиться | ||
| + | |||
| + | Далее запускаем тестовые поды в наших ns | ||
| + | |||
| + | Применяем файл test-pod.txt | ||
| + | |||
| + | И тестируем права | ||
| + | |||
| + | Пробуем заходить разными польз | ||
| + | |||
| + | Если не пускает польз когда у него права только на один ns то надо дать права на просмотр. | ||
| + | </code> | ||
| + | |||
| + | === Файлы === | ||
| + | <code> | ||
| + | === role.txt === | ||
| + | apiVersion: rbac.authorization.k8s.io/v1 | ||
| + | kind: ClusterRoleBinding | ||
| + | metadata: | ||
| + | name: k8s-admins | ||
| + | subjects: | ||
| + | - kind: Group | ||
| + | name: k8s-cluster-admins # ИМЯ ГРУППЫ из FreeIPA | ||
| + | apiGroup: rbac.authorization.k8s.io | ||
| + | roleRef: | ||
| + | kind: ClusterRole | ||
| + | name: cluster-admin | ||
| + | apiGroup: rbac.authorization.k8s.io | ||
| + | --- | ||
| + | # 1) Админы НС core-test-admin | ||
| + | apiVersion: rbac.authorization.k8s.io/v1 | ||
| + | kind: RoleBinding | ||
| + | metadata: | ||
| + | name: core-test-admin-admins | ||
| + | namespace: core-test-admin | ||
| + | subjects: | ||
| + | - kind: Group | ||
| + | name: k8s-ns-core-test-admin # ГРУППА из токена Dex / FreeIPA | ||
| + | apiGroup: rbac.authorization.k8s.io | ||
| + | roleRef: | ||
| + | kind: ClusterRole | ||
| + | name: admin # встроенная роль с полным доступом в ns | ||
| + | apiGroup: rbac.authorization.k8s.io | ||
| + | --- | ||
| + | # 2) Вьюеры НС core-test-view | ||
| + | apiVersion: rbac.authorization.k8s.io/v1 | ||
| + | kind: RoleBinding | ||
| + | metadata: | ||
| + | name: core-test-view-viewers | ||
| + | namespace: core-test-view | ||
| + | subjects: | ||
| + | - kind: Group | ||
| + | name: k8s-ns-core-test-view # ГРУППА для read-only в этом ns | ||
| + | apiGroup: rbac.authorization.k8s.io | ||
| + | roleRef: | ||
| + | kind: ClusterRole | ||
| + | name: view # встроенная read-only роль для ns | ||
| + | apiGroup: rbac.authorization.k8s.io | ||
| + | --- | ||
| + | # 3) Read-only по всему кластеру для группы k8s-ns | ||
| + | apiVersion: rbac.authorization.k8s.io/v1 | ||
| + | kind: ClusterRoleBinding | ||
| + | metadata: | ||
| + | name: k8s-ns-read-all | ||
| + | subjects: | ||
| + | - kind: Group | ||
| + | name: k8s-ns # ГРУППА "общие зрители k8s" | ||
| + | apiGroup: rbac.authorization.k8s.io | ||
| + | roleRef: | ||
| + | kind: ClusterRole | ||
| + | name: view # cluster-wide view | ||
| + | apiGroup: rbac.authorization.k8s.io | ||
| + | |||
| + | |||
| + | kubectl apply -f k8s-admins-rbac.yaml | ||
| + | kubectl get clusterrolebinding k8s-admins -o yaml | ||
| + | |||
| + | === issuer.txt === | ||
| + | apiVersion: cert-manager.io/v1 | ||
| + | kind: ClusterIssuer | ||
| + | metadata: | ||
| + | name: freeipa-acme | ||
| + | spec: | ||
| + | acme: | ||
| + | email: admin@teach.local | ||
| + | server: https://ipa-server-1.teach.local/acme/directory | ||
| + | privateKeySecretRef: | ||
| + | name: freeipa-acme-account-key | ||
| + | |||
| + | # ВАЖНО: одна длинная строка из ipa-ca.b64 | ||
| + | caBundle: ПОДСТАВЬ_ЗДЕСЬ_СТРОКУ_ИЗ_ipa-ca.b64 | ||
| + | |||
| + | solvers: | ||
| + | - http01: | ||
| + | ingress: | ||
| + | ingressClassName: nginx | ||
| + | |||
| + | === dex-auth-values.txt === | ||
| + | global: | ||
| + | deployEnv: dev | ||
| + | |||
| + | image: | ||
| + | repository: mintel/dex-k8s-authenticator | ||
| + | tag: 1.4.0 | ||
| + | pullPolicy: Always | ||
| + | |||
| + | dexK8sAuthenticator: | ||
| + | port: 5555 | ||
| + | debug: true | ||
| + | web_path_prefix: / | ||
| + | clusters: | ||
| + | - name: teach-cluster | ||
| + | short_description: "Учебный кластер" | ||
| + | description: "Kubespray кластер teach.local" | ||
| + | |||
| + | # ДОЛЖНО совпадать с issuer у Dex и --oidc-issuer-url у kube-apiserver | ||
| + | issuer: https://dex.teach.local/dex | ||
| + | |||
| + | # Адрес API-сервера кластера | ||
| + | k8s_master_uri: https://192.168.100.230:6443 | ||
| + | |||
| + | # Те же client_id / secret, что в staticClients Dex | ||
| + | client_id: dex-k8s-authenticator | ||
| + | client_secret: 9f3b2e3a7a8d14d0a5ff4b1b8e92c3de0b6b7f21a4e59bd8c1f3d2e7c8a9b0c1 | ||
| + | |||
| + | # ДОЛЖНО 1-в-1 совпадать с redirectURIs в Dex для этого клиента | ||
| + | | ||
| + | redirect_uri: https://auth.teach.local/callback | ||
| + | |||
| + | k8s_ca_pem: | | ||
| + | -----BEGIN CERTIFICATE----- | ||
| + | ТУТ ЦЕЛИКОМ СОДЕРЖИМОЕ /etc/kubernetes/ssl/ca.crt | ||
| + | БЕЗ base64, как есть | ||
| + | -----END CERTIFICATE----- | ||
| + | |||
| + | service: | ||
| + | type: ClusterIP | ||
| + | port: 5555 | ||
| + | |||
| + | ingress: | ||
| + | enabled: true | ||
| + | ingressClassName: nginx | ||
| + | annotations: {} | ||
| + | labels: {} | ||
| + | |||
| + | # В ЭТОМ чарте path один, для всех хостов: | ||
| + | path: / | ||
| + | |||
| + | # hosts — это СПИСОК СТРОК, а не объектов: | ||
| + | hosts: | ||
| + | - auth.teach.local | ||
| + | |||
| + | tls: | ||
| + | - secretName: dex-auth-tls | ||
| + | hosts: | ||
| + | - auth.teach.local | ||
| + | |||
| + | # Чтобы убрать x509: unknown authority для Dex (FreeIPA CA) | ||
| + | caCerts: | ||
| + | enabled: true | ||
| + | secrets: | ||
| + | - name: ipa-ca | ||
| + | filename: ipa-ca.crt | ||
| + | value: |- | ||
| + | ТУТ_ОДНА_СТРОКА_BASE64_ОТ /etc/ipa/ca.crt | ||
| + | |||
| + | === config.txt === | ||
| + | replicaCount: 1 | ||
| + | |||
| + | image: | ||
| + | repository: ghcr.io/dexidp/dex | ||
| + | tag: v2.44.0 | ||
| + | pullPolicy: IfNotPresent | ||
| + | |||
| + | service: | ||
| + | type: ClusterIP | ||
| + | ports: | ||
| + | http: | ||
| + | port: 5556 | ||
| + | |||
| + | https: | ||
| + | enabled: false | ||
| + | |||
| + | grpc: | ||
| + | enabled: false | ||
| + | |||
| + | rbac: | ||
| + | create: true | ||
| + | |||
| + | podDisruptionBudget: | ||
| + | enabled: false | ||
| + | |||
| + | networkPolicy: | ||
| + | enabled: false | ||
| + | |||
| + | autoscaling: | ||
| + | enabled: false | ||
| + | |||
| + | ingress: | ||
| + | enabled: true | ||
| + | className: nginx | ||
| + | hosts: | ||
| + | - host: dex.teach.local | ||
| + | paths: | ||
| + | - path: /dex/ | ||
| + | pathType: ImplementationSpecific | ||
| + | tls: | ||
| + | - secretName: dex-tls # СЕКРЕТ, который делает cert-manager/FreeIPA | ||
| + | hosts: | ||
| + | - dex.teach.local | ||
| + | |||
| + | configSecret: | ||
| + | create: true | ||
| + | |||
| + | serviceMonitor: | ||
| + | enabled: false | ||
| + | |||
| + | serviceAccount: | ||
| + | create: true # или false, если не нужен отдельный SA | ||
| + | name: "" # пусто = helm сам сгенерит имя по release | ||
| + | annotations: {} | ||
| + | |||
| + | config: | ||
| + | issuer: https://dex.teach.local/dex | ||
| + | |||
| + | storage: | ||
| + | type: kubernetes | ||
| + | config: | ||
| + | inCluster: true | ||
| + | type: memory | ||
| + | |||
| + | web: | ||
| + | http: 0.0.0.0:5556 | ||
| + | |||
| + | logger: | ||
| + | level: debug | ||
| + | format: text | ||
| + | |||
| + | oauth2: | ||
| + | skipApprovalScreen: true | ||
| + | |||
| + | staticClients: | ||
| + | - id: example-app | ||
| + | name: Example App | ||
| + | secret: example-app-secret | ||
| + | redirectURIs: | ||
| + | - http://127.0.0.1:5555/callback | ||
| + | - id: dex-k8s-authenticator | ||
| + | name: Dex K8s Authenticator | ||
| + | secret: SUPER-SECRET-STRING | ||
| + | redirectURIs: | ||
| + | - https://auth.teach.local/callback | ||
| + | |||
| + | |||
| + | connectors: | ||
| + | - type: ldap | ||
| + | id: freeipa | ||
| + | name: FreeIPA | ||
| + | config: | ||
| + | host: 192.168.100.252:389 | ||
| + | insecureNoSSL: true | ||
| + | startTLS: false | ||
| + | |||
| + | bindDN: "uid=dex-bind,cn=users,cn=accounts,dc=teach,dc=local" | ||
| + | bindPW: "123456" | ||
| + | |||
| + | userSearch: | ||
| + | baseDN: "cn=users,cn=accounts,dc=teach,dc=local" | ||
| + | filter: "(objectClass=person)" | ||
| + | emailAttr: "mail" | ||
| + | idAttr: "uid" | ||
| + | nameAttr: "cn" | ||
| + | username: "uid" | ||
| + | |||
| + | groupSearch: | ||
| + | baseDN: "cn=groups,cn=accounts,dc=teach,dc=local" | ||
| + | filter: "(objectClass=groupofnames)" | ||
| + | userAttr: "dn" | ||
| + | groupAttr: "member" | ||
| + | nameAttr: "cn" | ||
| + | |||
| + | |||
| + | === certificate-auth.txt === | ||
| + | apiVersion: cert-manager.io/v1 | ||
| + | kind: Certificate | ||
| + | metadata: | ||
| + | name: dex-auth-cert | ||
| + | namespace: dex-auth | ||
| + | spec: | ||
| + | secretName: dex-auth-tls | ||
| + | dnsNames: | ||
| + | - auth.teach.local | ||
| + | issuerRef: | ||
| + | name: freeipa-acme | ||
| + | kind: ClusterIssuer | ||
| + | |||
| + | === certificate.txt === | ||
| + | apiVersion: cert-manager.io/v1 | ||
| + | kind: Certificate | ||
| + | metadata: | ||
| + | name: dex-cert | ||
| + | namespace: dex | ||
| + | spec: | ||
| + | secretName: dex-tls | ||
| + | dnsNames: | ||
| + | - dex.teach.local | ||
| + | issuerRef: | ||
| + | name: freeipa-acme | ||
| + | kind: ClusterIssuer | ||
| + | |||
| + | === test-pod.txt === | ||
| + | apiVersion: v1 | ||
| + | kind: Pod | ||
| + | metadata: | ||
| + | name: admin-nginx | ||
| + | namespace: core-test-admin | ||
| + | labels: | ||
| + | app: admin-nginx | ||
| + | spec: | ||
| + | containers: | ||
| + | - name: nginx | ||
| + | image: nginx:1.27 | ||
| + | ports: | ||
| + | - containerPort: 80 | ||
| + | --- | ||
| + | apiVersion: v1 | ||
| + | kind: Pod | ||
| + | metadata: | ||
| + | name: view-nginx | ||
| + | namespace: core-test-view | ||
| + | labels: | ||
| + | app: view-nginx | ||
| + | spec: | ||
| + | containers: | ||
| + | - name: nginx | ||
| + | image: nginx:1.27 | ||
| + | ports: | ||
| + | - containerPort: 80 | ||
| </code> | </code> | ||