Skip to content

Commit

Permalink
Support for client_certificate and use_cli provider properties. T…
Browse files Browse the repository at this point in the history
…weaks to auth guides.
  • Loading branch information
manicminer committed Feb 7, 2023
1 parent 52efcae commit 548cec8
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 49 deletions.
33 changes: 30 additions & 3 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package provider

import (
"context"
"encoding/base64"
"fmt"
"log"
"os"
Expand Down Expand Up @@ -206,6 +207,13 @@ func azureProvider(supportLegacyTestSuite bool) *schema.Provider {
},

// Client Certificate specific fields
"client_certificate": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_CERTIFICATE", ""),
Description: "Base64 encoded PKCS#12 certificate bundle to use when authenticating as a Service Principal using a Client Certificate",
},

"client_certificate_path": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -349,6 +357,15 @@ func providerConfigure(p *schema.Provider) schema.ConfigureContextFunc {
return nil, diag.Errorf("the provider only supports 3 auxiliary tenant IDs")
}

var clientCertificateData []byte
if encodedCert := d.Get("client_certificate").(string); encodedCert != "" {
var err error
clientCertificateData, err = decodeCertificate(encodedCert)
if err != nil {
return nil, diag.FromErr(err)
}
}

var (
env *environments.Environment
err error
Expand All @@ -371,9 +388,6 @@ func providerConfigure(p *schema.Provider) schema.ConfigureContextFunc {
enableOidc = d.Get("use_oidc").(bool)
)

var clientCertificateData []byte
// TODO: add `client_certificate` provider prop and base64 decode the value

authConfig := auth.Credentials{
Environment: *env,
ClientID: d.Get("client_id").(string),
Expand Down Expand Up @@ -453,6 +467,19 @@ func providerConfigure(p *schema.Provider) schema.ConfigureContextFunc {
}
}

func decodeCertificate(clientCertificate string) ([]byte, error) {
var pfx []byte
if clientCertificate != "" {
out := make([]byte, base64.StdEncoding.DecodedLen(len(clientCertificate)))
n, err := base64.StdEncoding.Decode(out, []byte(clientCertificate))
if err != nil {
return pfx, fmt.Errorf("could not decode client certificate data: %v", err)
}
pfx = out[:n]
}
return pfx, nil
}

const resourceProviderRegistrationErrorFmt = `Error ensuring Resource Providers are registered.
Terraform automatically attempts to register the Resource Providers it supports to
Expand Down
110 changes: 82 additions & 28 deletions website/docs/guides/service_principal_client_certificate.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,24 @@ This guide will cover how to generate a client certificate, how to create an App

## Generating a Client Certificate

Firstly we need to create a certificate which can be used for authentication. To do that we're going to generate a Certificate Signing Request (also known as a CSR) using `openssl` (this can also be achieved using PowerShell, however, that's outside the scope of this document):
Firstly we need to create a certificate which can be used for authentication. To do that we're going to generate a private key and self-signed certificate using OpenSSL or LibreSSL (this can also be achieved using PowerShell, however that's outside the scope of this document):

```shell
openssl req -newkey rsa:4096 -nodes -keyout "service-principal.key" -out "service-principal.csr"
$ openssl req -subj '/CN=myclientcertificate/O=HashiCorp, Inc./ST=CA/C=US' \
-new -newkey rsa:4096 -sha256 -days 730 -nodes -x509 -keyout client.key -out client.crt
```

-> During the generation of the certificate you'll be prompted for various bits of information required for the certificate signing request - at least one item has to be specified for this to complete.

We can now sign that Certificate Signing Request, in this example we're going to self-sign this certificate using the Key we just generated; however it's also possible to do this using a Certificate Authority. In order to do that we're again going to use `openssl`:

```shell
openssl x509 -signkey "service-principal.key" -in "service-principal.csr" -req -days 365 -out "service-principal.crt"
```

Finally we can generate a PFX file which can be used to authenticate with Azure:
Next we generate a PKCS#12 bundle (.pfx file) which can be used by the AzureRM provider to authenticate with Azure:

```shell
openssl pkcs12 -export -out "service-principal.pfx" -inkey "service-principal.key" -in "service-principal.crt"
$ openssl pkcs12 -export -password pass:"Pa55w0rd123" -out client.pfx -inkey client.key -in client.crt
```

Now that we've generated a certificate, we can create the Azure Active Directory Application.

---

### Creating the Application and Service Principal
## Creating the Application and Service Principal

We're going to create the Application in the Azure Portal - to do this navigate to [the **Azure Active Directory** overview](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Overview) within the Azure Portal - [then select the **App Registration** blade](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps/RegisteredApps/Overview). Click the **New registration** button at the top to add a new Application within Azure Active Directory. On this page, set the following values then press **Create**:

Expand Down Expand Up @@ -86,18 +79,50 @@ At this point the newly created Azure Active Directory Application should be ass

---

### Configuring the Service Principal in Terraform
## Configuring Terraform to use the Client Certificate

As we've obtained the credentials for this Service Principal - it's possible to configure them in a few different ways.
Now that we have our Client Certificate uploaded to Azure and ready to use, it's possible to configure Terraform in a few different ways.

When storing the credentials as Environment Variables, for example:
The provider can be configured to read the certificate bundle from the .pfx file in your filesystem, or alternatively you can pass a base64-encoded copy of the certificate bundle directly to the provider.

### Environment Variables

Our recommended approach is storing the credentials as Environment Variables, for example:

*Reading the certificate bundle from the filesystem*
```shell-session
# sh
$ export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
$ export ARM_CLIENT_CERTIFICATE_PATH="/path/to/my/client/certificate.pfx"
$ export ARM_CLIENT_CERTIFICATE_PASSWORD="Pa55w0rd123"
$ export ARM_TENANT_ID="10000000-0000-0000-0000-000000000000"
$ export ARM_SUBSCRIPTION_ID="20000000-0000-0000-0000-000000000000"
```
```powershell
# PowerShell
> $env:ARM_CLIENT_ID = "00000000-0000-0000-0000-000000000000"
> $env:ARM_CLIENT_CERTIFICATE_PATH = "/path/to/my/client/certificate.pfx"
> $env:ARM_CLIENT_CERTIFICATE_PASSWORD = "Pa55w0rd123"
> $env:ARM_TENANT_ID = "10000000-0000-0000-0000-000000000000"
> $env:ARM_SUBSCRIPTION_ID = "20000000-0000-0000-0000-000000000000"
```

```bash
export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export ARM_CLIENT_CERTIFICATE_PATH="/path/to/my/client/certificate.pfx"
export ARM_CLIENT_CERTIFICATE_PASSWORD="Pa55w0rd123"
export ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
export ARM_TENANT_ID="00000000-0000-0000-0000-000000000000"
*Passing the encoded certificate bundle directly*
```shell-session
# sh
$ export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
$ export ARM_CLIENT_CERTIFICATE="$(base64 /path/to/my/client/certificate.pfx)"
$ export ARM_CLIENT_CERTIFICATE_PASSWORD="Pa55w0rd123"
$ export ARM_TENANT_ID="10000000-0000-0000-0000-000000000000"
$ export ARM_SUBSCRIPTION_ID="20000000-0000-0000-0000-000000000000"
```
```powershell
# PowerShell
> $env:ARM_CLIENT_ID = "00000000-0000-0000-0000-000000000000"
> $env:ARM_CLIENT_CERTIFICATE = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes("/path/to/my/client/certificate.pfx"))
> $env:ARM_CLIENT_CERTIFICATE_PASSWORD = "Pa55w0rd123"
> $env:ARM_TENANT_ID = "10000000-0000-0000-0000-000000000000"
> $env:ARM_SUBSCRIPTION_ID = "20000000-0000-0000-0000-000000000000"
```

The following Terraform and Provider blocks can be specified - where `3.0.0` is the version of the Azure Provider that you'd like to use:
Expand All @@ -124,14 +149,15 @@ More information on [the fields supported in the Provider block can be found her

At this point running either `terraform plan` or `terraform apply` should allow Terraform to run using the Service Principal to authenticate.

---
### Provider Block

It's also possible to configure these variables either in-line or from using variables in Terraform (as the `client_certificate_path` and `client_certificate_password` are in this example), like so:
It's also possible to configure these variables either directly, or from variables, in your provider block, like so:

~> **NOTE:** We'd recommend not defining these variables in-line since they could easily be checked into Source Control.
~> **Caution** We recommend not defining these variables in-line since they could easily be checked into Source Control.

*Reading the certificate bundle from the filesystem*
```hcl
variable "client_certificate_path" {}
variable "client_certificate" {}
variable "client_certificate_password" {}
# We strongly recommend using the required_providers block to set the
Expand All @@ -149,11 +175,39 @@ terraform {
provider "azurerm" {
features {}
subscription_id = "00000000-0000-0000-0000-000000000000"
client_id = "00000000-0000-0000-0000-000000000000"
client_certificate_path = var.client_certificate_path
client_certificate_password = var.client_certificate_password
tenant_id = "00000000-0000-0000-0000-000000000000"
tenant_id = "10000000-0000-0000-0000-000000000000"
subscription_id = "20000000-0000-0000-0000-000000000000"
}
```

*Passing the encoded certificate bundle directly*
```hcl
variable "client_certificate" {}
variable "client_certificate_password" {}
# We strongly recommend using the required_providers block to set the
# Azure Provider source and version being used
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=3.0.0"
}
}
}
# Configure the Microsoft Azure Provider
provider "azurerm" {
features {}
client_id = "00000000-0000-0000-0000-000000000000"
client_certificate = var.client_certificate
client_certificate_password = var.client_certificate_password
tenant_id = "10000000-0000-0000-0000-000000000000"
subscription_id = "20000000-0000-0000-0000-000000000000"
}
```

Expand Down
35 changes: 21 additions & 14 deletions website/docs/guides/service_principal_client_secret.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ It's possible to complete this task in either the [Azure CLI](#creating-a-servic

### Creating a Service Principal using the Azure CLI

~> **Note**: If you're using the **China**, **German** or **Government** Azure Clouds - you'll need to first configure the Azure CLI to work with that Cloud. You can do this by running:
~> **Note**: If you're using the **China** or **US Government** Azure Clouds - you'll need to first configure the Azure CLI to work with that Cloud. You can do this by running:

```shell
az cloud set --name AzureChinaCloud|AzureGermanCloud|AzureUSGovernment
az cloud set --name AzureChinaCloud|AzureUSGovernment
```

---
Expand All @@ -54,11 +54,11 @@ The output (similar to below) will display one or more Subscriptions - with the
[
{
"cloudName": "AzureCloud",
"id": "00000000-0000-0000-0000-000000000000",
"id": "20000000-0000-0000-0000-000000000000",
"isDefault": true,
"name": "PAYG Subscription",
"state": "Enabled",
"tenantId": "00000000-0000-0000-0000-000000000000",
"tenantId": "10000000-0000-0000-0000-000000000000",
"user": {
"name": "user@example.com",
"type": "user"
Expand All @@ -70,13 +70,13 @@ The output (similar to below) will display one or more Subscriptions - with the
Should you have more than one Subscription, you can specify the Subscription to use via the following command:

```shell
az account set --subscription="SUBSCRIPTION_ID"
az account set --subscription="20000000-0000-0000-0000-000000000000"
```

We can now create the Service Principal which will have permissions to manage resources in the specified Subscription using the following command:

```shell
az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/SUBSCRIPTION_ID"
az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/20000000-0000-0000-0000-000000000000"
```

This command will output 5 values:
Expand Down Expand Up @@ -169,13 +169,20 @@ As we've obtained the credentials for this Service Principal - it's possible to

When storing the credentials as Environment Variables, for example:

```bash
```shell-session
# sh
export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export ARM_CLIENT_SECRET="00000000-0000-0000-0000-000000000000"
export ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
export ARM_TENANT_ID="00000000-0000-0000-0000-000000000000"
export ARM_CLIENT_SECRET="12345678-0000-0000-0000-000000000000"
export ARM_TENANT_ID="10000000-0000-0000-0000-000000000000"
export ARM_SUBSCRIPTION_ID="20000000-0000-0000-0000-000000000000"
```
```powershell
# PowerShell
> $env:ARM_CLIENT_ID = "00000000-0000-0000-0000-000000000000"
> $env:ARM_CLIENT_SECRET = "12345678-0000-0000-0000-000000000000"
> $env:ARM_TENANT_ID = "10000000-0000-0000-0000-000000000000"
> $env:ARM_SUBSCRIPTION_ID = "20000000-0000-0000-0000-000000000000"
```

The following Terraform and Provider blocks can be specified - where `3.0.0` is the version of the Azure Provider that you'd like to use:

```hcl
Expand Down Expand Up @@ -204,7 +211,7 @@ At this point running either `terraform plan` or `terraform apply` should allow

It's also possible to configure these variables either in-line or from using variables in Terraform (as the `client_secret` is in this example), like so:

~> **NOTE:** We'd recommend not defining these variables in-line since they could easily be checked into Source Control.
~> **Caution** We recommend not defining these variables in-line since they could easily be checked into Source Control.

```hcl
variable "client_secret" {
Expand All @@ -225,10 +232,10 @@ terraform {
provider "azurerm" {
features {}
subscription_id = "00000000-0000-0000-0000-000000000000"
client_id = "00000000-0000-0000-0000-000000000000"
client_secret = var.client_secret
tenant_id = "00000000-0000-0000-0000-000000000000"
tenant_id = "10000000-0000-0000-0000-000000000000"
subscription_id = "20000000-0000-0000-0000-000000000000"
}
```

Expand Down
16 changes: 12 additions & 4 deletions website/docs/index.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ The following arguments are supported:

When authenticating as a Service Principal using a Client Certificate, the following fields can be set:

* `client_certificate` - (Optional) A base64-encoded PKCS#12 bundle to be used as the client certificate for authentication. This can also be sourced from the `ARM_CLIENT_CERTIFICATE` environment variable.

* `client_certificate_password` - (Optional) The password associated with the Client Certificate. This can also be sourced from the `ARM_CLIENT_CERTIFICATE_PASSWORD` Environment Variable.

* `client_certificate_path` - (Optional) The path to the Client Certificate associated with the Service Principal which should be used. This can also be sourced from the `ARM_CLIENT_CERTIFICATE_PATH` Environment Variable.
Expand Down Expand Up @@ -146,13 +148,19 @@ More information on [how to configure a Service Principal using OpenID Connect c

---

When authenticating using Managed Service Identity, the following fields can be set:
When authenticating using Managed Identity, the following fields can be set:

* `msi_endpoint` - (Optional) The path to a custom endpoint for Managed Identity - in most circumstances, this should be detected automatically. This can also be sourced from the `ARM_MSI_ENDPOINT` Environment Variable.

* `use_msi` - (Optional) Should Managed Identity be used for Authentication? This can also be sourced from the `ARM_USE_MSI` Environment Variable. Defaults to `false`.

* `msi_endpoint` - (Optional) The path to a custom endpoint for Managed Service Identity - in most circumstances, this should be detected automatically. This can also, be sourced from the `ARM_MSI_ENDPOINT` Environment Variable.
More information on [how to configure a Service Principal using Managed Identity can be found in this guide](guides/managed_service_identity.html).

---

* `use_msi` - (Optional) Should Managed Service Identity be used for Authentication? This can also be sourced from the `ARM_USE_MSI` Environment Variable. Defaults to `false`.
For Azure CLI authentication, the following fields can be set:

More information on [how to configure a Service Principal using Managed Service Identity can be found in this guide](guides/managed_service_identity.html).
* `use_cli` - (Optional) Should Azure CLI be used for authentication? This can also be sourced from the `ARM_USE_CLI` environment variable. Defaults to `true`.

---

Expand Down

0 comments on commit 548cec8

Please sign in to comment.