diff --git a/internal/services/network/application_gateway_resource.go b/internal/services/network/application_gateway_resource.go index 0c1412d481c7..71e4cc5fcf89 100644 --- a/internal/services/network/application_gateway_resource.go +++ b/internal/services/network/application_gateway_resource.go @@ -885,17 +885,16 @@ func resourceApplicationGateway() *pluginsdk.Resource { "data": { Type: pluginsdk.TypeString, - Required: true, + Optional: true, ValidateFunc: validation.StringIsNotEmpty, Sensitive: true, }, - // TODO required soft delete on the keyvault - /*"key_vault_secret_id": { + "key_vault_secret_id": { Type: pluginsdk.TypeString, Optional: true, - ValidateFunc: azure.ValidateKeyVaultChildId, - },*/ + ValidateFunc: keyVaultValidate.NestedItemIdWithOptionalVersion, + }, "id": { Type: pluginsdk.TypeString, @@ -913,6 +912,11 @@ func resourceApplicationGateway() *pluginsdk.Resource { Optional: true, }, + "force_firewall_policy_association": { + Type: pluginsdk.TypeBool, + Optional: true, + }, + "probe": { Type: pluginsdk.TypeList, Optional: true, @@ -1546,7 +1550,10 @@ func resourceApplicationGatewayCreateUpdate(d *pluginsdk.ResourceData, meta inte t := d.Get("tags").(map[string]interface{}) // Gateway ID is needed to link sub-resources together in expand functions - trustedRootCertificates := expandApplicationGatewayTrustedRootCertificates(d.Get("trusted_root_certificate").([]interface{})) + trustedRootCertificates, err := expandApplicationGatewayTrustedRootCertificates(d.Get("trusted_root_certificate").([]interface{})) + if err != nil { + return fmt.Errorf("expanding `trusted_root_certificate`: %+v", err) + } requestRoutingRules, err := expandApplicationGatewayRequestRoutingRules(d, id.ID()) if err != nil { @@ -1619,6 +1626,10 @@ func resourceApplicationGatewayCreateUpdate(d *pluginsdk.ResourceData, meta inte }, } + if v, ok := d.GetOk("force_firewall_policy_association"); ok { + gateway.ApplicationGatewayPropertiesFormat.ForceFirewallPolicyAssociation = utils.Bool(v.(bool)) + } + if _, ok := d.GetOk("identity"); ok { gateway.Identity = expandAzureRmApplicationGatewayIdentity(d) } @@ -1767,6 +1778,7 @@ func resourceApplicationGatewayRead(d *pluginsdk.ResourceData, meta interface{}) } d.Set("enable_http2", props.EnableHTTP2) + d.Set("force_firewall_policy_association", props.ForceFirewallPolicyAssociation) httpListeners, err := flattenApplicationGatewayHTTPListeners(props.HTTPListeners) if err != nil { @@ -1968,7 +1980,7 @@ func expandApplicationGatewayAuthenticationCertificates(certs []interface{}) *[] return &results } -func expandApplicationGatewayTrustedRootCertificates(certs []interface{}) *[]network.ApplicationGatewayTrustedRootCertificate { +func expandApplicationGatewayTrustedRootCertificates(certs []interface{}) (*[]network.ApplicationGatewayTrustedRootCertificate, error) { results := make([]network.ApplicationGatewayTrustedRootCertificate, 0) for _, raw := range certs { @@ -1976,20 +1988,28 @@ func expandApplicationGatewayTrustedRootCertificates(certs []interface{}) *[]net name := v["name"].(string) data := v["data"].(string) + kvsid := v["key_vault_secret_id"].(string) output := network.ApplicationGatewayTrustedRootCertificate{ Name: utils.String(name), ApplicationGatewayTrustedRootCertificatePropertiesFormat: &network.ApplicationGatewayTrustedRootCertificatePropertiesFormat{}, } - if data != "" { + switch { + case data != "" && kvsid != "": + return nil, fmt.Errorf("only one of `key_vault_secret_id` or `data` must be specified for the `trusted_root_certificate` block %q", name) + case data != "": output.ApplicationGatewayTrustedRootCertificatePropertiesFormat.Data = utils.String(utils.Base64EncodeIfNot(data)) + case kvsid != "": + output.ApplicationGatewayTrustedRootCertificatePropertiesFormat.KeyVaultSecretID = utils.String(kvsid) + default: + return nil, fmt.Errorf("either `key_vault_secret_id` or `data` must be specified for the `trusted_root_certificate` block %q", name) } results = append(results, output) } - return &results + return &results, nil } func flattenApplicationGatewayAuthenticationCertificates(certs *[]network.ApplicationGatewayAuthenticationCertificate, d *pluginsdk.ResourceData) []interface{} { @@ -2051,13 +2071,13 @@ func flattenApplicationGatewayTrustedRootCertificates(certs *[]network.Applicati output["id"] = *v } - /*kvsid := "" + kvsid := "" if props := cert.ApplicationGatewayTrustedRootCertificatePropertiesFormat; props != nil { if v := props.KeyVaultSecretID; v != nil { kvsid = *v - output["key_vault_secret_id"] = *v } - }*/ + } + output["key_vault_secret_id"] = kvsid if v := cert.Name; v != nil { output["name"] = *v diff --git a/internal/services/network/application_gateway_resource_test.go b/internal/services/network/application_gateway_resource_test.go index a014cd27fbff..2e486e796b3a 100644 --- a/internal/services/network/application_gateway_resource_test.go +++ b/internal/services/network/application_gateway_resource_test.go @@ -239,10 +239,7 @@ func TestAccApplicationGateway_customPathRuleFirewallPolicy(t *testing.T) { }) } -// TODO required soft delete on the keyvault func TestAccApplicationGateway_trustedRootCertificate_keyvault(t *testing.T) { - t.Skip() - data := acceptance.BuildTestData(t, "azurerm_application_gateway", "test") r := ApplicationGatewayResource{} @@ -255,6 +252,14 @@ func TestAccApplicationGateway_trustedRootCertificate_keyvault(t *testing.T) { ), }, data.ImportStep(), + { + Config: r.update_trustedRootCertificate_keyvault(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("trusted_root_certificate.0.name").Exists(), + ), + }, + data.ImportStep(), }) } @@ -1126,6 +1131,28 @@ func TestAccApplicationGateway_privateLink(t *testing.T) { }) } +func TestAccApplicationGateway_updateForceFirewallPolicyAssociation(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_application_gateway", "test") + r := ApplicationGatewayResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.updateForceFirewallPolicyAssociation(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.updateForceFirewallPolicyAssociation(data, false), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func (t ApplicationGatewayResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := parse.ApplicationGatewayID(state.ID) if err != nil { @@ -1940,6 +1967,7 @@ resource "azurerm_key_vault" "test" { resource_group_name = "${azurerm_resource_group.test.name}" tenant_id = "${data.azurerm_client_config.test.tenant_id}" sku_name = "standard" + soft_delete_enabled = true access_policy { tenant_id = "${data.azurerm_client_config.test.tenant_id}" @@ -2030,26 +2058,268 @@ resource "azurerm_application_gateway" "test" { key_vault_secret_id = "${azurerm_key_vault_certificate.test.secret_id}" } + http_listener { + name = local.listener_name + frontend_ip_configuration_name = local.frontend_ip_configuration_name + frontend_port_name = local.frontend_port_name + protocol = "Http" + } + 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 + } +} +`, r.template(data), data.RandomInteger) +} + +func (r ApplicationGatewayResource) update_trustedRootCertificate_keyvault(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]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" +} + +data "azurerm_client_config" "test" {} + +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" + soft_delete_enabled = true + + 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", "purge"] + } + + 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"] + } +} + +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_key_vault_certificate" "test2" { + name = "acctestkvc%[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 = 80 + } + + 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 = 443 + protocol = "Https" + request_timeout = 1 + } + + trusted_root_certificate { + name = local.auth_cert_name + key_vault_secret_id = azurerm_key_vault_certificate.test2.secret_id + } http_listener { - name = "${local.listener_name}" - frontend_ip_configuration_name = "${local.frontend_ip_configuration_name}" - frontend_port_name = "${local.frontend_port_name}" + name = local.listener_name + frontend_ip_configuration_name = local.frontend_ip_configuration_name + frontend_port_name = local.frontend_port_name protocol = "Http" } request_routing_rule { - name = "${local.request_routing_rule_name}" + 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}" + http_listener_name = local.listener_name + backend_address_pool_name = local.backend_address_pool_name + backend_http_settings_name = local.http_setting_name } } `, r.template(data), data.RandomInteger) } +func (r ApplicationGatewayResource) updateForceFirewallPolicyAssociation(data acceptance.TestData, forceFirewallPolicyAssociation bool) string { + return fmt.Sprintf(` +%s + +# since these variables are re-used - a locals block makes this more maintainable +locals { + 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" +} + +resource "azurerm_application_gateway" "test" { + name = "acctestag-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + force_firewall_policy_association = %t + + sku { + name = "Standard_Small" + tier = "Standard" + capacity = 2 + } + + gateway_ip_configuration { + name = "my-gateway-ip-configuration" + subnet_id = azurerm_subnet.test.id + } + + frontend_port { + name = local.frontend_port_name + port = 80 + } + + frontend_ip_configuration { + name = local.frontend_ip_configuration_name + public_ip_address_id = azurerm_public_ip.test.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 = "Http" + } + + 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 + } +} +`, r.template(data), data.RandomInteger, forceFirewallPolicyAssociation) +} + func (r ApplicationGatewayResource) trustedRootCertificate(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s diff --git a/website/docs/r/application_gateway.html.markdown b/website/docs/r/application_gateway.html.markdown index f4ef63b33eb1..05011688505f 100644 --- a/website/docs/r/application_gateway.html.markdown +++ b/website/docs/r/application_gateway.html.markdown @@ -161,6 +161,8 @@ The following arguments are supported: * `enable_http2` - (Optional) Is HTTP2 enabled on the application gateway resource? Defaults to `false`. +* `force_firewall_policy_association` - (Optional) Is the Firewall Policy associated with the Application Gateway? + * `probe` - (Optional) One or more `probe` blocks as defined below. * `ssl_certificate` - (Optional) One or more `ssl_certificate` blocks as defined below. @@ -195,7 +197,13 @@ A `trusted_root_certificate` block supports the following: * `name` - (Required) The Name of the Trusted Root Certificate to use. -* `data` - (Required) The contents of the Trusted Root Certificate which should be used. +* `data` - (optional) The contents of the Trusted Root Certificate which should be used. Required if `key_vault_secret_id` is not set. + +* `key_vault_secret_id` - (Optional) The Secret ID of (base-64 encoded unencrypted pfx) `Secret` or `Certificate` object stored in Azure KeyVault. You need to enable soft delete for the Key Vault to use this feature. Required if `data` is not set. + +-> **NOTE:** TLS termination with Key Vault certificates is limited to the [v2 SKUs](https://docs.microsoft.com/en-us/azure/application-gateway/key-vault-certs). + +-> **NOTE:** For TLS termination with Key Vault certificates to work properly existing user-assigned managed identity, which Application Gateway uses to retrieve certificates from Key Vault, should be defined via `identity` block. Additionally, access policies in the Key Vault to allow the identity to be granted *get* access to the secret should be defined. ---