Настройка
Для работы нам понадобится настроенный к8s кластер, можно использовать Minkube или k3s. Также необходимо установить helm и jq.
Установка Vault
HashiCorp рекомендует устанавливать Vault используя официальный helm chart.
Добавим официальный репозиторий.
$helm repo add hashicorp https://helm.releases.hashicorp.com
"hashicorp" has been added to your repositories
Обновим репозиторий до последней версии.
$helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "hashicorp" chart repository
Update Complete. ⎈Happy Helming!⎈
Установим последнюю версию чарта, наша установка будет без HA, при использовании в продуктивной среде желательно ставить с HA.
$helm install vault hashicorp/vault --set='server.ha.enabled=false'
NAME: vault
LAST DEPLOYED: Sun Dec 18 08:54:20 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
Thank you for installing HashiCorp Vault!
Now that you have deployed Vault, you should look over the docs on using
Vault with Kubernetes available here:
https://www.vaultproject.io/docs/
Your release is named vault. To learn more about the release, try:
$ helm status vault
$ helm get manifest vault
Проверяем, что все установилось.
$kubectl get po
NAME READY STATUS RESTARTS AGE
vault-0 0/1 Running 0 3m9s
vault-agent-injector-77fd4cb69f-4brdw 1/1 Running 0 3m9s
Под vault-0 в статусе Running, но еще не готов к обработке запросов, так как мы его еще не проинициализировали. Посмотрим его статус.
$kubectl exec vault-0 -- vault status
Key Value
--- -----
Seal Type shamir
Initialized false
Sealed true
Total Shares 0
Threshold 0
Unseal Progress 0/0
Unseal Nonce n/a
Version 1.12.1
Build Date 2022-10-27T12:32:05Z
Storage Type file
HA Enabled false
Мы видим, что Vault еще не проинициализирован и запечатан.
Инициализация Vault
При первичной установке Vault запускается не проинициализированным и запечатанным.
Инициализируем с сохранением настроек в файл.
$kubectl exec vault-0 -- vault operator init \
-key-shares=1 -key-threshold=1 -format=json > cluster-keys.json
Сохраним Vault unseal key в переменную среды и распечатаем.
$VAULT_UNSEAL_KEY=$(cat cluster-keys.json | jq -r ".unseal_keys_b64[]")
$kubectl exec vault-0 -- vault operator unseal $VAULT_UNSEAL_KEY
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.12.1
Build Date 2022-10-27T12:32:05Z
Storage Type file
Cluster Name vault-cluster-cdeb59eb
Cluster ID 67d6948e-529e-98e1-e86b-d83342f0584f
HA Enabled false
Vault настроен и готов к работе.
Настройка секретов
Наше приложение будет брать настройки из секретов. Для создания секрета нужно войти в систему с root token, включить key-value secret engine, и сохранить наши секреты.
Сначала сохраним рутовый токен в переменную среды.
$export VAULT_ROOT_TOKEN=$(cat cluster-keys.json | jq -r ".root_token")
Запустим новую терминальную сессию в контейнере Vault и передадим туда переменную.
$kubectl exec --stdin=true --tty=true vault-0 \
-- /bin/sh -c "env VAULT_ROOT_TOKEN=$(echo $VAULT_ROOT_TOKEN) /bin/sh"
Теперь логинимся в Vault.
/ $ vault login $VAULT_ROOT_TOKEN
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token <your token here>
token_accessor <your token here>
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
Включаем хранилище секретов kv-v2 с путем secrets.
/ $ vault secrets enable -path=secrets kv-v2
Success! Enabled the kv-v2 secrets engine at: secrets/
Создаем секрет для нашего приложения.
/ $ vault kv put secrets/services/dotnet username='Bob' password='Bob_Password'
======== Secret Path ========
secrets/data/services/dotnet
======= Metadata =======
Key Value
--- -----
created_time 2022-12-18T10:26:35.169923707Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
Проверяем что секрет доступен по пути secrets/services/dotnet.
/ $ vault kv get secrets/services/dotnet
======== Secret Path ========
secrets/data/services/dotnet
======= Metadata =======
Key Value
--- -----
created_time 2022-12-18T10:26:35.169923707Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
password Bob_Password
username Bob
Настройка аутентификации в Kubernetes
Рутовый токен является привилегированным пользователем, который может выполнять любые операции на любом пути. Нашему же приложению требуется только возможность чтения секретов, определенных на одном пути. Для этого приложение должно пройти аутентификацию и получить токен с ограниченным доступом.
Включаем аутентификацию через Kubernetes.
/ $ vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/
Настраиваем доступ к API Kubernetes.
/ $ vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
Success! Data written to: auth/kubernetes/config
Создаем политику с именем service, которая включает возможность чтения для секретов по пути secrets/data/services/dotnet.
/ $ vault policy write service - <<EOF
> path "secrets/data/services/dotnet" {
> capabilities = ["read"]
> }
> EOF
Success! Uploaded policy: service
Создаем роль для аутентификации в Kubernetes с именем service.
/ $ vault write auth/kubernetes/role/service \
> bound_service_account_names=* \
> bound_service_account_namespaces=* \
> policies=service \
> ttl=24h
Success! Data written to: auth/kubernetes/role/service
Настройка приложения
Тестовое приложение доступно на github.
$git clone https://github.com/nkz-soft/dotnet-k8s-vault
Приложение разворачивается в кластере с помощью helm chart.
$cd dotnet-k8s-vault/deployment/k8s/.helm/
$helm install dotnet-vault .
NAME: dotnet-vault
LAST DEPLOYED: Sun Dec 18 11:28:47 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services dotnet-vault)
export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
Проверим, что мы можем прочитать содержимое секрета.
$export DOTNET_K8S_VAULT_PORT=$(kubectl get svc dotnet-vault -o json | jq -r ".spec.ports[].nodePort")
$curl localhost:$DOTNET_K8S_VAULT_PORT/config
{"VaultSecrets":null,"VaultSecrets:userName":"Bob","VaultSecrets:password":"Bob_Password"}
Как это работает
Содержимое файла Program.cs.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
.AddJsonFile("/vault/secrets/appsettings.json", optional: true, reloadOnChange: true);
var app = builder.Build();
app.MapHealthChecks("/healthz");
app.MapGet("/config",
(IConfiguration configuration) => Results.Ok
(configuration.GetSection("VaultSecrets")
.AsEnumerable().ToDictionary(k => k.Key, v => v.Value)));
app.Run();
Приложение будет читать настройки из файла секрета /vault/secrets/appsettings.json и отображать текущие настройки по адресу http://localhost/config
Содержимое файла configmap.yaml на основании которого создается шаблон.
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "dotnet-vault.fullname" . }}
labels:
{{- include "dotnet-vault.labels" . | nindent 4 }}
data:
appsettings.json: |
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"VaultSecrets": {
{{` {{- with secret "secrets/data/services/dotnet" }}
"userName": "{{ .Data.data.username }}",
"password": "{{ .Data.data.password }}"
{{- end }} `}}
}
}
Deployment использует следующие аннотации:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-copy-volume-mounts: 'dotnet-vault'
vault.hashicorp.com/agent-inject-secret-appsettings.json: ""
vault.hashicorp.com/agent-inject-template-file-appsettings.json: '/vault/config/appsettings.json'
vault.hashicorp.com/role: service
Подключаем sidecar Vault агента.
Копируем данные из подключенного тома.
Подключаем конфигурационный и шаблонный файлы.
Устанавливаем необходимую роль для доступа.
В результате в поде агента будет лежать шаблон.
/ $ cat vault/config/appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"VaultSecrets": {
{{- with secret "secrets/data/services/dotnet" }}
"userName": "{{ .Data.data.username }}",
"password": "{{ .Data.data.password }}"
{{- end }}
}
}
А уже готовый сгенерированный файл настроек который будет создаваться при старте пода агента.
/ $ cat /vault/secrets/appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"VaultSecrets": {
"userName": "Bob",
"password": "Bob_Password"
}
}
Комментарии (11)
DANic
27.12.2022 11:46Подключаем sidecar Vault агента
А в чем преимущество sidecar агента перед CSI провайдером ?
nkz-soft Автор
27.12.2022 11:49Есть хорошая статья, где есть сравнение https://www.hashicorp.com/blog/kubernetes-vault-integration-via-sidecar-agent-injector-vs-csi-provider.
В целом sidecar поддерживает больше фич, не требует инсталляции как demonset.
TroyashkA
Плюсанул. Тоже недавно это реализовал, сначала думал сделать не просто json файл конфигурации, а через user secrets (https://learn.microsoft.com/ru-ru/aspnet/core/security/app-secrets?view=aspnetcore-7.0&tabs=linux#secret-manager), но это рекомендуется только для локальной разработки, да и в дефолтном докер-образе нет вроде тулы по умолчанию.
Так что в целом как-то так и получилось, только не храню в vault весь appsettings.json, а только то, что относится к секретам.
Конечно можно сделать более секурно, но это уже детали.
nkz-soft Автор
В Vault хранятся только секреты, сам конфиг, в данном случае
appsettings.json
генерируется на основе шаблона, который храниться в configmap.mayorovp
Никогда не понимал любителей генерировать конфиги из шаблонов, а уж при использовании ASP.NET Core — тем более. У вас есть key-value хранилище номер 1 (vault), вы из него делаете json, который будет считан и преобразован в key-value формат номер 2 (конфигурация asp.net core). Почему бы просто не пропустить шаг json?
А уж генерация неизменяемой секции Logging вместо помещения её куда-нибудь в appsettings.Production.json и вовсе выглядит как глупость
nkz-soft Автор
Шаблон хранится в configmap, это стандартный способ хранения конфигурации для приложений в k8s. В него подставляются только секреты из vault, по тому же принципу, как работает helm, то есть это скорее не генератор, как таковой, а шаблонизатор.
Не очень понятно, как в данном случае можно пропустить эту фазу, было бы очень интересно посмотреть на вашу реализацию в связке с Vault.
fedorro
Сразу в ENV (но я не знаток Vault-а, просто предполагаю):
mayorovp
Вот в ENV как раз лучше ничего не складывать при наличии таковой возможности. ENV проще "угнать" чем файл: оно мало того что хранится в /proc/2/environ, так ещё и передаётся всем дочерним процессам
mayorovp
Я не работал ни с k8s, ни с vault, так что идеального решения предложить не могу. Не исключаю и варианта, при котором авторы этих инструментов настолько привыкли к монструозных конфигам, нуждающимся в шаблонизации, что просто не предусмотрели сокращённого пути для нормальных программ.
Однако, как минимум, стоит либо убрать секцию Logging из вашего configmap, либо убрать подключение файла /vault/secrets/appsettings.json и писать всё в обычный appsettings.json. Потому что эти два механизма (статическая секция в шаблоне и статический appsettings.json) дублируют друг друга. В процессе доработок ваши настройки неизбежно разрастутся между этими двумя местами, и концов вы там не найдёте.