diff --git a/README.md b/README.md index 7bb791d..e51adac 100644 --- a/README.md +++ b/README.md @@ -98,11 +98,9 @@ To run this app locally, follow these steps: It's in `./deployment/cloud` and we're using [OpenTofu](https://opentofu.org/) -### Credentials +### Overview of AWS account setup, including information on getting credentials -For now, contact Niki for least-privileged IAM user credentials. These will be used to assume the necessary roles. ([TODO as part of this issue](https://github.com/uw-ssec/post-disaster-comms/issues/61): find a better way to distribute these) - -This user will assume the necessary roles to get things going +Please see our information [here](./deployment/cloud/aws/README.md) ### Editing deployment/values.cloud.yaml diff --git a/deployment/cloud/aws/README.md b/deployment/cloud/aws/README.md new file mode 100644 index 0000000..9fc5532 --- /dev/null +++ b/deployment/cloud/aws/README.md @@ -0,0 +1,86 @@ +# AWS Infrastructure Information + +## Info + +Infrastructure has been split into two separate stacks, with different considerations and ownership for each + +### `account` - Account-level configurations accessible by account owners and admistrators + +Everything in here is the foundation needed for everything else to work. + +This is intended to be run extremely rarely by a user with elevated permissions. + +It configures the S3 bucket that stores the terraform state, as well as the `deploy` IAM role that is assumed to deploy everything in `infrastructure` + +New resources should only be added here if they would be needed to set up a brand new account. + +### `infrastructure` - AWS resources and configuration to run the Support Sphere project + +This is where all the "real" resources needed to run the Support Sphere cloud server configurations -- server setup, IAM roles to run operational scripts, + +Every resource created here will be named starting with the "resource prefix", a combination of the project name (`supportsphere`) and the neighborhood for which this infrastructure is created. An example resource prefix is `supportsphere-laurelhurst`. + +Every resource here will also be tagged with the project name and neighborhood. + +This is probably where you want to add new resources. + +## Create and update account-level infrastructure + +#### Initialize the account-setup infrastructure on your machine + +From the `tofu init` docs + +> This is the first command that should be run for any new or existing + OpenTofu configuration per machine. This sets up all the local data + necessary to run OpenTofu that is typically not committed to version + control. + +``` +pixi run cloud-account-init +``` + +### View differences between live and local changes + +``` +pixi run cloud-account-plan +``` + +### Deploy configurations + +``` +pixi run cloud-account-deploy +``` + +## AWS user administration + +### Create users that can interact with ops scripts and deploy resources in `infrastructure` + +``` +pixi run cloud-account-user-controls add -u +``` + +This command creates a new IAM User `-assumer`, attaches them to the `ssec-eng` user group, creates access keys for the user to access the CLI, and prints out commands for someone to configure their AWS CLI with credentials for the new user. + +This should be run by an account owner or admin on behalf of an engineer who will work to deploy & maintain this infrastructure. + +The account owner/admin will run this command, and send the output to the engineer. + +### Revoke access to a user + +``` +pixi run cloud-account-user-controls delete -u +``` + +### Rotate a user's access keys + +Useful if existing keys have leaked but you don't want to outright delete the associate IAM user + +``` +pixi run cloud-account-user-controls rotate -u +``` + +### List all existing access users in the `ssec-eng` group and their associated access keys + +``` +pixi run cloud-account-user-controls list +``` \ No newline at end of file diff --git a/deployment/cloud/aws/account/.terraform.lock.hcl b/deployment/cloud/aws/account/.terraform.lock.hcl new file mode 100644 index 0000000..4fc96b5 --- /dev/null +++ b/deployment/cloud/aws/account/.terraform.lock.hcl @@ -0,0 +1,20 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "5.67.0" + constraints = "~> 5.61" + hashes = [ + "h1:6COU3BqSt/ysJO34vUx/UeCGnfJjnw8oaiX2v+b3cPE=", + "zh:009a21cda9ed0fca0605007e65fa417689d61e78c163482a945aa7966e75ab42", + "zh:086708088bbbe395edb6a11033cf7aec0b1e67efe9e879d21b6d63cbf08fcf7c", + "zh:3244b81ec778f00d31eff37393de2e5b986c6ee9c2f50267aae396e1104b3ff3", + "zh:3e4b1fcafc93e54a27cd0e8e49460c373a95982c42006ac06a7b38f0886a13d3", + "zh:4c53aa1c44c29ba84a9aab587fba67777818e48f7d2171ae62c220a76e5b1504", + "zh:5b623e4d9c4ae0ff2c535a40ce02f52b6f962239a69aea58df88753cc6ad77cc", + "zh:66f1532a8e9008454cbf313b5fd352859672f7d1baae4497445ed41038153bfa", + "zh:9e459e11ab435a48ca14d47132ed5875e0447c4f005da2992e487e57893257a4", + "zh:9fb97365442c60503690bae75101cbf8de64ae43a6714fd243d7d3c7f378e539", + "zh:afe4e1f57eac19a0a0cdc2f10d382fc9076f7ff5b7476bfaf3776a74a1a6cf3e", + ] +} diff --git a/deployment/cloud/aws/account/main.tf b/deployment/cloud/aws/account/main.tf new file mode 100644 index 0000000..a2b09e2 --- /dev/null +++ b/deployment/cloud/aws/account/main.tf @@ -0,0 +1,177 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.61" + } + } + + backend "s3" { + bucket = "${var.account_resource_prefix}-${var.account_id}-opentofu-state" + key = "account/terraform.tfstate" + region = "us-west-2" + } +} + +provider "aws" { + region = "us-west-2" + + // Tags all resources created from this provider with {"Project": , "Neighborhood": } + // as well as any additional tags provided + default_tags { + tags = merge( + var.account_additional_tags + ) + } +} + +# s3 tf state bucket + +resource "aws_s3_bucket" "tf_state" { + bucket = "${var.account_resource_prefix}-${var.account_id}-opentofu-state" +} + +resource "aws_s3_bucket_versioning" "this" { + bucket = aws_s3_bucket.tf_state.bucket + + versioning_configuration { + status = "Enabled" + } + +} + +resource "aws_s3_bucket_public_access_block" "example" { + bucket = aws_s3_bucket.tf_state.bucket + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# deploy role +resource "aws_iam_role" "deploy" { + name = "${var.account_resource_prefix}-deploy" + description = "Role used to deploy infrastructure for the Support Sphere app, part of the Post-Disaster communications project." + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = "arn:aws:iam::${var.account_id}:root" + }, + Action = "sts:AssumeRole" + } + ] + }) + + managed_policy_arns = [ + "arn:aws:iam::aws:policy/AmazonEC2FullAccess", + "arn:aws:iam::aws:policy/AmazonVPCFullAccess", + "arn:aws:iam::aws:policy/AutoScalingFullAccess", + "arn:aws:iam::aws:policy/IAMFullAccess", + "arn:aws:iam::aws:policy/ReadOnlyAccess", + "arn:aws:iam::aws:policy/ResourceGroupsandTagEditorFullAccess", + ] +} + +resource "aws_iam_role_policy" "deploy_bucket_access" { + name = "${var.account_resource_prefix}_access_state_from_s3_new" + role = aws_iam_role.deploy.name + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + "s3:ListBucket", + ], + Resource = [ + "${aws_s3_bucket.tf_state.arn}/*", + aws_s3_bucket.tf_state.arn, + ], + }, + ], + }) +} + +resource "aws_iam_role_policy" "kms_key_access" { + name = "${var.account_resource_prefix}_kms_key_admin" + role = aws_iam_role.deploy.name + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "kms:TagResource", + "kms:UntagResource", + "kms:UpdateKeyDescription", + "kms:CreateKey", + "kms:CreateAlias", + ], + Resource = "*", + }, + ], + }) +} + +resource "aws_iam_role_policy" "disallow_deploy_role_iam_user_operations" { + name = "${var.account_resource_prefix}_disallow_deploy_role_iam_user_operations" + role = aws_iam_role.deploy.name + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Deny", + Action = [ + "iam:CreateUser", + "iam:DeleteUser", + "iam:UpdateUser", + "iam:CreateLoginProfile", + "iam:DeleteLoginProfile", + "iam:UpdateLoginProfile", + "iam:CreateAccessKey", + "iam:DeleteAccessKey", + "iam:UpdateAccessKey", + "iam:AttachUserPolicy", + "iam:DetachUserPolicy", + "iam:PutUserPolicy", + "iam:DeleteUserPolicy", + "iam:UpdateUserPolicy", + ], + Resource = "*", + } + ] + }) +} + +# user group +resource "aws_iam_group" "this" { + # TODO: decide if we want to use the account_resource_prefix here + name = var.ops_group_name +} + +resource "aws_iam_group_policy" "assume_deploy" { + name = "assume-deploy-role" + group = aws_iam_group.this.name + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = "sts:AssumeRole", + Resource = aws_iam_role.deploy.arn, + }, + ], + }) +} + +resource "aws_iam_group_policy_attachment" "readonly" { + group = aws_iam_group.this.name + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess" +} diff --git a/deployment/cloud/aws/account/terraform.tfvars b/deployment/cloud/aws/account/terraform.tfvars new file mode 100644 index 0000000..d1343a3 --- /dev/null +++ b/deployment/cloud/aws/account/terraform.tfvars @@ -0,0 +1,6 @@ +account_resource_prefix = "supportsphere" +account_additional_tags = { + "Project" = "Support Sphere" +} +account_id = "871683513797" +ops_group_name = "ssec-eng" \ No newline at end of file diff --git a/deployment/cloud/aws/account/variables.tf b/deployment/cloud/aws/account/variables.tf new file mode 100644 index 0000000..5d25270 --- /dev/null +++ b/deployment/cloud/aws/account/variables.tf @@ -0,0 +1,21 @@ +variable "account_resource_prefix" { + description = "Prefix to apply to all account resources" + type = string +} + +variable "account_id" { + description = "The AWS account ID" + type = string +} + +variable "account_additional_tags" { + description = "Additional tags to apply to resources" + type = map(string) + default = {} +} + +variable "ops_group_name" { + description = "The name of the admin group" + type = string + default = "ssec-eng" +} \ No newline at end of file diff --git a/deployment/cloud/.terraform.lock.hcl b/deployment/cloud/aws/infrastructure/.terraform.lock.hcl similarity index 100% rename from deployment/cloud/.terraform.lock.hcl rename to deployment/cloud/aws/infrastructure/.terraform.lock.hcl diff --git a/deployment/cloud/main.tf b/deployment/cloud/aws/infrastructure/main.tf similarity index 68% rename from deployment/cloud/main.tf rename to deployment/cloud/aws/infrastructure/main.tf index 2bb2914..fc3d76a 100644 --- a/deployment/cloud/main.tf +++ b/deployment/cloud/aws/infrastructure/main.tf @@ -7,7 +7,7 @@ terraform { } backend "s3" { - bucket = "${local.resource_prefix}-${var.account_id}-opentofu-state" + bucket = "${local.account_resource_prefix}-${var.account_id}-opentofu-state" key = "infrastructure/${var.stage}/terraform.tfstate" region = "us-west-2" } @@ -16,8 +16,10 @@ terraform { locals { // The resource prefix is a combination of the project name and neighborhood // This will be used to name all resources created by this module + account_resource_prefix = lower(trimspace(replace(var.project_name, " ", ""))) + resource_prefix = join("-", - [lower(trimspace(replace(var.project_name, " ", ""))), + [local.account_resource_prefix, lower(trimspace(replace(var.neighborhood, " ", "")))] ) } @@ -45,15 +47,23 @@ provider "aws" { provider "aws" { alias = "east" region = "us-east-1" + + default_tags { + tags = merge({ + Project = var.project_name, + Neighborhood = var.neighborhood + }, + var.additional_tags + ) + } } -module "server" { - source = "./modules/server" +module "keys" { + source = "./modules/keys" resource_prefix = local.resource_prefix - instance_type = var.instance_type stage = var.stage - volume_size = var.volume_size + ops_group_name = var.ops_group_name providers = { aws = aws @@ -61,6 +71,26 @@ module "server" { } } +module "server" { + source = "./modules/server" + + resource_prefix = local.resource_prefix + instance_type = var.instance_type + stage = var.stage + volume_size = var.volume_size + kms_key_arn_west = module.keys.kms_key_arn_west + kms_key_arn_east = module.keys.kms_key_arn_east +} + +module "ops-roles" { + source = "./modules/ops-roles" + + resource_prefix = local.resource_prefix + stage = var.stage + ops_group_name = var.ops_group_name + autoscaling_group_arn = module.server.autoscaling_group_arn +} + resource "aws_resourcegroups_group" "this" { name = "${local.resource_prefix}-group" diff --git a/deployment/cloud/aws/infrastructure/modules/keys/.terraform.lock.hcl b/deployment/cloud/aws/infrastructure/modules/keys/.terraform.lock.hcl new file mode 100644 index 0000000..d250303 --- /dev/null +++ b/deployment/cloud/aws/infrastructure/modules/keys/.terraform.lock.hcl @@ -0,0 +1,20 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "5.68.0" + constraints = "~> 5.61" + hashes = [ + "h1:qnGTurtBw5Lbeqvvwjx/fwTFhoek+JSrZ+XtZgCul0U=", + "zh:0501ccb379b74832366860699ca6d5993b164ec44314a054453877d39c384869", + "zh:315b4eb957f84ce5580fed31e4b99b25d41634832a6939cd016fb0c4963164c9", + "zh:31defa4c379a4f1761504617824bae1b5efc93f456f055f85d1131676433085d", + "zh:3702a13f06369ee90eea413ec32db6ffa9c59648b3545301f9917f6774a840cb", + "zh:7c524cb809267ec68dd67124aa8d9fbab7722814fa875b1306d527f71b8b3bea", + "zh:ab37ec8b17be8062d804c17f5f4ddd9deaf50b3a48e6c0b979b60ef80f85192b", + "zh:baaf2c46edfe596f085f0f8f389e908a874e45c42ea5e5d5f24de1dbfed7542e", + "zh:cb37278073ede7b5e18116faebea49d5d47496d5093cec6c69065fb9ad1f622d", + "zh:ec4b64d66470b078162c13479446ad6819c93099149b478f43d990702f937fd3", + "zh:f55c3a3ba975ecfe73c729a085efb0432c02c74e91edaf40d351cdb231c3836b", + ] +} diff --git a/deployment/cloud/aws/infrastructure/modules/keys/main.tf b/deployment/cloud/aws/infrastructure/modules/keys/main.tf new file mode 100644 index 0000000..a843865 --- /dev/null +++ b/deployment/cloud/aws/infrastructure/modules/keys/main.tf @@ -0,0 +1,67 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.61" + configuration_aliases = [ aws.east ] + } + } +} + +data "aws_region" "west" {} + +data "aws_region" "east" { + provider = aws.east +} + +# KMS keys for encryption + +resource "aws_kms_key" "west" { + description = "Key in us-west-2 for encrypting and decrypting server config values for the Support Sphere project." + key_usage = "ENCRYPT_DECRYPT" +} + +resource "aws_kms_alias" "west" { + name = "alias/${var.resource_prefix}-kms-key-${data.aws_region.west.name}" + target_key_id = aws_kms_key.west.key_id +} + +resource "aws_kms_key" "east" { + provider = aws.east + description = "Key in us-east-1 for encrypting and decrypting server config values for the Support Sphere project." + key_usage = "ENCRYPT_DECRYPT" +} + +resource "aws_kms_alias" "east" { + provider = aws.east + name = "alias/${var.resource_prefix}-kms-key-${data.aws_region.east.name}" + target_key_id = aws_kms_key.east.key_id +} + +# KMS key attachment to group policy + +data "aws_iam_group" "this" { + group_name = var.ops_group_name +} + +resource "aws_iam_group_policy" "this" { + name = "${var.resource_prefix}-kms-key-policy" + group = var.ops_group_name + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "kms:Encrypt", + "kms:Decrypt" + ], + Resource = [ + aws_kms_alias.west.target_key_arn, + aws_kms_alias.east.target_key_arn + ] + } + ] + }) +} \ No newline at end of file diff --git a/deployment/cloud/aws/infrastructure/modules/keys/outputs.tf b/deployment/cloud/aws/infrastructure/modules/keys/outputs.tf new file mode 100644 index 0000000..c93f311 --- /dev/null +++ b/deployment/cloud/aws/infrastructure/modules/keys/outputs.tf @@ -0,0 +1,7 @@ +output "kms_key_arn_west" { + value = aws_kms_key.west.arn +} + +output "kms_key_arn_east" { + value = aws_kms_key.east.arn +} \ No newline at end of file diff --git a/deployment/cloud/aws/infrastructure/modules/keys/variables.tf b/deployment/cloud/aws/infrastructure/modules/keys/variables.tf new file mode 100644 index 0000000..da98f73 --- /dev/null +++ b/deployment/cloud/aws/infrastructure/modules/keys/variables.tf @@ -0,0 +1,19 @@ +variable "resource_prefix" { + description = "The resource prefix is a combination of the project name and neighborhood. This will be used to name all resources in this module." + type = string +} + +variable "stage" { + description = "Which stage this infrastructure's being deployed to - dev, beta, prod, etc." + type = string + + validation { + condition = can(regex("^(dev|beta|prod)$", var.stage)) + error_message = "Stage must be one of dev, beta, or prod" + } +} + +variable "ops_group_name" { + description = "The name of the admin group" + type = string +} \ No newline at end of file diff --git a/deployment/cloud/aws/infrastructure/modules/ops-roles/main.tf b/deployment/cloud/aws/infrastructure/modules/ops-roles/main.tf new file mode 100644 index 0000000..02145b0 --- /dev/null +++ b/deployment/cloud/aws/infrastructure/modules/ops-roles/main.tf @@ -0,0 +1,130 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.61" + } + } +} + +data "aws_region" "this" {} + +data "aws_default_tags" "this" {} + +data "aws_caller_identity" "this" {} + +data "aws_iam_group" "this" { + group_name = var.ops_group_name +} + + +locals { + roles_to_create = { + "scaling-role" = { + policy_statements = [ + { + Effect = "Allow", + Action = [ + "autoscaling:SetDesiredCapacity" + ], + Resource = var.autoscaling_group_arn + }, + { + Effect = "Allow", + Action = [ + "autoscaling:DescribeAutoScalingGroups" + ], + Resource = "*" + } + ] + }, + "server-access-role" = { + policy_statements = [ + { + Effect = "Allow", + Action = [ + "ssm:StartSession" + ], + Resource = [ + "arn:aws:ssm:${data.aws_region.this.name}::document/AWS-StartInteractiveCommand", + "arn:aws:ec2:${data.aws_region.this.name}:${data.aws_caller_identity.this.account_id}:instance/*" + ] + }, + { + Effect = "Allow", + Action = [ + "ec2:DescribeInstances" + ], + Resource = "*" + } + ] + } + } +} + +resource "aws_iam_role" "ops_roles" { + for_each = local.roles_to_create + name = "${var.resource_prefix}-${each.key}" + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.this.account_id}:root" + }, + Action = "sts:AssumeRole" + } + ] + }) + + inline_policy { + name = "${var.resource_prefix}_${each.key}_policy" + policy = jsonencode({ + Version = "2012-10-17", + Statement = each.value.policy_statements + }) + } + + # dynamic "inline_policy" { + # for_each = each.value.policy_statements + # content { + # name = "${var.resource_prefix}_${each.key}_policy" + # policy = jsonencode({ + # Version = "2012-10-17", + # Statement = inline_policy.value + # }) + # } + # } +} + + +moved { + from = aws_iam_role.scaling + to = aws_iam_role.ops_roles["scaling-role"] +} + +moved { + from = aws_iam_role.access + to = aws_iam_role.ops_roles["server-access-role"] +} + +// Allow the group to assume each role defined here + +resource "aws_iam_group_policy" "assume_ops_roles" { + for_each = aws_iam_role.ops_roles + name = "${var.resource_prefix}-assume-${each.key}-policy" + group = var.ops_group_name + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = "sts:AssumeRole", + Resource = each.value.arn + } + ] + }) +} + diff --git a/deployment/cloud/aws/infrastructure/modules/ops-roles/outputs.tf b/deployment/cloud/aws/infrastructure/modules/ops-roles/outputs.tf new file mode 100644 index 0000000..e69de29 diff --git a/deployment/cloud/modules/server/variables.tf b/deployment/cloud/aws/infrastructure/modules/ops-roles/variables.tf similarity index 75% rename from deployment/cloud/modules/server/variables.tf rename to deployment/cloud/aws/infrastructure/modules/ops-roles/variables.tf index bddfc65..c1b71b2 100644 --- a/deployment/cloud/modules/server/variables.tf +++ b/deployment/cloud/aws/infrastructure/modules/ops-roles/variables.tf @@ -13,13 +13,12 @@ variable "stage" { } } -variable "instance_type" { - description = "The instance type to use for the server" +variable "ops_group_name" { + description = "The name of the admin group" type = string } -variable "volume_size" { - description = "The instance volume size to use for the server" +variable "autoscaling_group_arn" { + description = "The ARN of the autoscaling group for the server" type = string - } \ No newline at end of file diff --git a/deployment/cloud/modules/server/main.tf b/deployment/cloud/aws/infrastructure/modules/server/main.tf similarity index 62% rename from deployment/cloud/modules/server/main.tf rename to deployment/cloud/aws/infrastructure/modules/server/main.tf index af845fc..426582b 100644 --- a/deployment/cloud/modules/server/main.tf +++ b/deployment/cloud/aws/infrastructure/modules/server/main.tf @@ -3,7 +3,6 @@ terraform { aws = { source = "hashicorp/aws" version = "~> 5.61" - configuration_aliases = [ aws.east ] } } } @@ -34,15 +33,6 @@ module "vpc" { enable_dhcp_options = true } -data "aws_kms_alias" "us_west_2" { - name = "alias/${var.resource_prefix}-kms-key-us-west-2" -} - -data "aws_kms_alias" "us_east_1" { - provider = aws.east - name = "alias/${var.resource_prefix}-kms-key-us-east-1" -} - // instance role @@ -96,8 +86,8 @@ resource "aws_iam_role" "instance" { "kms:Decrypt" ], Resource = [ - data.aws_kms_alias.us_west_2.target_key_arn, - data.aws_kms_alias.us_east_1.target_key_arn + var.kms_key_arn_west, + var.kms_key_arn_east ] } ] @@ -216,86 +206,4 @@ resource "aws_autoscaling_schedule" "scale_down" { max_size = 1 recurrence = "0 1 * * MON-FRI" autoscaling_group_name = aws_autoscaling_group.this.name -} - -// role to scale up and down the server asg -resource "aws_iam_role" "scaling" { - name = "${var.resource_prefix}-scaling-role" - assume_role_policy = jsonencode({ - Version = "2012-10-17", - Statement = [ - { - Effect = "Allow", - Principal = { - AWS = "arn:aws:iam::${data.aws_caller_identity.this.account_id}:root" - }, - Action = "sts:AssumeRole" - } - ] - }) - - inline_policy { - name = "${var.resource_prefix}_server_run_policy" - policy = jsonencode({ - Version = "2012-10-17", - Statement = [ - { - Effect = "Allow", - Action = [ - "autoscaling:SetDesiredCapacity" - ], - Resource = aws_autoscaling_group.this.arn - }, - { - Effect = "Allow", - Action = [ - "autoscaling:DescribeAutoScalingGroups" - ], - Resource = "*" - } - ] - }) - } -} - -resource "aws_iam_role" "access" { - name = "${var.resource_prefix}-server-access-role" - assume_role_policy = jsonencode({ - Version = "2012-10-17", - Statement = [ - { - Effect = "Allow", - Principal = { - AWS = "arn:aws:iam::${data.aws_caller_identity.this.account_id}:root" - }, - Action = "sts:AssumeRole" - } - ] - }) - - inline_policy { - name = "${var.resource_prefix}_server_access_policy" - policy = jsonencode({ - Version = "2012-10-17", - Statement = [ - { - Effect = "Allow", - Action = [ - "ssm:StartSession" - ], - Resource = [ - "arn:aws:ssm:${data.aws_region.this.name}::document/AWS-StartInteractiveCommand", - "arn:aws:ec2:${data.aws_region.this.name}:${data.aws_caller_identity.this.account_id}:instance/*" - ] - }, - { - Effect = "Allow", - Action = [ - "ec2:DescribeInstances" - ], - Resource = "*" - } - ] - }) - } } \ No newline at end of file diff --git a/deployment/cloud/modules/server/outputs.tf b/deployment/cloud/aws/infrastructure/modules/server/outputs.tf similarity index 100% rename from deployment/cloud/modules/server/outputs.tf rename to deployment/cloud/aws/infrastructure/modules/server/outputs.tf diff --git a/deployment/cloud/modules/server/userdata b/deployment/cloud/aws/infrastructure/modules/server/userdata similarity index 100% rename from deployment/cloud/modules/server/userdata rename to deployment/cloud/aws/infrastructure/modules/server/userdata diff --git a/deployment/cloud/aws/infrastructure/modules/server/variables.tf b/deployment/cloud/aws/infrastructure/modules/server/variables.tf new file mode 100644 index 0000000..c75ebe7 --- /dev/null +++ b/deployment/cloud/aws/infrastructure/modules/server/variables.tf @@ -0,0 +1,35 @@ +variable "resource_prefix" { + description = "The resource prefix is a combination of the project name and neighborhood. This will be used to name all resources in this module." + type = string +} + +variable "stage" { + description = "Which stage this infrastructure's being deployed to - dev, beta, prod, etc." + type = string + + validation { + condition = can(regex("^(dev|beta|prod)$", var.stage)) + error_message = "Stage must be one of dev, beta, or prod" + } +} + +variable "instance_type" { + description = "The instance type to use for the server" + type = string +} + +variable "volume_size" { + description = "The instance volume size to use for the server" + type = string + +} + +variable "kms_key_arn_west" { + description = "The ARN of the KMS key to use for encryption in the west region" + type = string +} + +variable "kms_key_arn_east" { + description = "The ARN of the KMS key to use for encryption in the east region" + type = string +} \ No newline at end of file diff --git a/deployment/cloud/variables.tf b/deployment/cloud/aws/infrastructure/variables.tf similarity index 88% rename from deployment/cloud/variables.tf rename to deployment/cloud/aws/infrastructure/variables.tf index cb2648c..c80c037 100644 --- a/deployment/cloud/variables.tf +++ b/deployment/cloud/aws/infrastructure/variables.tf @@ -39,4 +39,10 @@ variable "volume_size" { description = "The instance type to use for the server" type = string default = 16 +} + +variable "ops_group_name" { + description = "The name of the admin group" + type = string + default = "ssec-eng" } \ No newline at end of file diff --git a/pixi.lock b/pixi.lock index 33df9c2..cfac29b 100644 --- a/pixi.lock +++ b/pixi.lock @@ -760,7 +760,7 @@ packages: url: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl sha256: 1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 requires_dist: - - typing-extensions>=4.0.0 ; python_version < '3.9' + - typing-extensions>=4.0.0 ; python_full_version < '3.9' requires_python: '>=3.8' - kind: pypi name: anyio @@ -770,8 +770,8 @@ packages: requires_dist: - idna>=2.8 - sniffio>=1.1 - - exceptiongroup>=1.0.2 ; python_version < '3.11' - - typing-extensions>=4.1 ; python_version < '3.11' + - exceptiongroup>=1.0.2 ; python_full_version < '3.11' + - typing-extensions>=4.1 ; python_full_version < '3.11' - packaging ; extra == 'doc' - sphinx~=7.4 ; extra == 'doc' - sphinx-rtd-theme ; extra == 'doc' @@ -784,7 +784,7 @@ packages: - pytest>=7.0 ; extra == 'test' - pytest-mock>=3.6.1 ; extra == 'test' - trustme ; extra == 'test' - - uvloop>=0.21.0b1 ; (platform_python_implementation == 'CPython' and platform_system != 'Windows') and extra == 'test' + - uvloop>=0.21.0b1 ; platform_python_implementation == 'CPython' and platform_system != 'Windows' and extra == 'test' - trio>=0.26.1 ; extra == 'trio' requires_python: '>=3.8' - kind: conda @@ -1933,7 +1933,7 @@ packages: sha256: ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 requires_dist: - colorama ; platform_system == 'Windows' - - importlib-metadata ; python_version < '3.8' + - importlib-metadata ; python_full_version < '3.8' requires_python: '>=3.7' - kind: conda name: colorama @@ -2691,7 +2691,7 @@ packages: url: https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl sha256: e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 requires_dist: - - typing-extensions ; python_version < '3.8' + - typing-extensions ; python_full_version < '3.8' requires_python: '>=3.7' - kind: pypi name: h2 @@ -5186,10 +5186,10 @@ packages: requires_dist: - annotated-types>=0.6.0 - pydantic-core==2.23.4 - - typing-extensions>=4.12.2 ; python_version >= '3.13' - - typing-extensions>=4.6.1 ; python_version < '3.13' + - typing-extensions>=4.12.2 ; python_full_version >= '3.13' + - typing-extensions>=4.6.1 ; python_full_version < '3.13' - email-validator>=2.0.0 ; extra == 'email' - - tzdata ; (python_version >= '3.9' and sys_platform == 'win32') and extra == 'timezone' + - tzdata ; python_full_version >= '3.9' and sys_platform == 'win32' and extra == 'timezone' requires_python: '>=3.8' - kind: pypi name: pydantic-core @@ -5632,7 +5632,7 @@ packages: - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' - markdown-it-py>=2.2.0 - pygments>=2.13.0,<3.0.0 - - typing-extensions>=4.0.0,<5.0 ; python_version < '3.9' + - typing-extensions>=4.0.0,<5.0 ; python_full_version < '3.9' requires_python: '>=3.7.0' - kind: conda name: rich @@ -5943,8 +5943,8 @@ packages: sha256: eb60b026d8ad0c97917cb81d3662d0b39b8ff1335e3fabb24984c6acd0c900a2 requires_dist: - typing-extensions>=4.6.0 - - greenlet!=0.4.17 ; python_version < '3.13' and (platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))) - - importlib-metadata ; python_version < '3.8' + - greenlet!=0.4.17 ; (python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64') + - importlib-metadata ; python_full_version < '3.8' - greenlet!=0.4.17 ; extra == 'aiomysql' - aiomysql>=0.2.0 ; extra == 'aiomysql' - greenlet!=0.4.17 ; extra == 'aioodbc' @@ -5982,8 +5982,8 @@ packages: sha256: 6921ee01caf375363be5e9ae70d08ce7ca9d7e0e8983183080211a062d299468 requires_dist: - typing-extensions>=4.6.0 - - greenlet!=0.4.17 ; python_version < '3.13' and (platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))) - - importlib-metadata ; python_version < '3.8' + - greenlet!=0.4.17 ; (python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64') + - importlib-metadata ; python_full_version < '3.8' - greenlet!=0.4.17 ; extra == 'aiomysql' - aiomysql>=0.2.0 ; extra == 'aiomysql' - greenlet!=0.4.17 ; extra == 'aioodbc' @@ -6021,8 +6021,8 @@ packages: sha256: 93a71c8601e823236ac0e5d087e4f397874a421017b3318fd92c0b14acf2b6db requires_dist: - typing-extensions>=4.6.0 - - greenlet!=0.4.17 ; python_version < '3.13' and (platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))) - - importlib-metadata ; python_version < '3.8' + - greenlet!=0.4.17 ; (python_full_version < '3.13' and platform_machine == 'AMD64') or (python_full_version < '3.13' and platform_machine == 'WIN32') or (python_full_version < '3.13' and platform_machine == 'aarch64') or (python_full_version < '3.13' and platform_machine == 'amd64') or (python_full_version < '3.13' and platform_machine == 'ppc64le') or (python_full_version < '3.13' and platform_machine == 'win32') or (python_full_version < '3.13' and platform_machine == 'x86_64') + - importlib-metadata ; python_full_version < '3.8' - greenlet!=0.4.17 ; extra == 'aiomysql' - aiomysql>=0.2.0 ; extra == 'aiomysql' - greenlet!=0.4.17 ; extra == 'aioodbc' @@ -6139,7 +6139,7 @@ packages: sha256: 632f420a9d13e3ee2a6f18f437b0a9f1faecb0bc42e1942aa2ea0e379a4c4206 requires_dist: - anyio<5,>=3.4.0 - - typing-extensions>=3.10.0 ; python_version < '3.10' + - typing-extensions>=3.10.0 ; python_full_version < '3.10' - httpx>=0.22.0 ; extra == 'full' - itsdangerous ; extra == 'full' - jinja2 ; extra == 'full' @@ -6194,9 +6194,9 @@ packages: requires_python: '>=3.8,<4.0' - kind: pypi name: support-sphere-py - version: 0.0.7 + version: 0.0.8 path: ./src/support_sphere_py - sha256: 67b861bce2c950a07ce356877a21203b138f86d1268ca9f8f646cc2c00291319 + sha256: 6155a1e86be679b3c383235205d36249c211eab01e4e009072702f73a2ef3600 requires_dist: - sqlmodel>=0.0.21,<0.1 - supabase>=2.6.0,<2.7 @@ -6404,12 +6404,12 @@ packages: requires_dist: - click>=7.0 - h11>=0.8 - - typing-extensions>=4.0 ; python_version < '3.11' + - typing-extensions>=4.0 ; python_full_version < '3.11' - colorama>=0.4 ; sys_platform == 'win32' and extra == 'standard' - httptools>=0.5.0 ; extra == 'standard' - python-dotenv>=0.13 ; extra == 'standard' - pyyaml>=5.1 ; extra == 'standard' - - uvloop!=0.15.0,!=0.15.1,>=0.14.0 ; (sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')) and extra == 'standard' + - uvloop!=0.15.0,!=0.15.1,>=0.14.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' and extra == 'standard' - watchfiles>=0.13 ; extra == 'standard' - websockets>=10.4 ; extra == 'standard' requires_python: '>=3.8' @@ -6428,8 +6428,8 @@ packages: - pyopenssl~=23.0.0 ; extra == 'test' - mypy>=0.800 ; extra == 'test' - cython<0.30.0,>=0.29.36 ; extra == 'test' - - aiohttp>=3.8.1 ; python_version < '3.12' and extra == 'test' - - aiohttp==3.9.0b0 ; python_version >= '3.12' and extra == 'test' + - aiohttp>=3.8.1 ; python_full_version < '3.12' and extra == 'test' + - aiohttp==3.9.0b0 ; python_full_version >= '3.12' and extra == 'test' requires_python: '>=3.8.0' - kind: pypi name: uvloop @@ -6446,8 +6446,8 @@ packages: - pyopenssl~=23.0.0 ; extra == 'test' - mypy>=0.800 ; extra == 'test' - cython<0.30.0,>=0.29.36 ; extra == 'test' - - aiohttp>=3.8.1 ; python_version < '3.12' and extra == 'test' - - aiohttp==3.9.0b0 ; python_version >= '3.12' and extra == 'test' + - aiohttp>=3.8.1 ; python_full_version < '3.12' and extra == 'test' + - aiohttp==3.9.0b0 ; python_full_version >= '3.12' and extra == 'test' requires_python: '>=3.8.0' - kind: pypi name: uvloop @@ -6464,8 +6464,8 @@ packages: - pyopenssl~=23.0.0 ; extra == 'test' - mypy>=0.800 ; extra == 'test' - cython<0.30.0,>=0.29.36 ; extra == 'test' - - aiohttp>=3.8.1 ; python_version < '3.12' and extra == 'test' - - aiohttp==3.9.0b0 ; python_version >= '3.12' and extra == 'test' + - aiohttp>=3.8.1 ; python_full_version < '3.12' and extra == 'test' + - aiohttp==3.9.0b0 ; python_full_version >= '3.12' and extra == 'test' requires_python: '>=3.8.0' - kind: pypi name: watchfiles diff --git a/pixi.toml b/pixi.toml index 0cd3a64..02134db 100644 --- a/pixi.toml +++ b/pixi.toml @@ -185,6 +185,7 @@ depends-on = ["install-flutter"] "TF_VAR_neighborhood" = "Laurelhurst" "TF_VAR_stage" = "dev" "TF_VAR_account_id" = "871683513797" +"TF_VAR_ops_group_name" = "ssec-eng" [feature.cloud.tasks] # empty for now @@ -199,19 +200,34 @@ aws-session-manager-plugin = ">=1.2.0" # OpenTofu tasks [feature.cloud.tasks.cloud-deploy] cmd = "tofu apply" -cwd = "deployment/cloud" +cwd = "deployment/cloud/aws/infrastructure" [feature.cloud.tasks.cloud-init] cmd = "tofu init" -cwd = "deployment/cloud" +cwd = "deployment/cloud/aws/infrastructure" [feature.cloud.tasks.cloud-plan] cmd = "tofu plan" -cwd = "deployment/cloud" +cwd = "deployment/cloud/aws/infrastructure" [feature.cloud.tasks.cloud-destroy] cmd = "tofu destroy" -cwd = "deployment/cloud" +cwd = "deployment/cloud/aws/infrastructure" + +[feature.cloud.tasks.cloud-account-deploy] +cmd = "tofu apply" +cwd = "deployment/cloud/aws/account" +depends-on = ["cloud-account-setup"] + +[feature.cloud.tasks.cloud-account-init] +cmd = "tofu init" +cwd = "deployment/cloud/aws/account" +depends-on = ["cloud-account-setup"] + +[feature.cloud.tasks.cloud-account-plan] +cmd = "tofu plan" +cwd = "deployment/cloud/aws/account" +depends-on = ["cloud-account-setup"] # server tasks [feature.cloud.tasks.cloud-server-run] @@ -232,6 +248,9 @@ cmd = "sops edit deployment/values.cloud.yaml" [feature.cloud.tasks.cloud-account-setup] cmd = "python scripts/cloud-account-setup.py" +[feature.cloud.tasks.cloud-account-user-controls] +cmd = "python scripts/aws-user-controls.py" + # ==== Backup Secret environment ==== [feature.backup-secret.dependencies] gnupg = ">=2.4.5" diff --git a/scripts/aws-user-controls.py b/scripts/aws-user-controls.py new file mode 100644 index 0000000..10dc789 --- /dev/null +++ b/scripts/aws-user-controls.py @@ -0,0 +1,117 @@ +import boto3 +import argparse + +iam = boto3.client('iam') + +USER_GROUP_NAME = "ssec-eng" + +def create_user(username: str): + try: + iam.create_user( + UserName=username + ) + print(f'User {username} created successfully') + except iam.exceptions.EntityAlreadyExistsException: + print(f'User {username} already exists') + +def add_user_to_group(username: str): + try: + iam.add_user_to_group( + UserName=username, + GroupName=USER_GROUP_NAME + ) + print(f'User {username} added to group {USER_GROUP_NAME}') + except iam.exceptions.NoSuchEntityException: + print(f'User {username} or group {USER_GROUP_NAME} does not exist') + + +def create_access_key(username: str): + response = iam.create_access_key( + UserName=username + ) + + access_key = response['AccessKey'] + print(f'Access key created for user {username}') + print(f'Access key ID: {access_key["AccessKeyId"]}') + print(f'Secret access key: {access_key["SecretAccessKey"]}\n') + print('To safe these in a secure location, run the following commands:') + print('mkdir ~/.aws') + print(f'echo "[{username}]" >> ~/.aws/credentials') + print(f'echo "aws_access_key_id = {access_key["AccessKeyId"]}" >> ~/.aws/credentials') + print(f'echo "aws_secret_access_key = {access_key["SecretAccessKey"]}" >> ~/.aws/credentials') + print() + print('To use this profile, run the following commands:\n') + print(f'echo "[profile {username}]" >> ~/.aws/config') + print(f'echo "region = us-west-2" >> ~/.aws/config') + print(f'echo "output = json" >> ~/.aws/config') + print(f'echo "cli_pager =" >> ~/.aws/config') + print() + print('To use this profile, run the following command:') + print(f'export AWS_PROFILE={username}') + + +def revoke_access_key(username: str): + access_keys_response = iam.list_access_keys( + UserName=username + ) + + for access_key in access_keys_response['AccessKeyMetadata']: + iam.delete_access_key( + UserName=username, + AccessKeyId=access_key['AccessKeyId'] + ) + print(f'Access key {access_key["AccessKeyId"]} revoked for user {username}') + + +def remove_user(username: str): + iam.remove_user_from_group( + UserName=username, + GroupName=USER_GROUP_NAME + ) + print(f'User {username} removed from group {USER_GROUP_NAME}') + + iam.delete_user( + UserName=username + ) + print(f'User {username} removed successfully') + +def list_users(): + group = iam.get_group( + GroupName=USER_GROUP_NAME + ) + + for user in group['Users']: + print(user['UserName']) + access_keys_response = iam.list_access_keys( + UserName=user['UserName'] + ) + for access_key in access_keys_response['AccessKeyMetadata']: + print(f' Access key ID: {access_key["AccessKeyId"]}') + print(f' Status: {access_key["Status"]}') + print(f' Created: {access_key["CreateDate"]}') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Controls for AWS users. Meant to be run by the account owner or admin to grant limited access to other users for what\'s needed for Support Sphere operational work.') + parser.add_argument('action', choices=['add', 'remove', 'rotate', 'list'], help='Action to take. Add a user, remove a user, or rotate an existing user\'s access key') + parser.add_argument('-u', '--username', type=str, help='Name for the new user, will become -assumer') + args = parser.parse_args() + + if args.action == 'list': + list_users() + exit(0) + + username = args.username + '-assumer' + + if args.action == 'add': + create_user(username) + add_user_to_group(username) + create_access_key(username) + elif args.action == 'remove': + revoke_access_key(username) + remove_user(username) + elif args.action == 'rotate': + revoke_access_key(username) + create_access_key(username) + elif args.action == 'list': + list_users() \ No newline at end of file diff --git a/scripts/cloud-account-setup.py b/scripts/cloud-account-setup.py index cdb06a1..3e7d3d6 100644 --- a/scripts/cloud-account-setup.py +++ b/scripts/cloud-account-setup.py @@ -6,99 +6,33 @@ def sanitize(input): project_name = sanitize(os.environ.get('TF_VAR_project_name', 'Support Sphere')) neighborhood = sanitize(os.environ.get('TF_VAR_neighborhood', 'Laurelhurst')) +account_id = os.environ.get('TF_VAR_account_id', '123456789012') resource_prefix = f'{project_name}-{neighborhood}' -USER_GROUP_NAME = "ssec-eng" - iam = boto3.client('iam') -def kms_key_exists(alias: str, region: str): - kms = boto3.client('kms', region_name=region) - try: - kms.describe_key( - KeyId=alias - ) - return True - except kms.exceptions.NotFoundException: - return False - - -def grant_decrypt_permission_to_user_group(key_arn: str, region: str): - iam.put_group_policy( - GroupName=USER_GROUP_NAME, - PolicyName=f'AllowDecryptKey{region}', - PolicyDocument=f'''{{ - "Version": "2012-10-17", - "Statement": [ - {{ - "Effect": "Allow", - "Action": [ - "kms:Encrypt", - "kms:Decrypt" - ], - "Resource": "{key_arn}" - }} - ] - }}''' - ) +def setup_s3_bucket(): + s3 = boto3.client('s3') + bucket_name = f'{project_name}-{account_id}-opentofu-state' -def create_kms_key(region: str): - kms = boto3.client('kms', region_name=region) - - key_alias_name = f'alias/{resource_prefix}-kms-key-{region}' - - if kms_key_exists(key_alias_name, region): - print(f'Key already exists in {region}') - return - - response = kms.create_key( - Description='Key for encrypting and decrypting server config values for the Support Sphere project.', - KeyUsage='ENCRYPT_DECRYPT', - Origin='AWS_KMS', - Tags=[ - { - 'TagKey': 'Project', - 'TagValue': project_name + try: + s3.create_bucket( + Bucket=bucket_name, + CreateBucketConfiguration={ + 'LocationConstraint': 'us-west-2' }, - { - 'TagKey': 'Neighborhood', - 'TagValue': neighborhood - } - ] - ) - - key_id = response['KeyMetadata']['KeyId'] - key_arn = response['KeyMetadata']['Arn'] - print(f'Created key {key_id} in {region} with ARN {key_arn}') - - kms.create_alias( - AliasName=key_alias_name, - TargetKeyId=key_id - ) - - grant_decrypt_permission_to_user_group(key_arn, region) - -def setup_kms_keys(): - # Create key us-west-2 (Portland) - create_kms_key('us-west-2') - - # Create key us-east-1 (Virginia) - # This is mainly a backup key in case the Portland region experiences issues - create_kms_key('us-east-1') - + ACL='private' + ) + print(f'Created bucket {bucket_name}') + except s3.exceptions.BucketAlreadyExists: + print(f'Bucket {bucket_name} already exists') + except s3.exceptions.BucketAlreadyOwnedByYou: + print(f'Bucket {bucket_name} already owned by you, continuing :)') + except Exception as e: + print(f'Error creating bucket {bucket_name}: {e}') if __name__ == '__main__': - # The following TODOs will be implemented when working on the following issue - # https://github.com/uw-ssec/post-disaster-comms/issues/63 - # Not doing this now because this work is focused on setting up a KMS key for the project - - # TODO: set up user group - - # TODO: set up no-trust user - - # TODO: set up deploy role - - # KMS key setup - setup_kms_keys() \ No newline at end of file + # S3 bucket setup + setup_s3_bucket() \ No newline at end of file