How to restrict default access to KMS via key policy with Terraform
Tags: aws, kms, terraform, security
Published: 2024-03-31 23:49:59

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](https://dev.to/aws-builders/restrict-kms-access-with-terraform-3c18-temp-slug-3948918/edit) 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:
```json
{
    "Sid": "Enable IAM User Permissions",
    "Effect": "Allow",
    "Principal": {
        "AWS": "arn:aws:iam::123456789012:root"
    },
    "Action": "kms:*",
    "Resource": "*"
}
```

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:

```json
{
    "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": "*"
}
```

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.

```hcl
terraform {
  required_version = ">= 1.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0.0"
    }
  }
}
```

### 2. `main.tf` 

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

```hcl
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
}
```

### 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.

```hcl
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 = []
}
```

### 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.

```hcl
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
        }
      }
    }
  }
}
```

### 5. `outputs.tf`

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

```hcl
output arn {
  value       = aws_kms_key.this.arn
  description = "ARN KMS key"
}
```

### 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.

```hcl
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 = ["*"]
    }
]
```

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!](https://github.com/almeida-matheus/tf-security-enforcements/tree/master/aws/kms-policy).