← ResourcesDEVOPS Β· SECRETS

Infrastructure-as-code and AWS secrets management

Secrets Manager, Parameter Store and data sources so a secret is never hardcoded in Terraform.

STRALYA13 min readJuly 2026

Why secrets should never live in Terraform

Leaving a hardcoded secret in a Terraform file or in the project state is a critical security flaw. As soon as a commit contains an AWS key, an API token, or a database password, your infrastructure becomes vulnerable: any contributor to the git repository, any external auditor, or even an attacker who compromises your version control system gains immediate access to your cloud resources. The Terraform state (.tfstate) is even more sensitive, because it stores not only the declared configuration but also runtime data, including secrets in plain text once they have been injected into resources (generated RDS passwords, database tokens, encryption keys). If this state is stored locally without encryption or accidentally committed to git, or if access to the remote S3 bucket that holds it is not restricted, every secret in your production can be exfiltrated in seconds. Terraform does not encrypt sensitive variables marked with "sensitive = true": this marker only prevents Terraform from displaying the raw value in the logs and in the output of a terraform apply, it does not protect the actual storage of the state. This is why teams that adopt Terraform seriously immediately draw a clear line between non-sensitive configuration (which git can store) and sensitive data (which only a dedicated secrets manager knows and provides to Terraform at deployment time).

AWS Secrets Manager and Parameter Store to centralize secrets

