Skip to main content

5 posts tagged with "terraform"

View All Tags

How to rotate your AZURE_CREDENTIALS in GitHub with Terraform

路 3 min read

If you're deploying your Azure infrastructure from GitHub, you'll need Azure service principal credentials stored as secret variables. You may also want to rotate those credentials. This entry describes how you can use Terraform in your GitHub actions to configure and rotate your Azure service principal credentials.

In a previous post, I describe how to safely rotate credentials using Terraform. This post builds on that by showing you how to create client secrets for your Azure service principals and store them as secrets in GitHub.

First off, we'll create a Terraform module that will manage the secret rotation in Azure and GitHub. This stores everything you need to be able to deploy from GitHub to Azure, including information about the credentials, service principal, tenant and subscription.

It stores them both as an AZURE_CREDENTIALS json block that matches the output of az ad sp create-for-rbac --sdk-auth which is useful for the Azure Login GitHub Action under the variable AZURE_CREDENTIALS as well as broken out into ARM_CLIENT_ID, ARM_TENANT_ID, ARM_SUBSCRIPTION_ID and ARM_CLIENT_SECRET which is useful for running Terraform in GitHub Actions.

The format of the sdk-auth that the Azure Login GitHub Action requires is as follows:

{  "clientId": "APPLICATION_ID",  "clientSecret": "CLIENT_SECRET",  "subscriptionId": "SUBSCRIPTION_ID",  "tenantId": "TENANT_ID",  "activeDirectoryEndpointUrl": "https://login.microsoftonline.com",  "resourceManagerEndpointUrl": "https://management.azure.com/",  "activeDirectoryGraphResourceId": "https://graph.windows.net/",  "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",  "galleryEndpointUrl": "https://gallery.azure.com/",  "managementEndpointUrl": "https://management.core.windows.net/"}

The following files will create a Terraform module that will allow you to safely rotate secrets in Azure and store the currently active secret in GitHub secrets for use in GitHub Actions:

provider.tf

provider "azurerm" {  features {}}

variables.tf

variable "subscription_id" { type = string }variable "tenant_id" { type = string }variable "repository" { type = string }variable "application_id" { type = string }variable "application_object_id" { type = string }variable "date" { type = string }

main.tf

locals {  date        = tonumber(var.date)  odd_keeper  = floor((local.date + 1) / 2)  even_keeper = floor(local.date / 2)  use_even    = local.date % 2 == 0}
resource "random_uuid" "odd" {}
resource "azuread_application_password" "odd" {  application_object_id = var.application_object_id  description           = "odd"  value                 = random_password.odd.result  end_date_relative     = "1440h"  key_id                = random_uuid.odd.result}
resource "random_password" "odd" {  keepers = {    "date" = local.odd_keeper  }  length  = 64}
resource "random_uuid" "even" {}
resource "azuread_application_password" "even" {  application_object_id = var.application_object_id  description           = "even"  value                 = random_password.even.result  end_date_relative     = "1440h"  key_id                = random_uuid.even.result}
resource "random_password" "even" {  keepers = {    "date" = local.even_keeper  }  length  = 64}
resource "github_actions_secret" "terraform" {  repository      = var.repository  secret_name     = "AZURE_CREDENTIALS"  plaintext_value = <<-EOT{  "clientId": "${var.application_id}",  "clientSecret": "${local.use_even ? random_password.even.result : random_password.odd.result}",  "subscriptionId": "${var.subscription_id}",  "tenantId": "${var.tenant_id}",  "activeDirectoryEndpointUrl": "https://login.microsoftonline.com",  "resourceManagerEndpointUrl": "https://management.azure.com/",  "activeDirectoryGraphResourceId": "https://graph.windows.net/",  "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",  "galleryEndpointUrl": "https://gallery.azure.com/",  "managementEndpointUrl": "https://management.core.windows.net/"}EOT}
resource "github_actions_secret" "arm_client_id" {  repository = var.repository  secret_name = "ARM_CLIENT_ID"  plaintext_value = var.application_id}
resource "github_actions_secret" "arm_client_secret" {  repository = var.repository  secret_name = "ARM_CLIENT_SECRET"  plaintext_value = local.use_even ? random_password.even.result : random_password.odd.result}
resource "github_actions_secret" "arm_subscription_id" {  repository = var.repository  secret_name = "ARM_SUBSCRIPTION_ID"  plaintext_value = var.subscription_id}
resource "github_actions_secret" "arm_tenant_id" {  repository = var.repository  secret_name = "ARM_TENANT_ID"  plaintext_value = var.tenant_id}

