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

azurerm_application_gateway - support for Key Vault SSL certificate ids #4366

Merged
merged 16 commits into from
Mar 18, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

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

@katbyte i am quite sure this break the autorenew feature of the application gateway

After Application Gateway is configured to use Key Vault certificates, its instances retrieve the certificate from Key Vault and install them locally for SSL termination. The instances also poll Key Vault at 24-hour intervals to retrieve a renewed version of the certificate, if it exists. If an updated certificate is found, the SSL certificate currently associated with the HTTPS listener is automatically rotated.

With your validator you need to specify an exact version of the secret so the renew will not work.
You just need to point to the secret without the version.

Copy link

Choose a reason for hiding this comment

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

I agree. The current tf code wants 3 parts : vaulturl/certificate name/hash

This adds it but app gateway never sees new versions of the cert. In order to have this it's only needs 2 part: vaulturl/certificate name.

Copy link
Contributor

Choose a reason for hiding this comment

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

@francescopersico since this PR's been merged, can you open a new issue to track that? Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

It's already exists #6188

},

"id": {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -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{
Expand All @@ -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),
Expand Down Expand Up @@ -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)

Expand All @@ -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{} {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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(`
Expand Down
6 changes: 4 additions & 2 deletions website/docs/r/application_gateway.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down