Pull to refresh

.NET и HashiCorp Vault: Использование секретов в настройках .NET Core приложения

Reading time6 min
Views8.6K

Настройка

Для работы нам понадобится настроенный к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: |
	{
  	"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
{
  "VaultSecrets": {
 	{{- with secret "secrets/data/services/dotnet" }}
  	"userName": "{{ .Data.data.username }}",
  	"password": "{{ .Data.data.password }}"
	{{- end }}
  }
}

А уже готовый сгенерированный файл настроек который будет создаваться при старте пода агента.

/ $ cat /vault/secrets/appsettings.json
{
  "VaultSecrets": {
  	"userName": "Bob",
  	"password": "Bob_Password"
  }
}

Tags:
Hubs:
Total votes 9: ↑8 and ↓1+9
Comments14

Articles