diff --git a/src/tf/federated-identity.tf b/src/tf/federated-identity.tf new file mode 100644 index 0000000..1ddbc76 --- /dev/null +++ b/src/tf/federated-identity.tf @@ -0,0 +1,89 @@ +resource "aws_iam_openid_connect_provider" "sgfdevs_k3s" { + url = "https://k8s-oidc.sgf.dev" + + client_id_list = [ + "sts.amazonaws.com" + ] + + tags = { + Environment = "global" + ManagedBy = "OpenTofu" + Name = "sgfdevs-k3s-oidc" + Repository = "sgfdevs/infra-vm-workloads" + } +} + +resource "aws_iam_policy" "sgfdevs_k3s_kubernetes_workload_boundary" { + name = "SGFDevsK3sKubernetesWorkloadBoundary" + path = "/sgfdevs-k3s/" + description = "Require SGF Devs K3s workload roles to be used only by Kubernetes service accounts" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowWorkloadPermissions" + Effect = "Allow" + Action = "*" + Resource = "*" + }, + { + Sid = "DenyOtherFederatedProviders" + Effect = "Deny" + Action = "*" + Resource = "*" + Condition = { + ArnNotEquals = { + "aws:FederatedProvider" = aws_iam_openid_connect_provider.sgfdevs_k3s.arn + } + } + }, + { + Sid = "DenyNonServiceAccountSubjects" + Effect = "Deny" + Action = "*" + Resource = "*" + Condition = { + StringNotLike = { + "k8s-oidc.sgf.dev:sub" = "system:serviceaccount:*:*" + } + } + }, + { + Sid = "DenyUnexpectedAudience" + Effect = "Deny" + Action = "*" + Resource = "*" + Condition = { + StringNotEquals = { + "k8s-oidc.sgf.dev:aud" = "sts.amazonaws.com" + } + } + }, + { + Sid = "DenyPrivilegeEscalation" + Effect = "Deny" + Action = [ + "account:*", + "iam:*", + "identitystore:*", + "organizations:*", + "sso:*", + "sso-directory:*", + "sts:AssumeRole", + "sts:AssumeRoleWithSAML", + "sts:AssumeRoleWithWebIdentity", + "sts:GetFederationToken", + "sts:GetSessionToken", + ] + Resource = "*" + }, + ] + }) + + tags = { + Environment = "global" + ManagedBy = "OpenTofu" + Name = "sgfdevs-k3s-kubernetes-workload-boundary" + } +} diff --git a/src/tf/github-actions.tf b/src/tf/github-actions.tf index 1423a5b..f89aee6 100644 --- a/src/tf/github-actions.tf +++ b/src/tf/github-actions.tf @@ -1,14 +1,16 @@ module "github_actions" { source = "./modules/github-actions" - application_ses_policy_arns = local.application_ses_policy_arns - application_ses_senders = local.application_ses_senders - aws_account_id = data.aws_caller_identity.current.account_id - oidc_provider_arn = aws_iam_openid_connect_provider.github_actions.arn - ses_identity_arns = [for identity in aws_sesv2_email_identity.domain : identity.arn] - state_bucket_arn = aws_s3_bucket.tfstate_state.arn - state_bucket_name = aws_s3_bucket.tfstate_state.bucket - state_lock_table_arn = aws_dynamodb_table.tflock_state.arn + application_ses_policy_arns = local.application_ses_policy_arns + application_ses_senders = local.application_ses_senders + aws_account_id = data.aws_caller_identity.current.account_id + oidc_provider_arn = aws_iam_openid_connect_provider.github_actions.arn + ses_identity_arns = [for identity in aws_sesv2_email_identity.domain : identity.arn] + sgfdevs_k3s_oidc_provider_arn = aws_iam_openid_connect_provider.sgfdevs_k3s.arn + sgfdevs_k3s_workload_boundary_arn = aws_iam_policy.sgfdevs_k3s_kubernetes_workload_boundary.arn + state_bucket_arn = aws_s3_bucket.tfstate_state.arn + state_bucket_name = aws_s3_bucket.tfstate_state.bucket + state_lock_table_arn = aws_dynamodb_table.tflock_state.arn vm_workloads_bootstrap_parameter_arns = [ "arn:aws:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${local.sgfdevs_vms_eso_access_key_id_path}", "arn:aws:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${local.sgfdevs_vms_eso_secret_access_key_path}", diff --git a/src/tf/modules/github-actions/infra-vm-workloads.tf b/src/tf/modules/github-actions/infra-vm-workloads.tf index b4e9731..70d754b 100644 --- a/src/tf/modules/github-actions/infra-vm-workloads.tf +++ b/src/tf/modules/github-actions/infra-vm-workloads.tf @@ -1,3 +1,7 @@ +locals { + vm_workloads_workload_role_arn_pattern = "arn:aws:iam::${var.aws_account_id}:role/sgfdevs-k3s/*" +} + resource "aws_iam_role_policy" "github_actions_vm_workloads" { name = "InfraVMWorkloadsRepositoryAccess" role = aws_iam_role.github_actions["vm_workloads"].id @@ -35,6 +39,28 @@ resource "aws_iam_role_policy" "github_actions_vm_workloads" { Action = "ssm:DescribeParameters" Resource = "*" }, + { + Sid = "ReadSGFDevsK3sOIDCProvider" + Effect = "Allow" + Action = [ + "iam:GetOpenIDConnectProvider", + "iam:ListOpenIDConnectProviderTags", + ] + Resource = var.sgfdevs_k3s_oidc_provider_arn + }, + { + Sid = "ReadSGFDevsK3sWorkloadRole" + Effect = "Allow" + Action = [ + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListInstanceProfilesForRole", + "iam:ListRolePolicies", + "iam:ListRoleTags", + ] + Resource = local.vm_workloads_workload_role_arn_pattern + }, { Sid = "ManageWorkloadParametersFromMain" Effect = "Allow" @@ -49,6 +75,63 @@ resource "aws_iam_role_policy" "github_actions_vm_workloads" { } } }, + { + Sid = "CreateSGFDevsK3sWorkloadRoleFromMain" + Effect = "Allow" + Action = "iam:CreateRole" + Resource = local.vm_workloads_workload_role_arn_pattern + Condition = { + StringEquals = { + "iam:PermissionsBoundary" = var.sgfdevs_k3s_workload_boundary_arn + "token.actions.githubusercontent.com:sub" = "$${aws:PrincipalTag/GitHubMainSubject}" + } + } + }, + { + Sid = "ManageSGFDevsK3sWorkloadRoleTagsFromMain" + Effect = "Allow" + Action = [ + "iam:TagRole", + "iam:UntagRole", + ] + Resource = local.vm_workloads_workload_role_arn_pattern + Condition = { + "ForAllValues:StringEquals" = { + "aws:TagKeys" = [ + "KubernetesNamespace", + "KubernetesServiceAccount", + "ManagedBy", + "Repository", + ] + } + StringEquals = { + "token.actions.githubusercontent.com:sub" = "$${aws:PrincipalTag/GitHubMainSubject}" + } + StringEqualsIfExists = { + "aws:RequestTag/ManagedBy" = "OpenTofu" + "aws:RequestTag/Repository" = "sgfdevs/infra-vm-workloads" + } + } + }, + { + Sid = "ManageBoundedSGFDevsK3sWorkloadRoleFromMain" + Effect = "Allow" + Action = [ + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:PutRolePermissionsBoundary", + "iam:PutRolePolicy", + "iam:UpdateAssumeRolePolicy", + "iam:UpdateRole", + ] + Resource = local.vm_workloads_workload_role_arn_pattern + Condition = { + StringEquals = { + "iam:PermissionsBoundary" = var.sgfdevs_k3s_workload_boundary_arn + "token.actions.githubusercontent.com:sub" = "$${aws:PrincipalTag/GitHubMainSubject}" + } + } + }, ] }) } diff --git a/src/tf/modules/github-actions/variables.tf b/src/tf/modules/github-actions/variables.tf index 29ecb3a..ef59bba 100644 --- a/src/tf/modules/github-actions/variables.tf +++ b/src/tf/modules/github-actions/variables.tf @@ -27,6 +27,16 @@ variable "ses_identity_arns" { type = list(string) } +variable "sgfdevs_k3s_oidc_provider_arn" { + description = "ARN of the Kubernetes OIDC provider for the SGF Devs K3s cluster." + type = string +} + +variable "sgfdevs_k3s_workload_boundary_arn" { + description = "Permissions boundary ARN for SGF Devs K3s Kubernetes workload roles." + type = string +} + variable "state_bucket_arn" { description = "ARN of the shared OpenTofu state bucket." type = string diff --git a/src/tf/outputs.tf b/src/tf/outputs.tf index cbdb846..0260c3c 100644 --- a/src/tf/outputs.tf +++ b/src/tf/outputs.tf @@ -32,6 +32,16 @@ output "github_actions_vm_workloads_role_arn" { sensitive = true } +output "sgfdevs_k3s_oidc_provider_arn" { + description = "IAM OIDC provider ARN for the SGF Devs K3s cluster." + value = aws_iam_openid_connect_provider.sgfdevs_k3s.arn +} + +output "sgfdevs_k3s_workload_boundary_arn" { + description = "Permissions boundary ARN for SGF Devs K3s Kubernetes workload roles." + value = aws_iam_policy.sgfdevs_k3s_kubernetes_workload_boundary.arn +} + output "ses_email_identity_arns" { description = "ARNs of the SES identities used for outbound email." value = { for domain, identity in aws_sesv2_email_identity.domain : domain => identity.arn }