Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redis authentication managed identity #213

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
9 changes: 5 additions & 4 deletions infra/shared/terraform/modules/app-service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ resource "azurerm_linux_web_app" "application" {
virtual_network_subnet_id = var.appsvc_subnet_id

identity {
type = "SystemAssigned"
type = var.identity.type
identity_ids = var.identity.type == "SystemAssigned" ? [] : var.identity.identity_ids
}

tags = {
Expand Down Expand Up @@ -107,9 +108,9 @@ resource "azurerm_linux_web_app" "application" {
SPRING_CLOUD_AZURE_ACTIVE_DIRECTORY_CREDENTIAL_CLIENT_SECRET = var.contoso_webapp_options.contoso_active_directory_client_secret
SPRING_CLOUD_AZURE_ACTIVE_DIRECTORY_PROFILE_TENANT_ID = var.contoso_webapp_options.contoso_active_directory_tenant_id

SPRING_DATA_REDIS_HOST = var.contoso_webapp_options.redis_host_name
SPRING_DATA_REDIS_PORT = var.contoso_webapp_options.redis_port
SPRING_DATA_REDIS_PASSWORD = var.contoso_webapp_options.redis_password
AZURE_CACHE_REDIS_HOST = var.contoso_webapp_options.redis_host_name
AZURE_CACHE_REDIS_PORT = var.contoso_webapp_options.redis_port
AZURE_CACHE_REDIS_CLIENT_ID = var.contoso_webapp_options.redis_user_client_id

CONTOSO_RETRY_DEMO = "0"
}
Expand Down
21 changes: 20 additions & 1 deletion infra/shared/terraform/modules/app-service/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ variable "public_network_access_enabled" {
description = "Should public network access be enabled for the Web App."
}

variable "identity" {
type = object({
type = string
identity_ids = optional(list(string))
})

description = "The identity type and the list of identities ids"

default = {
type = "SystemAssigned"
identity_ids = []
}

validation {
condition = contains(["SystemAssigned", "UserAssigned", "SystemAssigned, UserAssigned"], var.identity.type)
error_message = "Please, choose among one of the following identity types: SystemAssigned, UserAssigned or SystemAssigned, UserAssigned."
}
}

variable "contoso_webapp_options" {
type = object({
contoso_active_directory_tenant_id = string
Expand All @@ -71,7 +90,7 @@ variable "contoso_webapp_options" {

redis_host_name = string
redis_port = number
redis_password = string
redis_user_client_id = string
})

description = "The options for the webapp"
Expand Down
3 changes: 2 additions & 1 deletion infra/shared/terraform/modules/cache/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ resource "azurerm_redis_cache" "cache" {
# public network access will be allowed for non-prod so devs can do integration testing while debugging locally
public_network_access_enabled = var.environment == "prod" ? false : true

# https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-configure#default-redis-server-configuration
redis_configuration {
enable_authentication = true
active_directory_authentication_enabled = true
}
}

Expand Down
6 changes: 3 additions & 3 deletions infra/shared/terraform/modules/cache/outputs.tf
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
output "cache_secret" {
value = azurerm_redis_cache.cache.primary_access_key
description = "The secret to use when connecting to Azure Cache for Redis"
output "cache_id" {
value = azurerm_redis_cache.cache.id
description = "The id of the Azure Cache for Redis"
}

output "cache_hostname" {
Expand Down
61 changes: 41 additions & 20 deletions infra/terraform/application.tf
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,23 @@ module "application" {
frontdoor_profile_uuid = module.frontdoor[0].resource_guid
public_network_access_enabled = false

identity = {
type = "SystemAssigned, UserAssigned"
identity_ids = [
azurerm_user_assigned_identity.primary_app_service_identity[0].id
]
}

contoso_webapp_options = {
contoso_active_directory_tenant_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_tenant_id[0].id})"
contoso_active_directory_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_id[0].id})"
contoso_active_directory_client_secret = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_secret[0].id})"
postgresql_database_url = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_url[0].id})"
postgresql_database_user = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin[0].id})"
postgresql_database_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin_password[0].id})"
redis_host_name = module.cache[0].cache_hostname
redis_port = module.cache[0].cache_ssl_port
redis_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_cache_secret[0].id})"
contoso_active_directory_tenant_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_tenant_id[0].id})"
contoso_active_directory_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_id[0].id})"
contoso_active_directory_client_secret = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_secret[0].id})"
postgresql_database_url = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_url[0].id})"
postgresql_database_user = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin[0].id})"
postgresql_database_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin_password[0].id})"
redis_host_name = module.cache[0].cache_hostname
redis_port = module.cache[0].cache_ssl_port
redis_user_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.primary_redis_user_secret[0].id})"
}
}

