Automatic database password rotation (e.g., every X days) is an important security practice that reduces the risk of credential compromise. This article will explore implementing password rotation using Terraform with Azure Database for PostgreSQL Flexible Server.
Terraform uses providers to interact with cloud platforms, APIs, and services. In this example, we use three providers:
azurerm(Azure Resource Manager Provider): Manages resources in Microsoft Azure, including PostgreSQL Flexible Server. Referencerandom(Random Provider): Generates random values, such as passwords. Referencetime(Time Provider): Provides time-based resources like rotation triggers. Reference
Terraform does not natively support periodic operations, but we can use the time provider to automate resource updates at specific intervals. In this example, we:
- Use
time_rotatingto control the password rotation frequency. - Generate a new login and password
using random_password. - Pass the updated credentials to
azurerm_postgresql_flexible_server.
provider "azurerm" {
features {}
}
provider "random" {}
provider "time" {}** the azurerm provider is used to create a resource that utilizes a login and password. It can be replaced with other providers like aws, google, or kubernetes, depending on the infrastructure.
Terraform uses the time_rotating resource to trigger automatic password regeneration daily. This ensures that credentials remain fresh and reduces security risks. The random_password resource generates new login and password values each time the time_rotating resource updates.
- The
rotation_days = 1setting ensures that passwords are regenerated every 24 hours. - The
random_password.postgresql_flexible_server_loginresource generates a 32-character login without special characters. - The
random_password.postgresql_flexible_server_passwordresource generates a 32-character password, including special characters for enhanced security. - The
keepersargument ties the generated values to thetime_rotatingresource, ensuring they are refreshed whenever the rotation trigger updates.
resource "time_rotating" "postgresql_flexible_server_login_password_rotating" {
rotation_days = 1
}
resource "random_password" "postgresql_flexible_server_login" {
length = 32
special = false
keepers = {
rotation_time = time_rotating.postgresql_flexible_server_login_password_rotating.id
}
}
resource "random_password" "postgresql_flexible_server_password" {
length = 32
special = true
override_special = "!#*()-_+[]{}<>"
keepers = {
rotation_time = time_rotating.postgresql_flexible_server_login_password_rotating.id
}
}Note: The current Terraform configuration generates a 32-character password with special characters (longer is better). This is a recommended and safe approach.
Note: Do not use the generated database admin password in connection strings. If an application or connection string needs a password, a new login with limited privileges should be created instead of using the admin password.
Once the login and password are generated using random_password, they need to be assigned to the database administrator account. This ensures that every time the credentials are rotated, the database uses the newly generated values.
- The
azurerm_resource_groupresource creates a resource group in Azure to hold the database server. - The
azurerm_postgresql_flexible_serverresource provisions a PostgreSQL Flexible Server instance. - The
administrator_loginvalue must start with a letter, so we prepend anlto the randomly generated login value to meet PostgreSQL requirements. - The
administrator_passwordis assigned fromrandom_password.postgresql_flexible_server_password.result. - The
lifecycleblock is configured to ignore certain changes, preventing unnecessary redeployment of the database instance due to zone or high availability settings.
With this setup, the database will always have an up-to-date administrator login and password, ensuring security and compliance with rotation policies.
resource "azurerm_resource_group" "default" {
name = "password-rotation-in-terraform-rg"
location = "West Europe"
}
resource "azurerm_postgresql_flexible_server" "default" {
name = "password-rotation-in-terraform-pgsql"
resource_group_name = azurerm_resource_group.default.name
location = azurerm_resource_group.default.location
version = "16"
administrator_login = "l${random_password.postgresql_flexible_server_login.result}"
administrator_password = random_password.postgresql_flexible_server_password.result
sku_name = "B_Standard_B1ms"
lifecycle {
ignore_changes = [
zone, high_availability.0.standby_availability_zone
]
}
}Before running Terraform, ensure that you have the necessary permissions and environment variables configured for your cloud provider. The following commands will initialize Terraform, generate an execution plan, and apply the changes to provision the infrastructure.
This downloads the required provider plugins and sets up the backend for state management
terraform initexpected output:
> Initializing the backend...
> Initializing provider plugins...
> - Finding latest version of hashicorp/azurerm...
> - Finding latest version of hashicorp/time...
> - Finding latest version of hashicorp/random...
> ...
> Terraform has been successfully initialized!This previews the changes that Terraform will apply
terraform plan -out=plan.tfplanexpected output:
> Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
> symbols:
> + create
>
> Terraform will perform the following actions:
>
> # azurerm_postgresql_flexible_server.default will be created
> + resource "azurerm_postgresql_flexible_server" "default" {
> ...
> Plan: 5 to add, 0 to change, 0 to destroy.
> Saved the plan to: plan.tfplanThis step applies the Terraform execution plan and provisions the necessary infrastructure.
terraform apply "plan.tfplan"expected output:
> time_rotating.postgresql_flexible_server_login_password_rotating: Creating...
> time_rotating.postgresql_flexible_server_login_password_rotating: Creation complete after 0s [id=2025-03-09T10:35:03Z]
> random_password.postgresql_flexible_server_login: Creating...
> random_password.postgresql_flexible_server_password: Creating...
> random_password.postgresql_flexible_server_login: Creation complete after 0s [id=none]
> random_password.postgresql_flexible_server_password: Creation complete after 0s [id=none]
> ...
> Apply complete! Resources: 5 added, 0 changed, 0 destroyed.After the password rotation period expires, Terraform will automatically replace the password. This process ensures that credentials remain fresh and secure.
Terraform recognizes changes in the time_rotating resource and marks dependent resources (such as random_password and azurerm_postgresql_flexible_server) for recreation or update.
terraform plan -out=plan.tfplanexpected output:
> time_rotating.postgresql_flexible_server_login_password_rotating: Refreshing state... [id=2025-03-09T10:57:27Z]
> random_password.postgresql_flexible_server_login: Refreshing state... [id=none]
> random_password.postgresql_flexible_server_password: Refreshing state... [id=none]
> ...
> Note: Objects have changed outside of Terraform
> ...
> Terraform detected the following changes made outside of Terraform since the last "terraform apply" which may have affected this plan:
>
> # time_rotating.postgresql_flexible_server_login_password_rotating has been deleted
> - resource "time_rotating" "postgresql_flexible_server_login_password_rotating" {
> - id = "2025-03-09T11:14:52Z" -> null
> # (10 unchanged attributes hidden)
> }
> ...
> Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
> + create
> -/+ destroy and then create replacement
>
> Terraform will perform the following actions:
>
> # azurerm_postgresql_flexible_server.default will be created
> + resource "azurerm_postgresql_flexible_server" "default" {
> + administrator_login = (known after apply)
> + administrator_password = (sensitive value)
> ...
> }
>
> # random_password.postgresql_flexible_server_login must be replaced
> -/+ resource "random_password" "postgresql_flexible_server_login" {
> ~ bcrypt_hash = (sensitive value)
> ~ id = "none" -> (known after apply)
> ~ keepers = { # forces replacement
> ~ "rotation_time" = "2025-03-09T11:14:52Z" -> (known after apply)
> }
> ~ result = (sensitive value)
> # (10 unchanged attributes hidden)
> }
>
> # random_password.postgresql_flexible_server_password must be replaced
> -/+ resource "random_password" "postgresql_flexible_server_password" {
> ~ bcrypt_hash = (sensitive value)
> ~ id = "none" -> (known after apply)
> ~ keepers = { # forces replacement
> ~ "rotation_time" = "2025-03-09T11:14:52Z" -> (known after apply)
> }
> ~ result = (sensitive value)
> # (11 unchanged attributes hidden)
> }
>
> # time_rotating.postgresql_flexible_server_login_password_rotating will be created
> + resource "time_rotating" "postgresql_flexible_server_login_password_rotating" {
> + day = (known after apply)
> + hour = (known after apply)
> + id = (known after apply)
> + minute = (known after apply)
> + month = (known after apply)
> + rfc3339 = (known after apply)
> + rotation_minutes = 1
> + rotation_rfc3339 = (known after apply)
> + second = (known after apply)
> + unix = (known after apply)
> + year = (known after apply)
> }
>
> Plan: 4 to add, 0 to change, 2 to destroy.Terraform destroys the outdated credentials and provisions new ones.
terraform apply "plan.tfplan"expected output:
> random_password.postgresql_flexible_server_login: Destroying... [id=none]
> random_password.postgresql_flexible_server_password: Destroying... [id=none]
> random_password.postgresql_flexible_server_login: Destruction complete after 0s
> random_password.postgresql_flexible_server_password: Destruction complete after 0s
> time_rotating.postgresql_flexible_server_login_password_rotating: Creating...
> ...
> Apply complete! Resources: 4 added, 0 changed, 2 destroyed.This process ensures that the database credentials are automatically rotated without manual intervention.
This process can be automated using CI/CD pipelines such as GitHub Actions, which can periodically trigger Terraform execution. While automation can be implemented using CI/CD pipelines such as GitHub Actions, this article focuses solely on manual execution. Additionally, aspects like backend configuration, secret management, and infrastructure state handling can further enhance the solution but are not covered here.
After testing, ensure all resources are deleted to free up cloud capacity and avoid unnecessary costs.
terraform plan -out=destroy.tfplan -destroyexpected output:
> time_rotating.postgresql_flexible_server_login_password_rotating: Refreshing state... [id=2025-03-09T11:26:57Z]
> random_password.postgresql_flexible_server_login: Refreshing state... [id=none]
> random_password.postgresql_flexible_server_password: Refreshing state... [id=none]
> ...
> Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
> - destroy
> ...
> Plan: 0 to add, 0 to change, 4 to destroy.terraform apply "destroy.tfplan"expected output:
> azurerm_postgresql_flexible_server.default: Destroying... [id=/subscriptions/.../resourceGroups/password-rotation-in-terraform-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/password-rotation-in-terraform-pgsql]
> azurerm_postgresql_flexible_server.default: Still destroying... [id=/subscriptions/.../password-rotation-in-terraform-pgsql, 10s elapsed]
> azurerm_postgresql_flexible_server.default: Destruction complete after 13s
> azurerm_resource_group.default: Destroying... [id=/subscriptions/.../resourceGroups/password-rotation-in-terraform-rg]
> ...
> Apply complete! Resources: 0 added, 0 changed, 4 destroyed.This command ensures that all resources created by Terraform are properly deleted, freeing up cloud resources and preventing unnecessary costs.