AWS offers two native services to store secrets securely and make them available to Terraform at deployment time: AWS Secrets Manager and AWS Systems Manager Parameter Store. AWS Secrets Manager is designed for critical, long-lived secrets such as database credentials, external API keys, or SSL certificates. Each stored secret is encrypted with AWS KMS by default, and access is controlled through granular IAM policies: only an authorized role or user can retrieve the secret, and every access is logged in CloudTrail. AWS Secrets Manager also supports automatic secret rotation: you can configure a Lambda function that renews your RDS password on a predefined schedule, with no manual intervention. Parameter Store is lighter and free for standard parameters (it only charges for advanced parameters, which offer features such as versioning or expiration policies). It works well for less sensitive values such as API URLs, deployment tokens, or external identifiers. Both services use AWS KMS for encryption at rest, and their IAM policies ensure that Terraform can only retrieve the secrets for which its execution role (the AWS account's AssumeRole role) has explicit permission. In Terraform, you use the "aws_secretsmanager_secret_version" data source for Secrets Manager or "aws_ssm_parameter" for Parameter Store, which means the secret is resolved only during terraform apply, never hardcoded in your configuration files. The common practice is to store the most critical secrets (business database credentials, production keys) in Secrets Manager, and the configuration values that require access control but are less critical in Parameter Store.

Using Terraform data sources to retrieve secrets at runtime

In Terraform, you never declare the secret itself in the configuration. Instead, you create a data source that queries Secrets Manager or Parameter Store at execution time. For AWS Secrets Manager, the syntax is simple: you define a data resource "aws_secretsmanager_secret_version" by specifying the ARN or name of the secret, and Terraform retrieves the current value only during terraform apply. A concrete example: if your secret is named "prod/rds/password" in Secrets Manager, you write data "aws_secretsmanager_secret_version" "rds_password" with secret_id = "prod/rds/password", then reference the secret through data.aws_secretsmanager_secret_version.rds_password.secret_string as the password for your RDS resource. Terraform will never display it in plain text in the logs, and the state will contain an encrypted reference. For Parameter Store, the pattern is similar with data "aws_ssm_parameter": you query the parameter by path, and Terraform resolves it at runtime. One important practice: combine this data source with the "sensitive = true" variable on the resource that consumes the secret (for example, the aws_db_instance resource for RDS). This means that even though Terraform technically has access to the secret in memory during plan or apply, it will not display it in the console output or in the logs, reducing the risk of accidental exposure. You can also use aws_secretsmanager_secret to create secrets directly through Terraform, but the real secret (its value) must always be injected externally, never hardcoded in the plan.

Securing Terraform state (tfstate) and remote backends

The Terraform state is a JSON file that describes the current state of your infrastructure and contains sensitive data in plain text, even if you use data sources to retrieve secrets. When you run terraform apply and Terraform creates a resource (for example an RDS instance), it stores the resolved password value in the local state (.tfstate) or in the remote backend (for example S3). Many teams discover this too late by accidentally grepping their state and finding the secrets exposed. The first line of defense is to never commit the local .tfstate file to git: always add .tfstate and .terraform.lock.hcl to .gitignore. The second, and more important, is to use a secure remote backend (S3, Terraform Cloud, or another) with encryption and strict access control. If you use S3 to store the state, configure the bucket with versioning enabled, CloudTrail access logging, and a very restrictive bucket policy that only allows the execution IAM roles (for example your CI/CD pipeline's role) to read and write the state. Also enable server-side encryption (SSE-S3 or, better still, SSE-KMS with a managed KMS key) to encrypt the state at rest, and enforce TLS transport (aws:SecureTransport) to encrypt the secret in transit. With Terraform Cloud or Terraform Enterprise (the SaaS or self-hosted version from HashiCorp), the state is automatically encrypted at rest and in transit, and HashiCorp manages the underlying infrastructure, which can simplify compliance. Whatever your approach, remember that the state is a secret: treat it with as much care as your AWS root keys.

Sensitive Terraform variables and management recommendations

Terraform supports a "sensitive = true" variable option that prevents the value from being displayed in plain text in the console during a terraform plan or apply. However, this only affects display, not storage: the value remains in plain text in the state. The recommendation is therefore to combine the sensitive marker with an external secret source (Secrets Manager, Parameter Store, or even environment variables injected by your pipeline). A common approach in DevOps teams: critical secrets (AWS keys, production tokens) come from Secrets Manager through a data source, while less sensitive configuration variables (internal API URLs, resource names) come from .tfvars files or TF_VAR_* environment variables. The .tfvars files themselves should be gitignored or stored outside the repository, in a secure system (for example, an encrypted S3 bucket accessible only by the CI/CD pipeline). If you manage multiple environments (dev, staging, prod), it is tempting to create .tfvars.dev, .tfvars.prod, and similar files, and to version them all together. This is an anti-pattern: production sensitive data should never be versioned alongside the source code, even encrypted, because it increases the exposure surface. Prefer an approach where the variables specific to each environment are injected at runtime by your CI/CD system or through webhooks to your secrets manager. With Terraform Cloud, you can mark variables as sensitive in the web interface, and they will never be displayed, even to the team that created them. This is an excellent practice for fully externalizing secrets management out of the source code.

Auditing and rotating secrets in a Terraform infrastructure

Once you have centralized your secrets in AWS Secrets Manager or Parameter Store, auditing and renewing them becomes systematic. AWS CloudTrail records every call to the Secrets Manager APIs (GetSecretValue, UpdateSecret, and so on), which means you can trace who accessed which secret, when, and from which IAM role. Configure CloudTrail to send its logs to CloudWatch Logs or S3 so you can analyze them with tools such as Amazon Athena or raise alerts if someone attempts an unauthorized access. Secret rotation is a critical step that many teams ignore: a secret stored indefinitely becomes a time bomb if a person with past access retrieves it out of band, or if an AWS key is accidentally exposed in an old repository. AWS Secrets Manager supports automatic rotation through Lambda functions: you can configure a policy that says "every 30 days, generate a new RDS password and update the resource", with no manual action. For secrets you generate yourself (such as API tokens or external API keys), Secrets Manager cannot rotate them automatically (because it has no access to the external system to regenerate the key), but you can set up a custom Lambda that, during rotation, calls the external system's API to issue a new key, stores it in Secrets Manager, and notifies your infrastructure to update its configurations. In Terraform, you can trigger resource updates when a secret changes by using the "triggers" meta-argument: if the version of the secret changes, you force a redeployment. This ensures your infrastructure always uses the latest version of a rotating secret.

Integrating Terraform secrets into a secure CI/CD pipeline

No Terraform secrets hardening is complete without a CI/CD pipeline that follows the same principles. In a modern approach, when an engineer pushes a Terraform change to git, the pipeline clones the repository, runs a terraform plan to show the proposed changes, and waits for approval before applying the changes. At every step, no secret should be stored locally or logged. Use tools such as GitHub Actions, GitLab CI, or Jenkins with AWS plugins that authenticate to the AWS account through temporary IAM roles (OIDC, OpenID Connect) rather than long-lived access keys. OIDC authentication means your pipeline connects to the AWS account without ever storing access keys: the OIDC token generated by GitHub, GitLab, or Jenkins is exchanged for a temporary, permission-limited AWS security token. Configure the pipeline to retrieve secrets from Secrets Manager or Parameter Store only at the moment of terraform apply, through the pipeline's own IAM role. Never version .tfvars files containing secrets, and never display sensitive variables in the logs: use the masked log output (masking of the secrets listed in the pipeline) provided by most CI/CD tools. For multi-account environments (dev in one AWS account, prod in another), use role delegation (assume-role) so the pipeline first authenticates to the main account, then assumes a role in the target account to apply Terraform. This extra layer isolates production secrets from development secrets and limits the impact of a pipeline compromise. Document and audit who has access to the pipeline, which roles it assumes, and how it accesses secrets, so that any compliance audit (PCI-DSS, ISO 27001, SOC 2) can verify that secrets security practices are followed end to end.

AWS TEARDOWN Β· FREE

Get the AWS Teardown: where your bill really goes.

The guide listing the 12 cost areas that leak the most at scale-ups, and how to plug them. Free, by email, no obligation.