Automatisation de la rotation des mots de passe des service connexions Azure DevOps avec Azure Function, Key vault et Managed Identity

Ousmane Barry
11 min readMay 23, 2023

--

Pour déployer sur Azure, la pratique courante consiste à créer une application Azure Active Directory (AAD), lui attribuer par exemple le rôle de «Contributor» sur le groupe de ressources, puis configurer une connexion de service avec l’application AAD sur le compte Azure DevOps pour permettre le déploiement sur Azure.

Workflow de déploiement Azure DevOps avec une ARM Service Connection

Cependant, cette approche présente un inconvénient majeur : lorsque le mot de passe de l’application Azure AD expire, les développeurs doivent manuellement en générer un nouveau et mettre à jour la connexion de service correspondante dans Azure DevOps. Ce processus manuel peut être fastidieux et augmenter les risques liés à la sécurité des mots de passe.

Dans cet article, nous explorerons une solution pour automatiser la rotation des mots de passe des connexions de service Azure DevOps en utilisant Azure Key Vault, Event Grid, Storage Account, Azure Function et Managed Identity. Cette approche permettra non seulement de gagner du temps, mais aussi d’améliorer la sécurité globale du processus de déploiement.

Il est important de noter que cette solution est particulièrement adaptée aux déploiements utilisant des Microsoft Hosted Agents. Cependant, si vous utilisez des Self Hosted Agents pour vos déploiements, une approche différente est recommandée. Dans ce cas, il est préférable d’utiliser une Managed Identity directement sur l’agent. Cela vous permettra de vous affranchir de la gestion et du renouvellement des mots de passe, simplifiant ainsi grandement le processus et renforçant la sécurité de votre environnement.

Solution d’automatisation de rotation de secrets proposée

Pour résoudre les problèmes liés à la gestion manuelle des mots de passe des connexions de service Azure DevOps, nous vous proposons une solution d’automatisation de la rotation des secrets. Voici les étapes détaillées pour mettre en place cette solution :

Workflow d’automatisation de rotation de secrets
  1. Détection de l’expiration des secrets : Le Key Vault surveille constamment l’état de ses secrets stockés. Lorsqu’un secret est sur le point d’expirer (événement “SecretNearExpiry”) ou a expiré (événement “SecretExpired”), le Key Vault déclenche l’événement correspondant.
  2. Envoi des événements à la file d’attente de stockage Azure : Les événements déclenchés par le Key Vault sont envoyés au sujet Event Grid associé au Key Vault. Ce sujet est configuré pour envoyer les événements à une file d’attente spécifique dans un compte de stockage Azure. Le topic utilise un System Managed Identity pour envoyer le message dans la queue. Ainsi, le trafic reste dans le backbone d’Azure et est sécurisé grâce au System Managed Identity avec le rôle nécessaire (least privilege) : Storage Queue Data Message Sender. (Reference)
  3. Traitement des événements de la queue par la Function App : La Function App surveille constamment la queue de stockage Azure. Lorsqu’elle détecte un nouvel message dans la queue, elle déclenche une fonction pour traiter cet événement. Cette fonction est responsable de la rotation des secrets. La fonction se connecte au Storage Account en utilisant un User Assigned Managed Identity (UAMI). Pour ce faire, ce dernier a les rôles suivants sur le Storage Account : Storage Account Contributor, Storage Blob Data Owner, Storage Queue Data Contributor. (Reference).
  4. Rotation des secrets de l’application Azure AD : Si l’événement traité est un événement de type “SecretNearExpiry” ou “SecretExpired”, la Function App utilise la User Assigned Managed Identity pour mettre à jour le secret de l’application Azure AD correspondante. Pour ce faire, la User Assign Managed Identity est Owner de l’application Azure AD et il a la permission Graph API Application.ReadWrite.OwnedBy; ce qui lui donne les droits nécessaires pour mettre à jour le secret. Ces deux actions ne peuvent être effectuées via le portail. Il faut passer obligatoirement par script (az cli) ou API. Nous le verrons dans la section suivante.
  5. Mise à jour du secret dans le Key Vault : Le secret dans le Key Vault est mis à jour avec le nouveau secret généré avec la nouvelle date d’expiration. Le User Assigned Managed Identity dispose ainsi du rôle Key Vault Secrets Officer sur le Key Vault.
  6. Mise à jour du mot de passe de la connexion de service Azure DevOps : Après avoir mis à jour le secret de l’application Azure AD, la Function App utilise également la Managed Identity pour mettre à jour le mot de passe de l’ARM Service Connection correspondante dans Azure DevOps. Nous utilisons une des dernières fonctionnalités de Azure DevOps: Utiliser des identités managées de & principaux de service Azure Active Directory. Ainsi, la User Assigned Managed Identity est ajoutée dans Azure DevOps et elle est aussi ajoutée comme membre du groupe Endpoint Administrators du projet concerné.

