diff --git a/terraform/implementation/ecs/ecs.sh b/terraform/implementation/ecs/ecs.sh index 245ae3d0..f184a54c 100755 --- a/terraform/implementation/ecs/ecs.sh +++ b/terraform/implementation/ecs/ecs.sh @@ -143,7 +143,7 @@ else fi if [ "$CI" = false ]; then - terraform apply -var-file="$ENVIRONMENT.tfvars" + terraform destroy -var-file="$ENVIRONMENT.tfvars" else - terraform apply -auto-approve -var-file="$ENVIRONMENT.tfvars" + terraform destroy -auto-approve -var-file="$ENVIRONMENT.tfvars" fi diff --git a/terraform/implementation/setup/iam.tf b/terraform/implementation/setup/iam.tf deleted file mode 100644 index 5774f835..00000000 --- a/terraform/implementation/setup/iam.tf +++ /dev/null @@ -1,126 +0,0 @@ -data "aws_caller_identity" "current" {} - -data "aws_iam_policy" "amazon_vpc_full_access" { - name = "AmazonVPCFullAccess" -} - -data "aws_iam_policy" "amazon_ec2_full_access" { - name = "AmazonEC2FullAccess" -} - -data "aws_iam_policy" "aws_appmesh_full_access" { - name = "AWSAppMeshFullAccess" -} - -data "aws_iam_policy" "amazon_dynamodb_full_access" { - name = "AmazonDynamoDBFullAccess" -} - -# no ecr, servicediscovery or ecs policies available - -data "aws_iam_policy" "elastic_load_balancing_full_access" { - name = "ElasticLoadBalancingFullAccess" -} - -data "aws_iam_policy" "aws_iam_full_access" { - name = "IAMFullAccess" -} - -data "aws_iam_policy" "aws_logs_full_access" { - name = "CloudWatchLogsFullAccess" -} - -data "aws_iam_policy" "aws_s3_full_access" { - name = "AmazonS3FullAccess" -} - -data "aws_iam_policy" "amazon_route53_full_access" { - name = "AmazonRoute53FullAccess" -} - -# # create a role that can be assumed to pull and push docker images from -data "aws_iam_policy_document" "github_assume_role" { - statement { - principals { - type = "Federated" - identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"] - } - actions = [ - "sts:AssumeRoleWithWebIdentity" - ] - condition { - test = "StringEquals" - variable = "token.actions.githubusercontent.com:aud" - values = ["sts.amazonaws.com", ] - } - condition { - test = "StringLike" - variable = "token.actions.githubusercontent.com:sub" - values = [ - "repo:${var.oidc_github_repo}:*", - ] - } - } -} - -# TODO: https://github.com/CDCgov/dibbs-aws/issues/8 -# trivy:ignore:AVD-AWS-0057 -data "aws_iam_policy_document" "github" { - statement { - actions = [ - "ecr:GetAuthorizationToken", - "ecr:BatchGetImage", - "ecr:BatchCheckLayerAvailability", - "ecr:CreateRepository", - "ecr:DescribeRepositories", - "ecr:DescribeImages", - "ecr:GetDownloadUrlForLayer", - "ecr:InitiateLayerUpload", - "ecr:ListTagsForResource", - "ecr:ListImages", - "ecr:PutImage", - "ecr:UploadLayerPart", - "ecr:CompleteLayerUpload", - "ecr:TagResource", - "ecs:CreateCluster", - "ecs:DescribeClusters", - "ecs:DescribeTaskDefinition", - "ecs:DescribeServices", - "ecs:UpdateService", - "ecs:TagResource", - "ecs:CreateService", - "ecs:RegisterTaskDefinition", - "servicediscovery:GetNamespace", - "servicediscovery:ListTagsForResource", - "servicediscovery:GetService", - "servicediscovery:CreatePrivateDnsNamespace", - "servicediscovery:TagResource", - "servicediscovery:GetOperation", - ] - resources = [ - "*" - ] - } -} - -resource "aws_iam_policy" "github" { - name = "${var.project}-github-policy-${var.owner}-${random_string.setup.result}" - policy = data.aws_iam_policy_document.github.json -} - -resource "aws_iam_role" "github" { - name = "${var.project}-github-role-${var.owner}-${random_string.setup.result}" - managed_policy_arns = [ - aws_iam_policy.github.arn, - data.aws_iam_policy.amazon_vpc_full_access.arn, - data.aws_iam_policy.amazon_ec2_full_access.arn, - data.aws_iam_policy.aws_appmesh_full_access.arn, - data.aws_iam_policy.amazon_dynamodb_full_access.arn, - data.aws_iam_policy.elastic_load_balancing_full_access.arn, - data.aws_iam_policy.aws_iam_full_access.arn, - data.aws_iam_policy.aws_logs_full_access.arn, - data.aws_iam_policy.aws_s3_full_access.arn, - data.aws_iam_policy.amazon_route53_full_access.arn, - ] - assume_role_policy = data.aws_iam_policy_document.github_assume_role.json -} diff --git a/terraform/implementation/setup/main.tf b/terraform/implementation/setup/main.tf index f427d16e..5a407e7e 100644 --- a/terraform/implementation/setup/main.tf +++ b/terraform/implementation/setup/main.tf @@ -11,6 +11,17 @@ provider "aws" { } } +# GitHub OIDC for prod +module "oidc" { + source = "../../modules/oidc" + + oidc_github_repo = var.oidc_github_repo + owner = var.owner + project = var.project + region = var.region + workspace = "prod" +} + resource "random_string" "setup" { length = 8 special = false @@ -69,7 +80,7 @@ resource "local_file" "setup_env" { BUCKET="${aws_s3_bucket.tfstate.bucket}" DYNAMODB_TABLE="${aws_dynamodb_table.tfstate_lock.id}" REGION="${var.region}" - TERRAFORM_ROLE="${aws_iam_role.github.arn}" + TERRAFORM_ROLE="${module.oidc.role.arn}" EOT filename = ".env" } @@ -79,7 +90,7 @@ resource "local_file" "ecs_env" { BUCKET="${aws_s3_bucket.tfstate.bucket}" DYNAMODB_TABLE="${aws_dynamodb_table.tfstate_lock.id}" REGION="${var.region}" - TERRAFORM_ROLE="${aws_iam_role.github.arn}" + TERRAFORM_ROLE="${module.oidc.role.arn}" EOT filename = "../ecs/.env" } diff --git a/terraform/implementation/setup/setup.sh b/terraform/implementation/setup/setup.sh index 4e7d59d4..f9800b2b 100755 --- a/terraform/implementation/setup/setup.sh +++ b/terraform/implementation/setup/setup.sh @@ -52,7 +52,7 @@ if ! grep -q "region" "$WORKSPACE.tfvars"; then fi if ! grep -q "oidc_github_repo" "$WORKSPACE.tfvars"; then - read -p "Are you using GitHub for your source control? (y/n): " github_choice + read -p "Do you want to setup a GitHub IODC role? (y/n): " github_choice if [[ "$github_choice" =~ ^[Yy]$ ]]; then read -p "What is the organization/repo value for assume role? ( default=\"\" ): " repo_choice repo_choice=${repo_choice:-""} diff --git a/terraform/modules/oidc/_data.tf b/terraform/modules/oidc/_data.tf new file mode 100644 index 00000000..e82b9a43 --- /dev/null +++ b/terraform/modules/oidc/_data.tf @@ -0,0 +1,274 @@ + +data "aws_caller_identity" "current" {} + +# # create a role that can be assumed to pull and push docker images from +data "aws_iam_policy_document" "github_assume_role" { + statement { + principals { + type = "Federated" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"] + } + actions = [ + "sts:AssumeRoleWithWebIdentity" + ] + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:aud" + values = ["sts.amazonaws.com", ] + } + condition { + test = "StringLike" + variable = "token.actions.githubusercontent.com:sub" + values = [ + "repo:${var.oidc_github_repo}:*", + ] + } + } +} + +# TODO: https://github.com/CDCgov/dibbs-aws/issues/8 +# trivy:ignore:AVD-AWS-0057 +data "aws_iam_policy_document" "tfstate" { + statement { + actions = [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:DeleteItem", + "s3:*" + ] + resources = [ + "*" + ] + } +} + +data "aws_iam_policy_document" "no_tags_get_actions" { + statement { + actions = [ + # get + "appmesh:ListTagsForResource", + "appmesh:DescribeVirtualNode", + "appmesh:DescribeMesh", + "appmesh:DescribeVirtuaNode", + "ec2:DescribeVpcs", + "ec2:DescribeVpcAttribute", + "ec2:DescribeSubnets", + "ec2:DescribeRouteTables", + "ec2:DescribeInternetGateways", + "ec2:DescribeSecurityGroups", + "ec2:DescribeNetworkAcls", + "ec2:DescribeFlowLogs", + "ec2:DescribeAddresses", + "ec2:DescribeSecurityGroupRules", + "ec2:DescribeAddressesAttribute", + "ec2:DescribeNatGateways", + "ec2:DescribeNetworkInterfaces", + "ecs:DescribeTaskDefinition", + "ecs:DescribeClusters", + "ecs:DescribeServices", + "ecr:GetAuthorizationToken", + "ecr:ListTagsForResource", + "ecr:DescribeRepositories", + "elasticloadbalancing:DescribeListeners", + "elasticloadbalancing:DescribeLoadBalancerAttributes", + "elasticloadbalancing:DescribeLoadBalancers", + "elasticloadbalancing:DescribeRules", + "elasticloadbalancing:DescribeTags", + "elasticloadbalancing:DescribeTargetGroupAttributes", + "elasticloadbalancing:DescribeTargetGroups", + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:ListPolicies", + "iam:ListAttachedRolePolicies", + "iam:ListRolePolicies", + "iam:ListPolicyVersions", + "iam:ListInstanceProfilesForRole", + "logs:DescribeLogGroups", + "logs:ListTagsLogGroup", + "logs:ListTagsForResource", + "logs:ListAttachedRolePolicies", + "servicediscovery:GetNamespace", + "servicediscovery:ListTagsForResource", + "servicediscovery:GetOperation", + # needs resources = ["*"] + "ec2:DisassociateAddress", + "ecs:DeregisterTaskDefinition", + "route53:CreateHostedZone", + ] + resources = [ + "*", + ] + } +} + +data "aws_iam_policy_document" "scoped_resources" { + statement { + actions = [ + # delete + "ec2:DeleteNetworkAclEntry", + + # update + "iam:PassRole", + + # create + "ec2:CreateSubnet", + "ec2:CreateFlowLogs", + "ec2:CreateSecurityGroup", + "ec2:CreateNetworkAclEntry", + "ec2:CreateRouteTable", + "ec2:CreateRoute", + "ec2:CreateNatGateway", + ] + resources = [ + "arn:aws:ec2:${var.region}:${data.aws_caller_identity.current.account_id}:vpc/${local.vpc_id}", + "arn:aws:ec2:${var.region}:${data.aws_caller_identity.current.account_id}:vpc-flow-log/*", + "arn:aws:ec2:${var.region}:${data.aws_caller_identity.current.account_id}:subnet/*", + "arn:aws:ec2:${var.region}:${data.aws_caller_identity.current.account_id}:route-table/*", + "arn:aws:ec2:${var.region}:${data.aws_caller_identity.current.account_id}:security-group/*", + "arn:aws:ec2:${var.region}:${data.aws_caller_identity.current.account_id}:network-acl/*", + "arn:aws:ec2:${var.region}:${data.aws_caller_identity.current.account_id}:elastic-ip/*", + "arn:aws:ec2:${var.region}:${data.aws_caller_identity.current.account_id}:natgateway/*", + "arn:aws:ecr:${var.region}:${data.aws_caller_identity.current.account_id}:repository/${local.project_owner_workspace}", + "arn:aws:ecr:${var.region}:${data.aws_caller_identity.current.account_id}:task-definition/*:${local.project_owner_workspace}", + "arn:aws:ecr:${var.region}:${data.aws_caller_identity.current.account_id}:route53:::hostedzone/${local.project_owner_workspace}", + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${local.project_owner_workspace}*", + ] + } +} + +data "aws_iam_policy_document" "request_tags_create_actions" { + statement { + actions = [ + "appmesh:CreateMesh", + "appmesh:DeleteMesh", + "appmesh:DeleteVirtualNode", + "appmesh:TagResource", + "appmesh:CreateVirtualNode", + "ec2:AllocateAddress", + "ec2:CreateInternetGateway", + "ec2:CreateTags", + "ec2:CreateRoute", + "ec2:CreateVpc", + "elasticloadbalancing:CreateLoadBalancer", + "elasticloadbalancing:CreateTargetGroup", + "elasticloadbalancing:CreateListener", + "elasticloadbalancing:CreateRule", + "ecs:CreateCluster", + "ecs:CreateService", + "ecr:CreateRepository", + "iam:CreateRole", + "iam:CreatePolicy", + "logs:CreateLogDelivery", + "logs:CreateLogGroup", + "servicediscovery:CreatePrivateDnsNamespace", + ] + resources = [ + "*", + ] + condition { + test = "StringEquals" + variable = "aws:RequestTag/workspace" + values = [ + var.project, + var.owner, + var.workspace + ] + } + } +} + +data "aws_iam_policy_document" "resource_tags_update_actions" { + statement { + actions = [ + "appmesh:TagResource", + "ec2:AttachInternetGateway", + "ec2:AuthorizeSecurityGroupEgress", + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RevokeSecurityGroupEgress", + "ec2:RevokeSecurityGroupIngress", + "ec2:AssociateRouteTable", + "ec2:ModifyVpcAttribute", + "elasticloadbalancing:AddTags", + "elasticloadbalancing:ModifyLoadBalancerAttributes", + "elasticloadbalancing:ModifyTargetGroupAttributes", + "elasticloadbalancing:RemoveTags", + "ecs:RegisterTaskDefinition", + "ecs:UpdateService", + "ecs:TagResource", + "ecs:UntagResource", + "ecs:ListTagsForResource", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + "ecr:PutImage", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:TagResource", + "ecr:UntagResource", + "iam:AttachRolePolicy", + "iam:TagRole", + "iam:TagPolicy", + "iam:UntagPolicy", + "logs:PutRetentionPolicy", + "servicediscovery:TagResource", + ] + resources = [ + "*", + ] + condition { + test = "StringEquals" + variable = "aws:ResourceTag/workspace" + values = [ + var.project, + var.owner, + var.workspace + ] + } + } +} + +data "aws_iam_policy_document" "resource_tags_delete_actions" { + statement { + actions = [ + "appmesh:DeleteMesh", + "appmesh:DeleteVirtualNode", + "ec2:DeleteFlowLogs", + "ec2:DeleteNatGateway", + "ec2:DeleteSecurityGroup", + "ec2:DeleteSubnet", + "ecs:DeleteCluster", + "ecs:DeleteService", + "ec2:DeleteVpc", + "ec2:DeleteTags", + "ec2:DisassociateRouteTable", + "ec2:DeleteRouteTable", + "ec2:DeleteRoute", + "ec2:ReleaseAddress", + "ec2:DetachInternetGateway", + "ec2:DeleteInternetGateway", + "ecr:DeleteRepository", + "elasticloadbalancing:DeleteLoadBalancer", + "elasticloadbalancing:DeleteTargetGroup", + "elasticloadbalancing:DeleteRule", + "elasticloadbalancing:DeleteListener", + "iam:DetachRolePolicy", + "iam:DeleteRole", + "iam:DeletePolicy", + "logs:DeleteLogGroup", + "servicediscovery:DeleteNamespace", + ] + resources = [ + "*", + ] + condition { + test = "StringEquals" + variable = "aws:ResourceTag/workspace" + values = [ + var.project, + var.owner, + var.workspace + ] + } + } +} diff --git a/terraform/modules/oidc/_local.tf b/terraform/modules/oidc/_local.tf new file mode 100644 index 00000000..576d4dd9 --- /dev/null +++ b/terraform/modules/oidc/_local.tf @@ -0,0 +1,7 @@ +locals { + github_role_name = "${var.project}-github-role-${var.owner}-${random_string.oidc.result}" + project_owner_workspace = "${var.project}-${var.owner}-${var.workspace}" + workspace = "${var.workspace}" + wildcard = "*" + vpc_id = var.vpc_id == "" ? local.wildcard : var.vpc_id +} \ No newline at end of file diff --git a/terraform/modules/oidc/_output.tf b/terraform/modules/oidc/_output.tf new file mode 100644 index 00000000..f2a8d8e7 --- /dev/null +++ b/terraform/modules/oidc/_output.tf @@ -0,0 +1,3 @@ +output "role" { + value = aws_iam_role.github +} \ No newline at end of file diff --git a/terraform/modules/oidc/_variable.tf b/terraform/modules/oidc/_variable.tf new file mode 100644 index 00000000..339e5380 --- /dev/null +++ b/terraform/modules/oidc/_variable.tf @@ -0,0 +1,35 @@ +variable "oidc_github_repo" { + description = "The GitHub repository for OIDC" + type = string + default = "" +} + +variable "owner" { + description = "The owner of the project" + type = string + default = "skylight" +} + +variable "project" { + description = "The name of the project" + type = string + default = "dibbs-ce" +} + +variable "region" { + type = string + description = "The AWS region where resources are created" + default = "" +} + +variable "workspace" { + default = "" + type = string + description = "terraform workspace that OIDC will have permissions to" +} + +variable "vpc_id" { + type = string + description = "ID of the VPC" + default = "" +} \ No newline at end of file diff --git a/terraform/modules/oidc/main.tf b/terraform/modules/oidc/main.tf new file mode 100644 index 00000000..b7c6e506 --- /dev/null +++ b/terraform/modules/oidc/main.tf @@ -0,0 +1,48 @@ +resource "random_string" "oidc" { + length = 8 + special = false + upper = false +} + +resource "aws_iam_policy" "no_tags_get_actions" { + name = "${var.project}-no-tags-policy-${var.owner}-${random_string.oidc.result}" + policy = data.aws_iam_policy_document.no_tags_get_actions.json +} + +resource "aws_iam_policy" "scoped_resources" { + name = "${var.project}-scoped-policy-${var.owner}-${random_string.oidc.result}" + policy = data.aws_iam_policy_document.scoped_resources.json +} + +resource "aws_iam_policy" "request_tags_create_actions" { + name = "${var.project}-request-tags-policy-${var.owner}-${random_string.oidc.result}" + policy = data.aws_iam_policy_document.request_tags_create_actions.json +} + +resource "aws_iam_policy" "resource_tags_update_actions" { + name = "${var.project}-resource-tags-update-policy-${var.owner}-${random_string.oidc.result}" + policy = data.aws_iam_policy_document.resource_tags_update_actions.json +} + +resource "aws_iam_policy" "resource_tags_delete_actions" { + name = "${var.project}-resource-tags-delete-policy-${var.owner}-${random_string.oidc.result}" + policy = data.aws_iam_policy_document.resource_tags_delete_actions.json +} + +resource "aws_iam_policy" "tfstate" { + name = "${var.project}-tfstate-policy-${var.owner}-${random_string.oidc.result}" + policy = data.aws_iam_policy_document.tfstate.json +} + +resource "aws_iam_role" "github" { + name = local.github_role_name + managed_policy_arns = [ + aws_iam_policy.no_tags_get_actions.arn, + aws_iam_policy.scoped_resources.arn, + aws_iam_policy.request_tags_create_actions.arn, + aws_iam_policy.resource_tags_update_actions.arn, + aws_iam_policy.resource_tags_delete_actions.arn, + aws_iam_policy.tfstate.arn, + ] + assume_role_policy = data.aws_iam_policy_document.github_assume_role.json +} \ No newline at end of file