Skip to main content
Jorge Bernhardt Jorge Bernhardt
  1. Posts/

Terraform – Secure your Azure Key Vaults with private endpoints

·1385 words·7 mins· 100 views · 5 likes ·
Terraform IaC Azure Key Vault Azure Private Endpoint

Hello everyone! The importance of securely managing our secrets, keys, and certificates has grown exponentially in our cloud environments. Azure Key Vault allows us to handle this sensitive data securely.

One of the ways to ensure that our Key Vaults remain inaccessible from the public Internet is through Azure private endpoints. By implementing a private endpoint for Azure Key Vault, we can ensure that all traffic to Key Vault is routed through an Azure virtual private network.

I’ll show you how to use Terraform to create and configure private endpoints for your Azure Key Vaults in this guide.

Prerequisites>

Prerequisites #

  • You need Terraform CLI on your local machine, if you’re new to using Terraform to deploy Microsoft Azure resources, then I recommend you check out this link.
  • A text editor or IDE of your choice (Visual Studio Code with Terraform extension is my recommendation)
Declare Azure Provider in Terraform>

Declare Azure Provider in Terraform #

The provider.tf file in Terraform is used to specify and configure the providers used in your Terraform configuration. A provider is a service or platform where the resources will be managed. This could be a cloud provider like Microsoft Azure, AWS, Google Cloud, etc.

This file is important because it tells Terraform which provider’s API to use when creating, updating, and deleting resources. Without it, Terraform wouldn’t know where to manage your resources.

provider "azurerm" {
  features {}
}
Deploy Azure Resources Using Terraform>

Deploy Azure Resources Using Terraform #

In the case of setting up Azure Private Endpoints for Azure Key Vaults, the main.tf file contains these key components:

  • locals: This block maps domain names for the Azure Key Vault service.
  • data “azurerm_resource_group” “existing”: This data block fetches details about the existing resource group where the private endpoints will be deployed.
  • data “azurerm_virtual_network” “existing”: Retrieves the specifications of the virtual network where the private endpoints will connect.
  • data “azurerm_subnet” “existing”: Gathers information about the subnet where the private endpoints will reside.
  • data “azurerm_private_dns_a_record” “records”: This data block is used to retrieve details of the ‘A’ records in the private DNS zone associated with the Azure Key Vault private endpoints.
  • azurerm_private_dns_zone: Sets up private DNS zones, ensuring the Azure Key Vault endpoints are resolved within the private network.
  • azurerm_private_dns_zone_virtual_network_link: Establishes the link between the private DNS zones and the virtual network.
  • azurerm_private_endpoint: This is the core of the module, creating private endpoints in the specified subnets and connecting them to the target Azure Key Vault resources. Additionally, it links the private endpoints to the appropriate DNS zones.
# Using locals to define a mapping of service domains for easy reference.
locals {
  service_domains = {
    keyvault = "privatelink.vaultcore.azure.net"
  }
}

# Fetch details of the existing resource group where private endpoints will be created.
data "azurerm_resource_group" "existing" {
  for_each = var.private_endpoints
  name     = each.value.resource_group_name
}

# Fetch details of the existing virtual network where private endpoints will connect.
data "azurerm_virtual_network" "existing" {
  for_each            = var.private_endpoints
  name                = each.value.virtual_network_name
  resource_group_name = each.value.resource_group_name
}

# Fetch details of the subnet where private endpoints will reside.
data "azurerm_subnet" "existing" {
  for_each             = var.private_endpoints
  name                 = each.value.subnet_name
  virtual_network_name = each.value.virtual_network_name
  resource_group_name  = each.value.resource_group_name
}

data "azurerm_private_dns_a_record" "records" {
  for_each = var.private_endpoints # Usa var.private_endpoints como iterador
  name                = split("/", each.value.resource_id_to_link)[length(split("/", each.value.resource_id_to_link)) - 1]
  resource_group_name = each.value.resource_group_name
  zone_name           = local.service_domains["keyvault"]

  depends_on = [azurerm_private_endpoint.pep]
}

# Create private DNS zones based on the service domains.
resource "azurerm_private_dns_zone" "pdnszone" {
  for_each            = var.private_endpoints
  name                = local.service_domains["keyvault"]
  resource_group_name = each.value.resource_group_name
  tags                = each.value.tags
}

# Establish the link between private DNS zones and the virtual network.
resource "azurerm_private_dns_zone_virtual_network_link" "pdnszlink" {
  for_each              = var.private_endpoints
  name                  = "link-${each.key}"
  resource_group_name   = each.value.resource_group_name
  private_dns_zone_name = azurerm_private_dns_zone.pdnszone[each.key].name
  virtual_network_id    = data.azurerm_virtual_network.existing[each.key].id
  tags                  = each.value.tags
}

