Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ All samples implement the same Vacation Planner web app. They only vary the unde
| [web-app-cosmosdb-nosql-api](samples/web-app-cosmosdb-nosql-api/) | Stores activities in a container of an [Azure Cosmos DB for NoSQL](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/) account. |
| [web-app-storage-account](samples/web-app-storage-account/) | Stores activities in an [Azure Blob Storage](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction) container, using a connection string. |
| [web-app-managed-identity](samples/web-app-managed-identity/) | Stores activities in an Azure Blob Storage container, authenticating with [Microsoft Entra Workload ID](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview) (federated credential plus workload identity) instead of a secret, and optionally exposes the app through the Gateway API with a managed TLS certificate. |
| [key-vault-csi-driver](samples/key-vault-csi-driver/) | Mounts secrets from [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview) into a pod with the [Azure Key Vault provider for Secrets Store CSI Driver](https://learn.microsoft.com/en-us/azure/aks/csi-secrets-store-driver), demonstrating both the Microsoft Entra Workload ID and the user-assigned managed identity access modes. |

Each sample folder follows the same layout:

Expand Down
25 changes: 25 additions & 0 deletions samples/key-vault-csi-driver/00-variables.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Azure Kubernetes Service (AKS)
PREFIX="local"
SUFFIX="test"
AKS_NAME="${PREFIX}-aks-${SUFFIX}"
AKS_RESOURCE_GROUP_NAME="${PREFIX}-rg"

# Azure Key Vault
KEY_VAULT_NAME="${PREFIX}-kv-${SUFFIX}"
KEY_VAULT_RESOURCE_GROUP_NAME="${PREFIX}-rg"
KEY_VAULT_SKU="Standard"
LOCATION="WestEurope" # Choose a location

# Secrets and Values
SECRETS=("username" "password")
VALUES=("admin" "trustno1!")

# Azure Subscription and Tenant
TENANT_ID=$(az account show --query tenantId --output tsv --only-show-errors)
SUBSCRIPTION_NAME=$(az account show --query name --output tsv --only-show-errors)
SUBSCRIPTION_ID=$(az account show --query id --output tsv --only-show-errors)
ENVIRONMENT_NAME=$(az account show --query environmentName --output tsv --only-show-errors)

# Others
RETRY_COUNT=5
SLEEP=3
83 changes: 83 additions & 0 deletions samples/key-vault-csi-driver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
## Azure Key Vault Provider for Secrets Store CSI Driver in AKS

The [Azure Key Vault provider for Secrets Store CSI Driver](https://learn.microsoft.com/en-us/azure/aks/csi-secrets-store-driver) enables retrieving secrets, keys, and certificates stored in Azure Key Vault and accessing them as files from mounted volumes in an AKS cluster. This method eliminates the need for Azure-specific libraries to access the secrets.

This [Secret Store CSI Driver for Key Vault](https://github.com/Azure/secrets-store-csi-driver-provider-azure) offers the following features:

- Mounts secrets, keys, and certificates to a pod using a CSI volume.
- Supports CSI inline volumes.
- Allows the mounting of multiple secrets store objects as a single volume.
- Offers pod portability with the SecretProviderClass CRD.
- Compatible with Windows containers.
- Keeps in sync with Kubernetes secrets.
- Supports auto-rotation of mounted contents and synced Kubernetes secrets.

When auto-rotation is enabled for the Azure Key Vault Secrets Provider, it automatically updates both the pod mount and the corresponding Kubernetes secret defined in the **secretObjects** field of SecretProviderClass. It continuously polls for changes based on the rotation poll interval (default is two minutes).

If a secret in an external secrets store is updated after the initial deployment of the pod, both the Kubernetes Secret and the pod mount will periodically update, depending on how the application consumes the secret data. Here are the recommended approaches for different scenarios:

1. Mount the Kubernetes Secret as a volume: Utilize the auto-rotation and sync K8s secrets features of Secrets Store CSI Driver. The application should monitor changes from the mounted Kubernetes Secret volume. When the CSI Driver updates the Kubernetes Secret, the volume contents will be automatically updated.
2. Application reads data from the container filesystem: Take advantage of the rotation feature of Secrets Store CSI Driver. The application should monitor file changes from the volume mounted by the CSI driver.
3. Use the Kubernetes Secret for an environment variable: Restart the pod to acquire the latest secret as an environment variable. You can use tools like Reloader to watch for changes on the synced Kubernetes Secret and perform rolling upgrades on pods.

### Advantages

- Secrets, keys, and certificates can be accessed as files from mounted volumes.
- Optionally, Kubernetes secrets can be created to store keys, secrets, and certificates from Key Vault.
- No need for Azure-specific libraries to access secrets.
- Simplifies secret management with transparent integration.

### Disadvantages

- Still requires accessing managed services such as Azure Service Bus or Azure Storage using their own connection strings from Azure Key Vault.
- Cannot utilize Microsoft Entra ID integrated security and managed identities for accessing managed services.

## Identity Access Modes

The Azure Key Vault provider for Secrets Store CSI Driver supports more than one way to authenticate to Azure Key Vault. This folder contains two samples, each demonstrating a different identity access mode. Both samples mount the same `username` and `password` secrets into a demo nginx pod, but they differ in how the CSI driver obtains the credentials used to read the secrets from Key Vault.

| Sample | Identity Access Mode | Identity Used |
| --- | --- | --- |
| [`workload-identity`](./workload-identity) | Microsoft Entra Workload ID | A customer-created user-assigned managed identity federated with the pod's Kubernetes service account |
| [`user-assigned-managed-identity`](./user-assigned-managed-identity) | User-assigned managed identity | The user-assigned managed identity created by the resource provider in the node resource group when enabling the addon |

### Workload Identity

The [`workload-identity`](./workload-identity) sample uses a customer-created user-assigned managed identity that is consumed by the pod through [Microsoft Entra Workload ID with Azure Kubernetes Service (AKS)](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview?tabs=dotnet), as described in [Configure Workload Identity to access Key Vault](https://github.com/Azure/secrets-store-csi-driver-provider-azure/blob/d5f7cf5b598c2eede99ad3683de0ba10f7a8736b/website/content/en/configurations/identity-access-modes/workload-identity-mode.md).

With this approach, a managed identity is created and a federated identity credential establishes trust between the AKS OIDC issuer and a Kubernetes service account. The service account is annotated with the managed identity client ID and labeled to use workload identity, and the demo pod references this service account. At runtime, the pod exchanges its projected service account token for an Entra ID token, which the CSI driver uses to read the secrets from Key Vault. This is the recommended, more secure, and portable approach.

### User-assigned Managed Identity

The [`user-assigned-managed-identity`](./user-assigned-managed-identity) sample uses the user-assigned managed identity that the resource provider automatically creates in the **node resource group** when the Azure Key Vault Secrets Provider addon is enabled on the cluster, as described in [Configure User-assigned Managed Identity to access Key Vault](https://github.com/Azure/secrets-store-csi-driver-provider-azure/blob/d5f7cf5b598c2eede99ad3683de0ba10f7a8736b/website/content/en/configurations/identity-access-modes/user-assigned-msi-mode.md).

With this approach, no additional managed identity or federated credential is created. Instead, the addon's built-in `azureKeyvaultSecretsProvider` identity is granted access to the Key Vault, and the `SecretProviderClass` references it via `useVMManagedIdentity` and `userAssignedIdentityID`. The demo pod does not require a workload identity service account or labels.

## Scripts

### `user-assigned-managed-identity`

- `00-variables.sh`: Sources parent variables and defines Kubernetes namespace, SecretProviderClass name, and pod name for this authentication approach.
- `01-enable-addon.sh`: Enables the Azure Key Vault Secrets Provider addon on the AKS cluster with secret rotation enabled (if not already active).
- `02-create-key-vault-and-secrets.sh`: Creates the resource group and Azure Key Vault, then populates it with test secrets (username and password).
- `03-create-role-assignment.sh`: Creates RBAC role assignments to grant the Key Vault Secrets Provider system-managed identity permission to read secrets from the Key Vault.
- `04-create-secret-provider-class.sh`: Creates the SecretProviderClass resource that configures the CSI driver to retrieve secrets from Key Vault using the system-managed identity.
- `05-create-demo-pod.sh`: Creates a demo nginx pod that mounts the secrets as a volume using the CSI driver.
- `06-list-secrets.sh`: Lists and displays the contents of secrets that were successfully mounted in the pod.

### `workload-identity`

- `00-variables.sh`: Sources parent variables and defines Kubernetes namespace, service account, SecretProviderClass name, and pod name for this authentication approach.
- `01-enable-addon.sh`: Enables the Azure Key Vault Secrets Provider addon on the AKS cluster with secret rotation enabled (if not already active).
- `02-create-key-vault-and-secrets.sh`: Creates the resource group and Azure Key Vault, then populates it with test secrets (username and password).
- `03-create-managed-identity.sh`: Creates the resource group and an Azure managed identity configured for Workload Identity Federation with the AKS cluster.
- `04-create-secret-provider-class.sh`: Creates the SecretProviderClass resource that configures the CSI driver to retrieve secrets using Workload Identity Federation.
- `05-create-demo-pod.sh`: Creates a demo nginx pod with workload identity labels and service account that mounts secrets via the CSI driver volume.
- `06-list-secrets.sh`: Lists and displays the contents of secrets that were successfully mounted in the pod.

### Resources

- [Using the Azure Key Vault Provider for Secrets Store CSI Driver in AKS](https://learn.microsoft.com/en-us/azure/aks/csi-secrets-store-driver)
- [Access Azure Key Vault with the CSI Driver Identity Provider](https://learn.microsoft.com/en-us/azure/aks/csi-secrets-store-identity-access?tabs=azure-portal&pivots=access-with-service-connector)
- [Configuration and Troubleshooting Options for Azure Key Vault Provider in AKS](https://learn.microsoft.com/en-us/azure/aks/csi-secrets-store-configuration-options)
- [Azure Key Vault Provider for Secrets Store CSI Driver](https://github.com/Azure/secrets-store-csi-driver-provider-azure)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Variables
source ../00-variables.sh

# Azure Managed Identity
MANAGED_IDENTITY_NAME="${PREFIX}-identity-${SUFFIX}"
FEDERATED_IDENTITY_NAME="federated-identity"

# Kubernetes
NAMESPACE="mi-secret-store-test"
SECRET_PROVIDER_CLASS_NAME="demo-secret-provider-class"
POD_NAME="demo-pod"
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash

# For more information, see:
# https://learn.microsoft.com/en-us/azure/aks/csi-secrets-store-driver
# https://learn.microsoft.com/en-us/azure/aks/csi-secrets-store-identity-access

# Variables
source ./00-variables.sh

# Enable Addon
echo "Checking if the [azure-keyvault-secrets-provider] addon is enabled in the [$AKS_NAME] AKS cluster..."
az aks addon show \
--addon azure-keyvault-secrets-provider \
--name $AKS_NAME \
--resource-group $AKS_RESOURCE_GROUP_NAME &>/dev/null

if [[ $? != 0 ]]; then
echo "The [azure-keyvault-secrets-provider] addon is not enabled in the [$AKS_NAME] AKS cluster"
echo "Enabling the [azure-keyvault-secrets-provider] addon in the [$AKS_NAME] AKS cluster..."

az aks addon enable \
--addon azure-keyvault-secrets-provider \
--enable-secret-rotation \
--name $AKS_NAME \
--resource-group $AKS_RESOURCE_GROUP_NAME
else
echo "The [azure-keyvault-secrets-provider] addon is already enabled in the [$AKS_NAME] AKS cluster"
fi
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/bin/bash

# Variables
source ./00-variables.sh

# Check if the resource group already exists
echo "Checking if [$KEY_VAULT_RESOURCE_GROUP_NAME] resource group actually exists in the [$SUBSCRIPTION_NAME] subscription..."

az group show --name $KEY_VAULT_RESOURCE_GROUP_NAME &>/dev/null

if [[ $? != 0 ]]; then
echo "No [$KEY_VAULT_RESOURCE_GROUP_NAME] resource group actually exists in the [$SUBSCRIPTION_NAME] subscription"
echo "Creating [$KEY_VAULT_RESOURCE_GROUP_NAME] resource group in the [$SUBSCRIPTION_NAME] subscription..."

# create the resource group
az group create --name $KEY_VAULT_RESOURCE_GROUP_NAME --location $LOCATION 1>/dev/null

if [[ $? == 0 ]]; then
echo "[$KEY_VAULT_RESOURCE_GROUP_NAME] resource group successfully created in the [$SUBSCRIPTION_NAME] subscription"
else
echo "Failed to create [$KEY_VAULT_RESOURCE_GROUP_NAME] resource group in the [$SUBSCRIPTION_NAME] subscription"
exit
fi
else
echo "[$KEY_VAULT_RESOURCE_GROUP_NAME] resource group already exists in the [$SUBSCRIPTION_NAME] subscription"
fi

# Check if the key vault already exists
echo "Checking if [$KEY_VAULT_NAME] key vault actually exists in the [$SUBSCRIPTION_NAME] subscription..."

az keyvault show --name $KEY_VAULT_NAME --resource-group $KEY_VAULT_RESOURCE_GROUP_NAME &>/dev/null

if [[ $? != 0 ]]; then
echo "No [$KEY_VAULT_NAME] key vault actually exists in the [$SUBSCRIPTION_NAME] subscription"
echo "Creating [$KEY_VAULT_NAME] key vault in the [$SUBSCRIPTION_NAME] subscription..."

# create the key vault
az keyvault create \
--name $KEY_VAULT_NAME \
--resource-group $KEY_VAULT_RESOURCE_GROUP_NAME \
--location $LOCATION \
--enabled-for-deployment \
--enabled-for-disk-encryption \
--enabled-for-template-deployment \
--sku $KEY_VAULT_SKU 1>/dev/null
Comment on lines +38 to +45

if [[ $? == 0 ]]; then
echo "[$KEY_VAULT_NAME] key vault successfully created in the [$SUBSCRIPTION_NAME] subscription"
else
echo "Failed to create [$KEY_VAULT_NAME] key vault in the [$SUBSCRIPTION_NAME] subscription"
exit
fi
else
echo "[$KEY_VAULT_NAME] key vault already exists in the [$SUBSCRIPTION_NAME] subscription"
fi

# Retrieve the resource id of the Key Vault resource
echo "Retrieving the resource id for the [$KEY_VAULT_NAME] key vault..."
KEY_VAULT_ID=$(az keyvault show \
--name $KEY_VAULT_NAME \
--resource-group $KEY_VAULT_RESOURCE_GROUP_NAME \
--query id \
--output tsv)

if [[ -n $KEY_VAULT_ID ]]; then
echo "[$KEY_VAULT_ID] resource id for the [$KEY_VAULT_NAME] key vault successfully retrieved"
else
echo "Failed to retrieve the resource id for the [$KEY_VAULT_NAME] key vault"
exit
fi

if [[ "$ENVIRONMENT_NAME" == "AzureCloud" ]]; then
# Get the signed-in user object id
LOGGED_IN_USER_ID=$(az ad signed-in-user show --query id --output tsv)
LOGGED_IN_USER_DISPLAY_NAME=$(az ad signed-in-user show --query displayName --output tsv)

# Assign the Key Vault Administrator role to the user on the key vault
ROLE="Key Vault Administrator"
USER_DISPLAY_NAME="$LOGGED_IN_USER_DISPLAY_NAME"
PRINCIPAL_ID="$LOGGED_IN_USER_ID"
SCOPE_ID="$KEY_VAULT_ID"
SCOPE_NAME="$KEY_VAULT_NAME"
SCOPE_TYPE="key vault"
echo "Checking if the [$USER_DISPLAY_NAME] user has the [$ROLE] role assignment on the [$SCOPE_NAME] $SCOPE_TYPE..."
current=$(az role assignment list \
--assignee "$PRINCIPAL_ID" \
--scope "$SCOPE_ID" \
--query "[?roleDefinitionName=='$ROLE'].roleDefinitionName" \
--output tsv 2>/dev/null)

if [[ $current == "$ROLE" ]]; then
echo "User [$USER_DISPLAY_NAME] already has the [$ROLE] role assignment on the [$SCOPE_NAME] $SCOPE_TYPE"
else
echo "User [$USER_DISPLAY_NAME] does not have the [$ROLE] role assignment on the [$SCOPE_NAME] $SCOPE_TYPE"
echo "Creating role assignment: assigning [$ROLE] role to user [$USER_DISPLAY_NAME] on the [$SCOPE_NAME] $SCOPE_TYPE..."
ATTEMPT=1
while [ $ATTEMPT -le $RETRY_COUNT ]; do
echo "Attempt $ATTEMPT of $RETRY_COUNT to assign role..."
az role assignment create \
--assignee "$PRINCIPAL_ID" \
--role "$ROLE" \
--scope "$SCOPE_ID" 1>/dev/null

if [[ $? == 0 ]]; then
break
else
if [ $ATTEMPT -lt $RETRY_COUNT ]; then
echo "Role assignment failed. Waiting [$SLEEP] seconds before retry..."
sleep $SLEEP
fi
ATTEMPT=$((ATTEMPT + 1))
fi
done

if [[ $? == 0 ]]; then
echo "Successfully assigned [$ROLE] role to user [$USER_DISPLAY_NAME] on the [$SCOPE_NAME] $SCOPE_TYPE"
else
echo "Failed to assign [$ROLE] role to user [$USER_DISPLAY_NAME] on the [$SCOPE_NAME] $SCOPE_TYPE"
exit 1
fi
fi
fi

# Create secrets
for INDEX in ${!SECRETS[@]}; do
# Check if the secret already exists
echo "Checking if [${SECRETS[$INDEX]}] secret actually exists in the [$KEY_VAULT_NAME] key vault..."

az keyvault secret show --name ${SECRETS[$INDEX]} --vault-name $KEY_VAULT_NAME &>/dev/null

if [[ $? != 0 ]]; then
echo "No [${SECRETS[$INDEX]}] secret actually exists in the [$KEY_VAULT_NAME] key vault"
ATTEMPT=1
while [ $ATTEMPT -le $RETRY_COUNT ]; do
echo "Attempt $ATTEMPT of $RETRY_COUNT to create [${SECRETS[$INDEX]}] secret in the [$KEY_VAULT_NAME] key vault..."

# Create the secret
az keyvault secret set \
--name ${SECRETS[$INDEX]} \
--vault-name $KEY_VAULT_NAME \
--value ${VALUES[$INDEX]} 1>/dev/null

if [[ $? == 0 ]]; then
echo "[${SECRETS[$INDEX]}] secret successfully created in the [$KEY_VAULT_NAME] key vault"
break
else
if [ $ATTEMPT -lt $RETRY_COUNT ]; then
echo "Failed to create [${SECRETS[$INDEX]}] secret in the [$KEY_VAULT_NAME] key vault. Waiting [$SLEEP] seconds before retry..."
sleep $SLEEP
fi
ATTEMPT=$((ATTEMPT + 1))
fi
done
else
echo "[${SECRETS[$INDEX]}] secret already exists in the [$KEY_VAULT_NAME] key vault"
fi
done

# Show secrets
for INDEX in ${!SECRETS[@]}; do
# retrieve the secret
echo "Retrieving [${SECRETS[$INDEX]}] secret from the [$KEY_VAULT_NAME] key vault..."
VALUE=$(az keyvault secret show \
--name ${SECRETS[$INDEX]} \
--vault-name $KEY_VAULT_NAME \
--query value \
--output tsv)

if [[ -n $VALUE ]]; then
echo "[$VALUE] value for [${SECRETS[$INDEX]}] secret successfully retrieved from the [$KEY_VAULT_NAME] key vault"
else
echo "Failed to retrieve [${SECRETS[$INDEX]}] secret from the [$KEY_VAULT_NAME] key vault"
exit
fi
done
Loading