Настраиваем управление секретами с Yandex Lockbox, AWS Secret Manager, Vault Secrets и shell-operator
Работая с секретами, хочется получить две возможности: просто и централизованно управлять секретами в кластере и в то же время вынести их за пределы кластера в целях безопасности. В этой статье мы подробно рассмотрим работу 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.
Читайте также в нашем блоге: