DEV Community

Matheus Almeida Costa for AWS Community Builders

Posted on • Updated on

How to restrict default access to KMS via key policy with Terraform

The objective of this post is to implement KMS key access security for AWS Identity and Access Management (IAM) identities by changing the default policy when provisioning the resource with Terraform.

This is a practical example, so I first recommend recommend read this post to better understand the objective of restricted key policy.

Note: This post demonstrates the AWS account ID 123456789012 with existing role named TERRAFORM, ADMIN and ANALYST. These values must be replaced for your environment.

The default KMS key policy contains the following statement:

{
    "Sid": "Enable IAM User Permissions",
    "Effect": "Allow",
    "Principal": {
        "AWS": "arn:aws:iam::123456789012:root"
    },
    "Action": "kms:*",
    "Resource": "*"
}
Enter fullscreen mode Exit fullscreen mode

By default KMS policy allow caller's account to use IAM policy to control key access.

The Effect and Principal elements do not refer to the AWS root user account. Instead, it allows any principal in AWS account 123456789012 to have root access to the KMS key as long as you have attached the required permissions to the IAM entity.

The created terraform blueprint will come with the following custom policy by default:

{
    "Sid": "Enable root access and prevent permission delegation",
    "Effect": "Allow",
    "Principal": {
        "AWS": "arn:aws:iam::123456789012:root"
    },
    "Action": "kms:*",
    "Resource": "*",
    "Condition": {
        "StringEquals": {
            "aws:PrincipalType": "Account"
        }
    }
},
{
    "Sid": "Allow access for key administrators",
    "Effect": "Allow",
    "Principal": {
        "AWS": [
            "arn:aws:iam::123456789012:role/TERRAFORM",
            "arn:aws:iam::123456789012:role/ADMIN"
        ]
    },
    "Action": [
        "kms:Create*",
        "kms:Describe*",
        "kms:Enable*",
        "kms:List*",
        "kms:Put*",
        "kms:Update*",
        "kms:Revoke*",
        "kms:Disable*",
        "kms:Get*",
        "kms:Delete*",
        "kms:TagResource",
        "kms:UntagResource",
        "kms:ScheduleKeyDeletion",
        "kms:CancelKeyDeletion"
    ],
    "Resource": "*"
},
{
    "Sid": "Enable read access to all identities",
    "Effect": "Allow",
    "Principal": {
        "AWS": "arn:aws:iam::123456789012:root"
    },
    "Action": [
        "kms:List*",
        "kms:Get*",
        "kms:Describe*"
    ],
    "Resource": "*"
}
Enter fullscreen mode Exit fullscreen mode

The key policy allows the following permissions:

  • First statement: The AWS root user account has full access to the key.
  • Second statement: The principals role ADMIN and TERRAFORM has access to perform management operations on the key.
  • Third statement All account principals are able to read the key.

Terraform restrict KMS blueprint

1. versions.tf

Define Terraform versions and providers.

terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0.0"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

2. main.tf

Use the AWS provider in a region and create the KMS resource with your configuration parameters including your policy.

provider "aws" {
  region = "us-east-1"
}

resource "aws_kms_key" "this" {
  description = "Restricted kms key policy example"
  policy = data.aws_iam_policy_document.restricted_key_policy.json
}
Enter fullscreen mode Exit fullscreen mode

3. variables.tf

Defines the variable that will be responsible for the value of the new desired policy to be attached to the KMS policy.

variable key_policy {
  description = "key policy"
  type = list(object({
    sid           = optional(string)
    effect        = string
    actions       = list(string)
    resources     = list(string)
    principals  = optional(list(object({
      type        = string
      identifiers = list(string)
    })))
    conditions = optional(list(object({
      test = optional(string),
      variable = string,
      values = list(string)
    })))
  }))
  default = []
}
Enter fullscreen mode Exit fullscreen mode

4. policy.json

Create a rule with the new default policy based on the current account id and merge it with the custom policy passed by variable.

data "aws_caller_identity" "current" {}

locals {
  aws_account_id = data.aws_caller_identity.current.account_id
  # Default key policy to restrict AWS access
  default_key_policy = [
    {
      sid    = "Enable root access and prevent permission delegation"
      effect = "Allow"
      principals = [
        {
          type        = "AWS"
          identifiers = [local.aws_account_id]
        },
      ]
      actions   = ["kms:*"]
      resources = ["*"]
      conditions = [
        {
          test     = "StringEquals"
          variable = "aws:PrincipalType"
          values   = ["Account"]
        },
      ]
    },
    {
      sid    = "Allow access for key administrators"
      effect = "Allow"
      principals = [
        {
          type = "AWS"
          identifiers = [
            "arn:aws:iam::${local.aws_account_id}:role/TERRAFORM",
            "arn:aws:iam::${local.aws_account_id}:role/ADMIN"
          ]
        },
      ]
      actions   = [
        "kms:Create*",
        "kms:Describe*",
        "kms:Enable*",
        "kms:List*",
        "kms:Put*",
        "kms:Update*",
        "kms:Revoke*",
        "kms:Disable*",
        "kms:Get*",
        "kms:Delete*",
        "kms:TagResource",
        "kms:UntagResource",
        "kms:ScheduleKeyDeletion",
        "kms:CancelKeyDeletion"
    ],
      resources = ["*"]
    },
    {
      sid    = "Enable read access to all identities"
      effect = "Allow"
      principals = [
        {
          type        = "AWS"
          identifiers = [local.aws_account_id]
        },
      ]
      actions = [
        "kms:List*",
        "kms:Describe*",
        "kms:Get*",
      ]
      resources = ["*"]
    }
  ]
}

# Merge the default key policy with the new key policy
data "aws_iam_policy_document" "restricted_key_policy" {
  dynamic "statement" {
    for_each = concat(local.default_key_policy, var.key_policy)
    content {
      sid       = statement.value.sid
      effect    = statement.value.effect
      actions   = statement.value.actions
      resources = statement.value.resources

      dynamic "principals" {
        for_each = try(statement.value.principals, null) == null ? [] : statement.value.principals
        content {
          type        = principals.value.type
          identifiers = principals.value.identifiers
        }
      }

      dynamic "condition" {
        for_each = try(statement.value.conditions, null) == null ? [] : statement.value.conditions
        content {
          test     = condition.value.test
          variable = condition.value.variable
          values   = condition.value.values
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

5. outputs.tf

Defines the output of the kms arn value after its creation.

output arn {
  value       = aws_kms_key.this.arn
  description = "ARN KMS key"
}
Enter fullscreen mode Exit fullscreen mode

6. terraform.tfvars

Enter a custom policy for the purpose of creating the KMS, in this case I will create one just to allow the use of actions for a specific role.

key_policy = [
    {
      sid       = "Allow use of the key"
      effect    = "Allow"
      principals = [
        {
          type        = "AWS"
          identifiers = ["arn:aws:iam::123456789012:role/ANALYST"]
        },
      ]
      actions   = [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:ReEncrypt*",
        "kms:GenerateDataKey*",
        "kms:DescribeKey"
        ]
      resources = ["*"]
    }
]
Enter fullscreen mode Exit fullscreen mode

After apply, as a result you will always have a restricted KMS with the possibility of customizing it by changing the value of the key_policy variable, making it not only a secure blueprint but also scalable.

Check out the full code on GitHub!.

Top comments (1)

Collapse
 
acontreras_mp profile image
Armando Contreras • Edited

nice post. useful to not recreate al that code manully