Dapr provides a basic but very functional secrets abstraction component we can use in our services. In our last topic we furthered the pub/sub knowledge with a custom perl subscriber. Today we will follow that by examining secrets integration with AKV and AWS SSM.
Setup
We are going to build off of the last blog (Dapr : Part 2). However, if you want to create a quick secret store demo to use some of these building blocks, you can follow the Dapr secretstore quick start here.
We'll assume you already have a kubernetes cluster created and available for steps below.
Let's set our subscription and create a resource group for our Key Vault
$ az account set -s Pay-As-You-Go
$ az group create -n idjakvrg --location centralus
{
"id": "/subscriptions/a283bc3e-01f1-4cec-8426-e04e9d02d95a/resourceGroups/idjakvrg",
"location": "centralus",
"managedBy": null,
"name": "idjakvrg",
"properties": {
"provisioningState": "Succeeded"
},
"tags": null,
"type": "Microsoft.Resources/resourceGroups"
}
Next, we can create our AKV instance
$ az keyvault create --location centralus --name idjakv --resource-group idjakvrg
{
"id": "/subscriptions/a283bc3e-01f1-4cec-8426-e04e9d02d95a/resourceGroups/idjakvrg/providers/Microsoft.KeyVault/vaults/idjakv",
"location": "centralus",
"name": "idjakv",
"properties": {
"accessPolicies": [
{
"applicationId": null,
"objectId": "1f5d835c-b129-41e6-b2fe-5858a5f4e41a",
"permissions": {
"certificates": [
"get",
"list",
"delete",
"create",
"import",
"update",
"managecontacts",
"getissuers",
"listissuers",
"setissuers",
"deleteissuers",
"manageissuers",
"recover"
],
"keys": [
"get",
"create",
"delete",
"list",
"update",
"import",
"backup",
"restore",
"recover"
],
"secrets": [
"get",
"list",
"set",
"delete",
"backup",
"restore",
"recover"
],
"storage": [
"get",
"list",
"delete",
"set",
"update",
"regeneratekey",
"setsas",
"listsas",
"getsas",
"deletesas"
]
},
"tenantId": "28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a"
}
],
"createMode": null,
"enablePurgeProtection": null,
"enableSoftDelete": null,
"enabledForDeployment": false,
"enabledForDiskEncryption": null,
"enabledForTemplateDeployment": null,
"networkAcls": null,
"provisioningState": "Succeeded",
"sku": {
"name": "standard"
},
"tenantId": "28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a",
"vaultUri": "https://idjakv.vault.azure.net/"
},
"resourceGroup": "idjakvrg",
"tags": {},
"type": "Microsoft.KeyVault/vaults"
}
I had some troubles creating the Service Principal with a cert credential
$ az ad sp create-for-rbac --name idjdaprsp --create-cert --cert idjdaprspcert --keyvault idjakv --skip-assignment --years 1
Changing "idjdaprsp" to a valid URI of "http://idjdaprsp", which is the required format used for service principal names
The command failed with an unexpected error. Here is the traceback:
cannot import name 'KeyVaultAuthentication' from 'azure.keyvault' (unknown location)
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/knack/cli.py", line 206, in invoke
cmd_result = self.invocation.execute(args)
File "/usr/lib/python3/dist-packages/azure/cli/core/commands/__init__.py", line 608, in execute
raise ex...
until I realized that my local az cli was out of date.
$ az version
This command is in preview. It may be changed/removed in a future release.
{
"azure-cli": "2.0.81",
"azure-cli-core": "2.0.81",
"azure-cli-telemetry": "1.0.4",
"extensions": {
"azure-devops": "0.17.0"
}
}
Once I upgraded to a newer version
$ az version
{
"azure-cli": "2.21.0",
"azure-cli-core": "2.21.0",
"azure-cli-telemetry": "1.0.6",
"extensions": {}
}
I was able to create
$ az ad sp create-for-rbac --name idjdaprsp --create-cert --cert idjdaprspcert --keyvault idjakv --skip-assignment --years 1
Changing "idjdaprsp" to a valid URI of "http://idjdaprsp", which is the required format used for service principal names
The output includes credentials that you must protect. Be sure that you do not include these credentials in your code or check the credentials into your source control. For more information, see https://aka.ms/azadsp-cli
{
"appId": "8049597e-4970-4430-8f9d-9d920f446f05",
"displayName": "idjdaprsp",
"name": "http://idjdaprsp",
"password": null,
"tenant": "28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a"
}
We can use the portal to view the keyvault and cert created
and via CLI
$ az ad sp show --id 8049597e-4970-4430-8f9d-9d920f446f05 -o json | jq -r '.objectId'
f17e37c4-6a48-4102-bd4c-3ec2bcb7947d
Now let’s grant that secrets.get permissions
$ az keyvault set-policy --name idjakv --object-id f17e37c4-6a48-4102-bd4c-3ec2bcb7947d --secret-permissions get
{- Finished ..
"id": "/subscriptions/a283bc3e-01f1-4cec-8426-e04e9d02d95a/resourceGroups/idjakvrg/providers/Microsoft.KeyVault/vaults/idjakv",
"location": "centralus",
"name": "idjakv",
"properties": {
"accessPolicies": [
{
"applicationId": null,
"objectId": "1f5d835c-b129-41e6-b2fe-5858a5f4e41a",
"permissions": {
"certificates": [
"get",
"list",
"delete",
"create",
"import",
"update",
"managecontacts",
"getissuers",
"listissuers",
"setissuers",
"deleteissuers",
"manageissuers",
"recover"
],
"keys": [
"get",
"create",
"delete",
"list",
"update",
"import",
"backup",
"restore",
"recover"
],
"secrets": [
"get",
"list",
"set",
"delete",
"backup",
"restore",
"recover"
],
"storage": [
"get",
"list",
"delete",
"set",
"update",
"regeneratekey",
"setsas",
"listsas",
"getsas",
"deletesas"
]
},
"tenantId": "28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a"
},
{
"applicationId": null,
"objectId": "f17e37c4-6a48-4102-bd4c-3ec2bcb7947d",
"permissions": {
"certificates": null,
"keys": null,
"secrets": [
"get"
],
"storage": null
},
"tenantId": "28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a"
}
],
"createMode": null,
"enablePurgeProtection": null,
"enableRbacAuthorization": null,
"enableSoftDelete": null,
"enabledForDeployment": false,
"enabledForDiskEncryption": null,
"enabledForTemplateDeployment": null,
"networkAcls": null,
"privateEndpointConnections": null,
"provisioningState": "Succeeded",
"sku": {
"family": "A",
"name": "standard"
},
"softDeleteRetentionInDays": null,
"tenantId": "28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a",
"vaultUri": "https://idjakv.vault.azure.net/"
},
"resourceGroup": "idjakvrg",
"tags": {},
"type": "Microsoft.KeyVault/vaults"
}
We now need that pfx we created when we made the kv.
$ az keyvault secret download --vault-name idjakv --name idjdaprspcert --encoding base64 --file idjdaprspcert.pfx
$ ls -ltra | tail -n2
-rw-r--r-- 1 builder builder 2644 Apr 7 21:50 idjdaprspcert.pfx
drwxr-xr-x 2 builder builder 4096 Apr 7 21:50 .
Now save it as a secret in Kubernetes
$ kubectl create secret generic idjdaprsp --from-file=idjdaprspcert.pfx
secret/idjdaprsp created
For sanity, let’s verify the key name, we’ll need that next.
$ kubectl get secret idjdaprsp -o yaml | grep pfx | head -n1 | sed s/:.*//
idjdaprspcert.pfx
We now have all the parts that we need to create the AKV component.
$ cat akvcomp.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: azurekeyvault
namespace: default
spec:
type: secretstores.azure.keyvault
version: v1
metadata:
- name: vaultName
value: idjakv
- name: spnTenantId
value: "28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a"
- name: spnClientId
value: "8049597e-4970-4430-8f9d-9d920f446f05"
- name: spnCertificate
secretKeyRef:
name: idjdaprsp
key: idjdaprspcert.pfx
auth:
secretStore: kubernetes
$ kubectl apply -f akvcomp.yaml
component.dapr.io/azurekeyvault created
verification
$ kubectl describe component azurekeyvault
Name: azurekeyvault
Namespace: default
Labels: <none>
Annotations: <none>
API Version: dapr.io/v1alpha1
Auth:
Secret Store: kubernetes
Kind: Component
Metadata:
Creation Timestamp: 2021-04-08T02:59:42Z
Generation: 1
Managed Fields:
API Version: dapr.io/v1alpha1
Fields Type: FieldsV1
fieldsV1:
f:auth:
.:
f:secretStore:
f:metadata:
f:annotations:
.:
f:kubectl.kubernetes.io/last-applied-configuration:
f:spec:
.:
f:metadata:
f:type:
f:version:
Manager: kubectl-client-side-apply
Operation: Update
Time: 2021-04-08T02:59:42Z
Resource Version: 23459341
Self Link: /apis/dapr.io/v1alpha1/namespaces/default/components/azurekeyvault
UID: 3659e9be-857a-44a7-bf98-6fb84e0b4415
Spec:
Metadata:
Name: vaultName
Value: idjakv
Name: spnTenantId
Value: 28c575f6-ade1-4838-8e7c-7e6d1ba0eb4a
Name: spnClientId
Value: 8049597e-4970-4430-8f9d-9d920f446f05
Name: spnCertificate
Secret Key Ref:
Key: idjdaprspcert.pfx
Name: idjdaprsp
Type: secretstores.azure.keyvault
Version: v1
Events: <none>
Let's create a secret in our AKV instance
$ az keyvault secret set --name MySecret --value Testing --vault-name idjakv
{
"attributes": {
"created": "2021-04-08T03:12:28+00:00",
"enabled": true,
"expires": null,
"notBefore": null,
"recoveryLevel": "Purgeable",
"updated": "2021-04-08T03:12:28+00:00"
},
"contentType": null,
"id": "https://idjakv.vault.azure.net/secrets/MySecret/a0b2e0360e9c4795967ac9a5db83b9dc",
"kid": null,
"managed": null,
"name": "MySecret",
"tags": {
"file-encoding": "utf-8"
},
"value": "Testing"
}
We can see that in the portal
Now, once we bounce a pod, or remove and reapply a deployment:
perl-subscriber-5f589bb996-t8vhq 1/2 Running 0 20s
We can see the secret:
$ kubectl port-forward perl-subscriber-5f589bb996-t8vhq 3500:3500
Forwarding from 127.0.0.1:3500 -> 3500
Forwarding from [::1]:3500 -> 3500
Handling connection for 3500
# in another session
$ curl http://localhost:3500/v1.0/secrets/azurekeyvault/MySecret
{"MySecret":"Testing"}
It should be noted here that the Dapr side car is providing this secret, not my subscriber app. The subscriber is serving port 8080 and Dapr defaults to 3500.
Updates
Let’s update the secret in the UI
And we can type in a value
Then we can check it
If I keep checking it, it does change
And we can see that i did not rotate that pod:
$ kubectl get pods | grep perl
perl-subscriber-5f589bb996-t8vhq 2/2 Running 0 8h
We can mix secrets components as well
Create the json with a secret.
Note: local secrets ends up not working, but I'll take you through what i tried
$ cat testinglocal.json
{
"localSecretYay" : "see, im a secret"
}
then apply
$ cat testinglocal.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: testinglocal
namespace: default
spec:
type: secretstores.local.file
version: v1
metadata:
- name: secretsFile
value: testinglocal.json
- name: nestedSeparator
value: ":"
$ kubectl apply -f testinglocal.yaml
component.dapr.io/testinglocal created
It crashed the pod.. Since that is a local file (which doesn't work in k8s)
time="2021-04-08T12:03:29.48793413Z" level=fatal msg="process component testinglocal error: open testinglocal.json: no such file or directory" app_id=perl-subscriber instance=perl-subscriber-5f589bb996-l8f99 scope=dapr.runtime type=log ver=1.1.0
I tried setting the files on the k8s nodes themselves:
isaac@isaac-MacBookAir:~$ cat /tmp/testing.json
{
"localSecretYay" : "see, im a secret"
}
isaac@isaac-MacBookPro:~$ cat /tmp/testing.json
{
"localSecretYay" : "see, im a secret"
}
then apply again
$ cat testinglocal.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: testinglocal
namespace: default
spec:
type: secretstores.local.file
version: v1
metadata:
- name: secretsFile
value: /tmp/testing.json
- name: nestedSeparator
value: ":"
$ kubectl apply -f testinglocal.yaml
component.dapr.io/testinglocal created
$ kubectl get component
NAME AGE
pubsub 5d23h
azurekeyvault 10h
awssecretstore 15m
testinglocal 19s
I even came back later after AWS (below) and Azure as well as upgrading to the latest Dapr (1.1.1) and couldn't get localfile to work in Kubernetes
time="2021-04-08T13:12:56.892848028Z" level=info msg="component loaded. name: azurekeyvault, type: secretstores.azure.keyvault/v1" app_id=perl-subscriber instance=perl-subscriber-6d89cf54fc-4kj49 scope=dapr.runtime type=log ver=1.1.1
time="2021-04-08T13:12:56.893142116Z" level=info msg="component loaded. name: awssecretstore, type: secretstores.aws.secretmanager/v1" app_id=perl-subscriber instance=perl-subscriber-6d89cf54fc-4kj49 scope=dapr.runtime type=log ver=1.1.1
time="2021-04-08T13:12:56.893278429Z" level=warning msg="failed to init state store secretstores.local.file/v1 named testinglocal: open /tmp/testing.json: no such file or directory" app_id=perl-subscriber instance=perl-subscriber-6d89cf54fc-4kj49 scope=dapr.runtime type=log ver=1.1.1
time="2021-04-08T13:12:56.893321458Z" level=fatal msg="process component testinglocal error: open /tmp/testing.json: no such file or directory" app_id=perl-subscriber instance=perl-subscriber-6d89cf54fc-4kj49 scope=dapr.runtime type=log ver=1.1.1
AWS
Let’s create us-east-1 SSM parameter
Note: again, here is a try and fail. We'll go through what I did for Parameter Store, which just would not work (as of Dapr 1.1.1 for me) but then we'll use Secret Manager which does work (was GA in 1.0)
We can see it created after we create
We can create a secret store for AWS using an IAM Access and Secret key (and if needed, a token as well)
$ cat aws-secrets.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: awsparameterstore
namespace: default
spec:
type: secretstores.aws.parameterstore
version: v1
metadata:
- name: region
value: "us-east-1"
- name: accessKey
value: "A***********************O"
- name: secretKey
value: "*****************************************************"
$ kubectl apply -f aws-secrets.yaml
component.dapr.io/awsparameterstore created
I’m going to stop here, since i tried many times over to get parameterstore to work. I even updated my cluster to the latest Dapr 1.1.1. No matter what i did, i kept getting the error from the Dapr sidecar:
time="2021-04-08T12:44:18.474799416Z" level=fatal msg="process component awsparameterstore2 error: couldn't find secret store secretstores.aws.parameterstore/v1" app_id=perl-subscriber instance=perl-subscriber-5f589bb996-hjgtd scope=dapr.runtime type=log ver=1.1.0
And after upgrade, the same
time="2021-04-08T12:54:54.905300902Z" level=fatal msg="process component awsparameterstore2 error: couldn't find secret store secretstores.aws.parameterstore/v1" app_id=perl-subscriber instance=perl-subscriber-6d89cf54fc-t4xzn scope=dapr.runtime type=log ver=1.1.1
The documentation indicates parameterstore was introduced in v1.1 but i do not see it.
AWS Secret Store
Using secret store worked, however, while they said sessionToken was optional, for my case, not needing it, i needed to still set the value anyhow (to "")
$ cat aws-secretstore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: awssecretstore
namespace: default
spec:
type: secretstores.aws.secretmanager
version: v1
metadata:
- name: region
value: "us-east-1"
- name: accessKey
value: "A******************"
- name: secretKey
value: "*****************************"
- name: sessionToken
value: ""
$ kubectl apply -f aws-secretstore.yaml
component.dapr.io/awssecretstore created
I created a quick value in AWS Secrets Manager
And when port-forwarding to a dapr pod, i can see the value returned now
$ curl http://localhost:3500/v1.0/secrets/awssecretstore/TestSecret
{"TestSecret":"{\"MySecret\":\"MyVault\"}"}
And this rather proves Dapr can easily pass forward from two engines at the same time
$ curl http://localhost:3500/v1.0/secrets/azurekeyvault/MySecret && echo && curl http://localhost:3500/v1.0/secrets/awssecretstore/TestSecret && echo
{"MySecret":"DarfDarf"}
{"TestSecret":"{\"MySecret\":\"MyVault\"}"}
In the pod
Let’s show how we can pull in the secrets from within the pod. We can actually go to the sidecar and pull the secrets from the dapr runner.
Here we can create another endpoint and fetch secrets from AWS and AKV at the same time.
I'll add a "/D" dispatcher
my %dispatch = (
'/dapr/subscribe' => \&resp_subscribe,
'/hello' => \&resp_hello,
'/A' => \&resp_A,
'/B' => \&resp_B,
'/C' => \&resp_C,
'/D' => \&resp_D,
# ...
);
And implementation of "resp_D"
sub resp_D {
my $cgi = shift; # CGI.pm object
return if !ref $cgi;
my $secretsURL = "http://localhost:3500/v1.0/secrets";
print $cgi->header('application/json');
# azure kv
my $cmd = "curl -H 'Content-Type: application/json' $secretsURL/azurekeyvault/MySecret";
print STDERR "\ncmd: $cmd\n";
my $rc =`$cmd`;
print STDERR "\n$rc\n";
print STDERR "\n";
# aws
$cmd = "curl -H 'Content-Type: application/json' $secretsURL/awssecretstore/TestSecret";
print STDERR "\ncmd: $cmd\n";
$rc =`$cmd`;
print STDERR "\n$rc\n";
print STDERR "\n";
}
When we build and push, we can port forward:
$ kubectl port-forward perl-subscriber-7b4457c4bf-4zghq 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080
Then hit the endpoint:
$ curl -X POST http://localhost:8080/D -H 'Content-Type: application/json'
And lastly check the logs:
$ kubectl logs perl-subscriber-7b4457c4bf-4zghq perl-subscriber
running on 8080
MyWebServer: You can connect to your server at http://localhost:8080/
cmd: curl -H 'Content-Type: application/json' http://localhost:3500/v1.0/secrets/azurekeyvault/MySecret
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 23 100 23 0 0 19 0 0:00:01 0:00:01 --:--:-- 19
{"MySecret":"DarfDarf"}
cmd: curl -H 'Content-Type: application/json' http://localhost:3500/v1.0/secrets/awssecretstore/TestSecret
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 43 100 43 0 0 113 0 --:--:-- --:--:-- --:--:-- 113
{"TestSecret":"{\"MySecret\":\"MyVault\"}"}
And again, this matches our values in AWS Secret Store
and AKV
Notes on Cloud Costs
And if we want a handle on costs, a few days of SecretsManager queries amounted to $0.11 for AWS
And AKV is effectively free at this level
Summary
Overall I really like the ease of which it took to add Dapr components for AWS SSM and Azure AKV. I would have tried Hashi Vault as well, which I'm sure works fine, but will save that for another demo.
I was a tad disappointed that i couldn't get localfile to work, however it would also be rather pointless in Kubernetes as there is a perfectly good basic secrets management reference to Kubernetes secrets already in Dapr and in Kubernetes itself. That said, i was bothered by not being able to use Parameter Store. For all my googling, i could not find others with similar issues. I'm hoping i can sort it out in future demos. While SSM is fine, for my daily work, i often use Parameter store for lightweight storage of key value pairs that don't need the heft of KMS keys for storage.