«Автомагия» между Vault Secrets Engine, Kubernetes и GitLab Pipelines.
Вступление
Если вы когда-либо пытались модернизировать процессы и инфраструктуру крупной компании, вы, вероятно, столкнетесь с недостаточным управлением конфигурациями / секретами или их отсутствием. Подумайте о .env
файле в корне проекта с общими учетными данными базы данных в производственной среде, звучит знакомо? Если нет, то вы в хорошем месте.
Я был и веб-разработчиком, и инженером DevOps. Как разработчик программного обеспечения, я хочу сосредоточиться только на своем приложении, и мне не нужны внешние инструменты. Как инженер DevOps, я хочу автоматизировать процесс и хранить секреты в безопасности. Если секреты скомпрометированы, максимально уменьшите риск.
Это мой самоуверенный способ автоматического развертывания приложений с ротацией секретов в масштабируемую инфраструктуру.
Требования
Существует разделение ответственности разработчиков перед DevOps. В контексте конфигураций и секретов у меня есть несколько требований:
- Мое приложение должно считывать конфигурационные и секретные переменные из файла. Сделайте это простым и понятным.
- Только мое приложение попадет в
Dockerfile
. Никакие инструменты управления конфигурацией не будут включены вDockerfile
. - Когда появится лучший инструмент управления секретами, я смогу легко его заменить.
Инструменты
оркестровка контейнеров:
Kubernetes (K8s) - это система с открытым исходным кодом для автоматизации развертывания, масштабирования и управления контейнерными приложениями.
Конвейеры CI / CD
Gitlab Pipelines - это компонент верхнего уровня непрерывной интеграции, доставки и развертывания.
Секреты и управление конфигурацией
Hashicorp Vault: переход к управлению секретами и защите данных в динамической инфраструктуре.
Секреты и загрузчик конфигурации: confd (Preference) или consul-template. Я покажу обе реализации.
Здесь нет логотипов 🙁
Confd - это легкий инструмент управления конфигурацией, ориентированный на поддержание актуальности локальных файлов конфигурации с использованием данных.
Consul-template: этот проект предоставляет удобный способ заполнения значений из Consul в файловой системе с помощью consul-template
daemon.
Снижение риска
Путь получения учетных данных
- Учетная запись службы Kubernetes JWT передается методу аутентификации Kubernetes в Vault.
- После того, как Vault проверяет JWT с помощью API проверки токенов, возвращается токен Vault с TTL. Этот токен хранилища может создавать новые учетные данные для базы данных и обновлять собственный TTL.
- Маркер хранилища используется для создания новых учетных данных базы данных с TTL и записывается в
.env
для использования приложением.
Стратегии
Переход от безопасного к более безопасному по порядку:
Один контейнер
Приложение и инструмент, используемые для получения секретов, упакованы в один контейнер.
Плюсы: приложение не знает ни одного инструмента управления конфигурацией и считывает файл .env
.
Минусы: если контейнер скомпрометирован - учетные данные базы данных, адрес хранилища, токен хранилища и служебная учетная запись JWT также будут скомпрометированы. Во-вторых, это нарушает одно из требований. Dockerfile
- это не просто приложение, у него есть дополнительный инструмент для извлечения секретов.
Контейнер приложения с боковой панелью
Это улучшает подход одного контейнера. Приложение и инструмент, используемый для получения секретов, разделены. Они используют только том, содержащий учетные данные.
Плюсы: контейнер приложения содержит только приложение. Если контейнер приложения скомпрометирован, только учетные данные базы данных находятся под угрозой, пока не истечет TTL. Токен Vault и сервисная учетная запись JWT не скомпрометированы, потому что они находятся только во вспомогательном элементе.
Минусы: Sidecar по-прежнему держит сервисный аккаунт JWT без причины. Если злоумышленник овладевает JWT, он / она может продолжать генерировать токены Vault.
InitContainer, App Container и Sidecar (предпочтительно)
InitContainer будет использовать служебную учетную запись JWT для аутентификации, передать токен Vault на общий том для использования сопроводительным файлом и умереть. Затем запустятся контейнер приложения и контейнер sidecar. Контейнер sidecar будет иметь токен Vault и будет отвечать за получение учетных данных и обновление общего тома по истечении TTL.
Плюсы: все, что взломано, имеет TTL.
Этот подход был первоначально продемонстрирован Сетом Варго в его Vault Kubernetes Workshop.
Предпосылки
Сейф должен быть включен и работает. Неважно, какой бэкэнд вы используете, и не имеет значения, где находится ваш экземпляр. У меня есть Vault, настроенный на GKE с внутренним балансировщиком нагрузки с частной записью DNS. Если у вас нет экземпляра, вы можете следить за моей записью о настройке Vault.
Вам понадобится кластер Kubernetes, который может получить доступ к вашему экземпляру Vault. Я еще не написал об этом. Я предпочитаю использовать Terraform для развертывания кластера GKE. Инфраструктура как код - это хорошо!
Создавайте экземпляр MySQL где угодно. Пока Vault и ваш кластер могут его задействовать, все в порядке. Я использую CloudSQL от GCP и отлично работаю.
Настройте Vault с помощью механизма секретов базы данных, политики и метода аутентификации Kubernetes. Вы можете следить за моей записью.
Наконец, проект GitLab. Я использую автономный GitLab, но gitlab.com работает так же. Я предоставлю вам исходный код.
Реализация
Это все части головоломки, порядок не имеет большого значения. Как только все будет на месте, все участки трубопровода будут использовать поток.
Приложение
Не стесняйтесь использовать собственное приложение, в любом случае нас интересует только файл конфигурации. Важно иметь репозиторий в GitLab. Вот пример приложения nodejs, которое я написал для демонстрации реализации:
Этот проект ожидает .env
файл в корне проекта и должен содержать учетные данные для нашего подключения к базе данных. Пример:
MYSQL_URL=mysql://username:[email protected]/mysql
Интеграция с GitLab Kubernetes
Разверните свой экземпляр Kubernetes и настройте проект GitLab для связи с вашим кластером. Следуйте документации GitLab по добавлению существующего кластера Kubernetes. Если вы следовали моему руководству по настройке Kubernetes Auth в Vault, убедитесь, что пространство имен равно demo
.
Добавьте базовый домен в свое общественное достояние.
GitLab предлагает управляемые приложения для установки в ваш кластер. Вам не требуется устанавливать какие-либо из этих приложений через GitLab, вместо этого вы можете установить Helm Tiller вручную. Я установил через GitLab следующее:
- Helm Tiller - менеджер пакетов для Kubernetes.
- Ingress - устанавливает контроллер входящего трафика nginx и запрашивает
LoadBalancer
службу. Дождитесь IP-адреса входящей конечной точки и укажите свой DNS-адрес на этот адрес. Возможно, вам придется подождать несколько минут, пока DNS обновится, перед следующим приложением. - Cert-Manager - собственный контроллер управления сертификацией Kubernets.
- Прометей - мониторинг с открытым исходным кодом.
Трубопровод
Мы будем использовать AutoDevOps GitLab, и нашему проекту не потребуется .gitlab-ci.yml
. AutoDevOps - это не что иное, как набор предопределенных шаблонов и заданий в соответствии с определенным набором этапов. Если AutoDevops не включен по умолчанию, следуйте инструкциям GitLab Docs. На момент написания этой статьи параметр находился здесь: Настройки ›CI / CD› Auto DevOps
AutoDevops использует переменные среды CI / CD для включения / отключения определенных заданий. Это можно найти здесь: Настройки ›CI / CD и разверните Переменные. Давайте отключим все, что нам сейчас не нужно.
- TEST_DISABLED =
1
- PERFORMANCE_DISABLED =
1
- CODE_QUALITY_DISABLED =
1
- CONTAINER_SCANNING_DISABLED =
1
- DEPENDENCY_SCANNING_DISABLED =
1
- DAST_DISABLED =
1
- SAST_DISABLED =
1
- LICENSE_MANAGEMENT_DISABLED =
1
- POSTGRES_ENABLED =
false
Полный список переменных среды AutoDevops можно найти здесь.
Пользовательское приложение для автоматического развертывания
AutoDevOps GitLab использует общую диаграмму управления под названием auto-deploy-app
. В диаграмме отсутствуют некоторые функции, а именно возможность добавлять контейнеры initContainers / sidecar, общее пространство имен процессов и монтирование томов. У меня есть мерж-реквест для GitLab, но он не подходит для многих. Продолжайте и настаивайте на этом, если вы сочтете это полезным.
А пока у меня есть разветвленная версия с нужным функционалом. Чтобы использовать мой репозиторий helm вместо репозитория GitLab, добавьте следующую переменную CI / CD:
- AUTO_DEVOPS_CHART_REPOSITORY =
https://jackalus.gitlab.io/charts
Для прозрачности вот настраиваемая диаграмма:
… И мой импровизированный репозиторий руля:
Ценности Helm
Нам нужно будет обновить некоторые значения диаграммы auto-deploy-app
. Вместо набора встроенных --set
мы добавим файл с именем helm_values.yaml
и добавим следующую переменную CI / CD:
- HELM_UPGRADE_EXTRA_ARGS =
-f helm_values.yaml
Вот хороший материал. Содержимое helm_values.yaml.
consul-template
vs confd
consul-template был создан Hashicorp. Он поддерживает только магазин Consul KV и Vault. confd был создан Келси Хайтауэр. Он поддерживает гораздо больше сервисов, но не может использовать более одного источника в конфигурации. Единственная разница в реализации - это коляска. Я покажу вам обоих. Если требуется более подробная информация, напишите мне или оставьте комментарий.
helm_values.yaml
с использованием шаблона консула
initContainers: - name: vault-authenticator image: sethvargo/vault-kubernetes-authenticator:0.2.0 imagePullPolicy: IfNotPresent volumeMounts: - name: vault-token mountPath: /var/run/secrets/vaultproject.io - name: vault-tls mountPath: /etc/vault/tls env: - name: VAULT_ADDR valueFrom: configMapKeyRef: name: vault key: vault_addr - name: VAULT_CACERT value: /etc/vault/tls/ca.pem - name: VAULT_ROLE value: demo additionalContainers: - name: consul-template image: hashicorp/consul-template:0.20.0-light imagePullPolicy: IfNotPresent securityContext: capabilities: add: ['SYS_PTRACE'] volumeMounts: - name: secrets mountPath: /secrets - name: vault-tls mountPath: /etc/vault/tls - name: vault-token mountPath: /var/run/secrets/vaultproject.io env: - name: VAULT_ADDR valueFrom: configMapKeyRef: name: vault key: vault_addr - name: VAULT_CACERT value: /etc/vault/tls/ca.pem - name: CT_LOCAL_CONFIG value: | vault { vault_agent_token_file = "/var/run/secrets/vaultproject.io/.vault-token" ssl { ca_cert = "/etc/vault/tls/ca.pem" } retry { backoff = "1s" } } template { contents = <<EOH {{- with secret "database/creds/demo-role" }} MYSQL_URL=mysql://{{ .Data.username }}:{{ .Data.password }}@demodb.stage.jackalus.dev/mysql {{ end }} EOH destination = "/secrets/.env" command = "/bin/sh -c \"kill -HUP $(pidof node index.js) || true\"" } volumes: - name: secrets emptyDir: {} - name: vault-tls secret: secretName: vault-tls - name: vault-token emptyDir: medium: Memory application: command: [/bin/sh, "-c", "ln -s /secrets/.env /app/.env; node index.js"] volumeMounts: - name: secrets mountPath: /secrets/ service: internalPort: 8080 externalPort: 8080 livenessProbe: path: version readinessProbe: path: version shareProcessNamespace: true serviceAccountName: vault
helm_values.yaml
с использованием confd
initContainers: - name: vault-authenticator image: sethvargo/vault-kubernetes-authenticator:0.2.0 imagePullPolicy: IfNotPresent volumeMounts: - name: vault-token mountPath: /var/run/secrets/vaultproject.io - name: vault-tls mountPath: /etc/vault/tls env: - name: VAULT_ADDR valueFrom: configMapKeyRef: name: vault key: vault_addr - name: VAULT_CACERT value: /etc/vault/tls/ca.pem - name: VAULT_ROLE value: demo additionalContainers: - name: confd image: jackalus/confd imagePullPolicy: Always securityContext: capabilities: add: ['SYS_PTRACE'] command: ["/bin/sh"] args: - -c - | export VAULT_TOKEN=$(cat /var/run/secrets/vaultproject.io/.vault-token) echo "backend = \"vault\" auth_type = \"token\" auth_token = \"$VAULT_TOKEN\" interval = 600 nodes = [ \"$VAULT_ADDR\" ]" > /etc/confd/confd.toml && echo '[template] src = "test.conf.tmpl" dest = "/secrets/.env" keys = [ "/database/creds/demo-role", ]' > /etc/confd/conf.d/test.toml && echo '{{ $data := json (getv "/database/creds/demo-role") }} MYSQL_URL=mysql://{{ $data.username }}:{{ $data.password }}@demo2db.stage.jackalus.dev/mysql'> /etc/confd/templates/test.conf.tmpl while sleep 300s; do vault token renew; done & confd volumeMounts: - name: secrets mountPath: /secrets - name: vault-tls mountPath: /etc/ssl/certs - name: vault-token mountPath: /var/run/secrets/vaultproject.io env: - name: VAULT_ADDR valueFrom: configMapKeyRef: name: vault key: vault_addr - name: VAULT_CACERT value: /etc/ssl/certs/ca.pem volumes: - name: secrets emptyDir: {} - name: vault-tls secret: secretName: vault-tls - name: vault-token emptyDir: medium: Memory application: command: [/bin/sh, "-c", "ln -s /secrets/.env /app/.env; node index.js"] volumeMounts: - name: secrets mountPath: /secrets/ service: internalPort: 8080 externalPort: 8080 livenessProbe: path: version readinessProbe: path: version shareProcessNamespace: true serviceAccountName: vault
Примечание. Я использую свою разветвленную версию confd с двоичным файлом клиента Vault. Это так, чтобы я мог продлить свой токен в одном контейнере. В качестве альтернативы я могу исключить клиент Vault из контейнера confd и добавить контейнер для продления токена Vault. Я полагаю, это сбивает с толку.
разветвленный confd Dockerfile
:
FROM golang:1.10.2-alpine ARG CONFD_VERSION=0.16.0 ADD https://github.com/kelseyhightower/confd/archive/v${CONFD_VERSION}.tar.gz /tmp/ RUN apk add --no-cache \ bzip2 \ make && \ mkdir -p /go/src/github.com/kelseyhightower/confd && \ cd /go/src/github.com/kelseyhightower/confd && \ tar --strip-components=1 -zxf /tmp/v${CONFD_VERSION}.tar.gz && \ go install github.com/kelseyhightower/confd && \ rm -rf /tmp/v${CONFD_VERSION}.tar.gz && \ mkdir -p /etc/confd/conf.d /etc/confd/templates COPY --from=vault /bin/vault /bin/vault CMD confd
Что у нас есть?
Учетная запись службы Kubernetes JWT доступна только в initContainer
. После создания токена Vault initContainer
умирает, и JWT не может быть скомпрометирован.
У каждого модуля будет свой уникальный токен хранилища с истечением срока действия. Жетоны автоматически обновляются, чтобы токены оставались живыми в течение всего срока службы контейнера. Токены не отображаются в контейнере приложения.
Каждый экземпляр приложения будет иметь свой собственный набор учетных данных базы данных. Учетные данные меняются по временному интервалу. Если учетные данные каким-то образом были скомпрометированы, доступ не будет бесконечным. Мы знаем модуль, который был взломан, и можем отозвать набор учетных данных.
Трубопровод
Выход
2019-08-23T00:16:43Z review-confd-pad63a-86c459744b-942jr confd[21]: INFO Target config /secrets/.env out of sync 2019-08-23T00:16:43Z review-confd-pad63a-86c459744b-942jr confd[21]: INFO Target config /secrets/.env has been updated Key Value --- ----- token s.VMUglVotKPB3xxjvstK8pLn5 token_accessor qNXDV2bF8rdPvle23Cbg9KIR token_duration 1h token_renewable true token_policies ["default" "demo-db-r"] identity_policies [] policies ["demo-db-r"] token_meta_service_account_secret_name vault-token-xcbzv token_meta_service_account_uid 6f552115-c527-11e9-b42d-42010aa8012d token_meta_role demo token_meta_service_account_name vault token_meta_service_account_namespace demo 2019-08-23T00:26:43Z review-confd-pad63a-86c459744b-942jr confd[21]: INFO /secrets/.env has md5sum 928696bacbd204577416b14afbad5c52 should be 0dd32ddc558166e3a50e91a5a412d04a 2019-08-23T00:26:43Z review-confd-pad63a-86c459744b-942jr confd[21]: INFO Target config /secrets/.env out of sync 2019-08-23T00:26:43Z review-confd-pad63a-86c459744b-942jr confd[21]: INFO Target config /secrets/.env has been updated
Проверить приложение
Конечная точка /mysql
подключается к базе данных mysql и выполняет следующий запрос: SELECT @@version
Нажатие на эту конечную точку доказывает, что у нас есть успешное соединение.
Следующие шаги
Эта статья предназначена только для начала. Здесь нужно сделать много улучшений.
Чтобы обеспечить это еще больше, мы можем добавить в смесь Istio и SPIFFE. Это позволит получить доступ к базе данных только в том случае, если ваш маршрут был прописан, даже если у вас есть учетные данные базы данных. Если будет достаточно интереса, я сделаю рецензию.