Back to blog
Secure AWS Secrets with Terraform Aws Secrets Manager In

Secure AWS Secrets with Terraform Aws Secrets Manager In

Master secure secret management with terraform aws secrets manager. Our 2026 guide covers provisioning, rotation, IAM, and best practices for leak prevention.

May 23, 2026
terraform aws secrets managerterraformaws secrets managerinfrastructure as codedevsecops

You're probably here because you already know the obvious rule: don't put secrets in Git. But the core issue with Terraform AWS Secrets Manager setups isn't usually the repository. It's the state file.

That's where teams get burned. They move passwords out of .env, stop hardcoding values in HCL, feel good about “using a secret manager,” and then persist the same secret in terraform.tfstate, a plan artifact, or a CI log. The secret is no longer in source control, but it's still exposed in the places operators interact with every day.

AWS Secrets Manager is a solid fit for Terraform on AWS. The hard part isn't creating a secret. The hard part is designing the workflow so Terraform doesn't become the leak path.

Table of Contents

The Inevitable Problem with Hardcoded Secrets

Teams frequently don't start with AWS Secrets Manager. They start with convenience.

A password lands in a .env file. Someone copies an API key into a Terraform variable. A teammate sends a production credential over Slack because deploys are blocked. Then a repo gets shared, a laptop gets backed up somewhere it shouldn't, or a CI job prints more than expected. That's usually how secret handling becomes a security incident.

The problem with hardcoded secrets isn't just storage. It's duplication. Once a credential shows up in code, chat, notes, shell history, and local files, nobody can say where the source of truth is anymore. Rotation gets messy, offboarding gets risky, and every environment drifts a little further from control.

If that sounds familiar, you're not alone. The daily habits around .env files are exactly why teams run into preventable leaks, and this breakdown of the env files security nightmare maps closely to what happens in real developer workflows.

Terraform improves part of this. It gives you repeatable infrastructure changes and a single declared workflow. AWS Secrets Manager improves another part. It centralizes storage and retrieval instead of scattering secrets across workstations and repos.

But neither tool saves you if you use them carelessly.

Hardcoding a secret in Terraform is still hardcoding a secret. Putting that value behind a variable doesn't magically make it safe if Terraform later writes it to state.

That's the trap. Teams often replace one bad pattern with a more intricate bad pattern. They stop committing passwords to Git, but they still allow Terraform to persist the same value in places with broad operational access. That's better than plaintext in source control, but it isn't the standard you want for production credentials.

The professional approach is simple in principle. Store secrets in AWS Secrets Manager, keep Terraform focused on secret infrastructure and access controls, and avoid workflows that force secret values into state, plans, outputs, or logs.

That's the whole game.

Foundations for Secure Secret Management

AWS describes Secrets Manager as a managed service to securely encrypt, store, and rotate credentials for databases and other services. AWS also notes that a secret can contain binary data, a single string, or multiple strings, and that Secrets Manager uses 256-bit AES symmetric data keys to encrypt secret values. In Terraform workflows, that matters because sensitive values injected directly into resources or outputs can end up in state, which is why AWS emphasizes retrieving secrets programmatically rather than hardcoding them in configuration (AWS prescriptive guidance).

That gives you the baseline: Secrets Manager is the vault. Terraform is the infrastructure control plane. They overlap, but they don't have the same job.

For teams that are formalizing review gates around infrastructure, this is also where policy-as-code implementation becomes useful. Secret handling mistakes are predictable, which means you can enforce some of them before they ever reach apply.

Understand the two Terraform resources

If you're working with Terraform AWS Secrets Manager, keep these two resources mentally separate:

Resource What it represents What you should think about
aws_secretsmanager_secret The secret container Name, tags, KMS key, recovery behavior, policy surface
aws_secretsmanager_secret_version A specific stored value Rotation, update behavior, and potential state exposure

That distinction matters more than most guides admit.

The secret resource is metadata and lifecycle control. The secret version is the actual payload. If you blur those together, you'll design bad workflows. If you keep them separate, you can manage the container declaratively while being more careful about how values are introduced and retrieved.

Treat state as a security boundary

You need the basics in place before any code goes live:

  • AWS access configured: Your CLI or execution role needs permission to create secrets and related IAM or KMS resources.
  • Terraform installed and remote state chosen: Don't normalize local state for shared infrastructure.
  • A decision on secret authorship: Will Terraform create the value, or will another process write it?

That last one is where most of the architecture quality shows up.

Practical rule: If a secret value must pass through Terraform in a normal resource or data source, assume it can end up in state unless you're explicitly using a pattern that avoids persistence.

A lot of mid-level engineers think “sensitive” means “safe.” It doesn't. Sensitive values are mainly redacted from casual output. That's useful, but it isn't the same thing as keeping the value out of persisted state.

Use a central source of truth for application and infrastructure secrets, and keep developer-facing secret references documented clearly. If your team needs a cleaner operating model for secret variables across environments, this guide to secrets in variables is a good complement to the AWS side of the workflow.

Provisioning Secrets Without State File Exposure

Here's the mistake I see most often: a team creates a secret in AWS Secrets Manager, stores the value with Terraform, and assumes the job is done because the secret is “in Secrets Manager now.”

That's incomplete thinking. The important question is whether Terraform also persisted the same value elsewhere.

The pattern that looks fine and leaks anyway

This configuration is common, readable, and risky:

resource "aws_secretsmanager_secret" "db" {
  name = "prod/app/db"
}

resource "aws_secretsmanager_secret_version" "db" {
  secret_id = aws_secretsmanager_secret.db.id
  secret_string = jsonencode({
    username = "app_user"
    password = "SuperSecretPassword"
  })
}

It creates the secret. It stores the value. It also hands Terraform the full plaintext payload.

That means the secret value may be persisted in state because Terraform had to manage it as an input to the resource. If your backend is weakly protected, your secret is weakly protected too. If someone can read the state object, they may be able to read the credential history of your infrastructure.

Even this “improved” version still has the same core issue:

variable "db_password" {
  type      = string
  sensitive = true
}

resource "aws_secretsmanager_secret" "db" {
  name = "prod/app/db"
}

resource "aws_secretsmanager_secret_version" "db" {
  secret_id = aws_secretsmanager_secret.db.id
  secret_string = jsonencode({
    username = "app_user"
    password = var.db_password
  })
}

The password is no longer hardcoded in HCL, which is better for source control. But Terraform still processes the value. In practice, that's where people confuse redaction with non-persistence.

A five-step diagram showing the secure secret provisioning flow using Terraform and AWS Secrets Manager.

A safer provisioning pattern

A practical low-leakage pattern is to create the secret container first and then write the actual value as a separate secret version resource, while keeping the state backend encrypted and tightly access-restricted. Guidance on Terraform and Secrets Manager also warns that standard data sources reading secret values can still place those values into state and plans, so state remains the primary control boundary (OneUptime writeup on managing Secrets Manager secrets with Terraform).

Start with the container:

resource "aws_kms_key" "secrets" {
  description = "KMS key for production application secrets"
}

resource "aws_secretsmanager_secret" "db" {
  name        = "prod/app/db"
  description = "Database credentials for the production application"
  kms_key_id  = aws_kms_key.secrets.arn

  tags = {
    Environment = "prod"
    ManagedBy   = "terraform"
    Service     = "app"
  }
}

Then add the value as a distinct concern:

variable "db_username" {
  type = string
}

variable "db_password" {
  type      = string
  sensitive = true
}

resource "aws_secretsmanager_secret_version" "db" {
  secret_id = aws_secretsmanager_secret.db.id
  secret_string = jsonencode({
    username = var.db_username
    password = var.db_password
  })
}

This pattern is still not perfect if Terraform receives the secret value directly. But it's the right structural model because it separates metadata from payload and gives you cleaner control over rotation and governance.

If you must bootstrap a value through Terraform, reduce the blast radius:

  • Use remote encrypted state: Store state in a backend with encryption at rest.
  • Restrict backend access aggressively: Only your Terraform execution roles and a very small operator set should have read access.
  • Avoid outputs for secret material: Even sensitive outputs are not a substitute for better architecture.
  • Keep plans controlled: Saved plans can become another persistence layer.

What to lock down even with a better pattern

A structurally sound configuration still fails if the surrounding workflow is sloppy.

Bad signs include:

  • Developers reading secrets back with standard data sources just to feed other resources.
  • CI jobs printing rendered JSON for debugging.
  • Broad read access to the remote state bucket or workspace.
  • Using Terraform to distribute application secrets when the application can fetch them at runtime instead.

If an application can retrieve a secret directly from AWS Secrets Manager at runtime, let it do that. Don't route the value through Terraform just because Terraform is already in the deployment path.

The strongest pattern is often this: Terraform creates the secret container, IAM permissions, and rotation configuration. A separate trusted process writes or rotates the value. The application retrieves it at runtime. Terraform never needs to know the plaintext.

That's the model to aim for.

Controlling Access with IAM Policies and KMS

