Managing AWS Secrets Manager with Terraform

A comprehensive guide to setting up AWS Secrets Manager using Terraform Infrastructure as Code

Managing AWS Secrets Manager with Terraform

AWS Secrets Manager helps you protect access to your applications, services, and IT resources. This guide shows how to set up Secrets Manager using Terraform.

Prerequisites

  • AWS CLI configured
  • Terraform installed
  • Understanding of secret management
  • Basic knowledge of KMS

Project Structure

aws-secrets-manager-terraform/
├── main.tf
├── variables.tf
├── outputs.tf
└── terraform.tfvars

Basic Secrets Manager Configuration

# main.tf
provider "aws" {
  region = var.aws_region
}

# KMS Key for Encryption
resource "aws_kms_key" "secrets" {
  description             = "KMS key for Secrets Manager"
  deletion_window_in_days = 7
  enable_key_rotation     = true

  tags = {
    Environment = var.environment
  }
}

resource "aws_kms_alias" "secrets" {
  name          = "alias/${var.project_name}-secrets"
  target_key_id = aws_kms_key.secrets.key_id
}

# Secret
resource "aws_secretsmanager_secret" "main" {
  name        = "${var.project_name}/${var.environment}/application"
  description = "Application secrets for ${var.project_name}"
  kms_key_id  = aws_kms_key.secrets.arn

  tags = {
    Environment = var.environment
  }
}

# Secret Version
resource "aws_secretsmanager_secret_version" "main" {
  secret_id = aws_secretsmanager_secret.main.id
  secret_string = jsonencode({
    username = var.db_username
    password = var.db_password
    host     = var.db_host
    port     = var.db_port
  })
}

# Secret Policy
resource "aws_secretsmanager_secret_policy" "main" {
  secret_arn = aws_secretsmanager_secret.main.arn

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

Variables Configuration

# variables.tf
variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-west-2"
}

variable "project_name" {
  description = "Project name"
  type        = string
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "dev"
}

variable "db_username" {
  description = "Database username"
  type        = string
  sensitive   = true
}

variable "db_password" {
  description = "Database password"
  type        = string
  sensitive   = true
}

variable "db_host" {
  description = "Database host"
  type        = string
}

variable "db_port" {
  description = "Database port"
  type        = number
  default     = 5432
}

Best Practices

  1. Secret Management

    • Use meaningful names
    • Implement rotation
    • Version control secrets
    • Regular secret reviews
  2. Security

    • Enable encryption
    • Use proper IAM roles
    • Implement secret policies
    • Regular security reviews
  3. Cost Optimization

    • Monitor API calls
    • Clean up unused secrets
    • Use appropriate retention
    • Regular cost reviews
  4. Performance

    • Cache secret values
    • Use regional endpoints
    • Monitor API limits
    • Regular performance reviews

Automatic Secret Rotation

# Lambda Function for Rotation
resource "aws_lambda_function" "rotation" {
  filename         = "rotation_function.zip"
  function_name    = "${var.project_name}-secret-rotation"
  role             = aws_iam_role.rotation.arn
  handler          = "index.handler"
  runtime          = "nodejs14.x"
  timeout          = 30

  environment {
    variables = {
      SECRETS_MANAGER_ENDPOINT = "https://secretsmanager.${var.aws_region}.amazonaws.com"
    }
  }

  tags = {
    Environment = var.environment
  }
}

# Rotation Schedule
resource "aws_secretsmanager_secret_rotation" "main" {
  secret_id           = aws_secretsmanager_secret.main.id
  rotation_lambda_arn = aws_lambda_function.rotation.arn

  rotation_rules {
    automatically_after_days = 30
  }
}

# IAM Role for Lambda
resource "aws_iam_role" "rotation" {
  name = "${var.project_name}-rotation-role"

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

# IAM Policy for Lambda
resource "aws_iam_role_policy" "rotation" {
  name = "${var.project_name}-rotation-policy"
  role = aws_iam_role.rotation.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "secretsmanager:DescribeSecret",
          "secretsmanager:GetSecretValue",
          "secretsmanager:PutSecretValue",
          "secretsmanager:UpdateSecretVersionStage"
        ]
        Resource = aws_secretsmanager_secret.main.arn
      },
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "arn:aws:logs:*:*:*"
      }
    ]
  })
}

Cross-Account Access

