Solving User Assigned Managed Identity Connection to Key Vault for Azure APIM with Terraform

When deploying an Azure API Management (APIM) service with custom domains, security considerations are paramount. A critical aspect of this deployment is connecting Azure APIM to Azure Key Vault using a User Assigned Managed Identity (UAMI). This enables secure access to the SSL certificates required for custom domain configuration.

In this guide, we'll explore how to solve a common issue where the UAMI is unable to connect to Key Vault. We'll also see how to implement this using Terraform, focusing on the ssl_keyvault_identity_client_id property.

Problem Statement

When utilizing User Assigned Managed Identity to connect APIM to Key Vault, you may encounter a challenge where the UAMI is not able to access the Key Vault. This may lead to failure in configuring custom domains in APIM.

Understanding Managed Identities

Azure Managed Identities are an Azure Active Directory feature that provides a robust solution for authenticating applications. They're used in various Azure services, including APIM and Key Vault, to enable secure access between different resources.

System-Assigned vs User-Assigned

There are two types of Managed Identities:

  • System-Assigned: Automatically managed by Azure and tied to a specific resource. It's deleted when the resource is deleted.
  • User-Assigned: Created by the user and can be assigned to multiple resources.

In our scenario, we used a User-Assigned Managed Identity to ensure flexibility and control.

Deeper Dive into Azure Key Vault

Azure Key Vault is a cloud service that helps manage cryptographic keys and secrets. It's essential to manage the SSL certificates required for custom domain configurations in APIM.

Key Vault Security

With various permissions and access policies, Key Vault provides granular control over who and what can access specific keys, secrets, or certificates. In our solution, we tailored the access policy specifically for UAMI, providing the necessary permissions to retrieve the required SSL certificates.

Extending to Azure API Management (APIM)

APIM is a fully managed service that enables customers to publish, secure, transform, maintain, and monitor APIs. Custom domains in APIM allow organizations to present APIs with custom domain names, improving branding and user experience.

Custom Domains and SSL Certificates

Configuring custom domains in APIM requires SSL certificates to ensure secure communication. By storing these certificates in Azure Key Vault and accessing them through Managed Identities, we enhance security.

Solution Overview

The solution involves creating a UAMI for our Azure API Management instance, defining its permissions in Key Vault, and then configuring APIM to use this identity to fetch the required SSL certificates to create custom domain.

Step-by-Step Guide

1. Create User Assigned Managed Identity (UAMI)

First lets talk about our dependencies. Define the UAMI , Log Analytics Workspace, Azure Key Vault, SSL Certificate and Azure Key Vault Policy in Terraform:

dependencies.tf


data "azurerm_client_config" "current" {}

