Mastering AWS Cross-Account Secrets with Terraform & KMS

Published: (December 3, 2025 at 09:05 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

The Multi-Account Reality Check

In modern cloud architecture, multi‑account strategies are the norm. We separate development from production, and often centralize shared services into their own hubs.

A very common scenario is having a central Security or Shared Services AWS account holding sensitive database credentials in AWS Secrets Manager, which need to be accessed by a Lambda function running in a completely different Workload account.

It sounds simple on paper:

  1. Create the secret in Account A.
  2. Attach a resource policy to the secret allowing Account B to read it.
  3. Give the Lambda in Account B IAM permission to read the secret.

You deploy your Terraform, invoke your Lambda, and… fail. You get an AccessDeniedException or a vague KMS error.

When debugging cross‑account access failures with Secrets Manager (or S3, or SQS), 90 % of the time developers focus on IAM policies. But when secrets are involved, you are fighting a two‑front war: authentication (IAM) and cryptography (KMS).

By default, when you create a secret in Secrets Manager without specifying encryption settings, AWS encrypts it using the AWS‑managed key for the service (alias/aws/secretsmanager).

The trap: You cannot modify the key policy of an AWS‑managed key. It is designed to only trust principals within the same account. No matter how wide‑open you make your IAM policies, the external account will never be allowed to decrypt the payload. The door is unlocked, but the box is welded shut.

To enable cross‑account access, you must take control of encryption. Create a Customer Managed Key (CMK) in KMS and explicitly trust the external account.

Terraform Implementation

Variables and Data Sources

variable "external_consumer_account_id" {
  description = "The AWS Account ID that needs read access to the secrets."
  type        = string
  # Example: "123456789012"
}

# Helper to get current account ID for policy definitions
data "aws_caller_identity" "current" {}

KMS Key and Alias

resource "aws_kms_key" "cross_account_secrets_key" {
  description             = "KMS Key for cross-account RDS credentials"
  deletion_window_in_days = 30
  enable_key_rotation     = true

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "EnableIAMUserPermissionsForCurrentAccount"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
        Action   = "kms:*"
        Resource = "*"
      },
      {
        Sid    = "AllowExternalAccountToDecrypt"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${var.external_consumer_account_id}:root"
        }
        Action = [
          "kms:Decrypt",
          "kms:DescribeKey"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_kms_alias" "secrets_key_alias" {
  name          = "alias/cross-account-secrets-key"
  target_key_id = aws_kms_key.cross_account_secrets_key.key_id
}

Secrets Manager Secret

resource "aws_secretsmanager_secret" "database_credentials" {
  name        = "prod/rds/read-replica-creds"
  description = "Database credentials accessible by workload accounts"

  # CRITICAL: Force the use of our custom KMS key
  kms_key_id = aws_kms_key.cross_account_secrets_key.id
}

Secret Resource Policy

resource "aws_secretsmanager_secret_policy" "database_credentials_policy" {
  secret_arn = aws_secretsmanager_secret.database_credentials.arn

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "AllowExternalRead"
        Effect    = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${var.external_consumer_account_id}:root"
        }
        Action   = "secretsmanager:GetSecretValue"
        Resource = "*"
      }
    ]
  })
}

Required Permissions

For a successful cross‑account secret retrieval, three gates must open simultaneously:

  1. KMS Key Policy (source account) must trust the destination account to Decrypt.
  2. Secret Resource Policy (source account) must trust the destination account to GetSecretValue.
  3. IAM Role attached to the Lambda/EC2 (destination account) must have permissions to perform both actions on the respective ARNs.

Using default AWS keys is the most common trap in multi‑account architectures. By shifting to Customer Managed Keys and handling policy definitions explicitly in Terraform, you ensure secure, repeatable cross‑account access.

Back to Blog

Related posts

Read more »

Terraform Project: Simple EC2 + Security Group

Project Structure terraform-project/ │── main.tf │── variables.tf │── outputs.tf │── providers.tf │── terraform.tfvars │── modules/ │ └── ec2/ │ ├── main.tf │...

Saving Terraform State in S3

Configuring S3 as a Terraform Backend Terraform can store its state in an S3 bucket. Below is a minimal configuration that sets up the S3 backend: hcl terrafor...