Всем привет. Меня зовут Добрый Кот Telegram(https://t.me/Dobry_kot).
От коллектива DK-Consulting и при поддержке Telegram(https://t.me/irbgeo): Продолжаем серию статей по сертификатам k8s.
В этой статье я опишу, как мы решили следующие задачи:
Централизовать выпуск сертификатов через Vault.
Причины: в связи с которыми это необходимо, описаны в выводах статьи:
** хранение приватных ключей CA на нодах - следовательно риск потенциальной кражи ключа.
Настроить контроль за сроком годности сертификатов и обеспечить их своевременный перевыпуск.
Сформировать для каждого сертификата vault role (спецификацию, т.е. какой сертификат можно заказывать данной ролью).
Сформировать ACL для Vault role (П.3 ПУНКТ ^).
Итак. Начну сразу с картинок:
На рисунке ниже приведена стандартная организация сертификатов k8s, которая была описана в статье.
Для выполнения первой задачи была придумана централизованная схема организации сертификатов.
* internal CA говорит о том, что vault не передает приватный ключ.
Для начала мы сформировали сейф PKI для Root CA и от него ответвил еще три для:
ETCD
Front-proxy
Kubernetes
Так же сформировал отдельный сейф KV-STORE в который, на этапе формирования кластера, помещаю приватный/публичный ключ для подписания токенов от service-account.
Дальше в рамках спецификации сформировал 12 ролей для каждого выпускаемого сертификата. В рамках одной роли разрешено выпускать сертификаты только с теми атрибутами, которые минимально достаточны для корректной работы каждой компоненты k8s кластера.
Спецификацию можно найти вот тут.
Базовые разъяснения вот тут.
Какой сертификат куда идет, описано тут
При работе над решением появились следующие вопросы:
Кто будет выпускать сертификаты?
В какой момент времени: до, после, во время создания/настройки ВМ?
Кто будет следить за сертификатами и перевыпускать их?
Как создать сертификат с нужными ipSans, если адреса ЛБ и виртуальных машин заранее не известны?
Как лимитировать доступ к Vault role?
Как передавать токены для авторизации?
Вопросов много, ответов мало…
Относительно проблемы П.2: для себя решили, чтобы тулинг заказывал сертификаты изнутри ВМ, сразу как она поднялась. Однако, этот подход порождает проблему доставки токена-доступа тулинга в виртуальную машину. Действительно, в рамках препроцессинга токен можно передать только через cloud-init или альтернативные схемы - planeText. И это дыра. Можно использовать IAM облака, хотя данный подход рассматривался только в YCloud и не имеет на данный момент терраформ модуля.
Рассмотрим, как можно митигировать риски использования открытого ключа:
Использовать CIDR для vault role, с которых может запрашиваться сертификат.
Время жизни токена - 1 использование.
Каждой виртуальной машине - отдельный токен
Один токен имеет доступ только к одной роли. *в ходе эксплуатации выявили, что логичнее использовать один токен на ВМ с доступом к ролям для создания “control plane” в рамках мастер нод или добавления воркера в кластер.
Можно также использовать approle, но тогда стоит вопрос доставки secret-id/role-id. *(На практике пришли к выводу, что одного токена для запроса approle secret_id/role_id достаточно)
*Цифрами обозначены уникальные ресурсы Vault.
** После долгих размышлений поняли, что первый уровень токенов можно сократить до трех, по одному на ВМ.
1-й уровень временный токен с ограниченным временем жизни
2-й уровень - описание токена (рисунок будущего токена *шаблон)
3-й уровень - политика доступа для прочтения secret-id/role-id у appRole
4-й уровень - аппроль, которая имеет secret-id/role-id для авторизации
5-й уровень - политика доступа к ролям от сертификатов.
6-й уровень - сами роли от сертификатов, в которых прописана спецификация, какой сертификат можно выпустить
Размышления:
Для выпуска сертификатов был выбор между шаблонами vault c кроном или самописным инструментом. Выбор пал на самописный по ряду причин:
При препроцессинге много неизвестных, которые надо добавить в сертификат при заказе:
ip address нод
ip address ЛБ
fqdn балансера
fqdn ноды
Не хотелось использовать bash скрипты в проекте.
Собственная разработка поможет больше разобраться в нюансах инструмента Vault (но это как приятная вишенка на торте)
Таким образом, мы реализовали первую версию инструмента под названием key-keeper.
Key-keeper:
Инструмент старались сделать максимально «User Friendly».
Основной функционал:
Выпуск/перевыпуск сертификатов.
Чтение секретов и сохранение их в файловой системе.
Чтение CA (public key) и сохранение их в файловой системе.
Умеет выполнять shell команду после перевыпуска сертификата=)) (Подходит для случаев, когда бекенд не умеет отслеживать актуальность сертификата в памяти и в файловой системе)
За основу схемы конфига, взяли всем привычный манифест от certmanager и немного расширили.
Настройка:
Настройка представляет из себя 2 инструкции (issuer и certificate/secret)
В описание issuer (*входной билет) описано:
инструкция для доступа к Vault server
инструкция для доступа к appRole
инструкция для доступа к vault role
---
issuers:
- name: kubernetes-ca
vault:
server: http://example.com:9200
auth:
caBundle:
tlsInsecure: true
bootstrap:
token: ${token} # <- или
path: /tmp/bootstrap-token # <- или
appRole:
name: kubernetes-sa
path: "clusters/cluster-1/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kubernetes-ca/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kubernetes-ca/role-id
resource:
kv:
path: "clusters/cluster-1/kv"
** Доступ на данный момент осуществляется через метод авторизации appRole, bootstrap.token используется только для получения secret_id и role_id, если они не созданы в системе заранее.
Описание заказываемого сертификата:
имя используемого issuer;
аргументы, требуемые для сертификата (ipSans,AltNames,Usages etc.);
время жизни сертификата и когда надо перезапрашивать;
путь, по которому сохраняется сертификат;
триггер команда при перевыпуске сертификата.
certificates:
- name: kubernetes-ca
issuerRef:
name: kubernetes-ca
isCa: true
ca:
exportedKey: false
generate: false
hostPath: "/etc/kubernetes/pki/ca"
- name: kubelet-server
issuerRef:
name: kubelet-server
spec:
subject:
commonName: "system:node:master-0.cluster-1.example.com"
usage:
- server auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ipAddresses:
static:
- 1.1.1.1
###
# * -> Позволяет указывать регексп интерфейсов (на выходе получаем список)
interfaces:
- lo
- eth*
###
# * -> В цикле будет пытаться отрезолвить имя, без выходного значения, сертификат не будет заказан.
dnsLookup:
- api.example.com
ttl: 200h
###
# * -> Указав $HOSTNAME - hostname хоста добавится в поле AltNames сертификата.
hostnames:
- $HOSTNAME
- localhost
- "master-0.cluster-1.example.com"
renewBefore: 100h
hostPath: "/etc/kubernetes/pki/certs/kubelet"
Описание запрашиваемого секрета:
имя используемого issuer;
путь, по которому сохраняется секрет;
ключ обращения к секрету.
secrets:
- name: kube-apiserver-sa
issuerRef:
name: kube-apiserver-sa
key: public
hostPath: /etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver-sa.pub
Инструмент на данный момент закрывает 100% задач и может использоваться для выпуска и обновления сертификатов в кластере.
* Тестовый кластер прожил с использованием key-keeper 3 недели c ttl сертификата 10m и renew каждые 2 минуты (потом отключили).
** Приятной неожиданностью был тот факт, что все компоненты кластера k8s умеют в autoreload сертификатов, из-за чего функция тригеров не используется в проекте. (***autoreload компоненты куба запускают каждый раз, когда видят, что на файловой системе изменился файл с сертификатом/ключом)
Нюансы при проектировании схем в Vault:
Мы наткнулись на проблему, связанную с методом получения secret_id для appRole.
Проблема заключается в том, что метод запроса secret_id -> POST, а значит каждый запрос равнозначен смене secret_id. Когда хост использует одну appRole, инструмент отрабатывает корректно, но если хостов, например 3, то при создании каждая ВМ получит общий токен через cloud-init, для запроса secret_id и role_id, с которым key-keeper сделает login в vault и получит временный токен (срок жизни токена указываете сами). Key-keeper закажет все сертификаты, но как только токен протухнет, вы получите 403, т.к secret_id под ногами будет уже не валидный - его перезаписала другая ВМ. *УУУПС
Решается данная проблема через создание выделенных appRole под каждую ВМ. Получаем следующую схему ->
В данном случае у токена права только на GET role-id и POST secret-id, а у appRole - достаточные для логина и подписания сертификата.
ИТОГО:
В данной статье мы хотели бы затронуть еще кусок по созданию самого кластера, но материала там еще на одну статью, так что будет продолжение - соберем все кубики вместе.
На данный момент мы хотели акцентировать внимание на базовые проблемы перехода на централизованный PKI и показать, что все не так печально в этом направлении.
На данный момент, как заявлялось в итогах предыдущей статьи, мы закрыли вектор атаки - кражу приватных ключей CA кластера. Однако, остались дыры в безопасности:
etcd в базовой конфигурации не имеет политик доступа и имея сертификат типа client Auth, подписанный CA ETCD, можно получить доступ к данным кластера;
kube-apiserver-kubelet-client - сертификат имет расширенные привилегии типа system:masters (на данный момент нет понимания, как можно от них избавиться);
kubernetes-sa - ключ для генерации токенов в кластере (также имеет вектора атаки при краже).
* Надеюсь в последующих статьях- мы сможем поделиться успехами.
К сожалению, мы живем в мире, где даже от админов инфраструктуры надо обезопаситься и минимизировать риски утечек информации.
В следующей статье мы хотели бы рассказать о kubernetes-hardway, о том как можно разворачивать его быстро и удобно через терраформ и о том, что сетап куба – это самое простое, что может произойти с вами =) (самое ужасное, что с вами может случиться - это настройка окружения под базовые задачи: мониторинг, логирование, cni, SSO, и многое другое).
Особое внимание хочу уделить тому, что key-keeper не разрабатывался как альтернатива cert-manager, а работает сугобо в тех местах, где cert-manager применить нельзя.
Полезное чтиво:
key-keeper
kubernetes-hardway