«Автомагия» между 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-templatedaemon.

Снижение риска

Путь получения учетных данных

  1. Учетная запись службы Kubernetes JWT передается методу аутентификации Kubernetes в Vault.
  2. После того, как Vault проверяет JWT с помощью API проверки токенов, возвращается токен Vault с TTL. Этот токен хранилища может создавать новые учетные данные для базы данных и обновлять собственный TTL.
  3. Маркер хранилища используется для создания новых учетных данных базы данных с 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:

Для прозрачности вот настраиваемая диаграмма:



… И мой импровизированный репозиторий руля:



Ценности 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. Это позволит получить доступ к базе данных только в том случае, если ваш маршрут был прописан, даже если у вас есть учетные данные базы данных. Если будет достаточно интереса, я сделаю рецензию.