Using the module#

module "example_github_azure" {  source                = "LOCATION_OF_MODULE"  subscription_id       = AZURE_SUBSCRIPTION_ID  tenant_id             = AZURE_TENANT_ID  repository            = GITHUB_REPO_NAME  application_id        = AZURE_APPLICATION_ID  application_object_id = AZURE_APPLICATION_OBJECT_ID  date                  = var.date}

To see it in action, have a look at this repository https://github.com/jamiemccrindle/azure-rotate-service-principal-github-secrets

Install a specific version of Terraform in a Github Action using apt

路 One min read

The standard way to install Terraform on ubuntu is via apt but the instructions typically don't show how to select a specific version. Most examples of how to install a specific version of terraform in a GitHub action just pull the binary directly.

This is how you can install a specific version of Terraform for use in a GitHub action using apt:

name: install-terraform-example
on:  push:    branches:      - main
jobs:
  install-terraform:    runs-on: ubuntu-latest    steps:    - run: |        curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -        sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"        sudo apt-get update        sudo apt-get install terraform=0.14.10

Microsoft Graph API Terraform Data Source

路 2 min read

Most Terraform examples that reference permissions from the Microsoft Graph use the GUIDs for the permissions. This makes it harder to write the Terraform config, as you have to look up the GUIDs for each permission. It also makes it harder to do code reviews where reviewers typically just believe the comment that describes what permission the GUID represents.

This is what the code for the required_resource_access for an azure ad application for k8s looks like using just the GUIDs (this is from a real example online):

