Introduction
In the world of modern infrastructure management, secrets handling and rotation remains a crucial yet challenging aspect of maintaining secure systems. Password rotation is a security best practice often neglected due to manual effort or complexity. Many organizations struggle to implement robust rotation policies without disrupting services or creating operational overhead. Fortunately, Terraform enables a secure and fully automated approach to secret rotation, which we’ll explore here.
The solution
Consider this common challenge: you need to create a principal with an automatically provisioned password that rotates periodically. Here’s an elegant solution to this challenge:
# main.tf
locals {
users = {
"user1" = {
name = "user1"
password_rotation = true
}
}
}
resource "time_rotating" "this" {
for_each = local.users
rotation_minutes = var.rotation_minutes
}
resource "random_password" "this" {
for_each = local.users
length = var.password_length
override_special = var.password_override_special
min_upper = var.password_min_upper
min_numeric = var.password_min_numeric
min_lower = var.password_min_lower
min_special = var.password_min_special
keepers = {
rotation_trigger = lookup(local.users[each.key], "password_rotation", true) ? time_rotating.this[each.key].id : null
}
depends_on = [
time_rotating.this
]
}
Copy code
The key points of notice of this solution are:
- The time_rotating resource
- The random_password resource
- Setting the time_rotating resource as a keeper on the random_password resource if passwords should be rotated, else set the keeper to null
- A variable that controls the frequency of rotating (var.rotation_minutes)
- Variables that control the password complexity (var.password_*)
The time_rotating resource is essentially a UTC timestamp that updates to the current timestamp every time the time in minutes equivalent to that of var.rotation_minutes has passed since the last created timestamp. The random_password resource generates a password upon the first Terraform apply and stores it in the state. It only does this once, and so called ‘keepers’ can be used to control recreation of the password. Keepers are variables you can set on the random_password resource to control the recreation of the underlying resource. Multiple keepers can be passed and if any value of a keeper that is set on the random_password resource changes, the password will be updated. In the example above we pass a single keeper which is the time_rotating resource timestamp if password_rotation is true, else we pass null. This means that, whenever the time_rotating resource updates, the keeper on the random_password updates and hence the password will be rotated.
With var.rotation_minutes set to 1 and the list of users containing a single user called ‘user1’, an initial Terraform apply would look like this (note the date and time):
# date; tf apply -auto-approve
Fri Jun 27 09:16:00 AM UTC 2025
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:
# random_password.this["user1"] will be created
+ resource "random_password" "this" {
+ bcrypt_hash = (sensitive value)
+ id = (known after apply)
+ keepers = {
+ "rotation_trigger" = (known after apply)
}
+ length = 24
+ lower = true
+ min_lower = 1
+ min_numeric = 1
+ min_special = 1
+ min_upper = 1
+ number = true
+ numeric = true
+ override_special = "_-"
+ result = (sensitive value)
+ special = true
+ upper = true
}
# time_rotating.this["user1"] will be created
+ resource "time_rotating" "this" {
+ 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: 2 to add, 0 to change, 0 to destroy.
time_rotating.this["user1"]: Creating...
time_rotating.this["user1"]: Creation complete after 0s [id=2025-06-27T09:16:01Z]
random_password.this["user1"]: Creating...
random_password.this["user1"]: Creation complete after 0s [id=none]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Copy code
When running another Terraform apply, 7 seconds later, no changes are being made:
# date; tf apply -auto-approve
Fri Jun 27 09:16:07 AM UTC 2025
time_rotating.this["user1"]: Refreshing state... [id=2025-06-27T09:16:01Z]
random_password.this["user1"]: Refreshing state... [id=none]
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Copy code
Finally, running an apply, when 1 minute (var.rotation_minutes) has passed, we can see that the password gets an update:
# date; tf apply -auto-approve
Fri Jun 27 09:17:01 AM UTC 2025
time_rotating.this["user1"]: Refreshing state... [id=2025-06-27T09:16:01Z]
random_password.this["user1"]: Refreshing state... [id=none]
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:
# random_password.this["user1"] must be replaced
-/+ resource "random_password" "this" {
~ bcrypt_hash = (sensitive value)
~ id = "none" -> (known after apply)
~ keepers = { # forces replacement
~ "rotation_trigger" = "2025-06-27T09:16:01Z" -> (known after apply)
}
~ result = (sensitive value)
# (11 unchanged attributes hidden)
}
# time_rotating.this["user1"] will be created
+ resource "time_rotating" "this" {
+ 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: 2 to add, 0 to change, 1 to destroy.
random_password.this["user1"]: Destroying... [id=none]
random_password.this["user1"]: Destruction complete after 0s
time_rotating.this["user1"]: Creating...
time_rotating.this["user1"]: Creation complete after 0s [id=2025-06-27T09:17:02Z]
random_password.this["user1"]: Creating...
random_password.this["user1"]: Creation complete after 0s [id=none]
Apply complete! Resources: 2 added, 0 changed, 1 destroyed.
Copy code
The last step needed to fully automate it is putting it in a scheduled CI/CD pipeline such that it runs periodically. Make sure to tailor your pipeline schedule to the configured var.rotation_minutes.
Summary
And that’s how one can implement automated secret rotation using Terraform, bringing practical benefits to your infrastructure management workflow. By eliminating manual secret rotation, you reduce both the risk of human error and the operational burden on your team. This approach scales effortlessly with your infrastructure, whether you’re managing a handful of systems or thousands of resources across multiple environments. The pattern demonstrated here can be adapted to various credential types and integrated into your existing CI/CD pipelines, ensuring consistent implementation across your organization.
Are you interested in finding out how this can be used for downstream resources, such as Linux users, Active Directory users or secret engines like HashiCorp Vault? Or are you interested in seeing how you can implement this into a CI/CD pipeline? Feel free to reach out to us for a chat!