Secret storage is only half the job. Access control is where the design either holds up or collapses.

A secret in AWS Secrets Manager doesn't help if every workload in the account can read it. It also doesn't help if your application role can read the secret but can't use the KMS key that protects it. Terraform AWS Secrets Manager configurations need both IAM and encryption decisions to line up.

Least privilege for secret consumers

Say a Lambda function needs database credentials. The role should get access to one secret, not every secret with a similar name.

Use an IAM policy like this:

resource "aws_iam_role" "app" {
  name = "app-runtime-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_policy" "app_secret_read" {
  name = "app-secret-read"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "secretsmanager:GetSecretValue",
          "secretsmanager:DescribeSecret"
        ]
        Resource = aws_secretsmanager_secret.db.arn
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "app_secret_read" {
  role       = aws_iam_role.app.name
  policy_arn = aws_iam_policy.app_secret_read.arn
}

That policy does one useful thing and nothing extra. That's what you want.

If the secret uses a customer managed KMS key, add the corresponding decrypt permission for the same role:

resource "aws_iam_policy" "app_kms_use" {
  name = "app-kms-use"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "kms:Decrypt",
          "kms:DescribeKey"
        ]
        Resource = aws_kms_key.secrets.arn
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "app_kms_use" {
  role       = aws_iam_role.app.name
  policy_arn = aws_iam_policy.app_kms_use.arn
}

A diagram illustrating the AWS Secrets Access Control hierarchy, showing the relationships between AWS accounts, IAM roles, policies, secrets, and KMS keys.

When a customer managed KMS key is worth it

Not every secret needs its own KMS story. But some do.

Use a customer managed key when you need tighter control over key policy, audit expectations, or cross-account access. The practical advantage is that you can define exactly who may use the key and under what conditions.

A simple KMS resource looks like this:

resource "aws_kms_key" "secrets" {
  description = "Customer managed key for application secrets"

  tags = {
    ManagedBy = "terraform"
    Scope     = "secrets"
  }
}

Use the default AWS managed option when your needs are straightforward and account-local. Reach for a customer managed key when your security team cares about explicit control boundaries, or when secret access spans accounts and you need the key permissions to reflect that cleanly.

A few access control foot-guns show up repeatedly:

  • Wildcard resources in IAM policies: Easy to write, painful to justify.
  • Giving Terraform humans direct read access to production secrets: Most operators only need to manage infrastructure, not inspect values.
  • Forgetting KMS permissions: The app can have GetSecretValue and still fail at runtime.
  • Sharing one broad role across unrelated services: That turns one secret read into many.

Keep the path narrow. One workload, one role, one secret, one key path when needed.

Implementing Automated Secret Rotation

Static secrets linger longer than teams intend. Rotation is how you stop a credential from becoming permanent infrastructure by accident.

AWS Secrets Manager supports rotation as part of the secret lifecycle, and that's one of the strongest reasons to use it instead of treating a secret store as a glorified key-value bucket.

A robot inserts a key into a secure vault linked to gears representing automated rotation processes.

Rotation only helps when retrieval is clean

A rotated secret is still dangerous if your Terraform workflow keeps pulling the fresh value back into state.

Security guidance has shifted toward Terraform 1.10 ephemeral resources, which GitGuardian says were introduced in November 2024 and allow data to be used at apply time without being stored in state or plan. The same guidance warns that standard data sources reading from AWS Secrets Manager can still place retrieved secret values into state, so the safer pattern is ephemeral retrieval combined with encrypted remote backends and strict IAM controls (tecracer summary referencing GitGuardian guidance).

That change matters because it moves the goal from “encrypt the place where secrets leak” to “stop persisting them at all.”

If you're building out repeatable security operations around rotation, automation discipline matters beyond just Terraform. This broader look at ThreatCrush on security AI savings is useful background on why security teams keep pushing repetitive secret handling into controlled automation.

Rotation is only as strong as the weakest retrieval path. If one pipeline still reads and persists the secret badly, your rotation cadence won't save you.

Terraform example for rotation

For an existing secret, you can define rotation like this:

resource "aws_lambda_function" "rotation" {
  function_name = "db-secret-rotation"
  role          = aws_iam_role.rotation_lambda.arn
  handler       = "lambda_function.lambda_handler"
  runtime       = "python3.11"
  filename      = "rotation-lambda.zip"
  source_code_hash = filebase64sha256("rotation-lambda.zip")
}

resource "aws_lambda_permission" "allow_secrets_manager" {
  statement_id  = "AllowSecretsManagerInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.rotation.function_name
  principal     = "secretsmanager.amazonaws.com"
}

resource "aws_secretsmanager_secret_rotation" "db" {
  secret_id           = aws_secretsmanager_secret.db.id
  rotation_lambda_arn = aws_lambda_function.rotation.arn

  rotation_rules {
    automatically_after_days = 30
  }
}

The shape is straightforward. The core work is operational:

  • Your Lambda must know how to update the target system such as a database user.
  • Your application must tolerate credential changes without long outages.
  • Your retrieval path must avoid pushing rotated values into state or logs.

For teams rotating database credentials, AWS-provided rotation templates for supported services are a good starting point. Don't custom-build the logic unless your environment requires a custom flow.

A short walkthrough helps if you want to see the moving pieces in context.

Integrating and Migrating Secrets in Your Workflow

Two real-world tasks show up after the first rollout. First, something in Terraform wants to read a secret that already exists. Second, your account already has manually created secrets that nobody wants to recreate.

Both are manageable. Both have traps.

A digital illustration showing a treasure chest migrating data across a bridge to a secure server.

Reading existing secrets without creating new leaks

This is the tempting approach:

data "aws_secretsmanager_secret" "db" {
  name = "prod/app/db"
}

data "aws_secretsmanager_secret_version" "db" {
  secret_id = data.aws_secretsmanager_secret.db.id
}

locals {
  db_credentials = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)
}

