diff --git a/iam.tf b/iam.tf index e51b3c8..a10c8b6 100644 --- a/iam.tf +++ b/iam.tf @@ -13,8 +13,9 @@ locals { data "aws_partition" "current" {} resource "aws_iam_role" "this" { - name = "${local.name}-instance" - description = "Role assumed by EC2 instance(s) running altinity/cloud-connect" + name = "${local.name}-instance" + description = "Role assumed by EC2 instance(s) running altinity/cloud-connect" + permissions_boundary = var.enable_permissions_boundary ? one(aws_iam_policy.altinity-permission-boundary).arn : null assume_role_policy = jsonencode({ Version = "2012-10-17", Statement = [ @@ -32,7 +33,6 @@ resource "aws_iam_role" "this" { resource "aws_iam_role_policy" "this" { name = "${aws_iam_role.this.name}-policy" role = aws_iam_role.this.id - policy = jsonencode({ Version = "2012-10-17", Statement = [ @@ -105,9 +105,8 @@ resource "aws_iam_role_policy" "altinity_break_glass_policy" { count = var.allow_altinity_access ? 1 : 0 name = "${aws_iam_role.altinity_break_glass[count.index].name}-policy" role = aws_iam_role.altinity_break_glass[count.index].id - policy = jsonencode({ - Version = "2012-10-17", + Version = "2012-10-17", Statement = [ { Effect = "Allow", diff --git a/iam_pb.tf b/iam_pb.tf new file mode 100644 index 0000000..e59f766 --- /dev/null +++ b/iam_pb.tf @@ -0,0 +1,320 @@ +data "aws_iam_policy_document" "permissions-boundary-policy" { + count = var.enable_permissions_boundary ? 1 : 0 + + statement { + sid = "DescribeResourcesInRegion" + actions = [ + "ec2:Describe*", + "autoscaling:Describe*", + "elasticloadbalancing:Describe*", + "route53:ListHostedZonesByVPC" + ] + resources = ["*"] + } + + statement { + sid = "MessageGatewayServiceInRegion" + actions = [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + ] + resources = ["*"] + } + + statement { + sid = "EnvRequestTagBasedAccess" + actions = [ + "ec2:CreateVpc", + "ec2:CreateInternetGateway", + "ec2:CreateRoute", + "ec2:CreateRouteTable", + "ec2:CreateVpcEndpoint", + "ec2:CreateSubnet", + "ec2:RunInstances", + "ec2:CreateLaunchTemplate", + "ec2:CreateVolume", + "ec2:CreateNetworkInterface", + "ec2:CreateSecurityGroup", + "ec2:AllocateAddress", + "ec2:CreateNatGateway", + "ec2:CreateVpcEndpointServiceConfiguration", + "ec2:CreateVpcPeeringConnection", + ] + resources = ["*"] + condition { + test = "ForAnyValue:StringEquals" + values = [local.env_name] + variable = "aws:RequestTag/altinity:cloud/env" + } + } + + statement { + effect = "Deny" + sid = "DenyTagsModificationOnNonManagedResources" + actions = [ + "ec2:CreateTags", + ] + resources = ["*"] + condition { + test = "ForAnyValue:StringNotEquals" + values = [local.env_name] + variable = "aws:ResourceTag/altinity:cloud/env" + } + } + + statement { + sid = "EnvCreateRequestTagBasedAccess" + actions = [ + "ec2:CreateTags", + ] + resources = ["*"] + condition { + test = "ForAnyValue:StringEquals" + values = [ + "CreateVpc", + "CreateInternetGateway", + "CreateRoute", + "CreateRouteTable", + "CreateVpcEndpoint", + "CreateSubnet", + "RunInstances", + "CreateLaunchTemplate", + "CreateVolume", + "CreateNetworkInterface", + "CreateSecurityGroup", + "AllocateAddress", + "CreateNatGateway", + "CreateVpcEndpointServiceConfiguration", + "CreateVpcPeeringConnection" + ] + variable = "ec2:CreateAction" + } + } + + + statement { + sid = "EnvResourceTagBasedAccess" + actions = [ + "ssm:*", + "ec2:*", + "eks:*", + "iam:*", + "ssm:*", + "lambda:*", + "autoscaling:*", + "elasticloadbalancing:*" + ] + resources = ["*"] + condition { + test = "ForAnyValue:StringEquals" + values = [local.env_name] + variable = "aws:ResourceTag/altinity:cloud/env" + } + } + + statement { + sid = "EKSPodIdentity" + actions = [ + "eks-auth:AssumeRoleForPodIdentity" + ] + resources = [ + "arn:aws:eks:${local.region}:${local.account_id}:cluster/${local.resource_prefix}" + ] + } + + statement { + sid = "EKSDescribeCluster" + actions = [ + "eks:DescribeCluster" + ] + resources = [ + "arn:aws:eks:${local.region}:${local.account_id}:cluster/${local.resource_prefix}" + ] + } + + statement { + sid = "EKSNodePoolsAMIs" + actions = [ + "ec2:RunInstances" + ] + resources = ["arn:aws:ec2:${local.region}::image/ami-*"] + condition { + test = "ForAnyValue:StringEquals" + values = ["amazon"] + variable = "ec2:Owner" + } + } + + statement { + sid = "EKSNodesImages" + actions = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ] + resources = ["*"] + } + + statement { + sid = "EKSOpenIDConnectProvider" + actions = [ + "iam:GetOpenIDConnectProvider", + ] + resources = ["arn:aws:iam::${local.account_id}:oidc-provider/oidc.eks.${local.region}.amazonaws.com/id/*"] + } + + statement { + sid = "EKSNodeGroups" + actions = [ + "eks:DescribeNodegroup", + ] + resources = ["arn:aws:eks:${local.region}:${local.account_id}:nodegroup/${local.resource_prefix}/*"] + } + + statement { + sid = "EKSAutoscalingGroups" + actions = [ + "autoscaling:DescribeAutoScalingGroups", + "autoscaling:CreateOrUpdateTags", + ] + resources = ["*"] + condition { + test = "ForAnyValue:StringEquals" + values = [local.resource_prefix] + variable = "aws:ResourceTag/eks:cluster-name" + } + } + + statement { + sid = "EKSTagSecurityGroup" + actions = [ + "ec2:CreateTags" + ] + resources = ["arn:aws:ec2:${local.region}:${local.account_id}:security-group/*"] + condition { + test = "ForAnyValue:StringEquals" + values = [local.resource_prefix] + variable = "aws:ResourceTag/aws:eks:cluster-name" + } + } + + statement { + sid = "EKSIAMRole" + actions = [ + "iam:GetRole", + ] + resources = [ + "arn:aws:iam::${local.account_id}:role/aws-service-role/eks-nodegroup.amazonaws.com/AWSServiceRoleForAmazonEKSNodegroup" + ] + } + + statement { + sid = "S3" + actions = [ + "s3:*", + ] + resources = ["arn:aws:s3:::${local.resource_prefix}*"] + } + + statement { + sid = "Lambda" + actions = [ + "lambda:*", + ] + resources = [ + "arn:aws:lambda:${local.region}:${local.account_id}:function:${local.resource_prefix}*" + ] + } + + // Not possible to set boundary until EKS lambda is replaced + statement { + sid = "LambdaNetworkInterface" + actions = [ + "ec2:CreateNetworkInterface", + "ec2:DeleteNetworkInterface", + ] + resources = ["*"] + } + + statement { + sid = "EnvAssumeAndPassCreatedRoles" + actions = [ + "sts:AssumeRole", + "sts:AssumeRoleWithWebIdentity", + "iam:PassRole", + ] + resources = [ + "arn:aws:iam::${local.account_id}:role/${local.resource_prefix}*" + ] + } + + statement { + sid = "EnvIAMEntities" + actions = [ + "iam:*" + ] + resources = [ + "arn:aws:iam::${local.account_id}:role/${local.resource_prefix}*", + "arn:aws:iam::${local.account_id}:user/${local.resource_prefix}*", + "arn:aws:iam::${local.account_id}:instance-profile/${local.resource_prefix}*", + "arn:aws:iam::${local.account_id}:policy/${local.resource_prefix}*", + ] + } + + statement { + sid = "RequirePermissionBoundaryForCreatedRoles" + actions = [ + "iam:CreateRole", + "iam:AttachRolePolicy", + "iam:PutRolePermissionsBoundary", + "iam:PutRolePolicy", + ] + resources = [ + "arn:aws:iam::${local.account_id}:role/${local.resource_prefix}*" + ] + condition { + test = "StringEquals" + variable = "iam:PermissionsBoundary" + values = [ + "arn:aws:iam::${local.account_id}:policy/${local.permissions_boundary_policy_name}" + ] + } + } + + statement { + sid = "DenyPermissionBoundaryChanges" + effect = "Deny" + actions = [ + "iam:CreatePolicyVersion", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:SetDefaultPolicyVersion" + ] + resources = [ + "arn:aws:iam::${local.account_id}:policy/${local.permissions_boundary_policy_name}" + ] + } + + dynamic "statement" { + for_each = var.allow_altinity_access ? [1] : [] + content { + sid = "BreakGlass" + actions = [ + "ssm:StartSession", + ] + resources = [ + "arn:aws:ssm:*:*:document/SSM-SessionManagerRunShell" + ] + } + } +} + +resource "aws_iam_policy" "altinity-permission-boundary" { + count = var.enable_permissions_boundary ? 1 : 0 + name = local.permissions_boundary_policy_name + description = "Altinity permission boundary for env ${local.env_name}" + policy = one(data.aws_iam_policy_document.permissions-boundary-policy).json +} diff --git a/main.tf b/main.tf index 84017a6..ae16a96 100644 --- a/main.tf +++ b/main.tf @@ -1,17 +1,39 @@ -locals { - ami_name = var.ami_name != "" ? var.ami_name : "al2023-ami-2023.2.20231113.0-kernel-6.1-${data.aws_ec2_instance_type.current.supported_architectures[0]}" +data "aws_ssm_parameter" "this" { + count = var.pem_ssm_parameter_name != "" ? 1 : 0 + name = var.pem_ssm_parameter_name +} + +data "tls_certificate" "env_pem" { + content = var.pem_ssm_parameter_name != "" ? one(data.aws_ssm_parameter.this).value : var.pem +} +data "aws_region" "current" {} + +data "aws_caller_identity" "current" {} + +locals { + env_name = regex("CN=([^,]+)", data.tls_certificate.env_pem.certificates[0].subject)[0] + ami_name = (var.ami_name != "" ? var.ami_name : + "al2023-ami-2023.2.20231113.0-kernel-6.1-${data.aws_ec2_instance_type.current.supported_architectures[0]}") name = "altinitycloud-connect-${random_id.this.hex}" tags = merge(var.tags, { - Name = local.name + Name = local.name + "altinity:cloud/env" = local.env_name }) + region = var.region != "" ? var.region : data.aws_region.current.name + account_id = var.aws_account_id != "" ? var.aws_account_id : data.aws_caller_identity.current.account_id } resource "random_id" "this" { byte_length = 7 } -data "aws_region" "current" {} +resource "random_string" "resource_prefix" { + count = var.enable_permissions_boundary ? 1 : 0 + length = 8 + special = false + upper = false +} data "aws_ec2_instance_type" "current" { instance_type = var.instance_type @@ -43,12 +65,17 @@ resource "aws_ssm_parameter" "this" { name = "${local.name}-secret" type = "String" value = var.pem - tier = "Advanced" # value is over 4kb + tier = "Intelligent-Tiering" + tags = local.tags } -data "aws_ssm_parameter" "this" { - count = var.pem_ssm_parameter_name != "" ? 1 : 0 - name = var.pem_ssm_parameter_name + +locals { + resource_prefix_base = (length(local.env_name) > 8 ? + "${substr(local.env_name, 0, 4)}${substr(local.env_name, length(local.env_name) - 4, 4)}" : local.env_name) + resource_prefix = (var.enable_permissions_boundary ? + "${local.resource_prefix_base}-${one(random_string.resource_prefix).result}" : null) + permissions_boundary_policy_name = var.enable_permissions_boundary ? "${local.env_name}-boundary" : null } resource "aws_launch_template" "this" { @@ -61,7 +88,6 @@ resource "aws_launch_template" "this" { network_interfaces { associate_public_ip_address = var.map_public_ip_on_launch } - vpc_security_group_ids = length(var.ec2_security_group_ids) > 0 ? var.ec2_security_group_ids : null block_device_mappings { device_name = "/dev/xvda" @@ -74,10 +100,12 @@ resource "aws_launch_template" "this" { } user_data = base64encode( templatefile("${path.module}/user-data.sh.tpl", { - image = var.image, - ssm_parameter_name = var.pem_ssm_parameter_name != "" ? data.aws_ssm_parameter.this[0].name : aws_ssm_parameter.this[0].name - url = var.url - + image = var.image, + ssm_parameter_name = (var.pem_ssm_parameter_name != "" ? data.aws_ssm_parameter.this[0].name : + aws_ssm_parameter.this[0].name) + url = var.url + ca_crt = var.ca_crt + host_aliases = var.host_aliases asg_name = local.name asg_hook_name = "launch" }) @@ -98,17 +126,30 @@ resource "aws_autoscaling_group" "this" { max_size = 3 launch_template { id = aws_launch_template.this.id - version = "$Latest" + version = aws_launch_template.this.latest_version } initial_lifecycle_hook { name = "launch" lifecycle_transition = "autoscaling:EC2_INSTANCE_LAUNCHING" - heartbeat_timeout = "420" // 8m + heartbeat_timeout = "420" default_result = "ABANDON" } + instance_refresh { + strategy = "Rolling" + } + wait_for_capacity_timeout = "7m" vpc_zone_identifier = length(var.subnets) > 0 ? var.subnets : ( var.use_default_subnets ? data.aws_subnets.default[0].ids : aws_subnet.this.*.id ) + + dynamic "tag" { + for_each = local.tags + content { + key = tag.key + value = tag.value + propagate_at_launch = true + } + } } diff --git a/output.tf b/output.tf new file mode 100644 index 0000000..8ed6ccc --- /dev/null +++ b/output.tf @@ -0,0 +1,9 @@ +output "resource_prefix" { + value = var.enable_permissions_boundary ? local.resource_prefix : null + description = "AWS resource prefix, only set if permission boundary is enabled" +} + +output "permissions_boundary_policy_arn" { + value = var.enable_permissions_boundary ? one(aws_iam_policy.altinity-permission-boundary).arn : null + description = "The ARN of the permission boundary policy" +} diff --git a/user-data.sh.tpl b/user-data.sh.tpl index 75c2800..6d75f96 100644 --- a/user-data.sh.tpl +++ b/user-data.sh.tpl @@ -19,9 +19,18 @@ on_exit() { trap on_exit EXIT mkdir -p /etc/altinitycloud + aws ssm get-parameter --name "${ssm_parameter_name}" --with-decryption --query "Parameter.Value" --output text > /etc/altinitycloud/cloud-connect.pem -docker run -d --name=altinitycloud-connect --restart=always -v /etc/altinitycloud:/etc/altinitycloud:rw --network=host "${image}" \ - --url=${url} -i /etc/altinitycloud/cloud-connect.pem --capability aws + +%{ if ca_crt != "" } +echo "${ca_crt}" > /etc/altinitycloud/ca.pem +%{ endif } + +docker run -d --name=altinitycloud-connect --restart=always -v /etc/altinitycloud:/etc/altinitycloud:rw --network=host \ + %{ for host, alias in host_aliases } --add-host="${host}:${alias}" %{ endfor } "${image}" \ + --url=${url} -i /etc/altinitycloud/cloud-connect.pem %{ if ca_crt != "" } --ca-crt=/etc/altinitycloud/ca.pem %{ endif } \ + --capability aws + aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id "$instance" \ --lifecycle-hook-name ${asg_hook_name} --auto-scaling-group-name ${asg_name} diff --git a/variables.tf b/variables.tf index 469acbc..9de6527 100644 --- a/variables.tf +++ b/variables.tf @@ -20,6 +20,20 @@ EOT default = "" } +variable "ca_crt" { + type = string + description = <