Работая с секретами, хочется получить две возможности: просто и централизованно управлять секретами в кластере и в то же время вынести их за пределы кластера в целях безопасности. В этой статье мы подробно рассмотрим работу External Secrets Operator в связке с Yandex Lockbox, AWS Secrets Manager, Vault by HashiCorp, а также решение на базе Open Source-утилиты от «Фланта» shell-operator.
Что такое External Secrets Operator
External Secrets Operator расширяет возможности Kubernetes с помощью Custom Resources, которые определяют местоположение секретов и способы их синхронизации. Контроллер запрашивает секреты из внешнего API и создает секреты Kubernetes. Если секрет во внешнем API изменяется, контроллер производит мониторинг состояния кластера и обновляет секреты.
Начнем с установки External Secrets Operator. Для этого воспользуемся самым простым методом — Helm
helm repo add external-secrets https://charts.external-secrets.io helm install external-secrets \ external-secrets/external-secrets \ -n external-secrets \ --create-namespace
-
external-secrets— это имя релиза, под которым будет установлен External Secrets. Имя релиза может быть любым, главное, чтобы оно оставалось уникальным в рамках одного кластера Kubernetes. -
external-secrets/external-secrets— это имя чарта, который мы хотим установить из репозитория External Secrets. -
-n external-secrets— это флаг, который указывает, что мы хотим создать новое пространство имен (namespace) с именемexternal-secretsдля установки релиза. -
--create-namespace— это флаг, который указывает на необходимость создания нового пространства имен (namespace), если оно ещё не существует. Если пространство имен уже существует, этот флаг можно опустить.
Проверяем, что все поды запустились и оператор работает:
kubectl -n external-secrets get po NAME READY STATUS RESTARTS AGE external-secrets-5b9599dbd4-czhnh 1/1 Running 0 49s external-secrets-cert-controller-6d45db4b4d-zxmmd 1/1 Running 0 49s external-secrets-webhook-5b9c855467-cxbcs 1/1 Running 0 49s
Теперь все готово для подключения наших секретов.
Работа с Yandex Lockbox
Настройка связки Yandex Lockbox и External Secrets Operator отлично описана в официальной инструкции от Яндекса. Но мы все равно кратко пробежимся по ней.
Для начала работы нам понадобится создать сервисный аккаунт в Яндекс Облаке для Lockbox.

В нем необходимо прописать две роли:
-
kms.keys.encrypterDecrypter— для расшифровки секретов; -
lockbox.viewer— для доступа к хранилищу.

После этого открываем сервисный аккаунт и создаем авторизованный ключ:


Сохраняем файл с ключами authorized-key.json — он еще понадобится нам для настройки — и запоминаем идентификатор секрета (в нашем примере e6qqssbs94tjpvdb78p9).
Чтобы создать наш первый секрет, необходимо зайти в Lockbox и нажать на кнопку «Создать секрет».

Тут же создадим ключ KMS для шифрования секретов:

И заполним все поля секрета:

Теперь добавим права для нашего сервисного аккаунта к созданному на прошлых шагах секрету. Для этого перейдем на вкладку «Права доступа» и нажмем кнопку «Назначить роли»:

И так же добавляем роль для kms ключа

