Using Secrets in .NET Core Application Settings


Setting

To work, we need a configured k8s cluster, you can use Mimkube or k3s. You also need to install helm and jg.

Vault Installation

HasiCorp recommends installing Vault using the official helm chart.

Let’s add the official repository.

$helm repo add hashicorp https://helm.releases.hashicorp.com
"hashicorp" has been added to your repositories

Update the repository to the latest version.

$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!⎈

Install the latest version of the chart, our installation will be without HA, when used in a productive environment, it is desirable to install with 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

We check that everything is installed.

$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

Under vault-0 in the status of Running, but not yet ready to process requests, since we have not yet initialized it. Let’s see his status.

$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

We see that Vault is not yet initialized and sealed.

Vault initialization

When first installed, Vault starts uninitialized and sealed.
We initialize with saving the settings to a file.

$kubectl exec vault-0 -- vault operator init \
-key-shares=1 -key-threshold=1 -format=json > cluster-keys.json

Save the Vault unseal key to an environment variable and print it out.

$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 is set up and ready to go.

Setting up secrets

Our application will take settings from secrets. To create a secret, you need to log in with the root token, enable key-value secret engineand keep our secrets.

First, save the root token to an environment variable.

$export VAULT_ROOT_TOKEN=$(cat cluster-keys.json | jq -r ".root_token")

Let’s start a new terminal session in the Vault container and pass the variable there.

$kubectl exec --stdin=true --tty=true vault-0 \
-- /bin/sh -c "env VAULT_ROOT_TOKEN=$(echo $VAULT_ROOT_TOKEN)  /bin/sh"

Now we log in to 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"]

Enable the kv-v2 secret store with the secrets path.

 / $ vault secrets enable -path=secrets kv-v2
Success! Enabled the kv-v2 secrets engine at: secrets/

We create a secret for our application.

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

We check that the secret is available along the path 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

Configuring Authentication in Kubernetes

The root token is a privileged user that can perform any operation on any path. Our application only needs the ability to read the secrets defined on the same path. To do this, the application must authenticate and receive a restricted access token.

Enable authentication through Kubernetes.

/ $ vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/

Setting up access to the Kubernetes API.

/ $ vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
Success! Data written to: auth/kubernetes/config

We create a policy named service that enables read access for secrets in the path secrets/data/services/dotnet.

/ $ vault policy write service - <<EOF
> path "secrets/data/services/dotnet" {
>   capabilities = ["read"]
> }
> EOF
Success! Uploaded policy: service

We create a role for authentication in Kubernetes with the name 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

Application setup

The test application is available at github.

$git clone https://github.com/nkz-soft/dotnet-k8s-vault

The application is deployed in a cluster using 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

Let’s check that we can read the contents of the secret.

$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"}

How it works

The contents of the Program.cs file.

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();

The application will read the settings from the /vault/secrets/appsettings.json secret file and display the current settings at http://localhost/config

The contents of the configmap.yaml file on the basis of which the template is created.

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 uses the following annotations:

  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. We connect the sidecar Vault agent.

  2. Copying data from the mounted volume.

  3. We connect the configuration and template files.

  4. Set the required role for access.

As a result, the agent pod will contain a template.

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

And a ready-made generated settings file that will be created at the start of the agent pod.

/ $ cat /vault/secrets/appsettings.json
{
  "Logging": {
	"LogLevel": {
  	"Default": "Information",
  	"Microsoft.AspNetCore": "Warning"
	}
  },
  "VaultSecrets": {
  	"userName": "Bob",
  	"password": "Bob_Password"
  }
}

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *