From 1ab0173e697965467ac41831a7ca377c33f28679 Mon Sep 17 00:00:00 2001 From: Adrien Delorme Date: Fri, 9 Nov 2018 17:39:03 +0100 Subject: [PATCH 1/4] azure builder: allow to auth with managed identities ( MSI ) --- builder/azure/arm/authenticate.go | 4 +++ builder/azure/arm/config.go | 13 ++++---- .../source/docs/builders/azure-setup.html.md | 32 ++++++++++++++----- website/source/docs/builders/azure.html.md | 5 ++- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/builder/azure/arm/authenticate.go b/builder/azure/arm/authenticate.go index 7f0b5bdc9..10b8e4bd7 100644 --- a/builder/azure/arm/authenticate.go +++ b/builder/azure/arm/authenticate.go @@ -31,6 +31,10 @@ func (a *Authenticate) getServicePrincipalTokenWithResource(resource string) (*a return nil, err } + if a.clientID == "" && a.clientSecret == "" { + return adal.NewServicePrincipalTokenFromMSI("http://169.254.169.254/metadata/identity/oauth2/token", resource) + } + spt, err := adal.NewServicePrincipalToken( *oauthConfig, a.clientID, diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index 77daa67c0..4cc15ebe9 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -514,16 +514,17 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { if isUseDeviceLogin(c) { c.useDeviceLogin = true } else { - if c.ClientID == "" { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("A client_id must be specified")) + if (c.ClientID == "" && c.ClientSecret != "") || (c.ClientID != "" && c.ClientSecret == "") { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A client_id and client_secret must be specified together or not specified at all")) } - if c.ClientSecret == "" { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("A client_secret must be specified")) + if c.ClientID != "" && c.SubscriptionID == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A subscription_id must be specified when client_id & client_secret are")) } - if c.SubscriptionID == "" { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("A subscription_id must be specified")) + if c.SubscriptionID != "" && c.ClientID == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("A subscription_id cannot be specified as it will be taken from MSI"+ + " (client_id and client_secret are not set)")) } } diff --git a/website/source/docs/builders/azure-setup.html.md b/website/source/docs/builders/azure-setup.html.md index d5909c122..de107a78c 100644 --- a/website/source/docs/builders/azure-setup.html.md +++ b/website/source/docs/builders/azure-setup.html.md @@ -59,14 +59,30 @@ mode. > Device login mode is for the Public and US Gov clouds only. The device login flow asks that you open a web browser, navigate to -, and input the supplied code. This authorizes the -Packer for Azure application to act on your behalf. An OAuth token will be -created, and stored in the user's home directory -(~/.azure/packer/oauth-TenantID.json). This token is used if the token file -exists, and it is refreshed as necessary. The token file prevents the need to -continually execute the device login flow. Packer will ask for two device login -auth, one for service management endpoint and another for accessing temp -keyvault secrets that it creates. +http://aka.ms/devicelogin, +and input the supplied code. This authorizes the Packer for Azure application +to act on your behalf. An OAuth token will be created, and stored in the user's +home directory (\~/.azure/packer/oauth-TenantID.json). This token is used if +the token file exists, and it is refreshed as necessary. The token file +prevents the need to continually execute the device login flow. Packer will ask +for two device login auth, one for service management endpoint and another for +accessing temp keyvault secrets that it creates. + +## Managed identities for Azure resources + +-> Managed identities for Azure resources is the new name for the service +formerly known as Managed Service Identity (MSI). + +Managed identities is an alternative way to authorize in Azure Packer. Managed +identities for Azure resources are automatically managed by Azure and enable +you to authenticate to services that support Azure AD authentication without +needing to insert credentials into your buildfile. Navigate to +managed identities azure resources overview to learn more about +this feature. + +This feature will be used when no `subscription_id`, `client_id` or +`client_secret` is set in your buildfile. ## Install the Azure CLI diff --git a/website/source/docs/builders/azure.html.md b/website/source/docs/builders/azure.html.md index a81406cc7..4ba951ee5 100644 --- a/website/source/docs/builders/azure.html.md +++ b/website/source/docs/builders/azure.html.md @@ -35,7 +35,7 @@ addition to the options listed here, a [communicator](/docs/templates/communicator.html) can be configured for this builder. -### Required: +### Required ( unless instance has [managed identities](/docs/builders/azure-setup.html#managed-identities-for-azure-resources) enabled): - `client_id` (string) The Active Directory service principal associated with your builder. @@ -48,6 +48,8 @@ builder. specified in which case it needs to have owner access to the existing resource group specified in build\_resource\_group\_name parameter.** +### Required: + - `image_publisher` (string) PublisherName for your base image. See [documentation](https://azure.microsoft.com/en-us/documentation/articles/resource-groups-vm-searching/) for details. @@ -250,6 +252,7 @@ Providing `temp_resource_group_name` or `location` in combination with type* - the target must be a *Managed Image*. + "shared_image_gallery": { "subscription": "00000000-0000-0000-0000-00000000000", "resource_group": "ResourceGroup", From 08e8b1850e3dea7b32ea0beb597ef2d5dbf0e9c5 Mon Sep 17 00:00:00 2001 From: Adrien Delorme Date: Mon, 12 Nov 2018 10:49:39 +0100 Subject: [PATCH 2/4] remove unnecessary parenthesis --- builder/azure/arm/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index 4cc15ebe9..822f7818d 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -514,7 +514,7 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { if isUseDeviceLogin(c) { c.useDeviceLogin = true } else { - if (c.ClientID == "" && c.ClientSecret != "") || (c.ClientID != "" && c.ClientSecret == "") { + if c.ClientID == "" && c.ClientSecret != "" || c.ClientID != "" && c.ClientSecret == "" { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A client_id and client_secret must be specified together or not specified at all")) } From 1958ef6e8148fc1dd8d85a83698486435d587a81 Mon Sep 17 00:00:00 2001 From: Adrien Delorme Date: Mon, 12 Nov 2018 11:32:49 +0100 Subject: [PATCH 3/4] remove unecessary check --- builder/azure/arm/config.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index 822f7818d..94b385c3c 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -522,10 +522,6 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) { errs = packer.MultiErrorAppend(errs, fmt.Errorf("A subscription_id must be specified when client_id & client_secret are")) } - if c.SubscriptionID != "" && c.ClientID == "" { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("A subscription_id cannot be specified as it will be taken from MSI"+ - " (client_id and client_secret are not set)")) - } } ///////////////////////////////////////////// From 175b6a7971c7b984215cb332fbce3ada5c7fed2b Mon Sep 17 00:00:00 2001 From: Adrien Delorme Date: Mon, 12 Nov 2018 11:39:57 +0100 Subject: [PATCH 4/4] add test for MSI configuration --- builder/azure/arm/config_test.go | 81 ++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/builder/azure/arm/config_test.go b/builder/azure/arm/config_test.go index d25329dc7..d9b79eabe 100644 --- a/builder/azure/arm/config_test.go +++ b/builder/azure/arm/config_test.go @@ -124,6 +124,87 @@ func TestConfigShouldNotDefaultImageVersionIfCustomImage(t *testing.T) { } } +func Test_newConfig_MSI(t *testing.T) { + baseConfig := map[string]string{ + "capture_name_prefix": "ignore", + "capture_container_name": "ignore", + "location": "ignore", + "image_url": "ignore", + "storage_account": "ignore", + "resource_group_name": "ignore", + "os_type": constants.Target_Linux, + } + + tests := []struct { + name string + args []interface{} + wantErr bool + }{ + { + name: "no client_id and no client_secret should enable MSI auth", + args: []interface{}{ + baseConfig, + getPackerConfiguration(), + }, + wantErr: false, + }, + { + name: "subscription_id is will be taken from MSI", + args: []interface{}{ + baseConfig, + map[string]string{ + "subscription_id": "error", + }, + getPackerConfiguration(), + }, + wantErr: false, + }, + { + name: "client_id without client_secret should error", + args: []interface{}{ + baseConfig, + map[string]string{ + "client_id": "error", + }, + getPackerConfiguration(), + }, + wantErr: true, + }, + { + name: "client_secret without client_id should error", + args: []interface{}{ + baseConfig, + map[string]string{ + "client_secret": "error", + }, + getPackerConfiguration(), + }, + wantErr: true, + }, + { + name: "missing subscription_id", + args: []interface{}{ + baseConfig, + map[string]string{ + "client_id": "ok", + "client_secret": "ok", + }, + getPackerConfiguration(), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, err := newConfig(tt.args...) + if (err != nil) != tt.wantErr { + t.Errorf("newConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + func TestConfigShouldNormalizeOSTypeCase(t *testing.T) { config := map[string]string{ "capture_name_prefix": "ignore",