Настройка

Для работы нам понадобится настроенный к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
  1. Подключаем sidecar Vault агента.

  2.  Копируем данные из подключенного тома.

  3. Подключаем конфигурационный и шаблонный файлы. 

  4. Устанавливаем необходимую роль для доступа.

В результате в поде агента будет лежать шаблон.

/ $ 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)


  1. TroyashkA
    26.12.2022 20:00
    +1

    Плюсанул. Тоже недавно это реализовал, сначала думал сделать не просто 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, а только то, что относится к секретам.

    Конечно можно сделать более секурно, но это уже детали.


    1. nkz-soft Автор
      27.12.2022 11:41
      +1

      В Vault хранятся только секреты, сам конфиг, в данном случае appsettings.json генерируется на основе шаблона, который храниться в configmap.


      1. mayorovp
        28.12.2022 18:14
        +1

        Никогда не понимал любителей генерировать конфиги из шаблонов, а уж при использовании ASP.NET Core — тем более. У вас есть key-value хранилище номер 1 (vault), вы из него делаете json, который будет считан и преобразован в key-value формат номер 2 (конфигурация asp.net core). Почему бы просто не пропустить шаг json?


        А уж генерация неизменяемой секции Logging вместо помещения её куда-нибудь в appsettings.Production.json и вовсе выглядит как глупость


        1. nkz-soft Автор
          28.12.2022 22:09

          Шаблон хранится в configmap, это стандартный способ хранения конфигурации для приложений в k8s. В него подставляются только секреты из vault, по тому же принципу, как работает helm, то есть это скорее не генератор, как таковой, а шаблонизатор.

          Не очень понятно, как в данном случае можно пропустить эту фазу, было бы очень интересно посмотреть на вашу реализацию в связке с Vault.


          1. fedorro
            28.12.2022 22:41

            Сразу в ENV (но я не знаток Vault-а, просто предполагаю):

                    # Environment variable export template
                    vault.hashicorp.com/agent-inject-template-config: |
                      {{ with secret "secret/data/web" -}}
                        export api_key="{{ .Data.data.payments_api_key }}"
                      {{- end }}


            1. mayorovp
              29.12.2022 17:51

              Вот в ENV как раз лучше ничего не складывать при наличии таковой возможности. ENV проще "угнать" чем файл: оно мало того что хранится в /proc/2/environ, так ещё и передаётся всем дочерним процессам


          1. mayorovp
            29.12.2022 17:46

            Я не работал ни с k8s, ни с vault, так что идеального решения предложить не могу. Не исключаю и варианта, при котором авторы этих инструментов настолько привыкли к монструозных конфигам, нуждающимся в шаблонизации, что просто не предусмотрели сокращённого пути для нормальных программ.


            Однако, как минимум, стоит либо убрать секцию Logging из вашего configmap, либо убрать подключение файла /vault/secrets/appsettings.json и писать всё в обычный appsettings.json. Потому что эти два механизма (статическая секция в шаблоне и статический appsettings.json) дублируют друг друга. В процессе доработок ваши настройки неизбежно разрастутся между этими двумя местами, и концов вы там не найдёте.


  1. mentin
    27.12.2022 09:48

    s/HasiCorp/HashiCorp/g


    1. nkz-soft Автор
      27.12.2022 11:50

      Поправил.


  1. DANic
    27.12.2022 11:46

    Подключаем sidecar Vault агента

    А в чем преимущество sidecar агента перед CSI провайдером ?


    1. nkz-soft Автор
      27.12.2022 11:49

      Есть хорошая статья, где есть сравнение https://www.hashicorp.com/blog/kubernetes-vault-integration-via-sidecar-agent-injector-vs-csi-provider.

      В целом sidecar поддерживает больше фич, не требует инсталляции как demonset.