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

Allow authentication via managed service identity #639

Merged
merged 12 commits into from
Mar 1, 2018
9 changes: 9 additions & 0 deletions azurerm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,15 @@ func getAuthorizationToken(c *authentication.Config, oauthConfig *adal.OAuthConf
return auth, nil
}

if c.UseMsi {
spt, err := adal.NewServicePrincipalTokenFromMSI(c.MsiEndpoint, endpoint)
if err != nil {
return nil, err
}
auth := autorest.NewBearerAuthorizer(spt)
return auth, nil
}

if c.IsCloudShell {
// load the refreshed tokens from the Azure CLI
err := c.LoadTokensFromAzureCLI()
Expand Down
2 changes: 2 additions & 0 deletions azurerm/helpers/authentication/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type Config struct {
// Bearer Auth
AccessToken *adal.Token
IsCloudShell bool
UseMsi bool
MsiEndpoint string
}

func (c *Config) LoadTokensFromAzureCLI() error {
Expand Down
19 changes: 19 additions & 0 deletions azurerm/helpers/authentication/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,22 @@ func (c *Config) ValidateServicePrincipal() error {

return err.ErrorOrNil()
}

func (c *Config) ValidateMsi() error {
var err *multierror.Error

if c.SubscriptionID == "" {
err = multierror.Append(err, fmt.Errorf("Subscription ID must be configured for the AzureRM provider"))
}
if c.TenantID == "" {
err = multierror.Append(err, fmt.Errorf("Tenant ID must be configured for the AzureRM provider"))
}
if c.Environment == "" {
err = multierror.Append(err, fmt.Errorf("Environment must be configured for the AzureRM provider"))
}
if c.MsiEndpoint == "" {
err = multierror.Append(err, fmt.Errorf("MSI endpoint must be configured for the AzureRM provider"))
}

return err.ErrorOrNil()
}
72 changes: 72 additions & 0 deletions azurerm/helpers/authentication/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,75 @@ func TestAzureValidateServicePrincipal(t *testing.T) {
}
}
}

func TestAzureValidateMsi(t *testing.T) {
cases := []struct {
Description string
Config Config
ExpectError bool
}{
{
Description: "Empty Configuration",
Config: Config{},
ExpectError: true,
},
{
Description: "Missing Subscription ID",
Config: Config{
MsiEndpoint: "http://localhost:50342/oauth2/token",
TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129",
Environment: "public",
},
ExpectError: true,
},
{
Description: "Missing Tenant ID",
Config: Config{
MsiEndpoint: "http://localhost:50342/oauth2/token",
SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2",
Environment: "public",
},
ExpectError: true,
},
{
Description: "Missing Environment",
Config: Config{
MsiEndpoint: "http://localhost:50342/oauth2/token",
SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2",
TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129",
},
ExpectError: true,
},
{
Description: "Missing MSI Endpoint",
Config: Config{
SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2",
TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129",
Environment: "public",
},
ExpectError: true,
},
{
Description: "Valid Configuration",
Config: Config{
MsiEndpoint: "http://localhost:50342/oauth2/token",
SubscriptionID: "8e8b5e02-5c13-4822-b7dc-4232afb7e8c2",
TenantID: "9834f8d0-24b3-41b7-8b8d-c611c461a129",
Environment: "public",
},
ExpectError: false,
},
}

for _, v := range cases {
err := v.Config.ValidateMsi()

if v.ExpectError && err == nil {
t.Fatalf("Expected an error for %q: didn't get one", v.Description)
}

if !v.ExpectError && err != nil {
t.Fatalf("Expected there to be no error for %q - but got: %v", v.Description, err)
}
}
}
29 changes: 28 additions & 1 deletion azurerm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"sync"