# Create private endpoints in the specified subnets and connect to the target resources.
resource "azurerm_private_endpoint" "pep" {
  for_each            = var.private_endpoints
  name                = "pep-${each.key}"
  location            = data.azurerm_resource_group.existing[each.key].location
  resource_group_name = each.value.resource_group_name
  subnet_id           = data.azurerm_subnet.existing[each.key].id

  private_service_connection {
    name                           = "connection-${each.key}"
    is_manual_connection           = false
    private_connection_resource_id = each.value.resource_id_to_link
    subresource_names              = ["vault"]
  }

  # Link private endpoints to the respective DNS zones.
  private_dns_zone_group {
    name                 = "pdnszgroup-${each.key}"
    private_dns_zone_ids = [azurerm_private_dns_zone.pdnszone[each.key].id]
  }

  tags = each.value.tags
}
Declaration of input variables>

Declaration of input variables #

In Terraform, the variables.tf file is essential to make our configurations. It allows us to define important parameters to be used in our main.tf. By using variables, we can create versatile configurations that can adapt to different scenarios.

In this example, the variables are defined in the variables.tf include:

  • private_endpoints: This complex variable is designed to capture an array of attributes essential for each private endpoint’s creation:
    • resource_group_name: The name of the Azure resource group where the private endpoint will be created.
    • virtual_network_name: Specifies the name of the Azure virtual network to which the private endpoint will attach.
    • subnet_name: Indicates the specific subnet within the virtual network where the private endpoint will be located.
    • resource_id_to_link: Represents the unique Azure resource ID of the Key Vault to which the private endpoint aims to connect.
    • tags: A collection of metadata tags intended to be linked to the private endpoint. This structured design ensures each private endpoint is correctly associated with its designated storage service and is set up appropriately.
variable "private_endpoints" {
  description = "Details for the Key Vault Private Endpoints creation"
  type = map(object({
    resource_group_name  = string
    virtual_network_name = string
    subnet_name          = string
    resource_id_to_link  = string
    tags                 = map(string)
  }))
  default = {}
}
Declaration of output values>

Declaration of output values #

The output.tf file in Terraform extracts and displays information about the resources created or managed by your Terraform configuration. These outputs are defined using the output keyword and can be used to return information that can be useful for the user, for other Terraform configurations, or for programmatically using the information in scripts or other tools.

In this example, the output.tf file returns the following information

  • The IDs of the created private endpoints
  • The private IP addresses linked to these endpoints
  • The fully qualified domain names (FQDNs) for the associated DNS A records

After Terraform successfully applies your configuration, it will showcase your defined outputs. This provides instant access to key attributes of the resources you’ve set up.

# Output Block for Key Vault Private Endpoints
output "keyvault_private_endpoints_output" {
  description = "Attributes of created Key Vault private endpoints and DNS records."

  value = {
    # IDs of private endpoints.
    ids = {
      for key, pe in azurerm_private_endpoint.pep :
      key => pe.id # Mapping each key to its respective private endpoint ID
    },

    # IP addresses of private endpoints.
    ips = {
      for key, pe in azurerm_private_endpoint.pep :
      key => pe.private_service_connection[0].private_ip_address # Mapping each key to its respective IP address
    },
  }
}

# Output Block for Private DNS A Records
output "private_dns_a_records" {
  description = "FQDNs of the A records in the private DNS zone."

  value = {
    for key, record in data.azurerm_private_dns_a_record.records :
    key => record.fqdn
  }
}
Executing the Terraform Deployment>

Executing the Terraform Deployment #

Now that you’ve declared the resources correctly, it’s time to take the following steps to deploy them in your Azure environment.

  • Initialization: To begin, execute the terraform init command. This will initialize your working directory that holds the .tf files and download the provider specified in the provider.tf file, and configure the Terraform backend. I suggest looking at this link if you’re curious about the process.

  • Planning: Next, execute the terraform plan. This command creates an execution plan and shows Terraform’s actions to achieve the desired state defined in your .tf files. This gives you a chance to review the changes before applying them.

  • Apply: When you’re satisfied with the plan, execute the terraform apply command. This will implement the required modifications to attain the intended infrastructure state. Before making any changes, you will be asked to confirm your decision.

  • Inspection: After applying the changes, you can use terraform show command to see the current state of your infrastructure.

  • Destroy (optional): when a project is no longer needed or when resources have become outdated. You can use the terraform destroy command. This will remove all the resources that Terraform has created.

References and useful links #

Thank you for taking the time to read my post. I sincerely hope that you find it helpful.