Skip to content

Commit 8b2ffb9

Browse files
authored
Add config option to enable the encryption of AWS EKS secrets (#2788)
2 parents 364c9e3 + 5f39265 commit 8b2ffb9

File tree

8 files changed

+133
-0
lines changed

8 files changed

+133
-0
lines changed

src/_nebari/provider/cloud/amazon_web_services.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import re
44
import time
5+
from dataclasses import dataclass
56
from typing import Dict, List, Optional
67

78
import boto3
@@ -121,6 +122,35 @@ def instances(region: str) -> Dict[str, str]:
121122
return {t: t for t in instance_types}
122123

123124

125+
@dataclass
126+
class Kms_Key_Info:
127+
Arn: str
128+
KeyUsage: str
129+
KeySpec: str
130+
KeyManager: str
131+
132+
133+
@functools.lru_cache()
134+
def kms_key_arns(region: str) -> Dict[str, Kms_Key_Info]:
135+
"""Return dict of available/enabled KMS key IDs and associated KeyMetadata for the AWS region."""
136+
session = aws_session(region=region)
137+
client = session.client("kms")
138+
kms_keys = {}
139+
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kms/client/list_keys.html
140+
for key in client.list_keys().get("Keys"):
141+
key_id = key["KeyId"]
142+
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/kms/client/describe_key.html#:~:text=Response%20Structure
143+
key_data = client.describe_key(KeyId=key_id).get("KeyMetadata")
144+
if key_data.get("Enabled"):
145+
kms_keys[key_id] = Kms_Key_Info(
146+
Arn=key_data.get("Arn"),
147+
KeyUsage=key_data.get("KeyUsage"),
148+
KeySpec=key_data.get("KeySpec"),
149+
KeyManager=key_data.get("KeyManager"),
150+
)
151+
return kms_keys
152+
153+
124154
def aws_get_vpc_id(name: str, namespace: str, region: str) -> Optional[str]:
125155
"""Return VPC ID for the EKS cluster namedd `{name}-{namespace}`."""
126156
cluster_name = f"{name}-{namespace}"

src/_nebari/stages/infrastructure/__init__.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ class AWSInputVars(schema.Base):
174174
eks_endpoint_access: Optional[
175175
Literal["private", "public", "public_and_private"]
176176
] = "public"
177+
eks_kms_arn: Optional[str] = None
177178
node_groups: List[AWSNodeGroupInputVars]
178179
availability_zones: List[str]
179180
vpc_cidr_block: str
@@ -490,6 +491,7 @@ class AmazonWebServicesProvider(schema.Base):
490491
eks_endpoint_access: Optional[
491492
Literal["private", "public", "public_and_private"]
492493
] = "public"
494+
eks_kms_arn: Optional[str] = None
493495
existing_subnet_ids: Optional[List[str]] = None
494496
existing_security_group_id: Optional[str] = None
495497
vpc_cidr_block: str = "10.10.0.0/16"
@@ -546,6 +548,42 @@ def _check_input(cls, data: Any) -> Any:
546548
f"Amazon Web Services instance {node_group.instance} not one of available instance types={available_instances}"
547549
)
548550

551+
# check if kms key is valid
552+
available_kms_keys = amazon_web_services.kms_key_arns(data["region"])
553+
if "eks_kms_arn" in data and data["eks_kms_arn"] is not None:
554+
key_id = [
555+
id for id in available_kms_keys.keys() if id in data["eks_kms_arn"]
556+
]
557+
# Raise error if key_id is not found in available_kms_keys
558+
if (
559+
len(key_id) != 1
560+
or available_kms_keys[key_id[0]].Arn != data["eks_kms_arn"]
561+
):
562+
raise ValueError(
563+
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() if v.KeyManager=='CUSTOMER' and v.KeySpec=='SYMMETRIC_DEFAULT']}"
564+
)
565+
key_id = key_id[0]
566+
# Raise error if key is not a customer managed key
567+
if available_kms_keys[key_id].KeyManager != "CUSTOMER":
568+
raise ValueError(
569+
f"Amazon Web Services KMS Key with ID {key_id} is not a customer managed key"
570+
)
571+
# Symmetric KMS keys with Encrypt and decrypt key-usage have the SYMMETRIC_DEFAULT key-spec
572+
# EKS cluster encryption requires a Symmetric key that is set to encrypt and decrypt data
573+
if available_kms_keys[key_id].KeySpec != "SYMMETRIC_DEFAULT":
574+
if available_kms_keys[key_id].KeyUsage == "GENERATE_VERIFY_MAC":
575+
raise ValueError(
576+
f"Amazon Web Services KMS Key with ID {key_id} does not have KeyUsage set to 'Encrypt and decrypt' data"
577+
)
578+
elif available_kms_keys[key_id].KeyUsage != "ENCRYPT_DECRYPT":
579+
raise ValueError(
580+
f"Amazon Web Services KMS Key with ID {key_id} is not of type Symmetric, and KeyUsage not set to 'Encrypt and decrypt' data"
581+
)
582+
else:
583+
raise ValueError(
584+
f"Amazon Web Services KMS Key with ID {key_id} is not of type Symmetric"
585+
)
586+
549587
return data
550588

