Foto de George Becker
La rotación de claves, secretos y certificados es esencial para mantener la seguridad. Azure Key Vault facilita este proceso mediante la automatización de la renovación y, al mismo tiempo, permite supervisión manual para garantizar el cumplimiento y control. En este artículo, exploraremos cómo integrar un sistema de notificaciones basado en Adaptive Cards de Office 365 para gestionar la aprobación de renovaciones de certificados en lugar de usar Azure Logic Apps.
- Key Vault Detecta Near Expiry:
- Azure Monitor detecta que un certificado en Azure Key Vault está próximo a expirar.
- Trigger de Logic App:
- El evento de «certificado próximo a expirar» activa una Logic App.
- Registro en Azure Storage Table:
- Se registra una nueva solicitud con estado
Pendiente
en la tabla ApprovalRequests.
- Se registra una nueva solicitud con estado
- Envío de Adaptive Card:
- La Logic App envía una Adaptive Card por Microsoft Teams o un correo Email.
- Respuesta del Usuario:
- El aprobador selecciona «Aprobar» o «Rechazar» en la Adaptive Card.
- Actualización en Storage Table:
- La Logic App actualiza el estado a
Aprobado
oRechazado
y almacena el email del usuario que realizó la acción.
- La Logic App actualiza el estado a
- Acciones Condicionales:
- Si el estado es Aprobado, el certificado se actualiza en Key Vault.
- Si el estado es Rechazado, se envía una notificación y se mantiene el certificado anterior.
- Reenvío Automático Cada 24 Horas:
- Si el estado sigue en
Pendiente
, se vuelve a enviar la solicitud hasta que se apruebe o rechace.
- Si el estado sigue en
IaC para Bicep
@description('Nombre de la cuenta de almacenamiento')
param storageAccountName string = 'stapprovacionsecret'
@description('Nombre de la Logic App')
param logicAppName string = 'logicAppSecretApproval'
@description('Nombre del Key Vault')
param keyVaultName string = 'kvSecretManager'
@description('Ubicación de los recursos')
param location string = resourceGroup().location
@description('Nombre de la tabla en Azure Storage')
param tableName string = 'ApprovalRequests'
@description('Nombre del Key Vault Secret')
param keyVaultSecretName string = 'mySecret'
@description('Plan de consumo para Logic App')
param logicAppPlanName string = 'logicAppPlanSecret'
/* Azure Storage Account */
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
/* Tabla en Azure Storage */
resource storageTable 'Microsoft.Storage/storageAccounts/tableServices/tables@2021-09-01' = {
parent: storageAccount
name: tableName
}
/* Azure Key Vault */
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = {
name: keyVaultName
location: location
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
accessPolicies: [
{
objectId: '<Object-ID-Usuario>' // Reemplaza con tu Object ID
permissions: {
secrets: [
'get'
'set'
'delete'
'list'
]
}
}
]
}
}
/* Logic App para la gestión de aprobaciones */
resource logicApp 'Microsoft.Logic/workflows@2019-05-01' = {
name: logicAppName
location: location
properties: {
state: 'Enabled'
definition: {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowDefinition.json#",
"actions": {
"CheckTableStatus": {
"inputs": {
"host": {
"api": {
"runtimeUrl": "https://logic.azure.com"
}
},
"method": "GET",
"uri": "https://${storageAccountName}.table.core.windows.net/${tableName}(PartitionKey='SecretRotation')",
"headers": {
"x-ms-version": "2021-04-10"
}
},
"runAfter": {},
"type": "Http"
},
"SendAdaptiveCard": {
"type": "MicrosoftTeams.PostMessage",
"inputs": {
"host": {
"api": {
"runtimeUrl": "https://logic.azure.com"
}
},
"method": "POST",
"uri": "https://api.powerapps.com/microsoftteams/messages",
"headers": {
"Content-Type": "application/json"
},
"body": {
"content": {
"body": [
{
"type": "TextBlock",
"text": "Secreto próximo a expirar. ¿Deseas aprobar la renovación?",
"weight": "Bolder"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Aprobar",
"data": {
"action": "approve"
}
},
{
"type": "Action.Submit",
"title": "Rechazar",
"data": {
"action": "reject"
}
}
]
},
"channel": "<ChannelID>", // Reemplazar por el ID del canal de Teams
"recipient": "<RecipientID>" // Reemplazar por el ID del destinatario
}
}
},
"Delay24Hours": {
"type": "Delay",
"inputs": {
"interval": {
"count": 24,
"unit": "Hour"
}
},
"runAfter": {
"SendAdaptiveCard": [
"Succeeded"
]
}
},
"UpdateTableStatus": {
"inputs": {
"method": "PUT",
"uri": "https://${storageAccountName}.table.core.windows.net/${tableName}(PartitionKey='SecretRotation',RowKey='<ID>')",
"headers": {
"Content-Type": "application/json",
"x-ms-version": "2021-04-10"
},
"body": {
"Status": "@{triggerBody()?['action']}",
"Email": "@{triggerBody()?['email']}"
}
},
"runAfter": {
"Delay24Hours": [
"Succeeded"
]
},
"type": "Http"
}
},
"triggers": {
"SecretNearExpiry": {
"type": "Request",
"inputs": {
"schema": {
"properties": {
"SecretName": {
"type": "string"
},
"ExpiryDate": {
"type": "string"
}
},
"required": [
"SecretName",
"ExpiryDate"
],
"type": "object"
}
}
}
},
"contentVersion": "1.0.0.0",
"outputs": {}
}
}
}
/* Grupo de Acción para Azure Monitor */
resource actionGroup 'Microsoft.Insights/actionGroups@2021-05-01' = {
name: 'actionGroupForSecretExpiry'
location: location
properties: {
enabled: true
groupShortName: 'SecExp'
webhookReceivers: [
{
name: 'LogicAppWebhook'
serviceUri: 'https://prod-XX.westeurope.logic.azure.com:443/workflows/<workflow-id>/triggers/SecretNearExpiry/paths/invoke?api-version=2016-06-01&sp=<signature>'
}
]
}
}
/* Configuración de la Alerta de Azure Monitor */
resource alertRule 'Microsoft.Insights/scheduledQueryRules@2022-10-01-preview' = {
name: 'SecretExpiryAlert'
location: location
properties: {
description: 'Alerta para secretos que expiran en 15 días'
severity: 3
enabled: true
source: {
dataSourceId: <logAnalyticsWorkspaceId> // Reemplaza con el ID de tu Log Analytics Workspace
query: '''
AzureDiagnostics
| where ResourceType == "VAULT" and Category == "SecretExpiry"
| where todatetime(properties_s) < now() + 15d
| project Resource, ResourceId, SecretName = properties_s, ExpiryDate = todatetime(properties_s)
'''
}
schedule: {
frequencyInMinutes: 1440 // Ejecutar cada día
timeWindowInMinutes: 1440 // Ventana de 1 día
}
actionGroups: [
{
actionGroupId: actionGroup.id
}
]
criteria: {
odataType: 'Microsoft.Azure.Monitor.MultipleResourceMultipleMetricCriteria'
allOf: [
{
name: 'SecretExpiryCriteria'
metricName: 'SecretExpiry'
dimensions: [
{
name: 'SecretName'
}
]
operator: 'GreaterThan'
threshold: 0
}
]
}
}
}
Con pocos cambios podrás cambiar este fichero de Bicep para que se usen Certificados, he puesto secretos debido a que es más sencillo probar el flujo completo; el problema habitual que me encuentro es en los cerfificados de los App Services.
Pero de momento para este fichero debes tener en cuenta las variables y:
- Log Analytics Workspace: Asegúrate de que los eventos de Key Vault se envíen al Log Analytics Workspace configurado. Reemplaza
<logAnalyticsWorkspaceId>
con el ID de tu Log Analytics Workspace. - Consulta KQL: La consulta está diseñada para buscar secretos que expiren dentro de los próximos 15 días. Asegúrate de que la sintaxis y los campos utilizados en la consulta KQL correspondan a los datos que se están registrando en tu Log Analytics.
- Acciones del Grupo: Configura el
actionGroup
para que envíe una notificación a través de un webhook a la Logic App. Asegúrate de que la URL del webhook esté correctamente formada; lo sé, podrías haber enviado un mail avisando directamente, pero he querido poner el ejemplo de forma más elegante y usando Logic Apps + Notificaciones a correo o Teams. - Programación de la Alerta: La alerta se ejecuta cada día (
frequencyInMinutes: 1440
) y analiza un período de tiempo de 24 horas (timeWindowInMinutes: 1440
). - Criterios de la Alerta: Los criterios especifican que cualquier secreto con un nombre capturado en la consulta y que cumpla con las condiciones debería desencadenar la acción.
Espero que te esa útil.