"github.com/Azure/azure-sdk-for-go/arm/resources/resources"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/hashicorp/terraform/helper/mutexkv"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
Expand Down Expand Up @@ -62,6 +63,16 @@ func Provider() terraform.ResourceProvider {
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARM_SKIP_PROVIDER_REGISTRATION", false),
},
"use_msi": {
Type: schema.TypeBool,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARM_USE_MSI", false),
Copy link
Contributor

Choose a reason for hiding this comment

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

Change to "USE_MSI".

Copy link
Contributor

Choose a reason for hiding this comment

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

given all the other Environment Variables are prefixed with ARM - we should leave this as ARM_USE_MSI for consistency (we may change this for AZURE_USE_MSI in future, but it should be consistent for now)

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree.

},
"msi_endpoint": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARM_MSI_ENDPOINT", ""),
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we change the default environment variable to "AZURE_MSI_ENDPOINT", coz this endpoint is not just for getting tokens for ARM, but also for graph/datalake/keyvault.

Copy link
Contributor

Choose a reason for hiding this comment

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

(same here) - given the prefix for the other Environment Variables is ARM - we'd be best to leave this as ARM_MSI_ENDPOINT rather than making this AZURE_MSI_ENDPOINT - in future we can migrate across to use AZURE as a prefix if needed)

Copy link
Contributor

Choose a reason for hiding this comment

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

should this be defaulted to http://localhost:50342/oauth2/token?

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tombuildsstuff by leaving it blank then the endpoint should be auto-discovered using GetMSIVMEndpoint(). I only put the option there in case the endpoint can't be discovered (such as when running in a container)

},
},