required_resource_access {  resource_app_id = "00000003-0000-0000-c000-000000000000"  resource_access {    id   = "7ab1d382-f21e-4acd-a863-ba3e13f7da61"    type = "Role"  }  resource_access {    id   = "06da0dbc-49e2-44d2-8312-53f166ab848a"    type = "Scope"  }  resource_access {    id   = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"    type = "Scope"  }}

Your AD tenant should have enterprise applications (service principals) for the various Microsoft services including the Microsoft Graph.

Microsoft Graph Enterprise Application

You can look this up as a data source using Terraform as follows:

data "azuread_service_principal" "graph" {    # graph api application id    application_id = "00000003-0000-0000-c000-000000000000"}

This will return a data source that has all of the oauth2 permissions and app roles for the Microsoft Graph. They can be awkward to work with, so I'll usually create a new object that maps the permission name to the permission id e.g.:

locals {    graph = {        application_id = data.azuread_service_principal.graph.application_id        app_roles = {for app_role in data.azuread_service_principal.graph.app_roles : app_role.value => app_role.id}        oauth2_permissions = {for oauth2_permission in data.azuread_service_principal.graph.oauth2_permissions : oauth2_permission.value => oauth2_permission.id}    }}

Using this, the example above would look like:

required_resource_access {  resource_app_id = local.graph.application_id  resource_access {    id   = local.graph.app_roles["Directory.Read.All"]    type = "Role"  }  resource_access {    id   = local.graph.oauth2_permissions["Directory.Read.All"]    type = "Scope"  }  resource_access {    id   = local.graph.oauth2_permissions["User.Read"]    type = "Scope"  }}
tip

Note: this works for more than just the Microsoft Graph, you can use the same technique to look up app roles and permissions from any of your enterprise applications.

Bootstrap terraform state in azure

路 2 min read

It's a good idea to store your Terraform state in a remote backend because:

  • Terraform state often contains sensitive information e.g. credentials, access tokens etc. so using a backend that has access control and encrypts the state at rest and in transit will keep it safer.
  • Your Terraform state can be backed up
  • You can retrieve your state when running scripts in CI / CD pipelines.

If you're using Terraform to configure Azure resources, you'll probably want to use the azurerm terraform backend.

This stores your state in an Azure Storage Account. The following Terraform will create a storage account that can be used to store your Terraform state in Azure:

main.tf

resource "azurerm_resource_group" "terraform_state" {  name     = var.resource_group_name  location = var.location}
resource "azurerm_storage_account" "terraform_state" {  name                     = var.storage_account_name  resource_group_name      = azurerm_resource_group.terraform_state.name  location                 = azurerm_resource_group.terraform_state.location  account_tier             = "Standard"  account_replication_type = "GRS"  min_tls_version          = "TLS1_2"}
resource "azurerm_storage_container" "terraform_state" {  name                  = var.container_name  storage_account_name  = azurerm_storage_account.terraform_state.name  container_access_type = "private"}
variable "resource_group_name" { type = string }variable "storage_account_name" { type = string }variable "container_name" { type = string }variable "location" { type = string }

To run it, you'll need to supply a file with the following variables set:

bootstrap.tfvars

# the name of the resource group# e.g. "rg-mytfstate-shared-001"resource_group_name = ""
# the name of the storage account# e.g. "sttfstate001"storage_account_name = ""
# the name of the container# e.g. "tfstate"container_name = "tfstate"
# the location of the resouce group # and storage account e.g. "West Europe"location = "West Europe"

To apply the terraform run the following

# log into azure, this assumes you have sufficient # privileges to create resource groupsaz login
# initialise terraformterraform init
# show a planterraform plan -var-file=bootstrap.tfvars
# apply the terraformterraform apply -var-file=bootstrap.tfvars

Safely rotate secrets with terraform

路 2 min read

Rotating secrets is a Good Thing鈩 as it limits the length of a time a compromised set of credentials can be used for.

It's quite difficult to make secret rotation atomic i.e. changing a secret in your identity provider at exactly the same time you change the secret in the system that uses it for authentication. Mismatches between the values will result in authentication failures.

The ideal is where the identity provider supports multiple valid secrets for a single identity e.g. Azure Service Principals. If that's the case, you can have 2 secrets active at the same time and rotate them on offset time periods e.g.:

password rotation

info

This example uses secrets that expire after 60 days and rotates them each month. The mechanism supports rotating secrets more frequently so pick an expiry window that meets your risk appetite. The limiting factor is often when infrastructure needs to be redeployed after a secret is rotated.

In the terraform below, I've set up 2 passwords, one called even and one called odd.

The odd password rotates at the beginning of the odd months and is the current password for those months i.e.:

  • January, March, May, July, September, November

And the even password rotates at the beginning of the even months and is the current password for those months i.e.:

  • February, April, June, August, October, December

And now for the terraform:

variable "date" {    type = string}
locals {  date        = tonumber(var.date)  odd_keeper  = floor((local.date + 1) / 2)  even_keeper = floor(local.date / 2)  use_even    = local.date % 2 == 0}
resource "random_password" "odd" {  keepers = {    "date" = local.odd_keeper  }  length           = 64  special          = true}
resource "random_password" "even" {  keepers = {    "date" = local.even_keeper  }  length           = 64  special          = true}
# this example just outputs the password# but you'd typically use this as a property of# the system(s) that need the password.output "current_secret" {    value = local.use_even                 ? random_password.even.result                 : random_password.odd.result}

For this to work, you need to supply a date variable when you call terraform that contains the current year and month e.g.:

terraform apply -auto-approve -var="date=`date +%Y%m`"
caution

Terraform will store these secrets in terraform state, so make sure you're using a backend that is appropriately secured.