En résumé, ce workflow fonctionne comme suit:

  • Lorsqu’un secret dans le Key Vault est proche de son expiration ou a expiré, un événement est déclenché.
  • Cet événement est capté par le topic d’Event Grid, qui l’envoie ensuite à la queue de Storage Account grâce à la souscription créée.
  • La Function App peut alors être déclenchée par ces messages dans la queue pour effectuer la rotation du secret de l’application Azure AD, mise à jour du secret dans dans le Key Vault et mise à jour du mot de passe de service connection dans Azure DevOps

Dans la suite, nous allons voir tout le code nécessaire pour mettre en place cette infrastructure et le code de la fonction en C#.

Mise en place du workflow

Tout le code est disponible sur le repo suivant : https://github.com/Thialala/automatic-secrets-rotation.git

Déploiement des ressources et assignation de la User Assigned Managed Identity Owner de l’application AAD

Le déploiement de toute l’infrastructure présentée ci-dessous se fait via de l’Infra As Code avec Bicep. Nous avons créé un script Powershell deploy.ps1 qui déploie le fichier main.bicep et récupère en sortie du déploiement le principalId du Managed Identity. Ce dernier est ensuite utilisé pour être ajouté comme Owner de l’application Azure AD dont le client id est fourni en entrée.

param (
[Parameter(Mandatory = $true)]
[string]$ResourceGroupName,

[Parameter(Mandatory = $true)]
[string]$ApplicationClientId
)

# Get the principalId of the Managed Identity from Bicep deployment outputs
$deploymentOutputs = az deployment group create --resource-group $ResourceGroupName --template-file .\main.bicep --query properties.outputs --output json | ConvertFrom-Json
$managedIdentityPrincipalId = $deploymentOutputs.managedIdentityPrincipalId.value

# Add the Managed Identity as an owner of the Azure AD application
az ad app owner add --id $ApplicationClientId --owner-object-id $managedIdentityPrincipalId

Le fichier main.bicep crée et configure plusieurs ressources Azure nécessaires pour la mise en place du workflow. Voici une description de chaque ressource déployée :

  • User Assigned Managed Identity : crée la Managed Identity pour l’application qui permettra d’accéder aux différentes ressources Azure de manière sécurisée.
  • Storage Account et Queue : crée un compte de stockage et la queue qui sera utilisée pour déclencher la fonction Azure. Les Shared Access Keys sont désactivés pour le Storage Account.
  • Assignations de rôles : ces ressources attribuent à la Managed Identity différents rôles sur le compte de stockage pour permettre un accès approprié.
  • Key Vault : crée une instance de Azure Key Vault pour stocker les secrets. Il attribue également à la Managed Identity le rôle de Key Vault Secrets Officer. Les Access Policies sont désactivées et le Key Vault n’est accessible que via RBAC.
  • Application Insights : crée une instance d’Application Insights pour le suivi et la surveillance de la Function App.
  • Plan de fonction Azure : crée un plan d’hébergement pour la Function App.
  • Function App : crée la Function App qui va héberger la fonction qui va exécuter le workflow de rotation de secrets.
  • Topic EventGrid : crée un topic EventGrid qui sera utilisé pour déclencher les événements de rotation des secrets dans Key Vault. Le System Assigned Identity est activé et ce dernier à le rôle Storage Queue Data Message Sender.
  • Souscription à l’événement : crée une souscription à l’événement qui publie dans la queue du Storage Account qui va ainsi déclencher la fonction lorsque les événements SecretNearExpiry et SecretExpired se produisent dans Azure Key Vault.