# Secret Policy for Cross-Account Access
resource "aws_secretsmanager_secret_policy" "cross_account" {
  secret_arn = aws_secretsmanager_secret.main.arn

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowCrossAccountAccess"
        Effect = "Allow"
        Principal = {
          AWS = var.cross_account_arns
        }
        Action = [
          "secretsmanager:GetSecretValue"
        ]
        Resource = "*"
      }
    ]
  })
}

# IAM Role in Target Account
resource "aws_iam_role" "secret_access" {
  provider = aws.target
  name     = "${var.project_name}-secret-access"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          AWS = var.source_account_arn
        }
      }
    ]
  })
}

Monitoring Configuration

# CloudWatch Dashboard
resource "aws_cloudwatch_dashboard" "secrets" {
  dashboard_name = "${var.project_name}-secrets-dashboard"

  dashboard_body = jsonencode({
    widgets = [
      {
        type   = "metric"
        x      = 0
        y      = 0
        width  = 12
        height = 6

        properties = {
          metrics = [
            ["AWS/SecretsManager", "GetSecretValue", "SecretId", aws_secretsmanager_secret.main.name],
            ["AWS/SecretsManager", "RotationSucceeded", "SecretId", aws_secretsmanager_secret.main.name]
          ]
          period = 300
          stat   = "Sum"
          region = var.aws_region
          title  = "Secrets Manager Activity"
        }
      }
    ]
  })
}

# CloudWatch Alarms
resource "aws_cloudwatch_metric_alarm" "rotation_failed" {
  alarm_name          = "${var.project_name}-rotation-failed"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "1"
  metric_name         = "RotationFailed"
  namespace           = "AWS/SecretsManager"
  period             = "300"
  statistic          = "Sum"
  threshold          = "0"
  alarm_description  = "This metric monitors secret rotation failures"
  alarm_actions      = [var.sns_topic_arn]

  dimensions = {
    SecretId = aws_secretsmanager_secret.main.name
  }
}

Deployment Steps

  1. Initialize Terraform:
terraform init
  1. Plan the deployment:
terraform plan
  1. Apply the configuration:
terraform apply

Clean Up

Remove all resources when done:

terraform destroy

Common Use Cases

  1. Database Credentials
resource "aws_secretsmanager_secret" "database" {
  name        = "${var.project_name}/${var.environment}/database"
  description = "Database credentials"
  kms_key_id  = aws_kms_key.secrets.arn

  tags = {
    Environment = var.environment
    Type        = "database"
  }
}

resource "aws_secretsmanager_secret_version" "database" {
  secret_id = aws_secretsmanager_secret.database.id
  secret_string = jsonencode({
    engine   = "postgresql"
    host     = aws_db_instance.main.address
    port     = aws_db_instance.main.port
    username = var.db_master_username
    password = random_password.db_master_password.result
  })
}
  1. API Keys
resource "aws_secretsmanager_secret" "api_keys" {
  name        = "${var.project_name}/${var.environment}/api-keys"
  description = "API keys for external services"
  kms_key_id  = aws_kms_key.secrets.arn

  tags = {
    Environment = var.environment
    Type        = "api-keys"
  }
}

resource "aws_secretsmanager_secret_version" "api_keys" {
  secret_id = aws_secretsmanager_secret.api_keys.id
  secret_string = jsonencode({
    stripe_secret_key     = var.stripe_secret_key
    sendgrid_api_key     = var.sendgrid_api_key
    twilio_account_sid   = var.twilio_account_sid
    twilio_auth_token    = var.twilio_auth_token
  })
}

Advanced Features

# Replicate Secrets to Other Regions
resource "aws_secretsmanager_secret_replica" "replica" {
  provider = aws.replica
  primary_secret_arn = aws_secretsmanager_secret.main.arn

  tags = {
    Environment = var.environment
    Type        = "replica"
  }
}

# Resource Policy for VPC Endpoint
resource "aws_secretsmanager_secret_policy" "vpc_endpoint" {
  secret_arn = aws_secretsmanager_secret.main.arn

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowVPCEndpointAccess"
        Effect = "Allow"
        Principal = "*"
        Action = "secretsmanager:GetSecretValue"
        Resource = "*"
        Condition = {
          StringEquals = {
            "aws:SourceVpc": var.vpc_id
          }
        }
      }
    ]
  })
}

Conclusion

This setup provides a comprehensive foundation for deploying Secrets Manager using Terraform. Remember to:

  • Plan your secret structure carefully
  • Implement proper security measures
  • Monitor secret usage and rotation
  • Keep your configurations versioned
  • Test thoroughly before production deployment

The complete code can be customized based on your specific requirements and use cases.