Expand All @@ -48,23 +55,30 @@ module "secondary_application" {
location = var.secondary_location
private_dns_resource_group = azurerm_resource_group.hub[0].name
appsvc_subnet_id = module.secondary_spoke_vnet[0].subnets[local.app_service_subnet_name].id
private_endpoint_subnet_id = module.secondary_spoke_vnet[0].subnets[local.private_link_subnet_name].id
private_endpoint_subnet_id = module.secondary_spoke_vnet[0].subnets[local.private_link_subnet_name].id
app_insights_connection_string = module.hub_app_insights[0].connection_string
log_analytics_workspace_id = module.hub_app_insights[0].log_analytics_workspace_id
frontdoor_host_name = module.frontdoor[0].host_name
frontdoor_profile_uuid = module.frontdoor[0].resource_guid
public_network_access_enabled = false

identity = {
type = "SystemAssigned, UserAssigned"
identity_ids = [
azurerm_user_assigned_identity.secondary_app_service_identity[0].id
]
}

contoso_webapp_options = {
contoso_active_directory_tenant_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_tenant_id[0].id})"
contoso_active_directory_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_id[0].id})"
contoso_active_directory_client_secret = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_secret[0].id})"
postgresql_database_url = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.secondary_contoso_database_url[0].id})"
postgresql_database_user = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin[0].id})"
postgresql_database_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin_password[0].id})"
redis_host_name = module.secondary_cache[0].cache_hostname
redis_port = module.secondary_cache[0].cache_ssl_port
redis_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_cache_secret[0].id})"
contoso_active_directory_tenant_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_tenant_id[0].id})"
contoso_active_directory_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_id[0].id})"
contoso_active_directory_client_secret = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_secret[0].id})"
postgresql_database_url = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.secondary_contoso_database_url[0].id})"
postgresql_database_user = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin[0].id})"
postgresql_database_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin_password[0].id})"
redis_host_name = module.secondary_cache[0].cache_hostname
redis_port = module.secondary_cache[0].cache_ssl_port
redis_user_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.secondary_redis_user_secret[0].id})"
}
}

Expand Down Expand Up @@ -92,6 +106,13 @@ module "dev_application" {
frontdoor_profile_uuid = module.dev_frontdoor[0].resource_guid
public_network_access_enabled = true

identity = {
type = "SystemAssigned, UserAssigned"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this create both a systemassigned and userassigned? do you need both?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this will create both. We need both as Redis is the first use case that uses user assigned managed identity. Key Vault still uses the System Assigned managed identity. We have an item in the backlog to transition to user assigned managed identity.

identity_ids = [
azurerm_user_assigned_identity.dev_app_service_identity[0].id
]
}

contoso_webapp_options = {
contoso_active_directory_tenant_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.dev_contoso_application_tenant_id[0].id})"
contoso_active_directory_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.dev_contoso_application_client_id[0].id})"
Expand All @@ -101,6 +122,6 @@ module "dev_application" {
postgresql_database_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.dev_contoso_database_admin_password[0].id})"
redis_host_name = module.dev-cache[0].cache_hostname
redis_port = module.dev-cache[0].cache_ssl_port
redis_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.dev_contoso_cache_secret[0].id})"
redis_user_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.dev_redis_user_secret[0].id})"
}
}
70 changes: 70 additions & 0 deletions infra/terraform/cache.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,30 @@ module "cache" {
log_analytics_workspace_id = module.hub_app_insights[0].log_analytics_workspace_id
}


