diff --git a/k8s/specs/tofu/data.tf b/k8s/specs/tofu/data.tf new file mode 100644 index 00000000..038d1e22 --- /dev/null +++ b/k8s/specs/tofu/data.tf @@ -0,0 +1,2 @@ +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} diff --git a/k8s/specs/tofu/locals.tf b/k8s/specs/tofu/locals.tf new file mode 100644 index 00000000..42a8cf3d --- /dev/null +++ b/k8s/specs/tofu/locals.tf @@ -0,0 +1,21 @@ +locals { + # Module identifier + iam_module_name = "iam" + + # Whether resources are created + iam_create = var.iam_create_role + + # Derived names (overridable via variables) + permissions_role_name = var.permissions_role_name != "" ? var.permissions_role_name : "nullplatform-${var.cluster_name}-agent-permissions-role" + policies_name_prefix = var.policies_name_prefix != "" ? var.policies_name_prefix : "nullplatform_${var.cluster_name}" + + # Primary agent role trusted by the permissions role. Defaults to the + # conventional agent role name for the cluster when not provided explicitly. + agent_role_arn = var.agent_role_arn != "" ? var.agent_role_arn : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/nullplatform-${var.cluster_name}-agent-role" + + # Default tags applied to every IAM resource + iam_default_tags = merge(var.iam_resource_tags_json, { + ManagedBy = "nullplatform-custom-scope-role" + Module = local.iam_module_name + }) +} diff --git a/k8s/specs/tofu/main.tf b/k8s/specs/tofu/main.tf new file mode 100644 index 00000000..9b4d63c6 --- /dev/null +++ b/k8s/specs/tofu/main.tf @@ -0,0 +1,166 @@ +################################################################################ +# IAM permissions role assumed by the nullplatform agent role +# +# Holds the actual workload policies (Route53, EKS, ELB). Its trust policy +# trusts only the agent IRSA role (plus any additional roles), so an agent's IRSA +# token cannot exercise these permissions without first assuming it (sts:AssumeRole). +# +# This is the "permissions role" half of the reference module +# tofu-modules/infrastructure/aws/iam/agent. The IRSA agent role itself is +# created once at cluster setup and is out of scope for this module. +################################################################################ + +resource "aws_iam_role" "nullplatform_agent_permissions" { + count = local.iam_create ? 1 : 0 + + name = local.permissions_role_name + description = "Permissions role assumed by the nullplatform agent role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { AWS = concat([local.agent_role_arn], var.additional_agent_role_arns) } + Action = "sts:AssumeRole" + }] + }) + + tags = local.iam_default_tags +} + +################################################################################ +# Policy attachments +################################################################################ + +resource "aws_iam_role_policy_attachment" "permissions_route53" { + count = local.iam_create ? 1 : 0 + + role = aws_iam_role.nullplatform_agent_permissions[0].name + policy_arn = aws_iam_policy.nullplatform_route53_policy[0].arn +} + +resource "aws_iam_role_policy_attachment" "permissions_eks" { + count = local.iam_create ? 1 : 0 + + role = aws_iam_role.nullplatform_agent_permissions[0].name + policy_arn = aws_iam_policy.nullplatform_eks_policy[0].arn +} + +resource "aws_iam_role_policy_attachment" "permissions_elb" { + count = local.iam_create ? 1 : 0 + + role = aws_iam_role.nullplatform_agent_permissions[0].name + policy_arn = aws_iam_policy.nullplatform_elb_policy[0].arn +} + +################################################################################ +# Route 53 IAM policy +# Manage Route 53 DNS records for service discovery. +################################################################################ + +resource "aws_iam_policy" "nullplatform_route53_policy" { + count = local.iam_create ? 1 : 0 + + name = "${local.policies_name_prefix}_route53_policy" + description = "Policy for managing Route 53 DNS records" + tags = local.iam_default_tags + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Action" : [ + "route53:ChangeResourceRecordSets", + "route53:ListResourceRecordSets", + "route53:GetHostedZone", + "route53:ListHostedZones", + "route53:ListHostedZonesByName" + ], + "Resource" : [ + "arn:aws:route53:::hostedzone/*" + ], + } + ] + }) +} + +################################################################################ +# Elastic Load Balancing (ELB) IAM policy +# Describe and monitor load balancers and target groups. +################################################################################ + +resource "aws_iam_policy" "nullplatform_elb_policy" { + count = local.iam_create ? 1 : 0 + + name = "${local.policies_name_prefix}_elb_policy" + description = "Policy for managing Elastic Load Balancing resources" + tags = local.iam_default_tags + policy = jsonencode( + { + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Action" : [ + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeTargetGroups" + ], + "Resource" : "*", + "Condition" : { + "StringEquals" : { + "aws:RequestedRegion" : [ + data.aws_region.current.region + ] + } + } + }, + { + "Effect" : "Allow", + "Action" : [ + "elasticloadbalancing:DescribeTargetHealth", + "elasticloadbalancing:DescribeListeners", + "elasticloadbalancing:DescribeRules" + ], + "Resource" : [ + "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/k8s-nullplatform-*", + "arn:aws:elasticloadbalancing:*:*:targetgroup/k8s-nullplatform-*" + ], + } + ] + } + ) +} + +################################################################################ +# EKS IAM policy +# Describe and list EKS cluster resources. +################################################################################ + +resource "aws_iam_policy" "nullplatform_eks_policy" { + count = local.iam_create ? 1 : 0 + + name = "${local.policies_name_prefix}_eks_policy" + description = "Policy for managing EKS cluster resources" + tags = local.iam_default_tags + policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Action" : [ + "eks:DescribeCluster", + "eks:ListClusters", + "eks:DescribeNodegroup", + "eks:ListNodegroups", + "eks:DescribeAddon", + "eks:ListAddons" + ], + "Resource" : [ + "arn:aws:eks:*:*:cluster/*", + "arn:aws:eks:*:*:nodegroup/*", + "arn:aws:eks:*:*:addon/*" + ], + } + ] + }) +} diff --git a/k8s/specs/tofu/outputs.tf b/k8s/specs/tofu/outputs.tf new file mode 100644 index 00000000..2e750313 --- /dev/null +++ b/k8s/specs/tofu/outputs.tf @@ -0,0 +1,14 @@ +output "permissions_role_arn" { + description = "ARN of the permissions role assumed by the nullplatform agent role" + value = local.iam_create ? aws_iam_role.nullplatform_agent_permissions[0].arn : "" +} + +output "permissions_role_name" { + description = "Name of the permissions role" + value = local.iam_create ? aws_iam_role.nullplatform_agent_permissions[0].name : "" +} + +output "permissions_role_id" { + description = "ID of the permissions role" + value = local.iam_create ? aws_iam_role.nullplatform_agent_permissions[0].id : "" +} diff --git a/k8s/specs/tofu/variables.tf b/k8s/specs/tofu/variables.tf new file mode 100644 index 00000000..84838ca4 --- /dev/null +++ b/k8s/specs/tofu/variables.tf @@ -0,0 +1,50 @@ +variable "agent_role_arn" { + description = "ARN of the primary nullplatform agent IRSA role allowed to assume this permissions role via sts:AssumeRole, and always a trusted principal of the role's trust policy. Defaults (when empty) to the conventional agent role for the cluster: arn:aws:iam:::role/nullplatform--agent-role." + type = string + default = "" + + validation { + condition = var.agent_role_arn == "" || can(regex("^arn:aws:iam::[0-9]{12}:role/.+", var.agent_role_arn)) + error_message = "agent_role_arn must be empty (to use the derived default) or match arn:aws:iam:::role/" + } +} + +variable "additional_agent_role_arns" { + description = "Extra IAM role ARNs allowed to assume this permissions role, appended to agent_role_arn in the trust policy. Defaults to none." + type = list(string) + default = [] + + validation { + condition = alltrue([for arn in var.additional_agent_role_arns : can(regex("^arn:aws:iam::[0-9]{12}:role/.+", arn))]) + error_message = "each additional_agent_role_arns entry must match arn:aws:iam:::role/" + } +} + +variable "cluster_name" { + description = "Name of the cluster where the agent runs. Used to derive default resource names." + type = string +} + +variable "permissions_role_name" { + description = "Override for the permissions IAM role name. Defaults to nullplatform-{cluster_name}-agent-permissions-role." + type = string + default = "" +} + +variable "policies_name_prefix" { + description = "Override for the IAM policy name prefix. Defaults to nullplatform_{cluster_name}." + type = string + default = "" +} + +variable "iam_create_role" { + description = "Whether to create the permissions role and its policies. When false, the module produces no resources." + type = bool + default = true +} + +variable "iam_resource_tags_json" { + description = "Tags to apply to IAM resources created by this module." + type = map(string) + default = {} +} diff --git a/k8s/specs/tofu/versions.tf b/k8s/specs/tofu/versions.tf new file mode 100644 index 00000000..e54c7789 --- /dev/null +++ b/k8s/specs/tofu/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + # v6+ required: the ELB policy reads data.aws_region.current.region, + # which replaced the v5 `.name` attribute. + version = ">= 6.0" + } + } +}