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)