From 0d34e5d97c7e42c23c159ce564c6dd228a6b0801 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Fri, 7 Aug 2020 11:58:33 +0200 Subject: [PATCH 1/2] backend/azurerm: support for authenticating using a Client Certificate fixes #24179 supersedes #19606 dependent on #25769 --- backend/remote-state/azure/arm_client.go | 14 +++++-- backend/remote-state/azure/backend.go | 41 +++++++++++++++------ backend/remote-state/azure/backend_test.go | 41 ++++++++++++++++++++- website/docs/backends/types/azurerm.html.md | 30 +++++++++++---- 4 files changed, 103 insertions(+), 23 deletions(-) diff --git a/backend/remote-state/azure/arm_client.go b/backend/remote-state/azure/arm_client.go index e44861d69..570a98b90 100644 --- a/backend/remote-state/azure/arm_client.go +++ b/backend/remote-state/azure/arm_client.go @@ -59,18 +59,26 @@ func buildArmClient(config BackendConfig) (*ArmClient, error) { builder := authentication.Builder{ ClientID: config.ClientID, - ClientSecret: config.ClientSecret, SubscriptionID: config.SubscriptionID, TenantID: config.TenantID, CustomResourceManagerEndpoint: config.CustomResourceManagerEndpoint, Environment: config.Environment, - MsiEndpoint: config.MsiEndpoint, + + // Service Principal (Client Certificate) + ClientCertPassword: config.ClientCertificatePassword, + ClientCertPath: config.ClientCertificatePath, + + // Service Principal (Client Secret) + ClientSecret: config.ClientSecret, + + // Managed Service Identity + MsiEndpoint: config.MsiEndpoint, // Feature Toggles SupportsAzureCliToken: true, + SupportsClientCertAuth: true, SupportsClientSecretAuth: true, SupportsManagedServiceIdentity: config.UseMsi, - // TODO: support for Client Certificate auth } armConfig, err := builder.Build() if err != nil { diff --git a/backend/remote-state/azure/backend.go b/backend/remote-state/azure/backend.go index d340048bd..a9a4eff00 100644 --- a/backend/remote-state/azure/backend.go +++ b/backend/remote-state/azure/backend.go @@ -71,11 +71,11 @@ func New() backend.Backend { DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_ID", ""), }, - "client_secret": { + "endpoint": { Type: schema.TypeString, Optional: true, - Description: "The Client Secret.", - DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_SECRET", ""), + Description: "A custom Endpoint used to access the Azure Resource Manager API's.", + DefaultFunc: schema.EnvDefaultFunc("ARM_ENDPOINT", ""), }, "subscription_id": { @@ -92,13 +92,35 @@ func New() backend.Backend { DefaultFunc: schema.EnvDefaultFunc("ARM_TENANT_ID", ""), }, + // Service Principal (Client Certificate) specific + "client_certificate_password": { + Type: schema.TypeString, + Optional: true, + Description: "The password associated with the Client Certificate specified in `client_certificate_path`", + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_CERTIFICATE_PASSWORD", ""), + }, + "client_certificate_path": { + Type: schema.TypeString, + Optional: true, + Description: "The path to the PFX file used as the Client Certificate when authenticating as a Service Principal", + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_CERTIFICATE_PATH", ""), + }, + + // Service Principal (Client Secret) specific + "client_secret": { + Type: schema.TypeString, + Optional: true, + Description: "The Client Secret.", + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_SECRET", ""), + }, + + // Managed Service Identity specific "use_msi": { Type: schema.TypeBool, Optional: true, Description: "Should Managed Service Identity be used?.", DefaultFunc: schema.EnvDefaultFunc("ARM_USE_MSI", false), }, - "msi_endpoint": { Type: schema.TypeString, Optional: true, @@ -106,13 +128,6 @@ func New() backend.Backend { DefaultFunc: schema.EnvDefaultFunc("ARM_MSI_ENDPOINT", ""), }, - "endpoint": { - Type: schema.TypeString, - Optional: true, - Description: "A custom Endpoint used to access the Azure Resource Manager API's.", - DefaultFunc: schema.EnvDefaultFunc("ARM_ENDPOINT", ""), - }, - // Deprecated fields "arm_client_id": { Type: schema.TypeString, @@ -167,6 +182,8 @@ type BackendConfig struct { // Optional AccessKey string ClientID string + ClientCertificatePassword string + ClientCertificatePath string ClientSecret string CustomResourceManagerEndpoint string Environment string @@ -199,6 +216,8 @@ func (b *Backend) configure(ctx context.Context) error { config := BackendConfig{ AccessKey: data.Get("access_key").(string), ClientID: clientId, + ClientCertificatePassword: data.Get("client_certificate_password").(string), + ClientCertificatePath: data.Get("client_certificate_path").(string), ClientSecret: clientSecret, CustomResourceManagerEndpoint: data.Get("endpoint").(string), Environment: data.Get("environment").(string), diff --git a/backend/remote-state/azure/backend_test.go b/backend/remote-state/azure/backend_test.go index 1753c6358..3f0a28534 100644 --- a/backend/remote-state/azure/backend_test.go +++ b/backend/remote-state/azure/backend_test.go @@ -123,7 +123,44 @@ func TestBackendSASTokenBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendServicePrincipalBasic(t *testing.T) { +func TestBackendServicePrincipalClientCertificateBasic(t *testing.T) { + testAccAzureBackend(t) + + clientCertPassword := os.Getenv("ARM_CLIENT_CERTIFICATE_PASSWORD") + clientCertPath := os.Getenv("ARM_CLIENT_CERTIFICATE_PATH") + if clientCertPath == "" { + t.Skip("Skipping since `ARM_CLIENT_CERTIFICATE_PATH` is not specified!") + } + + rs := acctest.RandString(4) + res := testResourceNames(rs, "testState") + armClient := buildTestClient(t, res) + + ctx := context.TODO() + err := armClient.buildTestResources(ctx, &res) + defer armClient.destroyTestResources(ctx, res) + if err != nil { + t.Fatalf("Error creating Test Resources: %q", err) + } + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "storage_account_name": res.storageAccountName, + "container_name": res.storageContainerName, + "key": res.storageKeyName, + "resource_group_name": res.resourceGroup, + "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), + "tenant_id": os.Getenv("ARM_TENANT_ID"), + "client_id": os.Getenv("ARM_CLIENT_ID"), + "client_certificate_password": clientCertPassword, + "client_certificate_path": clientCertPath, + "environment": os.Getenv("ARM_ENVIRONMENT"), + "endpoint": os.Getenv("ARM_ENDPOINT"), + })).(*Backend) + + backend.TestBackendStates(t, b) +} + +func TestBackendServicePrincipalClientSecretBasic(t *testing.T) { testAccAzureBackend(t) rs := acctest.RandString(4) res := testResourceNames(rs, "testState") @@ -152,7 +189,7 @@ func TestBackendServicePrincipalBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendServicePrincipalCustomEndpoint(t *testing.T) { +func TestBackendServicePrincipalClientSecretCustomEndpoint(t *testing.T) { testAccAzureBackend(t) // this is only applicable for Azure Stack. diff --git a/website/docs/backends/types/azurerm.html.md b/website/docs/backends/types/azurerm.html.md index 05ceb2d9d..72da9c098 100644 --- a/website/docs/backends/types/azurerm.html.md +++ b/website/docs/backends/types/azurerm.html.md @@ -15,7 +15,7 @@ Stores the state as a Blob with the given Key within the Blob Container within [ ## Example Configuration -When authenticating using the Azure CLI or a Service Principal: +When authenticating using the Azure CLI or a Service Principal (either with a Client Certificate or a Client Secret): ```hcl terraform { @@ -37,8 +37,8 @@ terraform { container_name = "tfstate" key = "prod.terraform.tfstate" use_msi = true - subscription_id = "00000000-0000-0000-0000-000000000000" - tenant_id = "00000000-0000-0000-0000-000000000000" + subscription_id = "00000000-0000-0000-0000-000000000000" + tenant_id = "00000000-0000-0000-0000-000000000000" } } ``` @@ -79,7 +79,7 @@ terraform { ## Data Source Configuration -When authenticating using a Service Principal: +When authenticating using a Service Principall (either with a Client Certificate or a Client Secret): ```hcl data "terraform_remote_state" "foo" { @@ -154,12 +154,12 @@ The following configuration options are supported: * `environment` - (Optional) The Azure Environment which should be used. This can also be sourced from the `ARM_ENVIRONMENT` environment variable. Possible values are `public`, `china`, `german`, `stack` and `usgovernment`. Defaults to `public`. -* `snapshot` - (Optional) Should the Blob used to store the Terraform Statefile be snapshotted before use? Defaults to `false`. This value can also be sourced from the `ARM_SNAPSHOT` environment variable. - * `endpoint` - (Optional) The Custom Endpoint for Azure Resource Manager. This can also be sourced from the `ARM_ENDPOINT` environment variable. ~> **NOTE:** An `endpoint` should only be configured when using Azure Stack. +* `snapshot` - (Optional) Should the Blob used to store the Terraform Statefile be snapshotted before use? Defaults to `false`. This value can also be sourced from the `ARM_SNAPSHOT` environment variable. + --- When authenticating using the Managed Service Identity (MSI) - the following fields are also supported: @@ -186,7 +186,23 @@ When authenticating using the Storage Account's Access Key - the following field --- -When authenticating using a Service Principal - the following fields are also supported: +When authenticating using a Service Principal with a Client Certificate - the following fields are also supported: + +* `resource_group_name` - (Required) The Name of the Resource Group in which the Storage Account exists. + +* `client_id` - (Optional) The Client ID of the Service Principal. This can also be sourced from the `ARM_CLIENT_ID` environment variable. + +* `client_certificate_password` - (Optional) The password associated with the Client Certificate specified in `client_certificate_path`. This can also be sourced from the `ARM_CLIENT_CERTIFICATE_PASSWORD` environment variable. + +* `client_certificate_path` - (Optional) The path to the PFX file used as the Client Certificate when authenticating as a Service Principal. This can also be sourced from the `ARM_CLIENT_CERTIFICATE_PATH` environment variable. + +* `subscription_id` - (Optional) The Subscription ID in which the Storage Account exists. This can also be sourced from the `ARM_SUBSCRIPTION_ID` environment variable. + +* `tenant_id` - (Optional) The Tenant ID in which the Subscription exists. This can also be sourced from the `ARM_TENANT_ID` environment variable. + +--- + +When authenticating using a Service Principal with a Client Secret - the following fields are also supported: * `resource_group_name` - (Required) The Name of the Resource Group in which the Storage Account exists. From d2154534950be7892fc85add5a857e90ce444c61 Mon Sep 17 00:00:00 2001 From: tombuildsstuff Date: Fri, 7 Aug 2020 12:01:01 +0200 Subject: [PATCH 2/2] backend/azurerm: adding a missing docs string. fixes #25765 --- backend/remote-state/azure/arm_client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/remote-state/azure/arm_client.go b/backend/remote-state/azure/arm_client.go index 570a98b90..cd9d7c711 100644 --- a/backend/remote-state/azure/arm_client.go +++ b/backend/remote-state/azure/arm_client.go @@ -63,6 +63,7 @@ func buildArmClient(config BackendConfig) (*ArmClient, error) { TenantID: config.TenantID, CustomResourceManagerEndpoint: config.CustomResourceManagerEndpoint, Environment: config.Environment, + ClientSecretDocsLink: "https://www.terraform.io/docs/providers/azurerm/guides/service_principal_client_secret.html", // Service Principal (Client Certificate) ClientCertPassword: config.ClientCertificatePassword,