551589

@@ -835,6 +873,7 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]):
835873
name=self.config.escaped_project_name,
836874
environment=self.config.namespace,
837875
eks_endpoint_access=self.config.amazon_web_services.eks_endpoint_access,
876+
eks_kms_arn=self.config.amazon_web_services.eks_kms_arn,
838877
existing_subnet_ids=self.config.amazon_web_services.existing_subnet_ids,
839878
existing_security_group_id=self.config.amazon_web_services.existing_security_group_id,
840879
region=self.config.amazon_web_services.region,

src/_nebari/stages/infrastructure/template/aws/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ module "kubernetes" {
9999

100100
endpoint_public_access = var.eks_endpoint_access == "private" ? false : true
101101
endpoint_private_access = var.eks_endpoint_access == "public" ? false : true
102+
eks_kms_arn = var.eks_kms_arn
102103
public_access_cidrs = var.eks_public_access_cidrs
103104
permissions_boundary = var.permissions_boundary
104105
}

src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/main.tf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,20 @@ resource "aws_eks_cluster" "main" {
1414
public_access_cidrs = var.public_access_cidrs
1515
}
1616

17+
# Only set encryption_config if eks_kms_arn is not null
18+
dynamic "encryption_config" {
19+
for_each = var.eks_kms_arn != null ? [1] : []
20+
content {
21+
provider {
22+
key_arn = var.eks_kms_arn
23+
}
24+
resources = ["secrets"]
25+
}
26+
}
27+
1728
depends_on = [
1829
aws_iam_role_policy_attachment.cluster-policy,
30+
aws_iam_role_policy_attachment.cluster_encryption,
1931
]
2032

2133
tags = merge({ Name = var.name }, var.tags)

src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/policy.tf

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,33 @@ resource "aws_iam_role_policy_attachment" "cluster-policy" {
3232
role = aws_iam_role.cluster.name
3333
}
3434

35+
data "aws_iam_policy_document" "cluster_encryption" {
36+
count = var.eks_kms_arn != null ? 1 : 0
37+
statement {
38+
actions = [
39+
"kms:Encrypt",
40+
"kms:Decrypt",
41+
"kms:ListGrants",
42+
"kms:DescribeKey"
43+
]
44+
resources = [var.eks_kms_arn]
45+
}
46+
}
47+
48+
resource "aws_iam_policy" "cluster_encryption" {
49+
count = var.eks_kms_arn != null ? 1 : 0
50+
name = "${var.name}-eks-encryption-policy"
51+
description = "IAM policy for EKS cluster encryption"
52+
policy = data.aws_iam_policy_document.cluster_encryption[count.index].json
53+
}
54+
55+
# Grant the EKS Cluster role KMS permissions if a key-arn is specified
56+
resource "aws_iam_role_policy_attachment" "cluster_encryption" {
57+
count = var.eks_kms_arn != null ? 1 : 0
58+
policy_arn = aws_iam_policy.cluster_encryption[count.index].arn
59+
role = aws_iam_role.cluster.name
60+
}
61+
3562
# =======================================================
3663
# Kubernetes Node Group Policies
3764
# =======================================================

src/_nebari/stages/infrastructure/template/aws/modules/kubernetes/variables.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ variable "endpoint_private_access" {
7272
default = false
7373
}
7474

75+
variable "eks_kms_arn" {
76+
description = "kms key arn for EKS cluster encryption_config"
77+
type = string
78+
default = null
79+
}
80+
7581
variable "public_access_cidrs" {
7682
type = list(string)
7783
default = ["0.0.0.0/0"]

src/_nebari/stages/infrastructure/template/aws/variables.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ variable "eks_endpoint_private_access" {
6969
default = false
7070
}
7171

72+
variable "eks_kms_arn" {
73+
description = "kms key arn for EKS cluster encryption_config"
74+
type = string
75+
default = null
76+
}
77+
7278
variable "eks_public_access_cidrs" {
7379
type = list(string)
7480
default = ["0.0.0.0/0"]

tests/tests_unit/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ def _mock_return_value(return_value):
5656
"m5.xlarge": "m5.xlarge",
5757
"m5.2xlarge": "m5.2xlarge",
5858
},
59+
"_nebari.provider.cloud.amazon_web_services.kms_key_arns": {
60+
"xxxxxxxx-east-zzzz": {
61+
"Arn": "arn:aws:kms:us-east-1:100000:key/xxxxxxxx-east-zzzz",
62+
"KeyUsage": "ENCRYPT_DECRYPT",
63+
"KeySpec": "SYMMETRIC_DEFAULT",
64+
},
65+
"xxxxxxxx-west-zzzz": {
66+
"Arn": "arn:aws:kms:us-west-2:100000:key/xxxxxxxx-west-zzzz",
67+
"KeyUsage": "ENCRYPT_DECRYPT",
68+
"KeySpec": "SYMMETRIC_DEFAULT",
69+
},
70+
},
5971
# Azure
6072
"_nebari.provider.cloud.azure_cloud.kubernetes_versions": [
6173
"1.18",

0 commit comments

Comments
 (0)