Assignation de la permission Graph API

L’identité gérée requiert l’autorisation Application.ReadWrite.OwnedBy, qui lui confère les droits pour gérer les applications dont elle est propriétaire. Pour attribuer cette autorisation, nous disposons d’un script PowerShell dédié nommé assignGraphPermission.ps1. Il est important de noter que cette action ne peut pas être réalisée à travers le portail Azure.

# Input Parameters:
# $appRoleName: The name of the app role to be assigned (e.g., "Application.ReadWrite.OwnedBy")
# $spnObjectId: The object ID of the service principal to which the app role should be assigned

param (
[Parameter(Mandatory=$true)]
[string]$appRoleName,
[Parameter(Mandatory=$true)]
[string]$spnObjectId
)

# Define the Microsoft Graph Application ID
$graphAppId = "00000003-0000-0000-c000-000000000000"

# Retrieve the resource ID of the Microsoft Graph service principal using the Microsoft Graph App ID
$graphResourceId=$(az ad sp show --id $graphAppId --query 'id' --output tsv)

# Retrieve the app role ID for the given appRoleName from the Microsoft Graph service principal
$appRoleId=$(az ad sp show --id $graphAppId --query "appRoles[?value=='$appRoleName' && contains(allowedMemberTypes, 'Application')].id" --output tsv)

# Define the URI for assigning the app role to the service principal
$uri="https://graph.microsoft.com/v1.0/servicePrincipals/$spnObjectId/appRoleAssignments"
Write-Output $uri
# Create the JSON request body containing the required information for the app role assignment
$body="{'principalId':'$spnObjectId','resourceId':'$graphResourceId','appRoleId':'$appRoleId'}"

# Send a POST request to the Microsoft Graph API to create the app role assignment
az rest --method post --uri $uri --body $body --headers "Content-Type=application/json"

Même si nous aurions pu intégrer ce script au sein du script précédent, il est généralement préférable de séparer ces responsabilités. En pratique, la gestion de l’Azure Active Directory est souvent assurée par une équipe spécifique, qui est distincte des équipes de développement d’applications.

Ajout de la Managed Identity dans Azure DevOps

Récemment, Azure DevOps a introduit la capacité d’intégrer un Service Principal ou une Managed Identity dans Azure DevOps, ce qui nous permet d’éviter l’utilisation d’un Personal Access Token (PAT). Dans le contexte actuel, nous pouvons ajouter la Managed Identity en lui attribuant une licence Stakeholder dans Azure DevOps. Par la suite, cette identité doit être ajoutée au groupe Endpoint Administrators du projet en question. Cette approche renforce la sécurité et simplifie la gestion des identités et des accès dans l’environnement Azure DevOps.

Code C# de la fonction

L’Azure Function s’appelle KeyVaultSecretNearExpiry et elle est déclenchée par tout message dans la queue kv-secrets-near-expiry.

Le message de la queue est désérialisée en EventGridData qui contient des informations sur le secret qui est près d’expirer. Le nom du secret (secretName) et le nom du Key Vault (keyVaultName) sont extraits à partir des données de l’Event Grid.

Ensuite, le code récupère le secret depuis le Key Vault. Le secret récupéré contient des métadonnées sous forme de tags avec les informations nécessaires:

  • azureADAppId
  • azureADAppName
  • azureDevOpsAccountUrl
  • azureDevOpsProjectName
  • azureDevOpsConnectionName
  • SecretDurationInMonths

L’application Azure Active Directory correspondante est récupérée en utilisant l’ID de l’application stockée dans les tags du secret. Si l’application est trouvée, un nouveau secret est généré pour l’application avec la date d’expiration appropriée.

Après cela, le secret est mis à jour dans le Key Vault, puis dans la connexion de service correspondante dans Azure DevOps.

