From 202f6dc9e64d29fd8a1d73fd39f2b49e7b423b52 Mon Sep 17 00:00:00 2001 From: David Gonzalez Date: Wed, 28 Oct 2020 17:28:39 +0000 Subject: [PATCH] v0.1.2 --- admins.tf | 51 +++++++++++++++++++++++ dns.tf | 79 +++++++++++++++++++++++++++++++++++- docs/create-new-env.md | 76 ++++++++++++++++++++++++++++++++++ ecs_interop.tf | 1 + gateway.tf | 8 +++- lambda-batch.tf | 25 +++++++++++- lambda-token.tf | 26 ++++++++++-- locals.tf | 3 ++ main.tf | 17 ++++---- operators.tf | 92 ++++++++++++++++++++++++++++++++++++++++++ outputs.tf | 24 +++++++++++ secrets.tf | 12 ++++++ variables.tf | 24 ++++++++--- 13 files changed, 417 insertions(+), 21 deletions(-) create mode 100644 admins.tf create mode 100644 docs/create-new-env.md create mode 100644 operators.tf diff --git a/admins.tf b/admins.tf new file mode 100644 index 0000000..19cb25d --- /dev/null +++ b/admins.tf @@ -0,0 +1,51 @@ +# ######################################### +# Admins - group with ability to assume a role with privileged access +# ######################################### +data "aws_iam_policy_document" "admins_group" { + statement { + actions = ["sts:AssumeRole"] + resources = [aws_iam_role.admins.arn] + } +} + +data "aws_iam_policy_document" "admins_role" { + statement { + actions = ["sts:AssumeRole"] + + dynamic condition { + for_each = var.admins_role_require_mfa ? { 1 : 1 } : {} + content { + test = "Bool" + variable = "aws:MultiFactorAuthPresent" + values = ["true"] + } + } + + principals { + type = "AWS" + identifiers = [format("arn:aws:iam::%s:root", data.aws_caller_identity.current.account_id)] + } + } +} + +resource "aws_iam_group" "admins" { + name = format("%s-%s", module.labels.id, "admins") + path = "/" +} + +resource "aws_iam_group_policy" "admins" { + group = aws_iam_group.admins.id + name = format("%s-%s", module.labels.id, "admins") + policy = data.aws_iam_policy_document.admins_group.json +} + +resource "aws_iam_role" "admins" { + assume_role_policy = data.aws_iam_policy_document.admins_role.json + name = format("%s-%s", module.labels.id, "admins") + tags = module.labels.tags +} + +resource "aws_iam_role_policy_attachment" "admins" { + role = aws_iam_role.admins.name + policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" +} \ No newline at end of file diff --git a/dns.tf b/dns.tf index fac1d70..f714268 100644 --- a/dns.tf +++ b/dns.tf @@ -3,17 +3,92 @@ # ######################################### data "aws_route53_zone" "primary" { count = local.enable_dns_count - provider = aws.root + provider = aws.dns name = var.route53_zone private_zone = false } +# ######################################### +# Certificate +# ######################################### +resource "aws_acm_certificate" "wildcard_cert" { + count = local.enable_certificates_count + domain_name = var.wildcard_domain + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_route53_record" "wildcard_cert_validation" { + count = local.enable_certificates_count + provider = aws.dns + name = aws_acm_certificate.wildcard_cert[0].domain_validation_options.0.resource_record_name + type = aws_acm_certificate.wildcard_cert[0].domain_validation_options.0.resource_record_type + zone_id = data.aws_route53_zone.primary[0].id + records = [aws_acm_certificate.wildcard_cert[0].domain_validation_options.0.resource_record_value] + ttl = 60 + allow_overwrite = true + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_acm_certificate_validation" "wildcard_cert" { + count = local.enable_certificates_count + certificate_arn = aws_acm_certificate.wildcard_cert[0].arn + validation_record_fqdns = [aws_route53_record.wildcard_cert_validation[0].fqdn] + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_acm_certificate" "wildcard_cert_us" { + count = local.enable_certificates_count + provider = aws.us_east_1 + domain_name = var.wildcard_domain + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_route53_record" "wildcard_cert_validation_us" { + count = local.enable_certificates_count + provider = aws.dns + name = aws_acm_certificate.wildcard_cert_us[0].domain_validation_options.0.resource_record_name + type = aws_acm_certificate.wildcard_cert_us[0].domain_validation_options.0.resource_record_type + zone_id = data.aws_route53_zone.primary[0].id + records = [aws_acm_certificate.wildcard_cert_us[0].domain_validation_options.0.resource_record_value] + ttl = 60 + allow_overwrite = true + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_acm_certificate_validation" "wildcard_cert_us" { + count = local.enable_certificates_count + provider = aws.us_east_1 + certificate_arn = aws_acm_certificate.wildcard_cert_us[0].arn + validation_record_fqdns = [aws_route53_record.wildcard_cert_validation_us[0].fqdn] + + lifecycle { + create_before_destroy = true + } +} + # ######################################### # DNS Records # ######################################### resource "aws_route53_record" "interop" { count = local.enable_dns_count - provider = aws.root + provider = aws.dns zone_id = data.aws_route53_zone.primary[0].id name = var.interop_dns type = "A" diff --git a/docs/create-new-env.md b/docs/create-new-env.md new file mode 100644 index 0000000..32937d9 --- /dev/null +++ b/docs/create-new-env.md @@ -0,0 +1,76 @@ +#How to create new interop environment + + +## Create an AWS profile +Add a interop-ENV profile to `~/.aws/credentials`, +where ENV stands for the name of your environment, e.g. `dev`, `qa` or `prod` + +## Create the Terraform state backend +Assuming covid-tracker-infrastructure repo is cloned in the same directory as current project: + +See [../../covid-tracker-infrastructure/scripts/create-tf-state-backend.sh] script + +``` +# Set your AWS_PROFILE +export AWS_PROFILE=interop-dev +export AWS_REGION=eu-west-1 + +# Create +./../../covid-tracker-infrastructure/scripts/create-tf-state-backend.sh eu-west-1 interop-dev-terraform-store interop-dev-terraform-lock +``` + + +## Create the AWS SecretsManager secrets + + +### header-x-secret Secret +The `header-x-secret` secret is used to secure communication between the APIGateway and ALB for the API traffic. + +The secret value should be a random alphanumeric string 96 characters in length. + +The format of the secret is as follows: +```json +{ + "header-secret":"Some random 96 alpanumeric characters" +} +``` + +### jwt Secret +The `jwt` secret is used for signing the JSON Web Tokens with the HMAC algorithm. These are issued to users for API authentication, +and the signature is checked by the service to ensure their legitimacy. + +The secret value should be a random string 32 characters in length. + +The format of the secret is as follows: +```json +{ + "key": "32 random characters" +} +``` + +### RDS Secrets +The `rds` secret contains the master RDS credentials. + +The format of the secret is as follows: +```json +{ + "password":"A strong password", + "username":"rds_admin_user" +} +``` + +The `rds-read-only`, `rds-read-write`, `rds-read-write-create` secrets contains the application RDS credentials. +The format of the secret is as follows: +```json +{ + "password":"A strong password", + "username":"user_name" +} +``` + +## Create the env-vars files + +| File | Content | +| ------------------------| ----------------------------------------------------------- | +| env-vars/ENV.tfvars | Contains the Interop values that are specific to the dev env | + diff --git a/ecs_interop.tf b/ecs_interop.tf index 042c735..31bd9cb 100644 --- a/ecs_interop.tf +++ b/ecs_interop.tf @@ -43,6 +43,7 @@ data "aws_iam_policy_document" "interop_ecs_task_policy" { aws_ssm_parameter.batch_size.arn, aws_ssm_parameter.batch_url.arn, data.aws_secretsmanager_secret_version.rds.arn, + data.aws_secretsmanager_secret_version.rds_read_write_create.arn, data.aws_secretsmanager_secret_version.jwt.arn ] } diff --git a/gateway.tf b/gateway.tf index 00f531c..32ce9a8 100644 --- a/gateway.tf +++ b/gateway.tf @@ -13,9 +13,15 @@ resource "aws_api_gateway_rest_api" "main" { ## custom domain name resource "aws_api_gateway_domain_name" "main" { count = local.enable_dns_count - certificate_arn = var.interop_us_certificate_arn + certificate_arn = aws_acm_certificate.wildcard_cert_us[0].arn domain_name = var.interop_dns security_policy = "TLS_1_2" + + + depends_on = [ + aws_acm_certificate.wildcard_cert_us[0], + aws_acm_certificate_validation.wildcard_cert_us[0] + ] } ## execution role with s3 access diff --git a/lambda-batch.tf b/lambda-batch.tf index cb7e852..9b812b0 100644 --- a/lambda-batch.tf +++ b/lambda-batch.tf @@ -12,12 +12,33 @@ data "aws_iam_policy_document" "batch_policy" { "ec2:DescribeNetworkInterfaces", "ec2:DetachNetworkInterface", "ec2:DeleteNetworkInterface", - "secretsmanager:GetSecretValue", - "ssm:GetParameter", "sqs:*" ] resources = ["*"] } + + statement { + actions = [ + "secretsmanager:GetSecretValue" + ] + resources = [ + data.aws_secretsmanager_secret_version.rds_read_write.arn, + data.aws_secretsmanager_secret_version.rds.arn, + ] + } + + statement { + actions = [ + "ssm:GetParameter" + ] + resources = [ + aws_ssm_parameter.db_host.arn, + aws_ssm_parameter.db_port.arn, + aws_ssm_parameter.db_database.arn, + aws_ssm_parameter.db_ssl.arn, + aws_ssm_parameter.batch_size.arn + ] + } } data "aws_iam_policy_document" "batch_assume_role" { diff --git a/lambda-token.tf b/lambda-token.tf index b0ada27..f6ab56f 100644 --- a/lambda-token.tf +++ b/lambda-token.tf @@ -11,12 +11,32 @@ data "aws_iam_policy_document" "token_policy" { "ec2:CreateNetworkInterface", "ec2:DescribeNetworkInterfaces", "ec2:DetachNetworkInterface", - "ec2:DeleteNetworkInterface", - "secretsmanager:GetSecretValue", - "ssm:GetParameter" + "ec2:DeleteNetworkInterface" ] resources = ["*"] } + + statement { + actions = [ + "secretsmanager:GetSecretValue", + ] + resources = [ + data.aws_secretsmanager_secret_version.rds_read_write.arn, + data.aws_secretsmanager_secret_version.rds.arn, + data.aws_secretsmanager_secret_version.jwt.arn, + ] + } + statement { + actions = [ + "ssm:GetParameter", + ] + resources = [ + aws_ssm_parameter.db_host.arn, + aws_ssm_parameter.db_port.arn, + aws_ssm_parameter.db_database.arn, + aws_ssm_parameter.db_ssl.arn + ] + } } data "aws_iam_policy_document" "token_assume_role" { diff --git a/locals.tf b/locals.tf index f084708..9b055f3 100644 --- a/locals.tf +++ b/locals.tf @@ -8,6 +8,9 @@ locals { # Will be used as a prefix for AWS parameters and secrets config_var_prefix = "${module.labels.id}-" + # Based on flag + enable_certificates_count = var.enable_certificates ? 1 : 0 + # Based on flag enable_dns_count = var.enable_dns ? 1 : 0 diff --git a/main.tf b/main.tf index a77796c..f498b5d 100644 --- a/main.tf +++ b/main.tf @@ -22,18 +22,21 @@ provider "aws" { profile = var.profile } +# Provider based on main but using us_east_1 as region +# Will use this if creating a TLS certificate in us-east-1 region as required by CloudFront Edge used by the APIGateway provider "aws" { - version = "2.68.0" - alias = "root" - region = var.aws_region - profile = var.root_profile + alias = "us_east_1" + region = "us-east-1" + profile = var.profile } +# DNS provider +# Will use this if managing DNS, in some cases the Route53 zones are managed on a different account provider "aws" { version = "2.68.0" - alias = "root-us" - region = "us-east-1" - profile = var.root_profile + alias = "dns" + region = var.aws_region + profile = var.dns_profile } # ######################################### diff --git a/operators.tf b/operators.tf new file mode 100644 index 0000000..aef738d --- /dev/null +++ b/operators.tf @@ -0,0 +1,92 @@ +# ######################################### +# Operators - group with restricted privileges +# See https://alestic.com/2015/10/aws-iam-readonly-too-permissive/ +# ######################################### +data "aws_iam_policy_document" "operators" { + statement { + actions = [ + "lambda:InvokeFunction" + ] + resources = [ + aws_lambda_function.batch.arn, + aws_lambda_function.token.arn + ] + } + + # Allow getting the RDS read_only_user credentials secret + statement { + actions = [ + "secretsmanager:GetSecretValue" + ] + resources = [data.aws_secretsmanager_secret_version.rds_read_only.arn] + } + + # Allow own MFA management + # See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_aws_my-sec-creds-self-manage.html + # For the $${user_name} escaping see https://github.com/terraform-providers/terraform-provider-aws/issues/5984#issuecomment-424470589 + statement { + actions = [ + "iam:CreateVirtualMFADevice", + "iam:DeleteVirtualMFADevice" + ] + resources = [format("arn:aws:iam::%s:mfa/$${aws:username}", data.aws_caller_identity.current.account_id)] + sid = "AllowManageOwnVirtualMFADevice" + } + statement { + actions = [ + "iam:DeactivateMFADevice", + "iam:EnableMFADevice", + "iam:ListMFADevices", + "iam:ResyncMFADevice" + ] + resources = [format("arn:aws:iam::%s:user/$${aws:username}", data.aws_caller_identity.current.account_id)] + sid = "AllowManageOwnUserMFA" + } + + # Conditional + # PENDING: This works from the CLI without autoscaling:UpdateAutoScalingGroup but we need autoscaling:UpdateAutoScalingGroup to work in the console + # autoscaling:UpdateAutoScalingGroup is too permissive + dynamic statement { + for_each = toset(aws_autoscaling_group.bastion.*.arn) + content { + actions = [ + "autoscaling:SetDesiredCapacity", + "autoscaling:TerminateInstanceInAutoScalingGroup", + "autoscaling:UpdateAutoScalingGroup" + ] + resources = [statement.key] + } + } + + # Conditional + dynamic statement { + for_each = toset(aws_autoscaling_group.bastion.*.name) + content { + actions = [ + "ssm:StartSession" + ] + resources = ["arn:aws:ec2:*:*:instance/*"] + condition { + test = "StringEquals" + values = [statement.key] + variable = "ssm:resourceTag/aws:autoscaling:groupName" + } + } + } +} + +resource "aws_iam_group" "operators" { + name = "${module.labels.id}-operators" + path = "/" +} + +resource "aws_iam_group_policy" "operators" { + group = aws_iam_group.operators.id + name = "${module.labels.id}-operators" + policy = data.aws_iam_policy_document.operators.json +} + +resource "aws_iam_group_policy_attachment" "operators" { + group = aws_iam_group.operators.name + policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess" +} \ No newline at end of file diff --git a/outputs.tf b/outputs.tf index c9fa4c1..b66edf4 100644 --- a/outputs.tf +++ b/outputs.tf @@ -9,3 +9,27 @@ output "secret" { output "api_aws_dns" { value = join("", aws_api_gateway_domain_name.main.*.cloudfront_domain_name) } + +output "ecs_cluster_name" { + value = aws_ecs_cluster.services.name +} + +output "ecs_cluster_service_name" { + value = aws_ecs_service.interop.name +} + +output "lambda_names" { + value = [ + aws_lambda_function.batch.function_name, + aws_lambda_function.token.function_name + ] +} + +output "rds_cluster_identifier" { + value = module.rds_cluster_aurora_postgres.cluster_identifier +} + +output "waf_acl_metric_name" { + value = aws_wafregional_web_acl.acl.metric_name +} + diff --git a/secrets.tf b/secrets.tf index 499da83..604b86a 100644 --- a/secrets.tf +++ b/secrets.tf @@ -12,3 +12,15 @@ data "aws_secretsmanager_secret_version" "jwt" { data "aws_secretsmanager_secret_version" "rds" { secret_id = "${local.config_var_prefix}rds" } + +data "aws_secretsmanager_secret_version" "rds_read_only" { + secret_id = "${local.config_var_prefix}rds-read-only" +} + +data "aws_secretsmanager_secret_version" "rds_read_write" { + secret_id = "${local.config_var_prefix}rds-read-write" +} + +data "aws_secretsmanager_secret_version" "rds_read_write_create" { + secret_id = "${local.config_var_prefix}rds-read-write-create" +} diff --git a/variables.tf b/variables.tf index bdb16cf..2058c80 100644 --- a/variables.tf +++ b/variables.tf @@ -5,17 +5,26 @@ variable "namespace" {} variable "full_name" {} variable "environment" {} variable "profile" {} -variable "root_profile" {} +variable "dns_profile" {} variable "aws_region" {} # ######################################### -# DNS and certificates (Imported/existing certs) +# Admins role # ######################################### -variable "enable_dns" { +variable "admins_role_require_mfa" { + # Turning this on is fine with the AWS CLI but is tricky with TF and we have multiple accounts in play in some envs + description = "Require MFA for assuming the admins IAM role" + default = false +} + +# ######################################### +# DNS and certificates +# ######################################### +variable "enable_certificates" { default = true } -variable "interop_us_certificate_arn" { - default = "" +variable "enable_dns" { + default = true } # ######################################### @@ -105,8 +114,11 @@ variable "default_ecr_max_image_count" { # ######################################### # R53 Settings # ######################################### -variable "route53_zone" {} variable "interop_dns" {} +variable "route53_zone" {} +variable "wildcard_domain" { + description = "DNS wildcard domain" +} # ######################################### # Bastion