Secrets and Credential Management
The idea in one sentence
Section titled “The idea in one sentence”Azure credentials and application secrets should never be hardcoded into .tf files or a committed .tfvars file — provider authentication comes from a local Azure CLI session, a managed identity, or OIDC, and application secrets are pulled from a real secrets manager at apply time rather than typed into a Terraform variable.
Provider authentication: never a hardcoded client secret
Section titled “Provider authentication: never a hardcoded client secret”The provider "azurerm" block accepts client_id, client_secret, tenant_id, and subscription_id arguments directly, and it is technically legal HCL to type a real client secret into one of them. It is also exactly the mistake this course keeps warning about: a value sitting in a .tf file is a value one accidental git add . away from a public GitHub history forever, regardless of how carefully anyone reviews the pull request afterward.
# do not do this — a real client secret should never appear in a .tf fileprovider "azurerm" { features {}
client_id = "11111111-2222-3333-4444-555555555555" client_secret = "this-is-a-real-secret-and-now-it-is-in-git-forever" tenant_id = "66666666-7777-8888-9999-000000000000" subscription_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}Every legitimate way to authenticate the azurerm provider avoids typing a secret into configuration at all:
- Local development — an engineer runs
az loginonce, and theazurermprovider picks up that Azure CLI session automatically. Nothing needs to be in the provider block beyondfeatures {}. - A managed identity — a system- or user-assigned managed identity attached to the compute running Terraform (an Azure VM, an Azure Container Instance, an AKS pod) lets the provider authenticate as that identity with no credential material anywhere, because Azure itself brokers the token.
- OIDC via federated credentials — covered in the CI/CD lesson:
azure/loginexchanges a short-lived, signed GitHub Actions identity token for temporary Azure credentials through a federated credential on an Azure AD App Registration.
# provider block for all three approaches — identical HCL, different actual auth mechanismprovider "azurerm" { features {}}Notice that the provider block itself barely changes between local development, a managed identity, and CI — the difference lives entirely in which identity is available in the environment Terraform runs in, never in a client_secret argument sitting in the file.
Pulling a secret in versus defining it as a variable
Section titled “Pulling a secret in versus defining it as a variable”Application secrets are a different problem from provider authentication — a database administrator password, an API key a resource needs at creation time, a TLS certificate’s private key. The State Management module already established that marking a Terraform variable sensitive = true only redacts it from CLI output; the value still lands in terraform.tfstate in full plaintext regardless. Given that, defining a secret as a variable at all means someone still has to get the real value into Terraform somehow — typically a .tfvars file, which is exactly the kind of file that ends up committed by accident.
The better pattern is pulling the secret in from a real secrets manager via a data source at apply time, so the secret’s actual value is never typed into any .tf or .tfvars file in the first place:
data "azurerm_key_vault" "main" { name = "app-keyvault" resource_group_name = "app-shared-rg"}
data "azurerm_key_vault_secret" "db_password" { name = "app-db-password" key_vault_id = data.azurerm_key_vault.main.id}
resource "azurerm_postgresql_flexible_server" "main" { name = "app-db" resource_group_name = azurerm_resource_group.main.name location = azurerm_resource_group.main.location version = "15" sku_name = "GP_Standard_D2s_v3" administrator_login = "appadmin" administrator_password = data.azurerm_key_vault_secret.db_password.value}Be precise about what this does and does not fix. It is not a way to keep the secret out of state entirely — a data source’s result is cached in state exactly like a resource’s attributes are, so data.azurerm_key_vault_secret.db_password.value and the administrator_password it feeds both end up recorded in terraform.tfstate just as plainly as a sensitive = true variable would. Protecting the state backend with Azure RBAC, as the State Management module covered, is still the non-negotiable part regardless of which approach you pick.
What a data source genuinely does solve is where the secret’s real value originates and how it gets rotated. With a variable, the value has to be typed somewhere by a human before Terraform ever runs — a .tfvars file, a CI secret, a value pasted into a prompt. With a data source reading Key Vault, the value is set and rotated in exactly one place — Key Vault itself — and every terraform plan or apply picks up whatever the current value is automatically, with nobody ever needing to touch Terraform configuration to rotate it.
A gitignore checklist for a Terraform and Terragrunt repository
Section titled “A gitignore checklist for a Terraform and Terragrunt repository”Independent of how carefully secrets are handled inside configuration, a Terraform or Terragrunt repository has several categories of file that should never reach git in the first place, because each one can contain resource attribute values, sensitive-looking configuration, or provider credentials in the local filesystem:
# .gitignore — Terraform / Terragrunt
# state files can contain plaintext secrets and full resource attributes*.tfstate*.tfstate.backup
# provider binaries and working files downloaded by terraform init.terraform/
# variable files often carry real values for local runs — audit case by case,# a checked-in *.tfvars with only non-sensitive defaults can be fine*.tfvars
# Terragrunt's per-unit cache of generated and downloaded Terraform files.terragrunt-cache/*.tfstate and *.tfstate.backup matter for the same reason the State Management module keeps returning to: state is a plaintext record of every attribute Terraform has ever set, sensitive or not. .terraform/ is pure derived output from init — providers, modules, and lock metadata — with nothing worth committing and everything worth regenerating fresh in CI. *.tfvars is a judgment call rather than an automatic block: a dev.tfvars containing only an instance size and a region name is harmless to commit, but any .tfvars that carries a real credential or connection string must never reach git, so most teams either exclude the pattern outright or enforce a naming convention like *.auto.tfvars for safe files and something clearly separate for anything sensitive. .terragrunt-cache/ is Terragrunt’s own generated working directory per unit, recreated automatically on every run.
flowchart LR hardcoded["Hardcoded client_secret in .tf file"] -->|committed| git["git history forever"] kv["Azure Key Vault secret"] -->|data source at apply time| tf["Terraform resource attribute"] tf -->|recorded| state["terraform.tfstate"] kv -->|rotated in one place| tf