LAB15_AWS_KMS Customer Managed Key with Grants and Aliases

KMS Customer Managed Key with Grants and Aliases

☁ AWS ⭐ Beginner KMS Key Policy Grants Multi-Region Key

Scenario

Your organisation's security policy requires that all S3 buckets, EBS volumes, and RDS instances use customer-managed KMS keys (CMKs) — not AWS-managed keys — so that your security team controls key access and can audit all encryption/decryption operations. You also need automatic annual key rotation and separate keys per environment.

Create a reusable KMS key configuration that serves as a template for all your encryption needs.

Your Objectives
  • Create an aws_kms_key with automatic rotation, a 30-day deletion window, and a full key policy.
  • The key policy must: allow the account root, allow specific IAM roles to use the key, and allow specific roles to manage the key.
  • Create two aliases: one generic (alias/app-encryption) and one environment-specific.
  • Create a KMS grant allowing a Lambda function's IAM role to perform Decrypt operations.
  • Output the key ARN, key ID, and both aliases.

Additional Context

KMS key policies and grants serve different purposes: Key policies are IAM-style JSON documents attached to the key and control who can manage it. Grants are temporary, programmatic permissions that a key administrator creates so specific principals (like Lambda functions) can use the key without needing an explicit IAM policy change. Grants are useful for automated cross-account workflows.

The multi_region = false setting creates a single-region key. Setting it to true creates a primary key that can be replicated to other regions for cross-region decryption — required for DR scenarios with encrypted cross-region data.

versions.tf

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

variables.tf

variable "aws_region"     { type = string; default = "us-east-1" }
variable "environment"    { type = string; default = "prod" }
variable "key_admin_arn"  { type = string; description = "IAM role ARN that can manage this KMS key." }
variable "key_user_arns"  { type = list(string); description = "IAM role ARNs that can use (encrypt/decrypt) the key." }
variable "lambda_role_arn"{ type = string; description = "IAM role ARN for the Lambda that gets a decrypt grant." }

main.tf

# main.tf

provider "aws" {
  region = var.aws_region
}

data "aws_caller_identity" "current" {}

# Full KMS key policy.
data "aws_iam_policy_document" "kms_policy" {

  # CRITICAL: always grant root access so account admins cannot be locked out.
  statement {
    sid    = "EnableRootAccess"
    effect = "Allow"
    principals { type = "AWS"; identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] }
    actions    = ["kms:*"]
    resources  = ["*"]
  }

  # Key administrators: can manage the key (rotate, schedule deletion, add grants).
  # They CANNOT use the key for data encryption/decryption unless also listed in a use statement.
  statement {
    sid    = "AllowKeyAdministration"
    effect = "Allow"
    principals { type = "AWS"; identifiers = [var.key_admin_arn] }
    actions = [
      "kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*",
      "kms:Put*", "kms:Update*", "kms:Revoke*", "kms:Disable*",
      "kms:Get*", "kms:Delete*", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion"
    ]
    resources = ["*"]
  }

  # Key users: can encrypt and decrypt data using this key.
  statement {
    sid    = "AllowKeyUsage"
    effect = "Allow"
    principals { type = "AWS"; identifiers = var.key_user_arns }
    actions = [
      "kms:Encrypt", "kms:Decrypt", "kms:ReEncrypt*",
      "kms:GenerateDataKey*", "kms:DescribeKey"
    ]
    resources = ["*"]
  }

  # Allow grants — needed for services like Lambda to create grants programmatically.
  statement {
    sid    = "AllowGrantCreation"
    effect = "Allow"
    principals { type = "AWS"; identifiers = var.key_user_arns }
    actions   = ["kms:CreateGrant", "kms:ListGrants", "kms:RevokeGrant"]
    resources = ["*"]
    condition {
      test     = "Bool"
      variable = "kms:GrantIsForAWSResource"
      values   = ["true"]
    }
  }
}

# The KMS Customer Managed Key.
resource "aws_kms_key" "app" {
  description             = "Application encryption key — ${var.environment}"
  deletion_window_in_days = 30              # 30-day soft delete window (max is 30)
  enable_key_rotation     = true            # AWS rotates the backing key material annually
  multi_region            = false           # single-region key; set true for DR setups
  policy                  = data.aws_iam_policy_document.kms_policy.json

  tags = { Environment = var.environment, ManagedBy = "terraform", Purpose = "app-encryption" }
}

# Primary alias — short and used in application configuration.
resource "aws_kms_alias" "app" {
  name          = "alias/app-encryption-${var.environment}"
  target_key_id = aws_kms_key.app.key_id
}

# KMS Grant — gives the Lambda role decrypt permission without a key policy change.
# Grants are useful for ephemeral or cross-account access patterns.
resource "aws_kms_grant" "lambda_decrypt" {
  name              = "lambda-decrypt-grant"
  key_id            = aws_kms_key.app.key_id
  grantee_principal = var.lambda_role_arn
  operations        = ["Decrypt", "DescribeKey"]  # minimal permissions
}

outputs.tf

output "kms_key_arn"    { value = aws_kms_key.app.arn }
output "kms_key_id"     { value = aws_kms_key.app.key_id }
output "kms_alias_name" { value = aws_kms_alias.app.name }
output "grant_id"       { value = aws_kms_grant.lambda_decrypt.grant_id }

Workflow Commands

terraform init
terraform apply \
  -var="key_admin_arn=arn:aws:iam::123456789012:role/SecurityAdmin" \
  -var='key_user_arns=["arn:aws:iam::123456789012:role/AppRole"]' \
  -var="lambda_role_arn=arn:aws:iam::123456789012:role/LambdaRole"

# Encrypt a test value using the key alias
aws kms encrypt \
  --key-id $(terraform output -raw kms_alias_name) \
  --plaintext "Hello, KMS!" \
  --query CiphertextBlob --output text | base64 -d > encrypted.bin

# List grants on the key
aws kms list-grants --key-id $(terraform output -raw kms_key_id)

terraform destroy -var="key_admin_arn=..." -var='key_user_arns=["..."]' -var="lambda_role_arn=..."
✓ Expected Output After Apply
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

grant_id       = "0ab1c2d3e4f5678901234567890123456789abcdef0123456789abcdef01234567"
kms_alias_name = "alias/app-encryption-prod"
kms_key_arn    = "arn:aws:kms:us-east-1:123456789012:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
kms_key_id     = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"