Добавим в кластер ключ от сервисного аккаунта. Для этого необходимо выполнить следующую команду:
kubectl --namespace secrets-test create secret generic yc-auth --from-file=authorized-key=authorized_key.json
Далее нам нужно будет создать либо SecretStore либо ClusterSecretStore. Их основное отличие в том, что ClusterSecretStore будет доступен из любого пространства имен, а SecretStore — нет (пространство имен в этом случае будет задаваться на этапе создания сущности SecretStore — в примере ниже за это отвечает строка namespace: secrets-test). Создадим манифест secret-store-yc.yaml для SecretStore:
--- apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: secret-store-yc namespace: secrets-test spec: provider: yandexlockbox: auth: authorizedKeySecretRef: name: yc-auth key: authorized-key
Теперь задеплоим его в наш кластер:
kubectl apply -f secret-store-yc.yaml
После этого создадим ExternalSecret с помощью файла external-secret-yc.yaml.
--- apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: external-secret namespace: secrets-test spec: refreshInterval: 10m secretStoreRef: name: secret-store-yc kind: SecretStore target: name: yc-secret data: - secretKey: yc-key remoteRef: key: e6qqssbs94tjpvdb78p9 property: SOME_KEY
Для более тонкой настройки или использования сразу нескольких ключей рекомендую ознакомиться с документацией по ExternalSecret.
Деплоим ExternalSecret в кластер с помощью соответствующего YAML-файла:
kubectl apply -f external-secret-yc.yaml
Проверим, работает ли наше решение: если мы все сделали правильно и у нас есть все необходимые права, в системе появится ExternalSecret в статусе SecretSynced:
kubectl -n secrets-test get externalsecrets.external-secrets.io NAME STORE REFRESH INTERVAL STATUS READY external-secret secret-store-yc 10m SecretSynced True
Кроме того, появится секрет yc-secret. Вот его значение:
kubectl -n secrets-test get secrets yc-secret -ojson | jq -r '.data | map_values(@base64d)' { "yc-key": "SOME_VALUE" }
Работа с AWS Secret Manager
У AWS тоже все отлично с документацией. Приступим к настройке.
Сначала создадим пользователя для работы с секретами — тут нам будет необходимо зайти в IAM → Users → Add users.

Задаем имя

Добавляем политику. Можно воспользоваться готовой от AWS — SecretsManagerReadWrite, а можно создать свою. Мы пойдем по второму пути и создадим свою:

