All documentation

Advanced Terraform Techniques: Managing Azure Application Proxy with AWS Secrets and Local-Exec Provisioner

Published July 13, 2023 Guide

This article dives into the limitations of Terraform's azuread provider, specifically its inability to manage Azure Application Proxy configurations. We'll explore a creative workaround involving AWS Secrets Manager, Microsoft Graph API, and the local-exec provisioner in Terraform. Project can be found here: https://github.com/ctejeda/azuread_appproxy

Limitations of Terraform Provider azuread

While the azuread provider has extensive support for Azure Active Directory resources, it unfortunately does not currently support the management of Azure Application Proxy configurations out-of-the-box. Azure Application Proxy allows on-premises applications to be published and accessed externally while retaining a secure connection. Without native support, developers cannot fully automate their infrastructure deployment and management, particularly for more complex environments.

To overcome this limitation, we utilize a combination of AWS Secrets Manager, Microsoft Graph API, and the local-exec provisioner in Terraform.

Leveraging AWS Secrets Manager

Firstly, we'll use AWS Secrets Manager to securely store and manage sensitive data, including Azure client IDs, secrets, and tenant IDs. The code snippet below creates two data sources, secrets and secrets_azure_token, to fetch the stored secret values using the AWS provider.

data "aws_secretsmanager_secret" "secrets" {
  arn = var.sysdata.awsarn1.name
}

data "aws_secretsmanager_secret_version" "current" {
  secret_id = data.aws_secretsmanager_secret.secrets.id
}

data "aws_secretsmanager_secret" "secrets_azure_token" {
  arn = var.sysdata.awsarn2.name
}

data "aws_secretsmanager_secret_version" "current_azure_token" {
  secret_id  = data.aws_secretsmanager_secret.secrets_azure_token.id
}

Executing Scripts with Local-Exec Provisioner

We use the local-exec provisioner to execute scripts and shell commands locally on the machine running Terraform. In this case, we're using it to call a bash script getazuretoken.sh, which retrieves an Azure token needed to authenticate with Microsoft Graph API.

data "external" "getazuretoken" {
  program = ["bash", "/home/path/to/scripts/getazuretoken.sh", "${local.azure_clientid}", "${local.azure_secret}", "${local.mask_data}"]
}

The script getazuretoken.sh updates an existing aws secret with the new azure token retrieved by a curl command. let's take a look at it.

#!/bin/bash
# getazuretoken.sh

# Retrieve the arguments passed from Terraform
arg1="$1"
arg2="$2"

# Perform some computations or retrieve data using the arguments

result=$(curl -X POST -d 'grant_type=client_credentials&client_id='${arg1}'&client_secret='${arg2}'&resource=https%3A%2F%2Fgraph.microsoft.com%2F' https://login.microsoftonline.com/TenantID/oauth2/token | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')

aws secretsmanager update-secret --secret-id azure_token_v2 --secret-string "{\"azure_token\":\"$result\"}" --region us-east-1

Additionally, we use the local-exec provisioner to execute CURL commands that interact with Microsoft Graph API for updating the Azure application configurations.

provisioner "local-exec" {
  command = <<EOF
  curl --location --request PATCH 'https://graph.microsoft.com/beta/applications/${each.value.object_id}' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer ${local.azure_token}' \
  --data '{
    "onPremisesPublishing": {
      "externalAuthenticationType": "aadPreAuthentication",
      "internalUrl": "${var.apps[each.key].internalurl}",
      "externalUrl": "${var.apps[each.key].externalurl}",
      "isHttpOnlyCookieEnabled": true,
      "isOnPremPublishingEnabled": true,
      "isPersistentCookieEnabled": true,
      "isSecureCookieEnabled": true,
      "isStateSessionEnabled": true,
      "isTranslateHostHeaderEnabled": true,
      "isTranslateLinksInBodyEnabled": true
    }
  }'
EOF
}

Securing Sensitive Data

To ensure sensitive data is secure and not exposed in logs or outputs, we've introduced a is_sensitive variable. When set to true, sensitive data like the Azure token, client ID, secret, and tenant ID are masked in the console and terraform show command.

variable "is_sensitive" {
  description = "is the data output sensitive"
  type        = bool
  default     = true
}

locals {
  mask_data = var.is_sensitive ? true : false
  azure_token =  var.is_sensitive ? jsondecode(sensitive(data.aws_secretsmanager_secret_version.current_azure_token.secret_string)).azure_token : jsondecode(nonsensitive(data.aws_secretsmanager_secret_version.current_azure_token.secret_string)).azure_token
  azure_clientid  =  var.is_sensitive ? jsondecode(sensitive(data.aws_secretsmanager_secret_version.current.secret_string)).client_id : jsondecode(nonsensitive(data.aws_secretsmanager_secret_version.current.secret_string)).client_id
  azure_secret  = var.is_sensitive ?  jsondecode(sensitive(data.aws_secretsmanager_secret_version.current.secret_string)).client_secret : jsondecode(nonsensitive(data.aws_secretsmanager_secret_version.current.secret_string)).client_secret
  azure_tenantid  = var.is_sensitive ?  jsondecode(sensitive(data.aws_secretsmanager_secret_version.current.secret_string)).tenant_id : jsondecode(nonsensitive(data.aws_secretsmanager_secret_version.current.secret_string)).tenant_id
}

However, we've provided a feature to disable this protection for debugging purposes. By setting is_sensitive to false, developers can expose this data if needed.

Utilizing a Vars.tf File

We use a vars.tf file to define internal and external URLs for the applications, and the Azure proxy connectors. It serves as a central place for configuring variables used throughout our Terraform configuration. It makes the code more reusable and easier to maintain as changes only need to be made in one place.

The local-exec provisioner is called only when values are changed in the vars.tf file for the applications, ensuring efficient utilization of resources.

The Benefits of this Workaround

This workaround offers several benefits:

  1. Fully Automated Process: It allows for the automation of Azure Application Proxy configurations that would otherwise require manual intervention.
  2. Sensitive Data Security: By integrating with AWS Secrets Manager, sensitive data is secured and managed effectively.
  3. Debugging Ease: With the flexibility to expose sensitive data, debugging and issue resolution become easier.
  4. Increased Efficiency: The local-exec provisioner is triggered only when necessary, saving resources.

Use Cases

This workaround can be highly beneficial in the following scenarios:

  1. Hybrid Cloud Environments: Organizations utilizing both Azure and AWS can manage resources across both platforms seamlessly.
  2. High-Security Requirements: For applications that require strong security measures, the ability to secure and manage sensitive data effectively is crucial.
  3. Large-Scale Deployments: In large environments with multiple applications, this automation can significantly reduce manual effort and human error.

In conclusion, while Terraform's azuread provider does not currently support Azure Application Proxy configurations, there are effective ways to manage this through workarounds. By integrating with AWS Secrets Manager and using the local-exec provisioner for running scripts and making Graph API calls, we can create a comprehensive and secure infrastructure management solution.