resource "azurerm_redis_cache_access_policy_assignment" "primary_current_user" {
count = var.environment == "prod" ? 1 : 0
name = "primarycurrentuser"
redis_cache_id = module.cache[0].cache_id
access_policy_name = "Data Contributor"
object_id = data.azuread_client_config.current.object_id
object_id_alias = "currentuser"
}

resource "azurerm_redis_cache_access_policy_assignment" "app_user" {
count = var.environment == "prod" ? 1 : 0
name = "primaryappuser"
redis_cache_id = module.cache[0].cache_id
access_policy_name = "Data Contributor"
object_id = azurerm_user_assigned_identity.primary_app_service_identity[0].principal_id
object_id_alias = azurerm_user_assigned_identity.primary_app_service_identity[0].principal_id

# Ensure that the current user has been created before creating the app user
depends_on = [
azurerm_redis_cache_access_policy_assignment.primary_current_user
]
}

# ----------------------------------------------------------------------------------------------
# Cache - Prod - Secondary Region
# ----------------------------------------------------------------------------------------------
Expand All @@ -29,6 +53,29 @@ module "secondary_cache" {
log_analytics_workspace_id = module.hub_app_insights[0].log_analytics_workspace_id
}

resource "azurerm_redis_cache_access_policy_assignment" "secondary_current_user" {
count = var.environment == "prod" ? 1 : 0
name = "secondarycurrentuser"
redis_cache_id = module.secondary_cache[0].cache_id
access_policy_name = "Data Contributor"
object_id = data.azuread_client_config.current.object_id
object_id_alias = "currentuser"
}

resource "azurerm_redis_cache_access_policy_assignment" "secondary_app_user" {
count = var.environment == "prod" ? 1 : 0
name = "secondaryappuser"
redis_cache_id = module.secondary_cache[0].cache_id
access_policy_name = "Data Contributor"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found Data Contributor was sufficient and least privilege

object_id = azurerm_user_assigned_identity.secondary_app_service_identity[0].principal_id
object_id_alias = azurerm_user_assigned_identity.secondary_app_service_identity[0].principal_id

# Ensure that the current user has been created before creating the app user
depends_on = [
azurerm_redis_cache_access_policy_assignment.secondary_current_user

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe leave a comment as an fyi that access policy assignments must be done serially, hence why you're depending on it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this was fixed - hashicorp/terraform-provider-azurerm#26085. I'm going to get rid of this dependency.

]
}

# ----------------------------------------------------------------------------------------------
# Cache - Dev
# ----------------------------------------------------------------------------------------------
Expand All @@ -43,3 +90,26 @@ module "dev-cache" {
private_endpoint_subnet_id = null
log_analytics_workspace_id = module.dev_app_insights[0].log_analytics_workspace_id
}

resource "azurerm_redis_cache_access_policy_assignment" "dev_current_user" {
count = var.environment == "dev" ? 1 : 0
name = "devcurrentuser"
redis_cache_id = module.dev-cache[0].cache_id
access_policy_name = "Data Contributor"
object_id = data.azuread_client_config.current.object_id
object_id_alias = "currentuser"
}

resource "azurerm_redis_cache_access_policy_assignment" "dev_app_user" {
count = var.environment == "dev" ? 1 : 0
name = "devappuser"
redis_cache_id = module.dev-cache[0].cache_id
access_policy_name = "Data Contributor"
object_id = azurerm_user_assigned_identity.dev_app_service_identity[0].principal_id
object_id_alias = azurerm_user_assigned_identity.dev_app_service_identity[0].principal_id

# Ensure that the current user has been created before creating the app user
depends_on = [
azurerm_redis_cache_access_policy_assignment.dev_current_user
]
}
54 changes: 54 additions & 0 deletions infra/terraform/identity.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# ------------------------------------------------
# Identity for the Production Primary App Service
# ------------------------------------------------

resource "azurecaf_name" "primary_app_service_identity_name" {
count = var.environment == "prod" ? 1 : 0
name = var.application_name
resource_type = "azurerm_user_assigned_identity"
suffixes = [var.location, var.environment]
}