Dans cette fonction Azure, ChainedTokenCredential est utilisé comme une stratégie d’authentification pour accéder à plusieurs services Azure : Azure Resource Manager, Graph API et Azure DevOps. Le ChainedTokenCredential tente d’authentifier en utilisant une liste de TokenCredential dans l’ordre qu’ils sont passés dans le constructeur. Si une erreur non fatale (par exemple, le service d’identité gérée n’est pas disponible) est rencontrée lors de l’authentification avec une TokenCredential, ChainedTokenCredential passera au prochain TokenCredential dans la liste.

Dans le code de la fonction, ChainedTokenCredential est initialisé avec deux types de TokenCredential : DefaultAzureCredential et ManagedIdentityCredential.

  • DefaultAzureCredential est un type de TokenCredential qui tente d’authentifier en utilisant plusieurs méthodes différentes. Il tente l’authentification en utilisant les informations d’identification du service géré, les informations d’identification basées sur le contexte de développement local (comme Azure CLI, Visual Studio, etc.), les informations d’identification de l’utilisateur final, etc.
  • ManagedIdentityCredential est un type de TokenCredential qui tente d’authentifier en utilisant la User Assigned Managed Identity.

L’utilisation de ChainedTokenCredential offre une flexibilité en permettant à votre application de fonctionner à la fois en local pendant le développement et dans Azure sans avoir à changer le code d’authentification. En outre, il fournit une redondance en cas d’échec d’une méthode d’authentification.

En particulier, dans ce scénario :

  • Pour Azure Resource Manager, le SecretClient utilise le ChainedTokenCredential pour s’authentifier et récupérer les secrets du Key Vault.
  • Pour la Graph API, le GraphServiceClient utilise également le ChainedTokenCredential pour s’authentifier et gérer les applications Azure AD, notamment la génération et la rotation des secrets.
  • Pour Azure DevOps, le ChainedTokenCredential est utilisé pour créer une connexion VSS (Visual Studio Services). Cette connexion est ensuite utilisée pour récupérer et mettre à jour la connexion de service Azure DevOps avec le nouveau secret.

Ainsi, l’utilisation de ChainedTokenCredential permet à la fonction Azure de s’authentifier de manière sécurisée et flexible sur plusieurs services Azure, ce qui est crucial pour effectuer la rotation des secrets.

Test du workflow

Pour tester le workflow, vous devez d’abord créer un secret dans le Key Vault avec une date d’expiration fixée à demain par exemple. Ensuite, assurez-vous de renseigner les six tags nécessaires : azureADAppId, azureADAppName, azureDevOpsAccountUrl, azureDevOpsProjectName, azureDevOpsConnectionName, et SecretDurationInMonths. Patience ensuite est de mise — accordez-lui environ 10 minutes pour permettre à l’événement de se déclencher et à la fonction de s’exécuter.

Je peux vous assurer, sans l’ombre d’un doute, que ce workflow a subi des tests rigoureux 😅.

Conclusion

En résumé, l’automatisation de la rotation des mots de passe des connexions de service Azure DevOps grâce à Azure Functions, Azure Key Vault et Managed Identity offre une solution robuste et sécurisée pour gérer les secrets d’application Azure AD. En suivant le workflow décrit dans cet article, les développeurs peuvent désormais éviter les tâches fastidieuses et les risques liés à la gestion manuelle des mots de passe. Ce qui rendra les CISO heureux 😄.

Ce workflow est facilement adaptable pour d’autres uses cases comme la rotation automatique de Shared Access Keys si nous n’avons pas le choix de leur utilisation.

Dans le cadre de cet article, il est important de noter que le fichier Bicep fourni ne correspond pas tout à fait aux critères de préparation pour un environnement d’entreprise, ni aux normes de sécurité optimales. Dans une configuration d’entreprise, il serait essentiel d’interdire l’accès public à tous les services PaaS, tout en leur attribuant un private endpoint. Pour la Function App, l’approche idéale consisterait probablement à opter pour le plan Premium (ou ASP dans un ASE), qui offre un point d’accès privé et une intégration VNET, garantissant ainsi une couche de sécurité supplémentaire.

Ressources

--

--

Ousmane Barry
Ousmane Barry

Written by Ousmane Barry

Azure Solutions Architect. Coding most of the time with .NET C#.

No responses yet