diff --git a/infrastructure/modules/key-vault/alerts.tf b/infrastructure/modules/key-vault/alerts.tf
new file mode 100644
index 00000000..2f0200ee
--- /dev/null
+++ b/infrastructure/modules/key-vault/alerts.tf
@@ -0,0 +1,97 @@
+resource "azurerm_monitor_scheduled_query_rules_alert_v2" "kv_secret_near_expiry" {
+ count = var.enable_alerting == true ? 1 : 0
+
+ name = "${azurerm_key_vault.keyvault.name}-secret-near-expiry"
+ resource_group_name = var.resource_group_name_monitoring != null ? var.resource_group_name_monitoring : var.resource_group_name
+ location = var.location
+
+ evaluation_frequency = var.secret_near_expiry_alert.evaluation_frequency
+ window_duration = var.secret_near_expiry_alert.window_duration
+ scopes = [azurerm_key_vault.keyvault.id]
+ severity = 2
+
+ criteria {
+ query = <<-QUERY
+AzureDiagnostics
+| where ResourceProvider == "MICROSOFT.KEYVAULT"
+| where OperationName contains "SecretNearExpiry"
+| project
+ SecretName = column_ifexists("eventGridEventProperties_data_ObjectName_s","")
+| summarize Events=count() by SecretName
+QUERY
+
+ time_aggregation_method = "Total"
+ threshold = var.secret_near_expiry_alert.threshold
+ operator = "GreaterThanOrEqual"
+
+ resource_id_column = "SecretName"
+ metric_measure_column = "Events"
+
+ dimension {
+ name = "SecretName"
+ operator = "Include"
+ values = ["*"]
+ }
+ }
+
+ description = "The Key Vault secret is nearing expiration."
+
+ action {
+ action_groups = [var.action_group_id]
+ }
+
+ lifecycle {
+ ignore_changes = [
+ tags
+ ]
+ }
+}
+
+resource "azurerm_monitor_scheduled_query_rules_alert_v2" "kv_secret_expired" {
+ count = var.enable_alerting == true ? 1 : 0
+
+ name = "${azurerm_key_vault.keyvault.name}-secret-expired"
+ resource_group_name = var.resource_group_name_monitoring != null ? var.resource_group_name_monitoring : var.resource_group_name
+ location = var.location
+
+ evaluation_frequency = var.secret_expired_alert.evaluation_frequency
+ window_duration = var.secret_expired_alert.window_duration
+ scopes = [azurerm_key_vault.keyvault.id]
+ severity = 2
+
+ criteria {
+ query = <<-QUERY
+AzureDiagnostics
+| where ResourceProvider == "MICROSOFT.KEYVAULT"
+| where OperationName contains "SecretExpired"
+| project
+ SecretName = column_ifexists("eventGridEventProperties_data_ObjectName_s","")
+| summarize Events=count() by SecretName
+QUERY
+
+ time_aggregation_method = "Total"
+ threshold = var.secret_expired_alert.threshold
+ operator = "GreaterThanOrEqual"
+
+ resource_id_column = "SecretName"
+ metric_measure_column = "Events"
+
+ dimension {
+ name = "SecretName"
+ operator = "Include"
+ values = ["*"]
+ }
+ }
+
+ description = "The Key Vault secret has expired."
+
+ action {
+ action_groups = [var.action_group_id]
+ }
+
+ lifecycle {
+ ignore_changes = [
+ tags
+ ]
+ }
+}
diff --git a/infrastructure/modules/key-vault/tfdocs.md b/infrastructure/modules/key-vault/tfdocs.md
index 659ec910..8dd88bd2 100644
--- a/infrastructure/modules/key-vault/tfdocs.md
+++ b/infrastructure/modules/key-vault/tfdocs.md
@@ -131,6 +131,59 @@ Description: Resource tags to be applied throughout the deployment.
Type: `map(string)`
Default: `{}`
+
+### [resource\_group\_name\_monitoring](#input\_resource\_group\_name\_monitoring)
+
+Description: The name of the resource group in which to create monitoring resources for the Key Vault. Changing this forces a new resource to be created.
+
+Type: `string`
+
+Default: `null`
+
+### [action\_group\_id](#input\_action\_group\_id)
+
+Description: The ID of the Action Group to use for alerts.
+
+Type: `string`
+
+Default: `null`
+
+### [enable\_alerting](#input\_enable\_alerting)
+
+Description: Whether monitoring and alerting is enabled for the Key Vault.
+
+Type: `bool`
+
+Default: `false`
+
+### [secret\_near\_expiry\_alert](#input\_secret\_near\_expiry\_alert)
+
+Description: Configuration for the Key Vault secret near expiry alert.
+
+Type:
+
+```hcl
+object({
+ evaluation_frequency = string
+ window_duration = string
+ threshold = number
+})
+```
+
+### [secret\_expired\_alert](#input\_secret\_expired\_alert)
+
+Description: Configuration for the Key Vault secret expired alert.
+
+Type:
+
+```hcl
+object({
+ evaluation_frequency = string
+ window_duration = string
+ threshold = number
+})
+```
+
## Modules
The following Modules are called:
diff --git a/infrastructure/modules/key-vault/variables.tf b/infrastructure/modules/key-vault/variables.tf
index 4e09fa37..36e3e690 100644
--- a/infrastructure/modules/key-vault/variables.tf
+++ b/infrastructure/modules/key-vault/variables.tf
@@ -14,6 +14,10 @@ variable "disk_encryption" {
default = true
}
+/* --------------------------------------------------------------------------------------------------
+ Monitoring and Diagnostics Variables
+-------------------------------------------------------------------------------------------------- */
+
variable "log_analytics_workspace_id" {
type = string
description = "id of the log analytics workspace to send resource logging to via diagnostic settings"
@@ -35,6 +39,72 @@ variable "monitor_diagnostic_setting_keyvault_metrics" {
description = "Controls what metrics will be enabled for the keyvault"
}
+variable "resource_group_name_monitoring" {
+ type = string
+ description = "The name of the resource group in which to create the Monitoring resources for the Key Vault. Changing this forces a new resource to be created."
+ default = null
+}
+
+variable "action_group_id" {
+ type = string
+ description = "The ID of the Action Group to use for alerts."
+ default = null
+}
+
+variable "enable_alerting" {
+ description = "Whether monitoring and alerting is enabled for the Key Vault."
+ type = bool
+ default = false
+}
+
+variable "secret_near_expiry_alert" {
+ type = object({
+ evaluation_frequency = string
+ window_duration = string
+ threshold = number
+ })
+
+ validation {
+ condition = contains(
+ ["PT1M", "PT5M", "PT15M", "PT30M", "PT1H", "PT6H", "PT12H", "P1D"],
+ var.secret_near_expiry_alert.evaluation_frequency
+ )
+ error_message = "secret_near_expiry_alert.evaluation_frequency must be one of: PT1M, PT5M, PT15M, PT30M, PT1H, PT6H, PT12H, P1D"
+ }
+
+ validation {
+ condition = contains(
+ ["PT1M", "PT5M", "PT15M", "PT30M", "PT1H", "PT6H", "PT12H", "P1D"],
+ var.secret_near_expiry_alert.window_duration
+ )
+ error_message = "secret_near_expiry_alert.window_duration must be one of: PT1M, PT5M, PT15M, PT30M, PT1H, PT6H, PT12H, P1D"
+ }
+}
+
+variable "secret_expired_alert" {
+ type = object({
+ evaluation_frequency = string
+ window_duration = string
+ threshold = number
+ })
+
+ validation {
+ condition = contains(
+ ["PT1M", "PT5M", "PT15M", "PT30M", "PT1H", "PT6H", "PT12H", "P1D"],
+ var.secret_expired_alert.evaluation_frequency
+ )
+ error_message = "secret_expired_alert.evaluation_frequency must be one of: PT1M, PT5M, PT15M, PT30M, PT1H, PT6H, PT12H, P1D"
+ }
+
+ validation {
+ condition = contains(
+ ["PT1M", "PT5M", "PT15M", "PT30M", "PT1H", "PT6H", "PT12H", "P1D"],
+ var.secret_expired_alert.window_duration
+ )
+ error_message = "secret_expired_alert.window_duration must be one of: PT1M, PT5M, PT15M, PT30M, PT1H, PT6H, PT12H, P1D"
+ }
+}
+
variable "name" {
description = "The name of the Key Vault."
type = string