diff --git a/src/_nebari/provider/cloud/amazon_web_services.py b/src/_nebari/provider/cloud/amazon_web_services.py index 1123c07fe..3a491c8b2 100644 --- a/src/_nebari/provider/cloud/amazon_web_services.py +++ b/src/_nebari/provider/cloud/amazon_web_services.py @@ -121,6 +121,30 @@ def instances(region: str) -> Dict[str, str]: return {t: t for t in instance_types} +@functools.lru_cache() +def kms_key_arns(region: str) -> Dict[str, dict]: + """Return dict of available/enabled KMS key IDs and associated KeyMetadata for the AWS region.""" + session = aws_session(region=region) + client = session.client("kms") + paginator = client.get_paginator("list_keys") + fields = [ + "Arn", + "KeyUsage", + "KeySpec", + # "KeyState", + # "Origin", + # "KeyManager", + # "EncryptionAlgorithms", + # "MultiRegion", + ] + kms_keys = [ + client.describe_key(KeyId=j["KeyId"]).get("KeyMetadata") + for i in paginator.paginate() + for j in i["Keys"] + ] + return {i["KeyId"]: {k: i[k] for k in fields} for i in kms_keys if i["Enabled"]} + + def aws_get_vpc_id(name: str, namespace: str, region: str) -> Optional[str]: """Return VPC ID for the EKS cluster namedd `{name}-{namespace}`.""" cluster_name = f"{name}-{namespace}" diff --git a/src/_nebari/stages/infrastructure/__init__.py b/src/_nebari/stages/infrastructure/__init__.py index 3e77f9bee..d64fadcea 100644 --- a/src/_nebari/stages/infrastructure/__init__.py +++ b/src/_nebari/stages/infrastructure/__init__.py @@ -182,6 +182,7 @@ class AWSInputVars(schema.Base): eks_endpoint_access: Optional[ Literal["private", "public", "public_and_private"] ] = "public" + eks_kms_arn: Optional[str] = None node_groups: List[AWSNodeGroupInputVars] availability_zones: List[str] vpc_cidr_block: str @@ -498,6 +499,7 @@ class AmazonWebServicesProvider(schema.Base): eks_endpoint_access: Optional[ Literal["private", "public", "public_and_private"] ] = "public" + eks_kms_arn: Optional[str] = None existing_subnet_ids: Optional[List[str]] = None existing_security_group_id: Optional[str] = None vpc_cidr_block: str = "10.10.0.0/16" @@ -554,6 +556,37 @@ def _check_input(cls, data: Any) -> Any: f"Amazon Web Services instance {node_group.instance} not one of available instance types={available_instances}" ) + # check if kms key is valid + available_kms_keys = amazon_web_services.kms_key_arns(data["region"]) + if "eks_kms_arn" in data and data["eks_kms_arn"] is not None: + key_id = [ + id for id in available_kms_keys.keys() if id in data["eks_kms_arn"] + ] + if ( + len(key_id) == 1 + and available_kms_keys[key_id[0]]["Arn"] == data["eks_kms_arn"] + ): + key_id = key_id[0] + # Symmetric KMS keys with Encrypt and decrypt key-usage have the SYMMETRIC_DEFAULT key-spec + # EKS cluster encryption requires a Symmetric key that is set to encrypt and decrypt data + if available_kms_keys[key_id]["KeySpec"] != "SYMMETRIC_DEFAULT": + if available_kms_keys[key_id]["KeyUsage"] == "GENERATE_VERIFY_MAC": + raise ValueError( + f"Amazon Web Services KMS Key with ID {key_id} does not have KeyUsage set to 'Encrypt and decrypt' data" + ) + elif available_kms_keys[key_id]["KeyUsage"] != "ENCRYPT_DECRYPT": + raise ValueError( + f"Amazon Web Services KMS Key with ID {key_id} is not of type Symmetric, and KeyUsage not set to 'Encrypt and decrypt' data" + ) + else: + raise ValueError( + f"Amazon Web Services KMS Key with ID {key_id} is not of type Symmetric" + ) + else: + raise ValueError( + f"Amazon Web Services KMS Key with ARN {data['eks_kms_arn']} not one of available/enabled keys={[v['Arn'] for v in available_kms_keys.values()]}" + ) + return data @@ -843,6 +876,7 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): name=self.config.escaped_project_name, environment=self.config.namespace, eks_endpoint_access=self.config.amazon_web_services.eks_endpoint_access, + eks_kms_arn=self.config.amazon_web_services.eks_kms_arn, existing_subnet_ids=self.config.amazon_web_services.existing_subnet_ids, existing_security_group_id=self.config.amazon_web_services.existing_security_group_id, region=self.config.amazon_web_services.region, diff --git a/src/_nebari/stages/infrastructure/template/aws/main.tf b/src/_nebari/stages/infrastructure/template/aws/main.tf index feffd3529..ec0cbb660 100644 --- a/src/_nebari/stages/infrastructure/template/aws/main.tf +++ b/src/_nebari/stages/infrastructure/template/aws/main.tf @@ -99,6 +99,7 @@ module "kubernetes" { endpoint_public_access = var.eks_endpoint_access == "private" ? false : true endpoint_private_access = var.eks_endpoint_access == "public" ? false : true + eks_kms_arn = var.eks_kms_arn public_access_cidrs = var.eks_public_access_cidrs permissions_boundary = var.permissions_boundary } diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/main.tf b/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/main.tf index 5b66201f8..1f3e688e6 100644 --- a/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/main.tf +++ b/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/main.tf @@ -14,8 +14,20 @@ resource "aws_eks_cluster" "main" { public_access_cidrs = var.public_access_cidrs } + # Only set encryption_config if eks_kms_arn is not null + dynamic "encryption_config" { + for_each = var.eks_kms_arn != null ? [1] : [] + content { + provider { + key_arn = var.eks_kms_arn + } + resources = ["secrets"] + } + } + depends_on = [ aws_iam_role_policy_attachment.cluster-policy, + aws_iam_role_policy_attachment.cluster_encryption, ] tags = merge({ Name = var.name }, var.tags) diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/policy.tf b/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/policy.tf index 6916bc653..d72b64eda 100644 --- a/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/policy.tf +++ b/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/policy.tf @@ -32,6 +32,33 @@ resource "aws_iam_role_policy_attachment" "cluster-policy" { role = aws_iam_role.cluster.name } +data "aws_iam_policy_document" "cluster_encryption" { + count = var.eks_kms_arn != null ? 1 : 0 + statement { + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ListGrants", + "kms:DescribeKey" + ] + resources = [var.eks_kms_arn] + } +} + +resource "aws_iam_policy" "cluster_encryption" { + count = var.eks_kms_arn != null ? 1 : 0 + name = "${var.name}-eks-encryption-policy" + description = "IAM policy for EKS cluster encryption" + policy = data.aws_iam_policy_document.cluster_encryption[count.index].json +} + +# Grant the EKS Cluster role KMS permissions if a key-arn is specified +resource "aws_iam_role_policy_attachment" "cluster_encryption" { + count = var.eks_kms_arn != null ? 1 : 0 + policy_arn = aws_iam_policy.cluster_encryption[count.index].arn + role = aws_iam_role.cluster.name +} + # ======================================================= # Kubernetes Node Group Policies # ======================================================= diff --git a/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/variables.tf b/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/variables.tf index 4d38d10a1..63558e550 100644 --- a/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/variables.tf +++ b/src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/variables.tf @@ -72,6 +72,12 @@ variable "endpoint_private_access" { default = false } +variable "eks_kms_arn" { + description = "kms key arn for EKS cluster encryption_config" + type = string + default = null +} + variable "public_access_cidrs" { type = list(string) default = ["0.0.0.0/0"] diff --git a/src/_nebari/stages/infrastructure/template/aws/variables.tf b/src/_nebari/stages/infrastructure/template/aws/variables.tf index a3f37b9eb..a71df81d0 100644 --- a/src/_nebari/stages/infrastructure/template/aws/variables.tf +++ b/src/_nebari/stages/infrastructure/template/aws/variables.tf @@ -69,6 +69,12 @@ variable "eks_endpoint_private_access" { default = false } +variable "eks_kms_arn" { + description = "kms key arn for EKS cluster encryption_config" + type = string + default = null +} + variable "eks_public_access_cidrs" { type = list(string) default = ["0.0.0.0/0"]