resource "azurerm_key_vault" "example" {
  name                      = var.key_vault_name
  location                  = var.location
  resource_group_name       = var.resource_group_name
  tenant_id                 = "your Azure ID"
  purge_protection_enabled  = false
  # enable_rbac_authorization = true

  sku_name = "standard"
  network_acls {
    default_action = "Deny"
    bypass         = "AzureServices"
    ip_rules = [""20.40.125.155", "20.40.160.107"] # Whitelist your regional Azure APIM Control Plane IPs to allow APIM UAMI access to SSL certificate.
  }
}

output "key_vault_id" {
  description = "The ID of the Key Vault"
  value       = azurerm_key_vault.example.id
}

resource "azurerm_key_vault_certificate" "this" {
  name         = "example-certificate"
  key_vault_id = azurerm_key_vault.example.id

  certificate_policy {
    issuer_parameters {
      name = "Self"
    }

    key_properties {
      exportable = true
      key_size   = 2048
      key_type   = "RSA"
      reuse_key  = true
    }

    secret_properties {
      content_type = "application/x-pkcs12"
    }

    x509_certificate_properties {
      subject            = "CN=*.some.domain.name"
      validity_in_months = 12

      key_usage = [
        "cRLSign",
        "dataEncipherment",
        "digitalSignature",
        "keyAgreement",
        "keyCertSign",
        "keyEncipherment",
      ]

      subject_alternative_names {
        dns_names = ["*.some.domain.name"]
      }
    }
  }


}

resource "azurerm_log_analytics_workspace" "example" {
  name                = "example-workspace"
  location            = var.location
  resource_group_name = var.resource_group_name
  sku                 = "PerGB2018"
}

resource "azurerm_key_vault_access_policy" "this" {
  key_vault_id = azurerm_key_vault.example.id
  tenant_id    = data.azurerm_client_config.current.tenant_id

  object_id = azurerm_user_assigned_identity.this.principal_id

  key_permissions         = ["Get", "List", "Delete", "Recover", "Backup", "Restore", "Create", "Import", "Update", "Purge"]
  secret_permissions      = ["Get", "List", "Set", "Delete", "Recover", "Backup", "Restore"]
  certificate_permissions = ["Get", "List", "Delete", "Recover", "Backup", "Restore", "Create", "Import", "Update", "ManageContacts", "ManageIssuers", "ListIssuers", "SetIssuers", "DeleteIssuers", "Purge", "GetIssuers"]
}

resource "azurerm_monitor_diagnostic_setting" "key_vault_example" {
  name                       = "keyvault-example-setting"
  target_resource_id         = azurerm_key_vault.example.id
  log_analytics_workspace_id = azurerm_log_analytics_workspace.example.id

  enabled_log {
    category = "AuditEvent"
  }

  metric {
    category = "AllMetrics"
    enabled  = true
  }
}

Configure APIM with UAMI and custom domain

Now, configure APIM with custom domains to use UAMI configuring ssl_keyvault_identity_client_id argument

main.tf


resource "azurerm_api_management" "this" {
  name                = var.name
  location            = var.location
  resource_group_name = var.resource_group_name
  publisher_name      = var.publisher_name
  publisher_email     = var.publisher_email
  sku_name            = var.sku_name

  identity {
    type         = "UserAssigned"
    identity_ids = [azurerm_user_assigned_identity.this.id]
  }

  depends_on = [azurerm_key_vault_certificate.this]

}
resource "azurerm_api_management_custom_domain" "apim_domain" {
  count             = var.requires_custom_host_name_configuration ? 1 : 0
  api_management_id = azurerm_api_management.this.id

  dynamic "developer_portal" {
    for_each = var.developer_portal_host_name != "" ? [1] : []
    content {
      host_name                       = var.developer_portal_host_name
      key_vault_id                    = azurerm_key_vault_certificate.example.secret_id
      negotiate_client_certificate    = false
      ssl_keyvault_identity_client_id = azurerm_user_assigned_identity.this.client_id

    }
  }

  dynamic "management" {
    for_each = var.management_host_name != "" ? [1] : []
    content {
      host_name                       = var.management_host_name
      key_vault_id                    = azurerm_key_vault_certificate.example.secret_id
      negotiate_client_certificate    = false
      ssl_keyvault_identity_client_id = azurerm_user_assigned_identity.this.client_id

    }
  }

  dynamic "gateway" {
    for_each = var.gateway_host_name != "" ? [1] : []
    content {
      host_name                       = var.gateway_host_name
      key_vault_id                    = azurerm_key_vault_certificate.example.secret_id
      negotiate_client_certificate    = true
      ssl_keyvault_identity_client_id = azurerm_user_assigned_identity.this.client_id

    }
  }

  lifecycle {
    ignore_changes = [
      gateway
    ]
  }

}

resource "azurerm_monitor_diagnostic_setting" "apim_example" {
  name                       = "apim-example-setting"
  target_resource_id         = azurerm_api_management.this.id
  log_analytics_workspace_id = azurerm_log_analytics_workspace.example.id

  log {
    category = "GatewayLogs"
    enabled  = true
  }

  metric {
    category = "AllMetrics"
  }
}

A pivotal aspect of our configuration involves securely handling SSL certificates. In Azure, Key Vault is commonly used to store and manage sensitive information, such as SSL certificates. However, in our case, a problem arose: our User Assigned Managed Identity was unable to connect to Key Vault. This prevented the creation of custom domains for the Azure API Management (APIM) instance, a critical part of our infrastructure.

This is where ssl_keyvault_identity_client_id comes into play. It serves as a bridge between the User Assigned Managed Identity and Key Vault, enabling secure and authenticated access to the SSL certificates. Here's how it works:

  1. Defining the Client ID: ssl_keyvault_identity_client_id is set to the ID of the User Assigned Managed Identity (If you do not specify it , APIM instance will try to connect to Key Vault using APIM's system assigned identity). This identity has the appropriate permissions to access the Key Vault.

  2. Integration with Key Vault: Within Key Vault, you can set up an access policy that grants the specified identity access to the necessary certificates.

  3. Terraform Configuration: In the Terraform code, ssl_keyvault_identity_client_id is used to specify the client ID in the APIM configuration. This connects the APIM instance to the Key Vault and ensures that the correct SSL certificates are used.

  4. Result: The issue of the User Assigned Managed Identity being unable to connect to Key Vault is resolved. Custom domains can be created for the Azure APIM instance, and the entire system functions seamlessly.

Defining Variables in variables.tf 

In our Terraform configuration, we use a dedicated file called variables.tf to declare and describe the variables required for our setup. This approach ensures modularity and reusability across different environments and projects. Let's dive into the details of the variables defined:

  • API Management Base Name (name): Specifies the base name for the APIM instance.
  • Resource Group (resource_group_name): The name of the resource group where the APIM service will be created.
  • Location (location): Defines the location for the deployment, such as "Australia East."
  • Tags (tags): Key-value pairs that can be used to tag all created resources.
  • Publisher Details (publisher_name, publisher_email): Information about the publisher.
  • SKU Name (sku_name): Specifies the type of SKU like 'Basic', 'Developer', 'Premium', etc.
  • Key Vault Details (key_vault_id, key_vault_name): The ID and name of the Key Vault to store certificates and secrets.
  • Custom Hostname Configuration (requires_custom_host_name_configuration): Determines if custom hostname configuration is needed.
  • Host Names (developer_portal_host_name, management_host_name, gateway_host_name): Custom hostnames for various endpoints.
  • Identity Name (identity_name): The name for the APIM identity.
  • User Assigned Managed Identity Resource ID (uami_resource_id): The User Assigned Managed Identity resource ID to associate with the APIM instance.

Here's a snippet from variables.tf:


variable "name" {
  type        = string
  description = "The apim base name"
  default     = "your-apim-test-name-01"
}

variable "resource_group_name" {
  type        = string
  description = "The name of the resource group in which to create the API Management service"
  default     = "your-RG-test-name-01"
}

variable "location" {
  type        = string
  description = "the location for the deployment"
  default     = "Australia East"
}

variable "tags" {
  description = "Tags to set for all resources"
  type        = map(string)
  default     = {}
}

variable "publisher_name" {
  description = "The name of the publisher"
  type        = string
  default     = "Publisher name"
}

variable "publisher_email" {
  description = "The email of the publisher"
  type        = string
  default     = "This email address is being protected from spambots. You need JavaScript enabled to view it."
}

variable "sku_name" {
  type        = string
  default     = "Developer_1"
  description = "Name of the sku - 'Basic', 'Consumption', 'Developer', 'Isolated', 'Premium', 'Standard'"
}

variable "key_vault_id" {
  description = "The ID of the Key Vault which stores the certificate and secret"
  type        = string
  default     = ""
}

variable "key_vault_name" {
  description = "The ID of the Key Vault which stores the certificate and secret"
  type        = string
  default     = "your-kv-tes-name-01"
}

variable "requires_custom_host_name_configuration" {
  description = "Determines whether custom hostname configuration is required."
  type        = bool
  default     = true
}

variable "developer_portal_host_name" {
  description = "Custom hostname for the developer portal."
  type        = string
  default     = "developer-portal.some.domain.name"
}

variable "management_host_name" {
  description = "Custom hostname for the management endpoint."
  type        = string
  default     = "management.some.domain.name"
}

variable "gateway_host_name" {
  description = "Custom hostname for the gateway."
  type        = string
  default     = ""
}

variable "identity_name" {
  description = "identity_name"
  type        = string
  default     = "apim-identity"

}
variable "uami_resource_id" {
  description = "User Assigned Managed Identity resource ID to associate with the API Management instance"
  type        = string
  default     = ""
}

These variables make the Terraform code flexible and customizable, enabling adjustments to the specific requirements of different environments or projects. By utilizing such an approach, we not only enhance the maintainability but also lay a robust foundation for potential scaling and further development of the infrastructure.

Conclusion

This guide illustrates a strategic approach to connect Azure APIM to Azure Key Vault securely using User Assigned Managed Identity. By employing the ssl_keyvault_identity_client_id and specifying our UAMI's client ID, we can solve problem with connectivity to a Key Vault, vital for configuring custom domains in Azure APIM.

This Terraform-based solution adds automation and repeatability, providing a scalable and efficient way to manage complex infrastructure requirements. Understanding how to leverage Managed Identities and Key Vault in Azure is vital for building secure and scalable solutions.