diff --git a/azurerm/internal/services/network/resource_arm_application_gateway.go b/azurerm/internal/services/network/resource_arm_application_gateway.go index 90fa875465e2..023253cf8583 100644 --- a/azurerm/internal/services/network/resource_arm_application_gateway.go +++ b/azurerm/internal/services/network/resource_arm_application_gateway.go @@ -989,17 +989,23 @@ func resourceArmApplicationGateway() *schema.Resource { "data": { Type: schema.TypeString, - Required: true, + Optional: true, Sensitive: true, StateFunc: base64EncodedStateFunc, }, "password": { Type: schema.TypeString, - Required: true, + Optional: true, Sensitive: true, }, + "key_vault_secret_id": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: azure.ValidateKeyVaultChildId, + }, + "id": { Type: schema.TypeString, Computed: true, @@ -1362,6 +1368,11 @@ func resourceArmApplicationGatewayCreateUpdate(d *schema.ResourceData, meta inte return fmt.Errorf("Error expanding `redirect_configuration`: %+v", err) } + sslCertificates, err := expandApplicationGatewaySslCertificates(d) + if err != nil { + return fmt.Errorf("Error expanding `ssl_certificate`: %+v", err) + } + gatewayIPConfigurations, stopApplicationGateway := expandApplicationGatewayIPConfigurations(d) gateway := network.ApplicationGateway{ @@ -1385,7 +1396,7 @@ func resourceArmApplicationGatewayCreateUpdate(d *schema.ResourceData, meta inte RequestRoutingRules: requestRoutingRules, RedirectConfigurations: redirectConfigurations, Sku: expandApplicationGatewaySku(d), - SslCertificates: expandApplicationGatewaySslCertificates(d), + SslCertificates: sslCertificates, SslPolicy: expandApplicationGatewaySslPolicy(d), RewriteRuleSets: expandApplicationGatewayRewriteRuleSets(d), @@ -3185,7 +3196,7 @@ func flattenApplicationGatewaySku(input *network.ApplicationGatewaySku) []interf return []interface{}{result} } -func expandApplicationGatewaySslCertificates(d *schema.ResourceData) *[]network.ApplicationGatewaySslCertificate { +func expandApplicationGatewaySslCertificates(d *schema.ResourceData) (*[]network.ApplicationGatewaySslCertificate, error) { vs := d.Get("ssl_certificate").([]interface{}) results := make([]network.ApplicationGatewaySslCertificate, 0) @@ -3195,22 +3206,38 @@ func expandApplicationGatewaySslCertificates(d *schema.ResourceData) *[]network. name := v["name"].(string) data := v["data"].(string) password := v["password"].(string) - - // data must be base64 encoded - data = utils.Base64EncodeIfNot(data) + kvsid := v["key_vault_secret_id"].(string) output := network.ApplicationGatewaySslCertificate{ Name: utils.String(name), - ApplicationGatewaySslCertificatePropertiesFormat: &network.ApplicationGatewaySslCertificatePropertiesFormat{ - Data: utils.String(data), - Password: utils.String(password), - }, + ApplicationGatewaySslCertificatePropertiesFormat: &network.ApplicationGatewaySslCertificatePropertiesFormat{}, + } + + if data != "" && kvsid != "" { + return nil, fmt.Errorf("only one of `key_vault_secret_id` or `data` must be specified for the `ssl_certificate` block %q", name) + } else if data != "" { + // data must be base64 encoded + output.ApplicationGatewaySslCertificatePropertiesFormat.Data = utils.String(utils.Base64EncodeIfNot(data)) + + if password == "" { + return nil, fmt.Errorf("'password' is required if `data` is specified for the `ssl_certificate` block %q", name) + } + + output.ApplicationGatewaySslCertificatePropertiesFormat.Password = utils.String(password) + } else if kvsid != "" { + if password != "" { + return nil, fmt.Errorf("only one of `key_vault_secret_id` or `password` must be specified for the `ssl_certificate` block %q", name) + } + + output.ApplicationGatewaySslCertificatePropertiesFormat.KeyVaultSecretID = utils.String(kvsid) + } else { + return nil, fmt.Errorf("either `key_vault_secret_id` or `data` must be specified for the `ssl_certificate` block %q", name) } results = append(results, output) } - return &results + return &results, nil } func flattenApplicationGatewaySslCertificates(input *[]network.ApplicationGatewaySslCertificate, d *schema.ResourceData) []interface{} { @@ -3237,6 +3264,10 @@ func flattenApplicationGatewaySslCertificates(input *[]network.ApplicationGatewa if data := props.PublicCertData; data != nil { output["public_cert_data"] = *data } + + if kvsid := props.KeyVaultSecretID; kvsid != nil { + output["key_vault_secret_id"] = *kvsid + } } // since the certificate data isn't returned we have to load it from the same index diff --git a/azurerm/internal/services/network/tests/resource_arm_application_gateway_test.go b/azurerm/internal/services/network/tests/resource_arm_application_gateway_test.go index 9721bb2c227b..d815599f908e 100644 --- a/azurerm/internal/services/network/tests/resource_arm_application_gateway_test.go +++ b/azurerm/internal/services/network/tests/resource_arm_application_gateway_test.go @@ -540,6 +540,26 @@ func TestAccAzureRMApplicationGateway_settingsPickHostNameFromBackendAddress(t * }) } +func TestAccAzureRMApplicationGateway_sslCertificate_keyvault(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_application_gateway", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMApplicationGatewayDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMApplicationGateway_sslCertificate_keyvault(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMApplicationGatewayExists(data.ResourceName), + resource.TestCheckResourceAttrSet(data.ResourceName, "ssl_certificate.0.key_vault_secret_id"), + ), + }, + data.ImportStep(), + }, + }) +} + func TestAccAzureRMApplicationGateway_sslCertificate(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_application_gateway", "test") @@ -2918,6 +2938,161 @@ resource "azurerm_application_gateway" "test" { `, template, data.RandomInteger) } +func testAccAzureRMApplicationGateway_sslCertificate_keyvault(data acceptance.TestData) string { + template := testAccAzureRMApplicationGateway_template(data) + return fmt.Sprintf(` +%s + +# since these variables are re-used - a locals block makes this more maintainable +locals { + auth_cert_name = "${azurerm_virtual_network.test.name}-auth" + backend_address_pool_name = "${azurerm_virtual_network.test.name}-beap" + frontend_port_name = "${azurerm_virtual_network.test.name}-feport" + frontend_ip_configuration_name = "${azurerm_virtual_network.test.name}-feip" + http_setting_name = "${azurerm_virtual_network.test.name}-be-htst" + listener_name = "${azurerm_virtual_network.test.name}-httplstn" + request_routing_rule_name = "${azurerm_virtual_network.test.name}-rqrt" + ssl_certificate_name = "${azurerm_virtual_network.test.name}-sslcert" +} + +data "azurerm_client_config" "test" {} + +data "azuread_service_principal" "test" { + display_name = "Microsoft Azure App Service" +} + +resource "azurerm_user_assigned_identity" "test" { + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + name = "acctest%[2]d" +} + +resource "azurerm_public_ip" "testStd" { + name = "acctest-PubIpStd-%[2]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + allocation_method = "Static" + sku = "Standard" +} + +resource "azurerm_key_vault" "test" { + name = "acct%[2]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + tenant_id = data.azurerm_client_config.test.tenant_id + sku_name = "standard" + + access_policy { + tenant_id = data.azurerm_client_config.test.tenant_id + object_id = data.azurerm_client_config.test.object_id + secret_permissions = ["delete", "get", "set"] + certificate_permissions = ["create", "delete", "get", "import"] + } + + access_policy { + tenant_id = data.azurerm_client_config.test.tenant_id + object_id = azurerm_user_assigned_identity.test.principal_id + secret_permissions = ["get"] + certificate_permissions = ["get"] + } + + soft_delete_enabled = true +} + +resource "azurerm_key_vault_certificate" "test" { + name = "acctest%[2]d" + key_vault_id = azurerm_key_vault.test.id + + certificate { + contents = filebase64("testdata/app_service_certificate.pfx") + password = "terraform" + } + + certificate_policy { + issuer_parameters { + name = "Self" + } + + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = false + } + + secret_properties { + content_type = "application/x-pkcs12" + } + } +} + +resource "azurerm_application_gateway" "test" { + name = "acctestag-%[2]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + + sku { + name = "WAF_v2" + tier = "WAF_v2" + capacity = 2 + } + + gateway_ip_configuration { + name = "my-gateway-ip-configuration" + subnet_id = azurerm_subnet.test.id + } + + identity { + identity_ids = [azurerm_user_assigned_identity.test.id] + } + + frontend_port { + name = local.frontend_port_name + port = 443 + } + + frontend_ip_configuration { + name = local.frontend_ip_configuration_name + public_ip_address_id = azurerm_public_ip.testStd.id + } + + backend_address_pool { + name = local.backend_address_pool_name + } + + backend_http_settings { + name = local.http_setting_name + cookie_based_affinity = "Disabled" + port = 80 + protocol = "Http" + request_timeout = 1 + } + + http_listener { + name = local.listener_name + frontend_ip_configuration_name = local.frontend_ip_configuration_name + frontend_port_name = local.frontend_port_name + protocol = "Https" + ssl_certificate_name = local.ssl_certificate_name + } + + request_routing_rule { + name = local.request_routing_rule_name + rule_type = "Basic" + http_listener_name = local.listener_name + backend_address_pool_name = local.backend_address_pool_name + backend_http_settings_name = local.http_setting_name + } + + ssl_certificate { + name = local.ssl_certificate_name + key_vault_secret_id = azurerm_key_vault_certificate.test.secret_id + } +} +`, template, data.RandomInteger) +} + func testAccAzureRMApplicationGateway_sslCertificate(data acceptance.TestData) string { template := testAccAzureRMApplicationGateway_template(data) return fmt.Sprintf(` diff --git a/website/docs/r/application_gateway.html.markdown b/website/docs/r/application_gateway.html.markdown index 307e04a30273..2c0b894f5754 100644 --- a/website/docs/r/application_gateway.html.markdown +++ b/website/docs/r/application_gateway.html.markdown @@ -388,9 +388,11 @@ A `ssl_certificate` block supports the following: * `name` - (Required) The Name of the SSL certificate that is unique within this Application Gateway -* `data` - (Required) PFX certificate. +* `data` - (Optional) PFX certificate. Required if `key_vault_secret_id` is not set. -* `password` - (Required) Password for the pfx file specified in data. +* `password` - (Optional) Password for the pfx file specified in data. Required if `data` is set. + +* `key_vault_secret_id` - (Optional) Secret Id of (base-64 encoded unencrypted pfx) `Secret` or `Certificate` object stored in Azure KeyVault. You need to enable soft delete for keyvault to use this feature. Required if `data` is not set. ---