Automated Service Principal Password Rotation
Tags: terraform, azure, github, devops
Published: 2021-11-04 19:53:26

## Assumptions
* Knowledge of Terraform
* Knowledge of using Terraform with Azure
* Knowledge of GitHub Actions
* Knowledge of Bash Scripting

## Setup
This code block holds all the Azure resources built out by Terraform. The resource group and key vault are not required. It's only to show the storage of the service principal password and to show that when a terraform apply -replace is done that whatever is consuming the output of that resource is also updated. After the resource group and key vault are two service principals examples for this example.
```
terraform {
  backend "azurerm" {
    subscription_id      = "xxx-xxx-xxx-xxx"
    resource_group_name  = "tfstate"
    storage_account_name = "dronetfstate"
    container_name       = "tfstate"
    key                  = "sp_rotate.tfstate"
  }
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "2.83.0"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "2.8.0"
    }
  }
}

provider "azurerm" {
  features {}
}

provider "azuread" {
}

resource "azurerm_resource_group" "example" {
  name     = "example"
  location = "Eastus2"
}

data "azurerm_client_config" "current" {}

resource "azurerm_key_vault" "example" {
  name                        = "droneexamplekeyvault"
  location                    = azurerm_resource_group.example.location
  resource_group_name         = azurerm_resource_group.example.name
  enabled_for_disk_encryption = true
  tenant_id                   = data.azurerm_client_config.current.tenant_id
  soft_delete_retention_days  = 7
  purge_protection_enabled    = false

  sku_name = "standard"

  access_policy {
    tenant_id = data.azurerm_client_config.current.tenant_id
    object_id = data.azurerm_client_config.current.object_id

    secret_permissions = [
      "Get",
      "List",
      "Set"
    ]
  }
}

data "azuread_client_config" "current" {}

resource "azuread_application" "example1" {
  display_name = "example1"
  owners       = [data.azuread_client_config.current.object_id]
}

resource "azuread_service_principal" "example1" {
  application_id               = azuread_application.example1.application_id
  app_role_assignment_required = false
  owners                       = [data.azuread_client_config.current.object_id]
}

resource "azuread_service_principal_password" "example1" {
  service_principal_id = azuread_service_principal.example1.object_id
}

resource "azurerm_key_vault_secret" "example1" {
  name         = "example1"
  value        = azuread_service_principal_password.example1.value
  key_vault_id = azurerm_key_vault.example.id
}

resource "azuread_application" "example2" {
  display_name = "example2"
  owners       = [data.azuread_client_config.current.object_id]
}

resource "azuread_service_principal" "example2" {
  application_id               = azuread_application.example2.application_id
  app_role_assignment_required = false
  owners                       = [data.azuread_client_config.current.object_id]
}

resource "azuread_service_principal_password" "example2" {
  service_principal_id = azuread_service_principal.example2.object_id
}

resource "azurerm_key_vault_secret" "example2" {
  name         = "example2"
  value        = azuread_service_principal_password.example2.value
  key_vault_id = azurerm_key_vault.example.id
}
```

When having service principals created its good security practice to rotate those passwords on a regular basis. If you are using GitHub Actions to already apply/execute your resource changes, then adding a workflow to then rotate the password by doing forcing a replacement of the password resource causes a new password to be generated.

## Rotation Workflow
```
name: Rotate Secrets

on:
  schedule:
    - cron: '0 1 1 12/3 *' # run every 4 months.
  workflow_dispatch:

env:
  ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
  ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
  ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
  ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
  TF_IN_AUTOMATION: true

jobs:
  replace:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: 1.0.8
      - name: Terraform Init
        run: terraform init -input=false -no-color
        # Pull all the resources from the state file put them in a file for the next step
      - name: Terraform List State Resources
        run: terraform state list > stateList
        # We are going to loop through each line in the file of the resources
        # We only want to replace the service_principal_password resource, so we
        # need to check the start of each resource starts with the correct address.
      - name: Terraform Replace
        run: while read target; do if [[ "${target:0:34}" == "azuread_service_principal_password" ]]; then terraform apply -replace="$target" -input=false -no-color -auto-approve; fi; done < stateList
```
The last two steps are where the work is really done. First we want to create a temporary file to store all the resources within the state file. Echoing out the created file

```
data.azuread_client_config.current
data.azurerm_client_config.current
azuread_application.example1
azuread_application.example2
azuread_service_principal.example1
azuread_service_principal.example2
azuread_service_principal_password.example1
azuread_service_principal_password.example2
azurerm_key_vault.example
azurerm_key_vault_secret.example1
azurerm_key_vault_secret.example2
azurerm_resource_group.example
```

Then the following step we are iterating over all those resources and looking for the ones with the block label of `azuread_service_principal_password`. When we find those block labels we then want to pass the entire address to Terraform to force  a replacement of those resources.

```
while read target; do 
	if [[ "${target:0:34}" == "azuread_service_principal_password" ]]; then 
		terraform apply -replace="$target" -input=false -no-color -auto-approve
	fi
done < stateList
```