Azure Key Vault AKS integration with CSI Driver
In this Post I will show you how you can use CSI Driver to mount secrets from Azure Key Vault to AKS.
Create key vault and add keys
First we need to create a key vault
az aks enable-addons \
--addons=azure-keyvault-secrets-provider \
--name=$CLUSTER \
--resource-group=$RG
# create the key vault and turn on Azure RBAC; we will grant a managed identity access to this key vault below
az keyvault create \
--name $KV \
--resource-group $RG \
--location westeurope \
--enable-rbac-authorization true
# get the subscription id
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
# get your user object id
USER_OBJECT_ID=$(az ad signed-in-user show --query objectId -o tsv)
# grant yourself access to key vault
az role assignment create \
--assignee-object-id $USER_OBJECT_ID \
--role "Key Vault Administrator" \
--scope /subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.KeyVault/vaults/$KV
# add a secret to the key vault
az keyvault secret set --vault-name $KV --name $SECRET --value $VALUE
Create secret in keyvault:
az keyvault secret set \
--vault-name "$KV" \
--name "sqldatabase" \
--value 'kvdemo'
az keyvault secret set \
--vault-name "$KV" \
--name "sqlusername" \
--value 'root'
az keyvault secret set \
--vault-name "$KV" \
--name "sqlpassword" \
--value 'Password1'
Acces key vault with system-assigned managed identity
If you created your AKS cluster with managed identity you can go to grant access to a managed identity:
# grab the managed identity principalId assuming it is in the default
# MC_ group for your cluster and resource group
IDENTITY_ID=$(az identity show -g MC\_$RG\_$CLUSTER\_westeurope --name azurekeyvaultsecretsprovider-$CLUSTER --query principalId -o tsv)
# grant access rights on Key Vault
az role assignment create \
--assignee-object-id $IDENTITY_ID \
--role "Key Vault Administrator" \
--scope /subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.KeyVault/vaults/$KV
If not yet enabled you need to enable system-assigned managed identity on AKS:
az aks update \
--resource-group $RG \
--name $CLUSTER \
--enable-managed-identity
Create a SecretProviderClass:
AZURE_TENANT_ID=$(az account show --query tenantId -o tsv)
CLIENT_ID=$(az aks show -g $RG -n $CLUSTER --query addonProfiles.azureKeyvaultSecretsProvider.identity.clientId -o tsv)
cat <<EOF | kubectl apply -f -
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: demo-secret
namespace: default
spec:
provider: azure
parameters:
usePodIdentity: "false"
# use managed identity
useVMManagedIdentity: "true"
# add managed identity id manually
userAssignedIdentityID: "$CLIENT_ID"
tenantId: "$AZURE_TENANT_ID"
keyvaultName: "$KV"
# name and type in keyvault
objects: |
array:
- |
objectName: "sqldatabase"
objectType: secret
- |
objectName: "sqlusername"
objectType: secret
- |
objectName: "sqlpassword"
objectType: secret
secretObjects:
- secretName: databasesecrets
type: Opaque
# name and key in secret
data:
- objectName: "sqldatabase"
key: sqldatabase
- objectName: "sqlusername"
key: sqlusername
- objectName: "sqlpassword"
key: sqlpassword
EOF
Mount the secrets in pods
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: secretpods
name: secretpods
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: secretpods
template:
metadata:
labels:
app: secretpods
spec:
containers:
- image: nginx
name: nginx
# get as environment variables
env:
- name: sqldatabase
valueFrom:
secretKeyRef:
name: databasesecrets
key: sqldatabase
- name: sqlusername
valueFrom:
secretKeyRef:
name: databasesecrets
key: sqlusername
- name: sqlpassword
valueFrom:
secretKeyRef:
name: databasesecrets
key: sqlpassword
# mount as file
volumeMounts:
- name: secret-store
mountPath: "mnt/secret-store"
readOnly: true
# get the SecretProviderClass object
volumes:
- name: secret-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "demo-secret"
EOF
Acces key vault with service principal
Create an identity in Azure
export CLIENT_SECRET=$(az ad sp create-for-rbac \
--role Contributor --scopes /subscriptions/$SUBSCRIPTION_ID \
--name http://secrets-store-test --query 'password' -otsv)
export CLIENT_ID=$(az ad sp show --id http://secrets-store-test --query 'appId' -otsv)
Provide policy to the identity to access the Azure key vault
az keyvault set-policy -n $KV --secret-permissions get --spn $CLIENT_ID
Create the Kubernetes secret with credentials
cat <<EOF | kubectl apply -f -
apiVersion: v1
data:
clientid: $CLIENT_ID
clientsecret: $CLIENT_SECRET
kind: Secret
metadata:
labels:
secrets-store.csi.k8s.io/used: "true"
name: secrets-store-creds
namespace: default
type: Opaque
EOF
Create and apply your own SecretProviderClass object
cat <<EOF | kubectl apply -f -
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: demo-secret
namespace: default
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "false"
tenantId: "$AZURE_TENANT_ID"
keyvaultName: "$KV"
# name and type in keyvault
objects: |
array:
- |
objectName: "sqldatabase"
objectType: secret
- |
objectName: "sqlusername"
objectType: secret
- |
objectName: "sqlpassword"
objectType: secret
secretObjects:
- secretName: databasesecrets
type: Opaque
# name and key in secret
data:
- objectName: "sqldatabase"
key: sqldatabase
- objectName: "sqlusername"
key: sqlusername
- objectName: "sqlpassword"
key: sqlpassword
EOF
Mount the secrets in pods
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: secretpods
name: secretpods
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: secretpods
template:
metadata:
labels:
app: secretpods
spec:
containers:
- image: nginx
name: nginx
# get as environment variables
env:
- name: sqldatabase
valueFrom:
secretKeyRef:
name: databasesecrets
key: sqldatabase
- name: sqlusername
valueFrom:
secretKeyRef:
name: databasesecrets
key: sqlusername
- name: sqlpassword
valueFrom:
secretKeyRef:
name: databasesecrets
key: sqlpassword
# mount as file
volumeMounts:
- name: secret-store
mountPath: "mnt/secret-store"
readOnly: true
# get the SecretProviderClass object
volumes:
- name: secret-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "demo-secret"
# Only required when using service principal mode
nodePublishSecretRef:
name: secrets-store-creds
EOF
Acces key vault with federated workload identity
This scanario is similar to the previous one, but instead of using managed identities we use the AKS cluster’s workload identity to authenticate. In normal situation the identity is autenticates with a password. Password authentication is not the safest option. Instad we will use a federation to authenticate with OIDC SSO authentication.
Enable OIDC issuer and Workload identity in an existing AKS cluster.
az aks update \
--resource-group $RG \
--name $CLUSTER \
--enable-oidc-issuer \
--enable-workload-identity
Create a managed identity:
az identity create \
--name secrets-store-test \
--resource-group $RG \
--location $RG_LOCATION \
--subscription $SUBSCRIPTION
Get the OIDC issuer url and export it as an environment variable.
export AKS_OIDC_ISSUER=$(az aks show --name $CLUSTER --resource-group $RG --query "oidcIssuerProfile.issuerUrl" -o tsv)
Export managed identity variable. We will assign this to the AKS service account.
export USER_ASSIGNED_CLIENT_ID=$(az identity show --resource-group $RG --name secrets-store-test --query 'clientId' -otsv)
export IDENTITY_TENANT=$(az aks show --name $CLUSTER --resource-group $RG)
Create AKS service account and we will assign the Managed Identity ClientID to it using azure.workload.identity/client-id
annotation.
az aks get-credentials -n $CLUSTER -g $RG
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
azure.workload.identity/client-id: ${USER_ASSIGNED_CLIENT_ID}
labels:
azure.workload.identity/use: "true"
name: keyvault-sa
namespace: default
EOF
Create the federated identity credential between the managed identity, service account issuer and subject.
export FEDERATED_IDENTITY_NAME="aksfederatedidentity" # can be changed as needed
az identity federated-credential create \
--name $FEDERATED_IDENTITY_NAME \
--identity-name secrets-store-test \
--resource-group $RG \
--issuer ${AKS_OIDC_ISSUER} \
--subject system:serviceaccount:default:keyvault-sa
Provide policy to the identity to access the Azure key vault
az keyvault set-policy -n $KV --secret-permissions get --spn $USER_ASSIGNED_CLIENT_ID
Create a SecretProviderClass:
AZURE_TENANT_ID=$(az account show --query tenantId -o tsv)
cat <<EOF | kubectl apply -f -
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: demo-secret
namespace: default
spec:
provider: azure
parameters:
# use managed identity
useVMManagedIdentity: "false"
clientID: "$USER_ASSIGNED_CLIENT_ID"
tenantId: "$AZURE_TENANT_ID"
keyvaultName: "$KV"
# name and type in keyvault
objects: |
array:
- |
objectName: "sqldatabase"
objectType: secret
- |
objectName: "sqlusername"
objectType: secret
- |
objectName: "sqlpassword"
objectType: secret
secretObjects:
- secretName: databasesecrets
type: Opaque
# name and key in secret
data:
- objectName: "sqldatabase"
key: sqldatabase
- objectName: "sqlusername"
key: sqlusername
- objectName: "sqlpassword"
key: sqlpassword
EOF
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: secretpods
name: secretpods
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: secretpods
template:
metadata:
labels:
app: secretpods
spec:
serviceAccountName: keyvault-sa
containers:
- image: nginx
name: nginx
# get as environment variables
env:
- name: sqldatabase
valueFrom:
secretKeyRef:
name: databasesecrets
key: sqldatabase
- name: sqlusername
valueFrom:
secretKeyRef:
name: databasesecrets
key: sqlusername
- name: sqlpassword
valueFrom:
secretKeyRef:
name: databasesecrets
key: sqlpassword
# mount as file
volumeMounts:
- name: secret-store
mountPath: "mnt/secret-store"
readOnly: true
# get the SecretProviderClass object
volumes:
- name: secret-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "demo-secret"
EOF
Demo time
Now we you can werify the secret in the pod:
export POD_NAME=$(kubectl get pods -n default -l "app=secretpods" -o jsonpath="{.items[0].metadata.name}")
# if this does not work, check the status of the pod
# if still in ContainerCreating there might be an issue
kubectl exec -it $POD_NAME -- sh
cd /mnt/secret-store
ls
sqldatabase sqlpassword sqlusername
# the file containing the secret is listed
cat sqldatabase
# echo the value of the environment variable
echo $sqldatabase