diff --git a/.spacelift/config.yml b/.spacelift/config.yml index 8bc8d32..b94571f 100644 --- a/.spacelift/config.yml +++ b/.spacelift/config.yml @@ -1,6 +1,9 @@ version: 1 -module_version: 0.0.6 +module_version: 0.0.7 tests: - name: System-assigned identity project_root: examples/system-assigned-identity + + - name: Password authentication + project_root: examples/password-authentication diff --git a/examples/bastion/main.tf b/examples/bastion/main.tf index b173804..81d858e 100644 --- a/examples/bastion/main.tf +++ b/examples/bastion/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=2.68.0" + version = "=3.61.0" } random = { diff --git a/examples/password-authentication/README.md b/examples/password-authentication/README.md new file mode 100644 index 0000000..52b8404 --- /dev/null +++ b/examples/password-authentication/README.md @@ -0,0 +1,38 @@ +# Private Worker with a User Assigned Identity + +This example shows how to provision a Spacelift private worker using an Azure VMSS, using a +User-Assigned Managed Identity to grant the workers permission to manage your Azure subscription. +It also creates a KeyVault to store the worker pool credentials in, and uses the user-assigned +identity to grant the VMSS permission to those secrets. + +Using a user-assigned managed identity gives you more control than using a [system-assigned identity](../system-assigned-identity/README.md), +allowing you to do things like provision secrets in KeyVault and grant the correct permissions +before your scale set is created. + +**NOTES:** + +- When using a user-assigned identity, you need to pass the identity's `client_id` + to the Terraform provider via the `ARM_CLIENT_ID` environment variable when following the + [authentication instructions](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/managed_service_identity). +- Although we're using KeyVault to store the secrets, please note that the Azure RM provider + stores the secret values in plain text in your state, so the usual warnings about treating + your state as a secret apply. + +## Usage + +To run the example, create a tfvars file with the following variables specified: + +```hcl +admin_public_key = "" +worker_pool_config = "" +worker_pool_private_key = "" +worker_pool_id = "" +location = "West Europe" +``` + +Now just initialise Terraform, and run an apply: + +```shell +terraform init +terraform apply -var-file myvars.tfvars +``` diff --git a/examples/password-authentication/data.tf b/examples/password-authentication/data.tf new file mode 100644 index 0000000..d2bfbdb --- /dev/null +++ b/examples/password-authentication/data.tf @@ -0,0 +1,2 @@ +data "azurerm_client_config" "current" {} +data "azurerm_subscription" "primary" {} diff --git a/examples/password-authentication/group.tf b/examples/password-authentication/group.tf new file mode 100644 index 0000000..a0b4f15 --- /dev/null +++ b/examples/password-authentication/group.tf @@ -0,0 +1,8 @@ +resource "random_pet" "this" {} + +resource "azurerm_resource_group" "this" { + name = "rg-${var.application}-${random_pet.this.id}-${var.env}" + location = var.location + + tags = local.tags +} diff --git a/examples/password-authentication/identity.tf b/examples/password-authentication/identity.tf new file mode 100644 index 0000000..8e630e3 --- /dev/null +++ b/examples/password-authentication/identity.tf @@ -0,0 +1,14 @@ +# Create a user-assigned identity for the VMSS. This allows us to grant it permissions +# over our subscription, as well as configure its access to KeyVault secrets. +resource "azurerm_user_assigned_identity" "vmss" { + resource_group_name = azurerm_resource_group.this.name + location = azurerm_resource_group.this.location + name = "sp5ft-${var.worker_pool_id}" +} + +# Uncomment the following resource to grant the VMSS instances access to your current subscription. +# resource "azurerm_role_assignment" "vmss_contributor" { +# scope = data.azurerm_subscription.primary.id +# role_definition_name = "Contributor" +# principal_id = azurerm_user_assigned_identity.vmss.principal_id +# } diff --git a/examples/password-authentication/keyvault.tf b/examples/password-authentication/keyvault.tf new file mode 100644 index 0000000..e830327 --- /dev/null +++ b/examples/password-authentication/keyvault.tf @@ -0,0 +1,57 @@ +# KeyVault names need to be globally unique, so we'll just generate a random ID with a specified prefix. +resource "random_id" "keyvault" { + byte_length = 9 + prefix = "sp5ft" +} + +resource "azurerm_key_vault" "this" { + name = random_id.keyvault.hex + location = azurerm_resource_group.this.location + resource_group_name = azurerm_resource_group.this.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "premium" + soft_delete_retention_days = 7 + + # Allow the user running the apply access to KeyVault. You may want to configure + # policies for other users/groups in your organization. + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + key_permissions = [ + "Create", + "Get", + ] + + secret_permissions = [ + "Set", + "Get", + "List", + "Delete", + "Purge", + "Recover" + ] + } + + # Grant the VMSS identity permission to download secrets. + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_user_assigned_identity.vmss.principal_id + + secret_permissions = [ + "Get" + ] + } +} + +resource "azurerm_key_vault_secret" "worker_pool_config" { + name = "worker-pool-config" + value = base64encode(var.worker_pool_config) + key_vault_id = azurerm_key_vault.this.id +} + +resource "azurerm_key_vault_secret" "worker_pool_private_key" { + name = "worker-pool-private-key" + value = base64encode(var.worker_pool_private_key) + key_vault_id = azurerm_key_vault.this.id +} diff --git a/examples/password-authentication/main.tf b/examples/password-authentication/main.tf new file mode 100644 index 0000000..99a116b --- /dev/null +++ b/examples/password-authentication/main.tf @@ -0,0 +1,59 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=3.61.0" + } + + random = { + source = "hashicorp/random" + version = "=3.1.0" + } + } +} + +# Configure the Microsoft Azure Provider +provider "azurerm" { + features {} +} + +module "azure-worker" { + source = "../../" + + admin_username = var.admin_username + admin_password = var.admin_password + + # This custom configuration block logs into Azure, downloads the worker pool credentials + # from KeyVault, and then configures the environment variables the Spacelift worker will + # read them from. + configuration = <<-EOT + az login --identity + + echo "Downloading worker pool credentials from KeyVault" >> /var/log/spacelift/info.log + az keyvault secret download --name "${azurerm_key_vault_secret.worker_pool_config.name}" \ + --vault-name "${azurerm_key_vault.this.name}" \ + --file "/tmp/worker-pool-config" 1>>/var/log/spacelift/info.log 2>>/var/log/spacelift/error.log + + az keyvault secret download --name "${azurerm_key_vault_secret.worker_pool_private_key.name}" \ + --vault-name "${azurerm_key_vault.this.name}" \ + --file "/tmp/worker-pool-private-key" 1>>/var/log/spacelift/info.log 2>>/var/log/spacelift/error.log + + export SPACELIFT_TOKEN=$(cat /tmp/worker-pool-config | base64 --decode) + export SPACELIFT_POOL_PRIVATE_KEY=$(cat /tmp/worker-pool-private-key | base64 --decode) + + rm /tmp/worker-pool-config + rm /tmp/worker-pool-private-key + + echo "Worker pool credentials configured" >> /var/log/spacelift/info.log + EOT + + resource_group = azurerm_resource_group.this + subnet_id = azurerm_subnet.worker.id + + identity_type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.vmss.id] + + worker_pool_id = var.worker_pool_id + name_prefix = "sp5ft-user-identity" + tags = local.tags +} diff --git a/examples/password-authentication/network.tf b/examples/password-authentication/network.tf new file mode 100644 index 0000000..831f99b --- /dev/null +++ b/examples/password-authentication/network.tf @@ -0,0 +1,15 @@ +resource "azurerm_virtual_network" "this" { + name = "vnet" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.this.location + resource_group_name = azurerm_resource_group.this.name + tags = local.tags +} + +# Create a subnet for our workers +resource "azurerm_subnet" "worker" { + name = "worker" + resource_group_name = azurerm_resource_group.this.name + virtual_network_name = azurerm_virtual_network.this.name + address_prefixes = ["10.0.2.0/24"] +} diff --git a/examples/password-authentication/outputs.tf b/examples/password-authentication/outputs.tf new file mode 100644 index 0000000..36bceab --- /dev/null +++ b/examples/password-authentication/outputs.tf @@ -0,0 +1,3 @@ +output "identity_client_id" { + value = azurerm_user_assigned_identity.vmss.client_id +} diff --git a/examples/password-authentication/variables.tf b/examples/password-authentication/variables.tf new file mode 100644 index 0000000..91a2a78 --- /dev/null +++ b/examples/password-authentication/variables.tf @@ -0,0 +1,43 @@ +locals { + tags = { + application = var.application + env = var.env + region = var.location + } +} + +variable "admin_username" { + type = string + default = "spacelift" +} + +variable "application" { + type = string + default = "sp5ft-user-identity" +} + +variable "admin_password" { + type = string +} + +variable "env" { + type = string + default = "test" +} + +variable "location" { + type = string + default = "westeurope" +} + +variable "worker_pool_config" { + type = string +} + +variable "worker_pool_private_key" { + type = string +} + +variable "worker_pool_id" { + type = string +} diff --git a/examples/system-assigned-identity/main.tf b/examples/system-assigned-identity/main.tf index 958d649..224c59a 100644 --- a/examples/system-assigned-identity/main.tf +++ b/examples/system-assigned-identity/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=2.68.0" + version = "=3.61.0" } random = { diff --git a/examples/user-assigned-identity/main.tf b/examples/user-assigned-identity/main.tf index 6918188..accff7b 100644 --- a/examples/user-assigned-identity/main.tf +++ b/examples/user-assigned-identity/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=2.68.0" + version = "=3.61.0" } random = {