resource "azurerm_user_assigned_identity" "primary_app_service_identity" {
count = var.environment == "prod" ? 1 : 0
location = azurerm_resource_group.spoke[0].location
name = azurecaf_name.primary_app_service_identity_name[0].result
resource_group_name = azurerm_resource_group.spoke[0].name
}

# ------------------------------------------------
# Identity for the Production Secondary App Service
# ------------------------------------------------

resource "azurecaf_name" "secondary_app_service_identity_name" {
count = var.environment == "prod" ? 1 : 0
name = var.application_name
resource_type = "azurerm_user_assigned_identity"
suffixes = [var.secondary_location, var.environment]
}

resource "azurerm_user_assigned_identity" "secondary_app_service_identity" {
count = var.environment == "prod" ? 1 : 0
location = azurerm_resource_group.secondary_spoke[0].location
name = azurecaf_name.secondary_app_service_identity_name[0].result
resource_group_name = azurerm_resource_group.secondary_spoke[0].name
}


# ------------------------------------------------
# Identity for the Production Dev App Service
# ------------------------------------------------

resource "azurecaf_name" "dev_app_service_identity_name" {
count = var.environment == "dev" ? 1 : 0
name = var.application_name
resource_type = "azurerm_user_assigned_identity"
suffixes = [var.location, var.environment]
}

resource "azurerm_user_assigned_identity" "dev_app_service_identity" {
count = var.environment == "dev" ? 1 : 0
location = azurerm_resource_group.dev[0].location
name = azurecaf_name.dev_app_service_identity_name[0].result
resource_group_name = azurerm_resource_group.dev[0].name
}
22 changes: 16 additions & 6 deletions infra/terraform/secrets.tf
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,10 @@ resource "azurerm_key_vault_secret" "contoso_application_client_secret" {
]
}

resource "azurerm_key_vault_secret" "contoso_cache_secret" {
resource "azurerm_key_vault_secret" "primary_redis_user_secret" {
count = var.environment == "prod" ? 1 : 0
name = "contoso-redis-password"
value = module.cache[0].cache_secret
name = "contoso-primary-redis-user-object-id"
value = azurerm_user_assigned_identity.primary_app_service_identity[0].client_id
key_vault_id = module.hub_key_vault[0].vault_id
depends_on = [
azurerm_role_assignment.kv_administrator_user_role_assignement
Expand Down Expand Up @@ -128,6 +128,16 @@ resource "azurerm_key_vault_secret" "secondary_contoso_database_url" {
]
}

resource "azurerm_key_vault_secret" "secondary_redis_user_secret" {
count = var.environment == "prod" ? 1 : 0
name = "contoso-secondary-redis-user-object-id"
value = azurerm_user_assigned_identity.secondary_app_service_identity[0].client_id
key_vault_id = module.hub_key_vault[0].vault_id
depends_on = [
azurerm_role_assignment.kv_administrator_user_role_assignement
]
}

# Give the app access to the key vault secrets - https://learn.microsoft.com/azure/key-vault/general/rbac-guide?tabs=azure-cli#secret-scope-role-assignment
resource azurerm_role_assignment app_keyvault_role_assignment {
count = var.environment == "prod" ? 1 : 0
Expand Down Expand Up @@ -216,10 +226,10 @@ resource "azurerm_key_vault_secret" "dev_contoso_application_client_secret" {
]
}

resource "azurerm_key_vault_secret" "dev_contoso_cache_secret" {
resource "azurerm_key_vault_secret" "dev_redis_user_secret" {
count = var.environment == "dev" ? 1 : 0
name = "contoso-redis-password"
value = module.dev-cache[0].cache_secret
name = "contoso-dev-redis-user-object-id"
value = azurerm_user_assigned_identity.dev_app_service_identity[0].client_id
key_vault_id = module.dev_key_vault[0].vault_id
depends_on = [
azurerm_role_assignment.dev_kv_administrator_user_role_assignement
Expand Down
2 changes: 1 addition & 1 deletion infra/terraform/versions.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "3.112.0"
version = "3.114.0"
}
azurecaf = {
source = "aztfmod/azurecaf"
Expand Down
Loading