It works. It's also the exact kind of pattern that can place retrieved secret values into Terraform state, as covered earlier.

Use standard reads only when you've accepted that trade-off and locked down the backend accordingly. Better yet, redesign so the consuming service fetches the secret directly at runtime instead of Terraform acting as the middleman.

Safer patterns usually look like one of these:

  • Application runtime retrieval: ECS, Lambda, or application code pulls the secret directly from Secrets Manager.
  • Infrastructure-only reference passing: Terraform passes the secret ARN, name, or metadata, but not the plaintext value.
  • Apply-time ephemeral retrieval: Use the newer non-persistent approach where your Terraform version and provider support it.

If your team is standardizing AWS integrations for secret delivery outside raw Terraform data-source reads, this AWS Secrets Manager integration guide is a useful reference point for organizing that handoff.

Don't use Terraform as a secret relay unless there's no cleaner option. Most of the time, there is.

Importing manually created secrets

Importing existing secrets is much safer than recreating them just to satisfy IaC purity.

First, define the container resource in code:

resource "aws_secretsmanager_secret" "existing" {
  name = "prod/legacy/api"
}

Then import the existing secret into state:

terraform import aws_secretsmanager_secret.existing arn:aws:secretsmanager:REGION:ACCOUNT_ID:secret:prod/legacy/api

After import, run terraform plan and reconcile any drift carefully. At this stage, manage metadata, tags, KMS assignment, and policies. Be careful about immediately adding a secret version resource if you don't intend Terraform to own the current value lifecycle.

A good migration sequence is:

  1. Import the secret container first.
  2. Review what Terraform wants to change.
  3. Add IAM and resource policies next.
  4. Only decide later whether Terraform should manage versions or whether another rotation process owns them.

That sequence avoids the common mistake of taking a manually managed production secret and forcing plaintext value handling into Terraform on day one.

From Secure Provisioning to a Full Secret Strategy

The right Terraform AWS Secrets Manager setup is opinionated.

Create the secret container separately from the value. Give workloads the narrowest IAM access that still lets them function. Use a customer managed KMS key when control requirements justify it. Automate rotation where the target system supports it. Above all, treat Terraform state as a sensitive system, not a convenience file.

That last point is where most secret strategies fail. Teams spend energy protecting Git and forget that state, plan files, CI logs, and debugging output are often the more realistic leak paths.

A mature secret strategy also recognizes scope. AWS Secrets Manager is excellent for AWS-native infrastructure secrets and runtime retrieval patterns. It isn't the full answer for every developer workflow, every local environment, or every multi-platform team process.


If you want the same kind of discipline for developer-facing environment variables, shared app secrets, and local-to-CI workflows, EnvManager complements the AWS infrastructure side well. It gives teams a cleaner way to replace ad hoc .env sharing, centralize secret access by environment, and reduce the everyday leak paths that Terraform alone doesn't address.

Published via the Outrank tool

Ready to manage your environment variables securely?

EnvManager helps teams share secrets safely, sync configurations across platforms, and maintain audit trails.

Get started for free

Get DevOps tips in your inbox

Weekly security tips, environment management best practices, and product updates.

No spam. Unsubscribe anytime.