diff --git a/internal/backend/remote-state/azure/arm_client.go b/internal/backend/remote-state/azure/arm_client.go index 85bbc4c5fae6..5419e41112f7 100644 --- a/internal/backend/remote-state/azure/arm_client.go +++ b/internal/backend/remote-state/azure/arm_client.go @@ -81,11 +81,16 @@ func buildArmClient(ctx context.Context, config BackendConfig) (*ArmClient, erro // Managed Service Identity MsiEndpoint: config.MsiEndpoint, + // OIDC + IDTokenRequestURL: config.OIDCRequestURL, + IDTokenRequestToken: config.OIDCRequestToken, + // Feature Toggles SupportsAzureCliToken: true, SupportsClientCertAuth: true, SupportsClientSecretAuth: true, SupportsManagedServiceIdentity: config.UseMsi, + SupportsOIDCAuth: config.UseOIDC, UseMicrosoftGraph: config.UseMicrosoftGraph, } armConfig, err := builder.Build() @@ -106,13 +111,13 @@ func buildArmClient(ctx context.Context, config BackendConfig) (*ArmClient, erro sender := sender.BuildSender("backend/remote-state/azure") var auth autorest.Authorizer if builder.UseMicrosoftGraph { - log.Printf("[DEBUG] Obtaining a MSAL / Microsoft Graph token for Resource Manager..") + log.Printf("[DEBUG] Obtaining an MSAL / Microsoft Graph token for Resource Manager..") auth, err = armConfig.GetMSALToken(ctx, hamiltonEnv.ResourceManager, sender, oauthConfig, env.TokenAudience) if err != nil { return nil, err } } else { - log.Printf("[DEBUG] Obtaining a ADAL / Azure Active Directory Graph token for Resource Manager..") + log.Printf("[DEBUG] Obtaining an ADAL / Azure Active Directory Graph token for Resource Manager..") auth, err = armConfig.GetADALToken(ctx, sender, oauthConfig, env.TokenAudience) if err != nil { return nil, err @@ -121,14 +126,14 @@ func buildArmClient(ctx context.Context, config BackendConfig) (*ArmClient, erro if config.UseAzureADAuthentication { if builder.UseMicrosoftGraph { - log.Printf("[DEBUG] Obtaining a MSAL / Microsoft Graph token for Storage..") + log.Printf("[DEBUG] Obtaining an MSAL / Microsoft Graph token for Storage..") storageAuth, err := armConfig.GetMSALToken(ctx, hamiltonEnv.Storage, sender, oauthConfig, env.ResourceIdentifiers.Storage) if err != nil { return nil, err } client.azureAdStorageAuth = &storageAuth } else { - log.Printf("[DEBUG] Obtaining a ADAL / Azure Active Directory Graph token for Storage..") + log.Printf("[DEBUG] Obtaining an ADAL / Azure Active Directory Graph token for Storage..") storageAuth, err := armConfig.GetADALToken(ctx, sender, oauthConfig, env.ResourceIdentifiers.Storage) if err != nil { return nil, err diff --git a/internal/backend/remote-state/azure/backend.go b/internal/backend/remote-state/azure/backend.go index 5a8f15008c12..91db6936e7fd 100644 --- a/internal/backend/remote-state/azure/backend.go +++ b/internal/backend/remote-state/azure/backend.go @@ -135,6 +135,28 @@ func New() backend.Backend { DefaultFunc: schema.EnvDefaultFunc("ARM_MSI_ENDPOINT", ""), }, + // OIDC auth specific fields + "use_oidc": { + Type: schema.TypeBool, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_USE_OIDC", false), + Description: "Allow OIDC to be used for authentication", + }, + + "oidc_request_url": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ARM_OIDC_REQUEST_URL", "ACTIONS_ID_TOKEN_REQUEST_URL"}, ""), + Description: "The URL for the OIDC provider from which to request an ID token", + }, + + "oidc_request_token": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ARM_OIDC_REQUEST_TOKEN", "ACTIONS_ID_TOKEN_REQUEST_TOKEN"}, ""), + Description: "The bearer token for the request to the OIDC provider", + }, + // Feature Flags "use_azuread_auth": { Type: schema.TypeBool, @@ -182,11 +204,14 @@ type BackendConfig struct { MetadataHost string Environment string MsiEndpoint string + OIDCRequestURL string + OIDCRequestToken string ResourceGroupName string SasToken string SubscriptionID string TenantID string UseMsi bool + UseOIDC bool UseAzureADAuthentication bool UseMicrosoftGraph bool } @@ -213,12 +238,15 @@ func (b *Backend) configure(ctx context.Context) error { MetadataHost: data.Get("metadata_host").(string), Environment: data.Get("environment").(string), MsiEndpoint: data.Get("msi_endpoint").(string), + OIDCRequestURL: data.Get("oidc_request_url").(string), + OIDCRequestToken: data.Get("oidc_request_token").(string), ResourceGroupName: data.Get("resource_group_name").(string), SasToken: data.Get("sas_token").(string), StorageAccountName: data.Get("storage_account_name").(string), SubscriptionID: data.Get("subscription_id").(string), TenantID: data.Get("tenant_id").(string), UseMsi: data.Get("use_msi").(bool), + UseOIDC: data.Get("use_oidc").(bool), UseAzureADAuthentication: data.Get("use_azuread_auth").(bool), UseMicrosoftGraph: data.Get("use_microsoft_graph").(bool), } diff --git a/internal/backend/remote-state/azure/backend_test.go b/internal/backend/remote-state/azure/backend_test.go index 2411ddedb9b6..70bc272f892e 100644 --- a/internal/backend/remote-state/azure/backend_test.go +++ b/internal/backend/remote-state/azure/backend_test.go @@ -39,7 +39,7 @@ func TestBackendConfig(t *testing.T) { } } -func TestBackendAccessKeyBasic(t *testing.T) { +func TestAccBackendAccessKeyBasic(t *testing.T) { testAccAzureBackend(t) rs := acctest.RandString(4) res := testResourceNames(rs, "testState") @@ -65,7 +65,7 @@ func TestBackendAccessKeyBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendSASTokenBasic(t *testing.T) { +func TestAccBackendSASTokenBasic(t *testing.T) { testAccAzureBackend(t) rs := acctest.RandString(4) res := testResourceNames(rs, "testState") @@ -95,7 +95,35 @@ func TestBackendSASTokenBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendADALAzureADAuthBasic(t *testing.T) { +func TestAccBackendOIDCBasic(t *testing.T) { + testAccAzureBackend(t) + 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, + "use_oidc": true, + "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), + "tenant_id": os.Getenv("ARM_TENANT_ID"), + "environment": os.Getenv("ARM_ENVIRONMENT"), + "endpoint": os.Getenv("ARM_ENDPOINT"), + })).(*Backend) + + backend.TestBackendStates(t, b) +} + +func TestAccBackendADALAzureADAuthBasic(t *testing.T) { testAccAzureBackend(t) rs := acctest.RandString(4) res := testResourceNames(rs, "testState") @@ -123,7 +151,7 @@ func TestBackendADALAzureADAuthBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendADALManagedServiceIdentityBasic(t *testing.T) { +func TestAccBackendADALManagedServiceIdentityBasic(t *testing.T) { testAccAzureBackendRunningInAzure(t) rs := acctest.RandString(4) res := testResourceNames(rs, "testState") @@ -151,7 +179,7 @@ func TestBackendADALManagedServiceIdentityBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendADALServicePrincipalClientCertificateBasic(t *testing.T) { +func TestAccBackendADALServicePrincipalClientCertificateBasic(t *testing.T) { testAccAzureBackend(t) clientCertPassword := os.Getenv("ARM_CLIENT_CERTIFICATE_PASSWORD") @@ -188,7 +216,7 @@ func TestBackendADALServicePrincipalClientCertificateBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendADALServicePrincipalClientSecretBasic(t *testing.T) { +func TestAccBackendADALServicePrincipalClientSecretBasic(t *testing.T) { testAccAzureBackend(t) rs := acctest.RandString(4) res := testResourceNames(rs, "testState") @@ -217,7 +245,7 @@ func TestBackendADALServicePrincipalClientSecretBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendADALServicePrincipalClientSecretCustomEndpoint(t *testing.T) { +func TestAccBackendADALServicePrincipalClientSecretCustomEndpoint(t *testing.T) { testAccAzureBackend(t) // this is only applicable for Azure Stack. @@ -253,7 +281,7 @@ func TestBackendADALServicePrincipalClientSecretCustomEndpoint(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendMSALAzureADAuthBasic(t *testing.T) { +func TestAccBackendMSALAzureADAuthBasic(t *testing.T) { testAccAzureBackend(t) rs := acctest.RandString(4) res := testResourceNames(rs, "testState") @@ -282,7 +310,7 @@ func TestBackendMSALAzureADAuthBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendMSALManagedServiceIdentityBasic(t *testing.T) { +func TestAccBackendMSALManagedServiceIdentityBasic(t *testing.T) { testAccAzureBackendRunningInAzure(t) rs := acctest.RandString(4) res := testResourceNames(rs, "testState") @@ -311,7 +339,7 @@ func TestBackendMSALManagedServiceIdentityBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendMSALServicePrincipalClientCertificateBasic(t *testing.T) { +func TestAccBackendMSALServicePrincipalClientCertificateBasic(t *testing.T) { testAccAzureBackend(t) clientCertPassword := os.Getenv("ARM_CLIENT_CERTIFICATE_PASSWORD") @@ -349,7 +377,7 @@ func TestBackendMSALServicePrincipalClientCertificateBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendMSALServicePrincipalClientSecretBasic(t *testing.T) { +func TestAccBackendMSALServicePrincipalClientSecretBasic(t *testing.T) { testAccAzureBackend(t) rs := acctest.RandString(4) res := testResourceNames(rs, "testState") @@ -379,7 +407,7 @@ func TestBackendMSALServicePrincipalClientSecretBasic(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendMSALServicePrincipalClientSecretCustomEndpoint(t *testing.T) { +func TestAccBackendMSALServicePrincipalClientSecretCustomEndpoint(t *testing.T) { testAccAzureBackend(t) // this is only applicable for Azure Stack. @@ -416,7 +444,7 @@ func TestBackendMSALServicePrincipalClientSecretCustomEndpoint(t *testing.T) { backend.TestBackendStates(t, b) } -func TestBackendAccessKeyLocked(t *testing.T) { +func TestAccBackendAccessKeyLocked(t *testing.T) { testAccAzureBackend(t) rs := acctest.RandString(4) res := testResourceNames(rs, "testState") @@ -454,7 +482,7 @@ func TestBackendAccessKeyLocked(t *testing.T) { backend.TestBackendStateForceUnlockInWS(t, b1, b2, "foo") } -func TestBackendServicePrincipalLocked(t *testing.T) { +func TestAccBackendServicePrincipalLocked(t *testing.T) { testAccAzureBackend(t) rs := acctest.RandString(4) res := testResourceNames(rs, "testState") diff --git a/internal/backend/remote-state/azure/helpers_test.go b/internal/backend/remote-state/azure/helpers_test.go index dadb9aa26047..7fc51801eccc 100644 --- a/internal/backend/remote-state/azure/helpers_test.go +++ b/internal/backend/remote-state/azure/helpers_test.go @@ -39,6 +39,15 @@ func testAccAzureBackendRunningInAzure(t *testing.T) { } } +// these kind of tests can only run when within GitHub Actions (e.g. OIDC) +func testAccAzureBackendRunningInGitHubActions(t *testing.T) { + testAccAzureBackend(t) + + if os.Getenv("TF_RUNNING_IN_GITHUB_ACTIONS") == "" { + t.Skip("Skipping test since not running in GitHub Actions") + } +} + func buildTestClient(t *testing.T, res resourceNames) *ArmClient { subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID") tenantID := os.Getenv("ARM_TENANT_ID") diff --git a/website/docs/language/settings/backends/azurerm.mdx b/website/docs/language/settings/backends/azurerm.mdx index a2b91bc9f527..a70356072702 100644 --- a/website/docs/language/settings/backends/azurerm.mdx +++ b/website/docs/language/settings/backends/azurerm.mdx @@ -46,6 +46,24 @@ terraform { *** +When authenticating using OpenID Connect (OIDC): + +```hcl +terraform { + backend "azurerm" { + resource_group_name = "StorageAccount-ResourceGroup" + storage_account_name = "abcd1234" + container_name = "tfstate" + key = "prod.terraform.tfstate" + use_oidc = true + subscription_id = "00000000-0000-0000-0000-000000000000" + tenant_id = "00000000-0000-0000-0000-000000000000" + } +} +``` + +*** + When authenticating using Azure AD Authentication: ```hcl @@ -137,6 +155,25 @@ data "terraform_remote_state" "foo" { *** +When authenticating using OpenID Connect (OIDC): + +```hcl +data "terraform_remote_state" "foo" { + backend = "azurerm" + config = { + resource_group_name = "StorageAccount-ResourceGroup" + storage_account_name = "terraform123abc" + container_name = "terraform-state" + key = "prod.terraform.tfstate" + use_oidc = true + subscription_id = "00000000-0000-0000-0000-000000000000" + tenant_id = "00000000-0000-0000-0000-000000000000" + } +} +``` + +*** + When authenticating using AzureAD Authentication: ```hcl @@ -231,6 +268,18 @@ When authenticating using the Managed Service Identity (MSI) - the following fie *** +When authenticating using a Service Principal with OpenID Connect (OIDC) - the following fields are also supported: + +* `oidc_request_url` - (Optional) The URL for the OIDC provider from which to request an ID token. This can also be sourced from the `ARM_OIDC_REQUEST_URL` or `ACTIONS_ID_TOKEN_REQUEST_URL` environment variables. + +* `oidc_request_token` - (Optional) The bearer token for the request to the OIDC provider. This can also be sourced from the `ARM_OIDC_REQUEST_TOKEN` or `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variables. + +* `use_oidc` - (Optional) Should OIDC authentication be used? This can also be sourced from the `ARM_USE_OIDC` environment variable. + +~> **Note:** When using OIDC for authentication, `use_microsoft_graph` must be set to `true` (which is the default). + +*** + When authenticating using a SAS Token associated with the Storage Account - the following fields are also supported: * `sas_token` - (Optional) The SAS Token used to access the Blob Storage Account. This can also be sourced from the `ARM_SAS_TOKEN` environment variable.