{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "secretsmanager:GetResourcePolicy", "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "secretsmanager:ListSecretVersionIds" ], "Resource": "arn:aws:secretsmanager:*:111111111111:secret:*" } ] }
Прикрепляем политику к новому пользователю и создаем этого самого пользователя, а после этого создадим ключ доступа:

Выбираем Other, добавляем тег и сохраняем свои данные для авторизации:

Теперь идем в AWS Secrets Manager и создаем свой первый секрет:


Теперь подготовим манифесты для подключения в кластер:
--- apiVersion: v1 kind: Secret metadata: name: aws-creds namespace: secrets-test type: Opaque stringData: accessKey: "AKIA46CKSTMN32RJDFXY" secretAccessKey: "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" --- apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: secret-store-aws namespace: secrets-test spec: provider: aws: auth: secretRef: accessKeyIDSecretRef: key: accessKey name: aws-creds secretAccessKeySecretRef: key: secretAccessKey name: aws-creds region: eu-central-1 service: SecretsManager --- apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: external-secret-aws namespace: secrets-test spec: refreshInterval: 10m secretStoreRef: name: secret-store-aws kind: SecretStore target: name: aws-secret creationPolicy: Owner data: - secretKey: aws-key remoteRef: key: aws-test-secret property: SOME_AWS_KEY
В итоге получился секрет aws-secret с нашим значением:
kubectl -n secrets-test get secrets aws-secret -ojson | jq '.data | map_values(@base64d)' { "aws-key": "SOME_AWS_VALUE" }
Работа с HashiCorp Cloud Platform (HCP) Vault Secrets
У этой платформы тоже есть толковая документация.
werf helm install vault hashicorp/vault --namespace secrets-hcp-vault
После установки необходимо инициализировать наш vault. Обязательно сохраните ключи для последующей распечатки хранилища.
kubectl -n secrets-hcp-vault exec -it vault-0 -- sh / $ vault operator init Unseal Key 1: AOFvS1ZVHpLwdKOIX/PFX1YFH9w213ayxsvKIGapEMam Unseal Key 2: 3Z32U3CruHi83LJnrfz8ptd3tJRTe5DPghQxYG34JtDS Unseal Key 3: 1re6jWEJuemQXGvwByQzYR4TH02nfcDcf3glK8m/gAI4 Unseal Key 4: AEV3pLV3ddFndToUu2X40fduM1B2167zmeFj/1EmMqBo Unseal Key 5: XRK2XDxo9NQzN8EVXcLpNBppkW12v+Dm9Hzqq+CBlNp8 Initial Root Token: hvs.Y87NbYmHLvvaZVH0oIsPskZW
C помощью трех ключей распечатки запускаем хранилище и заходим в него под root:
vault operator unseal <key> vault login <root_key>
Теперь мы можем добавить в хранилище наш тестовый секрет:
vault secrets enable --path=secret kv vault kv put secret/test-hcpv hcpv-key=HCPV_VALUE vault list secret vault kv get secret/test-hcpv
Создадим политику для доступа к нашему секрету и токен для дальнейшей настройки:
vault policy write my-vault-policy - <<EOF path "secret/*" { capabilities = ["create", "read", "update", "patch", "delete", "list"] } EOF
vault token create -policy=my-vault-policy Key Value --- ----- token hvs.CAESIKVdmmqykGVg6yiqDMeB8oC-lAUI_veOWxBKe2AoY490Gh4KHGh2cy40aFpVUldkVlU5SmhuSFQ1TWo2RGt3OFI token_accessor QSvnCPet91BAUn0ZZpmBeiGe token_duration 768h token_renewable true token_policies ["default" "my-vault-policy"] identity_policies [] policies ["default" "my-vault-policy"]
После этого, аналогично предыдущим примерам, подготовим манифесты для подключения в кластер:
--- apiVersion: v1 kind: Secret metadata: name: hcpv-creds namespace: secrets-test type: Opaque stringData: hcpvToken: "hvs.CAESIKVdmmqykGVg6yiqDMeB8oC-lAUI_veOWxBKe2AoY490Gh4KHGh2cy40aFpVUldkVlU5SmhuSFQ1TWo2RGt3OFI" --- apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: secret-store-hcpv namespace: secrets-test spec: provider: vault: server: "http://vault.secrets-hcp-vault.svc.cluster.local:8200" path: "secret" version: "v1" auth: tokenSecretRef: name: "hcpv-creds" key: "hcpvToken" --- apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: external-secret-hcpv namespace: secrets-test spec: refreshInterval: 10m secretStoreRef: name: secret-store-hcpv kind: SecretStore target: name: hcpv-secret data: - secretKey: hcpv-key remoteRef: key: secret/test-hcpv property: hcpv-key
И, наконец, проверяем наш секрет hcpv-secret с заданным значением:
kubectl -n secrets-test get secrets hcpv-secret -ojson | jq '.data | map_values(@base64d)' { "hcpv-secret-key": "HCPV_VALUE" }
Работа с shell-operator
Shell-operator — это наша Open Source-утилита для более простого создания Kubernetes-операторов.
А что делать, если мы не хотим использовать секреты в качестве объектов K8s? Для этого мы создали образ shell-operator с Yandex-cli, в котором были хуки, работающие либо по расписанию, либо по триггерам. С помощью оператора можно реализовать и создать любую логику.
Мы в одном из проектов «Фланта» ходим в Яндекс Облако с помощью сервисного аккаунта и запрашиваем JWT Token, который меняем раз в шесть часов для безопасности. Кроме того, сам shell-operator подписан на события: как только в кластере появляется секрет с аннотацией lockbox=yes оператор создает новый секрет или заменяет его, а после удаляет ненужные секреты. Ниже приведен пример хука:
hook::trigger() { # Copy secrets to the other namespaces. for secret in $(kubectl -n ${WATCHED_NAMESPACE} get secret -l lockbox=yes --no-headers -o custom-columns=":metadata.name"); do ELAPSED=`kubectl::get_expire_seconds ${WATCHED_NAMESPACE} ${secret}-iam` if (( $ELAPSED > 21600)); then echo "${secret}-iam doesn't require the update and have ttl - ${ELAPSED} seconds" exit 0 fi kubectl -n ${WATCHED_NAMESPACE} get secret $secret -o json| jq -r ".data.sa" | base64 -d > /tmp/sa.json ${YC_BIN} config set service-account-key /tmp/sa.json rm /tmp/sa.json PAYLOAD=$(${YC_BIN} iam create-token --format json) TOKEN=$(echo $PAYLOAD | jq -r .iam_token) EXPIRATION=$(echo $PAYLOAD | jq -r .expires_at) EXPIRATION_TIMESTAMP=$(date -d ${EXPIRATION} +%s) # copy secret with a necessary data common::generate_secret $secret ${WATCHED_NAMESPACE} ${EXPIRATION} ${TOKEN} ${EXPIRATION_TIMESTAMP}| kubectl::replace_or_create done # Delete secrets with the 'secret-copier: yes' label in namespaces except 'default', which are not exist in the 'default' namespace. kubectl -n ${WATCHED_NAMESPACE} get secret -o json | \ jq -r '([.items[] | select(.metadata.labels."lockbox" == "yes").metadata.name]) as $secrets | .items[] | select(.metadata.labels."lockbox.managed" == "yes" and ([.metadata.labels."lockbox.parent"] | inside($secrets) | not)) | "\(.metadata.namespace) secret \(.metadata.name)"' | \ while read -r secret do kubectl delete -n $secret done }
Что здесь важно понимать:
-
Функция
common::generate_secret()генерирует JSON для создания секрета Kubernetes. -
Функция
kubectl::replace_or_create()выполняет замещение или создание секрета. -
Функция
kubectl::get_expire_seconds()получает количество оставшихся секунд до истечения срока действия секрета.
function common::generate_secret(){ echo "{ \"apiVersion\": \"v1\", \"kind\": \"Secret\", \"metadata\": { \"name\": \"$1-iam\", \"namespace\": \"$2\", \"annotations\": { \"lockbox.expires\": \"$3\", \"lockbox.expires_timestamp\": \"$5\" }, \"labels\": { \"lockbox.parent\": \"$1\", \"lockbox.managed\": \"yes\" } }, \"type\": \"Opaque\", \"stringData\": { \"token\": \"$4\" } }" } function kubectl::replace_or_create() { object=$(cat) if ! kubectl get -f - <<< "$object" >/dev/null 2>/dev/null; then kubectl create -f - <<< "$object" >/dev/null else kubectl replace --force -f - <<< "$object" >/dev/null fi } function kubectl::get_expire_seconds() { NAMESPACE=$1 SECRET=$2 if PAYLOAD=$(kubectl -n ${NAMESPACE} get secret ${SECRET} -o custom-columns=":metadata.name"); then EXPIRATION_TIME=$(kubectl -n ${NAMESPACE} get secret ${SECRET} -o json | jq -r '.metadata.annotations."lockbox.expires_timestamp"' || echo "0" ) else EXPIRATION_TIME="0" fi NOW=$(date +%s) echo $((${EXPIRATION_TIME} - ${NOW})) }
Заключение
Как мы выяснили, External Secrets Operator отлично решает задачу безопасного хранения секретов вне кластера. А вот выбор хранилища в первую очередь будет зависеть от облака, в котором располагается инфраструктура, и от необходимой функциональности.
AWS за счет гибкого механизма IAM позволяет ограничивать ресурсы, доступные для текущего сервисного аккаунта. Если же нужен полный контроль, то можно использовать HCP Vault. Правда, его придется достаточно глубоко изучить, прежде чем запускать в production.
P.S.
Читайте также в нашем блоге:
ссылка на оригинал статьи https://habr.com/ru/articles/751656/
Добавить комментарий