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
We connect the sidecar Vault agent.
Copying data from the mounted volume.
We connect the configuration and template files.
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"
}
}