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
Top comments (0)