DataSourcesMap: map[string]*schema.Resource{
Expand Down Expand Up @@ -188,11 +199,27 @@ func providerConfigure(p *schema.Provider) schema.ConfigureFunc {
ClientSecret: d.Get("client_secret").(string),
TenantID: d.Get("tenant_id").(string),
Environment: d.Get("environment").(string),
UseMsi: d.Get("use_msi").(bool),
MsiEndpoint: d.Get("msi_endpoint").(string),
SkipCredentialsValidation: d.Get("skip_credentials_validation").(bool),
SkipProviderRegistration: d.Get("skip_provider_registration").(bool),
}

if config.ClientSecret != "" {
if config.UseMsi {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shall we use the same if-else logic as in the azurerm/config.go ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean from getAuthorizationToken ? I think the logic is slightly different in each

Copy link
Contributor

@metacpp metacpp Feb 1, 2018

Choose a reason for hiding this comment

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

Yes, I mean why we don't check if ClientSecret exists firstly.

log.Printf("[DEBUG] use_msi specified - using MSI Authentication")
if config.MsiEndpoint == "" {
msiEndpoint, err := adal.GetMSIVMEndpoint()
if err != nil {
return nil, fmt.Errorf("Could not retrieve MSI endpoint from VM settings."+
"Ensure the VM has MSI enabled, or try setting msi_endpoint. Error: %s", err)
}
config.MsiEndpoint = msiEndpoint
}
log.Printf("[DEBUG] Using MSI endpoint %s", config.MsiEndpoint)
if err := config.ValidateMsi(); err != nil {
return nil, err
}
} else if config.ClientSecret != "" {
log.Printf("[DEBUG] Client Secret specified - using Service Principal for Authentication")
if err := config.ValidateServicePrincipal(); err != nil {
return nil, err
Expand Down
4 changes: 4 additions & 0 deletions website/azurerm.erb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<li<%= sidebar_current("docs-azurerm-index-authentication-service-principal") %>>
<a href="/docs/providers/azurerm/authenticating_via_service_principal.html">Authenticating via a Service Principal (Shared Account)</a>
</li>

<li<%= sidebar_current("docs-azurerm-index-authentication-msi") %>>
<a href="/docs/providers/azurerm/authenticating_via_msi.html">Authenticating via Managed Service Identity</a>
</li>
</ul>
</li>

Expand Down
97 changes: 97 additions & 0 deletions website/docs/authenticating_via_msi.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
layout: "azurerm"
page_title: "AzureRM: Authenticating via Managed Service Identity"
sidebar_current: "docs-azurerm-index-authentication-msi"
description: |-
The Azure Resource Manager provider supports authenticating via multiple means. This guide will cover configuring a Managed Service Identity which can be used to access Azure Resource Manager.

---

# Authenticating to Azure Resource Manager using Managed Service Identity

Terraform supports authenticating to Azure through Managed Service Identity, Service Principal or the Azure CLI.

We recommend using Managed Service Identity when running in a Shared Environment (such as within a CI server/automation) that you do not wish to configure credentials for - and [authenticating via the Azure CLI](authenticating_via_azure_cli.html) when you're running Terraform locally. Note that managed service identity is only available for virtual machines within Azure.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd suggest we change this to:

Managed Service Identity can be used to access other Azure Services from within a Virtual Machine in Azure instead of specifying a Service Principal or Azure CLI credentials.


## Configuring Managed Service Identity

Managed Service Identity allows an Azure virtual machine to retrieve a token to access the Azure API without needing to pass in credentials. This works by creating a service principal in Azure Active Directory that is associated to a virtual machine. This service principal can then be granted permissions to Azure resources.
There are various ways to configure managed service identity - see the [Microsoft documentation](https://docs.microsoft.com/en-us/azure/active-directory/msi-overview) for details.
You can then run Terraform from the MSI enabled virtual machine by setting the use_msi provider option to true.
Copy link
Contributor

Choose a reason for hiding this comment

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

can we quote use_msi and true here?


### Configuring Managed Service Identity using Terraform

Managed service identity can also be configured using Terraform. The following template shows how. Note that for a Linux VM you must use the ManagedIdentityExtensionForLinux.
Copy link
Contributor

Choose a reason for hiding this comment

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

can we make this "ManagedIdentityExtensionForLinux extension"


```hcl
resource "azurerm_virtual_machine" "virtual_machine" {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you put a Linux example ? This example creates WindowsServer and the documentation above says about Linux.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As noted, for Linux it would be exactly the same except you use ManagedIdentityExtensionForLinux instead of ManagedIdentityExtensionForWindows extension

name = "test"
location = "${var.location}"
resource_group_name = "test"
network_interface_ids = ["${azurerm_network_interface.test.id}"]
vm_size = "Standard_DS1_v2"

identity = {
type = "SystemAssigned"
}

storage_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2016-Datacenter-smalldisk"
version = "latest"
}

storage_os_disk {
name = "test"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Standard_LRS"
}

os_profile {
computer_name = "test"
admin_username = "username"
admin_password = "password"
}

os_profile_windows_config {
provision_vm_agent = true
enable_automatic_upgrades = false
}
}

resource "azurerm_virtual_machine_extension" "virtual_machine_extension" {
name = "test"
location = "${var.location}"
resource_group_name = "test"
virtual_machine_name = "${azurerm_virtual_machine.virtual_machine.name}"
publisher = "Microsoft.ManagedIdentity"
type = "ManagedIdentityExtensionForWindows"
type_handler_version = "1.0"

settings = <<SETTINGS
{
"port": 50342
}
SETTINGS
}

data "azurerm_subscription" "subscription" {}

data "azurerm_builtin_role_definition" "builtin_role_definition" {
name = "Contributor"
}

# Grant the VM identity contributor rights to the current subscription
resource "azurerm_role_assignment" "role_assignment" {
name = "${uuid()}"
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe this field is optional - so we should be able to not specify it and it'll be populated automatically?

scope = "${data.azurerm_subscription.subscription.id}"
role_definition_id = "${data.azurerm_subscription.subscription.id}${data.azurerm_builtin_role_definition.builtin_role_definition.id}"
principal_id = "${lookup(azurerm_virtual_machine.virtual_machine.identity[0], "principal_id")}"

lifecycle {
ignore_changes = ["name"]
}
}
```
7 changes: 7 additions & 0 deletions website/docs/index.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ The following arguments are supported:
* `tenant_id` - (Optional) The tenant ID to use. It can also be sourced from the
`ARM_TENANT_ID` environment variable.

* `use_msi` - (Optional) Set to true to authenticate using managed service identity.
It can also be sourced from the `ARM_USE_MSI` environment variable.

* `msi_endpoint` - (Optional) The REST endpoint to retrieve an MSI token from. Terraform
will attempt to discover this automatically but it can be specified manually here.
It can also be sourced from the `ARM_MSI_ENDPOINT` environment variable.

* `environment` - (Optional) The cloud environment to use. It can also be sourced
from the `ARM_ENVIRONMENT` environment variable. Supported values are:
* `public` (default)
Expand Down