diff --git a/services/terraform/modules/shared/dynamodb.tf b/services/terraform/modules/shared/dynamodb.tf index 4e3e1e9b0..4c2e14e13 100644 --- a/services/terraform/modules/shared/dynamodb.tf +++ b/services/terraform/modules/shared/dynamodb.tf @@ -1,299 +1,301 @@ resource "aws_dynamodb_table" "backup-service-backup" { name = "backup-service-backup" hash_key = "userID" range_key = "backupID" billing_mode = "PAY_PER_REQUEST" attribute { name = "userID" type = "S" } attribute { name = "backupID" type = "S" } attribute { name = "created" type = "S" } global_secondary_index { name = "userID-created-index" hash_key = "userID" range_key = "created" projection_type = "INCLUDE" non_key_attributes = ["userKeys"] } } resource "aws_dynamodb_table" "backup-service-log" { name = "backup-service-log" hash_key = "backupID" range_key = "logID" billing_mode = "PAY_PER_REQUEST" attribute { name = "backupID" type = "S" } attribute { name = "logID" type = "N" } } resource "aws_dynamodb_table" "blob-service-blobs" { name = "blob-service-blobs" hash_key = "blob_hash" range_key = "holder" billing_mode = "PAY_PER_REQUEST" attribute { name = "blob_hash" type = "S" } attribute { name = "holder" type = "S" } attribute { name = "last_modified" type = "N" } attribute { name = "unchecked" type = "S" } global_secondary_index { name = "unchecked-index" hash_key = "unchecked" range_key = "last_modified" projection_type = "KEYS_ONLY" } } resource "aws_dynamodb_table" "tunnelbroker-undelivered-messages" { name = "tunnelbroker-undelivered-messages" hash_key = "deviceID" range_key = "messageID" billing_mode = "PAY_PER_REQUEST" attribute { name = "deviceID" type = "S" } attribute { name = "messageID" type = "S" } } resource "aws_dynamodb_table" "identity-users" { name = "identity-users" hash_key = "userID" billing_mode = "PAY_PER_REQUEST" stream_enabled = true stream_view_type = "NEW_AND_OLD_IMAGES" attribute { name = "userID" type = "S" } attribute { name = "username" type = "S" } # walletAddress not defined in prod dynamic "attribute" { # Create a dummy list to iterate over if is_dev is true for_each = var.is_dev ? [1] : [] content { name = "walletAddress" type = "S" } } global_secondary_index { name = "username-index" hash_key = "username" projection_type = "KEYS_ONLY" } # walletAddress not defined in prod dynamic "global_secondary_index" { # Create a dummy list to iterate over if is_dev is true for_each = var.is_dev ? [1] : [] content { name = "walletAddress-index" hash_key = "walletAddress" projection_type = "KEYS_ONLY" } } } resource "aws_dynamodb_table" "identity-devices" { name = "identity-devices" hash_key = "userID" range_key = "itemID" billing_mode = "PAY_PER_REQUEST" attribute { name = "userID" type = "S" } # this is either device ID or device list datetime attribute { name = "itemID" type = "S" } # only for sorting device lists attribute { name = "timestamp" type = "N" } # sparse index allowing to sort device list updates by timestamp local_secondary_index { name = "deviceList-timestamp-index" range_key = "timestamp" projection_type = "ALL" } } # Identity users with opaque_ke 2.0 credentials resource "aws_dynamodb_table" "identity-users-opaque2" { # This table doesnt exist in prod count = var.is_dev ? 1 : 0 name = "identity-users-opaque2" hash_key = "userID" billing_mode = "PAY_PER_REQUEST" attribute { name = "userID" type = "S" } attribute { name = "username" type = "S" } attribute { name = "walletAddress" type = "S" } global_secondary_index { name = "username-index" hash_key = "username" projection_type = "KEYS_ONLY" } global_secondary_index { name = "walletAddress-index" hash_key = "walletAddress" projection_type = "KEYS_ONLY" } } resource "aws_dynamodb_table" "identity-tokens" { name = "identity-tokens" hash_key = "userID" range_key = "signingPublicKey" billing_mode = "PAY_PER_REQUEST" attribute { name = "userID" type = "S" } attribute { name = "signingPublicKey" type = "S" } } resource "aws_dynamodb_table" "identity-nonces" { name = "identity-nonces" hash_key = "nonce" billing_mode = "PAY_PER_REQUEST" attribute { name = "nonce" type = "S" } ttl { attribute_name = "expirationTimeUnix" enabled = true } } resource "aws_dynamodb_table" "identity-reserved-usernames" { - name = "identity-reserved-usernames" - hash_key = "username" - billing_mode = "PAY_PER_REQUEST" + name = "identity-reserved-usernames" + hash_key = "username" + billing_mode = "PAY_PER_REQUEST" + stream_enabled = true + stream_view_type = "NEW_AND_OLD_IMAGES" attribute { name = "username" type = "S" } } resource "aws_dynamodb_table" "identity-one-time-keys" { name = "identity-one-time-keys" hash_key = "deviceID" range_key = "oneTimeKey" billing_mode = "PAY_PER_REQUEST" attribute { name = "deviceID" type = "S" } attribute { name = "oneTimeKey" type = "S" } } resource "aws_dynamodb_table" "feature-flags" { name = "feature-flags" hash_key = "platform" range_key = "feature" billing_mode = "PAY_PER_REQUEST" attribute { name = "platform" type = "S" } attribute { name = "feature" type = "S" } } resource "aws_dynamodb_table" "reports-service-reports" { name = "reports-service-reports" hash_key = "reportID" billing_mode = "PAY_PER_REQUEST" attribute { name = "reportID" type = "S" } } diff --git a/services/terraform/modules/shared/outputs.tf b/services/terraform/modules/shared/outputs.tf index 81b3c71ee..74e7903d1 100644 --- a/services/terraform/modules/shared/outputs.tf +++ b/services/terraform/modules/shared/outputs.tf @@ -1,26 +1,27 @@ locals { exported_dynamodb_tables = [ aws_dynamodb_table.feature-flags, aws_dynamodb_table.backup-service-backup, aws_dynamodb_table.reports-service-reports, aws_dynamodb_table.tunnelbroker-undelivered-messages, aws_dynamodb_table.identity-users, + aws_dynamodb_table.identity-reserved-usernames, ] } # map table names to their resources output "dynamodb_tables" { value = { for table in local.exported_dynamodb_tables : table.name => table } } output "opensearch_domain_identity" { value = aws_opensearch_domain.identity-search } output "search_index_lambda" { value = aws_lambda_function.search_index_lambda } diff --git a/services/terraform/modules/shared/search_index_lambda.tf b/services/terraform/modules/shared/search_index_lambda.tf index 46c1017b7..91e8eddd8 100644 --- a/services/terraform/modules/shared/search_index_lambda.tf +++ b/services/terraform/modules/shared/search_index_lambda.tf @@ -1,61 +1,67 @@ variable "search_index_lambda_iam_role_arn" { default = "arn:aws:iam::000000000000:role/lambda-role" } variable "lambda_zip_dir" { type = string default = "../../search-index-lambda/target/lambda/search-index-lambda" } resource "aws_lambda_function" "search_index_lambda" { function_name = "search-index-lambda-function" filename = "${var.lambda_zip_dir}/bootstrap.zip" source_code_hash = filebase64sha256("${var.lambda_zip_dir}/bootstrap.zip") handler = "bootstrap" role = var.search_index_lambda_iam_role_arn runtime = "provided.al2" architectures = ["arm64"] timeout = 300 vpc_config { subnet_ids = var.subnet_ids security_group_ids = [aws_security_group.search_index_lambda.id] } environment { variables = { RUST_BACKTRACE = "1" OPENSEARCH_ENDPOINT = aws_opensearch_domain.identity-search.endpoint } } tracing_config { mode = "Active" } } resource "aws_lambda_event_source_mapping" "identity_users_trigger" { event_source_arn = aws_dynamodb_table.identity-users.stream_arn function_name = aws_lambda_function.search_index_lambda.arn starting_position = "LATEST" } +resource "aws_lambda_event_source_mapping" "identity_reserved_usernames_trigger" { + event_source_arn = aws_dynamodb_table.identity-reserved-usernames.stream_arn + function_name = aws_lambda_function.search_index_lambda.arn + starting_position = "LATEST" +} + resource "aws_security_group" "search_index_lambda" { name = "search_index_lambda_sg" vpc_id = var.vpc_id egress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } resource "aws_lambda_function_event_invoke_config" "search-index-lambda" { function_name = aws_lambda_function.search_index_lambda.function_name maximum_event_age_in_seconds = 60 maximum_retry_attempts = 2 } diff --git a/services/terraform/remote/aws_iam.tf b/services/terraform/remote/aws_iam.tf index 5e5f1ac79..9983adec2 100644 --- a/services/terraform/remote/aws_iam.tf +++ b/services/terraform/remote/aws_iam.tf @@ -1,320 +1,321 @@ ### General AWS Utility IAM resources # Docs: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html resource "aws_iam_role" "ecs_instance_role" { name = "ecsInstanceRole" description = "Allows EC2 instances to call AWS services on your behalf." assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ec2.amazonaws.com" } } ] }) managed_policy_arns = [ "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role", # Let instances download Docker images from ECR "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" ] } # ECS Task execution role # Docs: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html resource "aws_iam_role" "ecs_task_execution" { name = "ecsTaskExecutionRole" assume_role_policy = jsonencode({ Version = "2008-10-17" Statement = [ { Sid = "" Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ecs-tasks.amazonaws.com" } } ] }) managed_policy_arns = [ "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess", "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", # Let ECS write logs to CloudWatch "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess", # Let ECS tasks access secrets to expose them as env vars "arn:aws:iam::aws:policy/SecretsManagerReadWrite", ] } # Assume Role Policy Document for EC2 and ECS # This policy allows ECS and EC2 use roles that it is assigned to data "aws_iam_policy_document" "assume_role_ecs_ec2" { statement { effect = "Allow" actions = [ "sts:AssumeRole", ] principals { type = "Service" identifiers = [ "ec2.amazonaws.com", "ecs-tasks.amazonaws.com" ] } } } # Allows ECS Exec to SSH into service task containers resource "aws_iam_policy" "allow_ecs_exec" { name = "allow-ecs-exec" description = "Adds SSM permissions to enable ECS Exec" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "ssmmessages:CreateControlChannel", "ssmmessages:CreateDataChannel", "ssmmessages:OpenControlChannel", "ssmmessages:OpenDataChannel" ] Resource = "*" } ] }) } ### App IAM resources # Our app role - this is to give access to DynamoDB etc # Has trust policy with EC2 and ECS # Also allows to SSH into containers resource "aws_iam_role" "services_ddb_full_access" { name = "dynamodb-s3-full-access" description = "Full RW access to DDB and S3. Allows to SSH into ECS containers" assume_role_policy = data.aws_iam_policy_document.assume_role_ecs_ec2.json managed_policy_arns = [ aws_iam_policy.allow_ecs_exec.arn, "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess", "arn:aws:iam::aws:policy/AmazonS3FullAccess", ] } # Feature Flags IAM data "aws_iam_policy_document" "read_feature_flags" { statement { sid = "FeatureFlagsDDBReadAccess" effect = "Allow" actions = [ "dynamodb:BatchGetItem", "dynamodb:GetItem", "dynamodb:Query", "dynamodb:Scan", ] resources = [ module.shared.dynamodb_tables["feature-flags"].arn ] } } resource "aws_iam_policy" "read_feature_flags" { name = "feature-flags-ddb-read-access" policy = data.aws_iam_policy_document.read_feature_flags.json description = "Allows full read access to feature-flags DynamoDB table" } resource "aws_iam_role" "feature_flags_service" { name = "feature-flags-service-role" assume_role_policy = data.aws_iam_policy_document.assume_role_ecs_ec2.json managed_policy_arns = [ aws_iam_policy.read_feature_flags.arn ] } # Backup Service IAM data "aws_iam_policy_document" "manage_backup_ddb" { statement { sid = "BackupFullDDBAccess" effect = "Allow" actions = [ "dynamodb:*", ] resources = [ module.shared.dynamodb_tables["backup-service-backup"].arn, "${module.shared.dynamodb_tables["backup-service-backup"].arn}/index/*" ] } } resource "aws_iam_policy" "manage_backup_ddb" { name = "backup-ddb-full-access" policy = data.aws_iam_policy_document.manage_backup_ddb.json description = "Allows full access to backup DynamoDB table" } resource "aws_iam_role" "backup_service" { name = "backup-service-role" assume_role_policy = data.aws_iam_policy_document.assume_role_ecs_ec2.json managed_policy_arns = [ aws_iam_policy.allow_ecs_exec.arn, aws_iam_policy.manage_backup_ddb.arn ] } # Reports Service IAM data "aws_iam_policy_document" "manage_reports_ddb" { statement { sid = "ReportsFullDDBAccess" effect = "Allow" actions = [ "dynamodb:*", ] resources = [ module.shared.dynamodb_tables["reports-service-reports"].arn ] } } resource "aws_iam_policy" "manage_reports_ddb" { name = "reports-ddb-full-access" policy = data.aws_iam_policy_document.manage_reports_ddb.json description = "Allows full access to reports DynamoDB table" } resource "aws_iam_role" "reports_service" { name = "reports-service-role" assume_role_policy = data.aws_iam_policy_document.assume_role_ecs_ec2.json managed_policy_arns = [ aws_iam_policy.allow_ecs_exec.arn, aws_iam_policy.manage_reports_ddb.arn ] } data "aws_iam_policy_document" "assume_identity_search_role" { statement { effect = "Allow" principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } actions = ["sts:AssumeRole"] } } resource "aws_iam_role" "search_index_lambda" { name = "search_index_lambda" assume_role_policy = data.aws_iam_policy_document.assume_identity_search_role.json managed_policy_arns = [ aws_iam_policy.manage_cloudwatch_logs.arn, aws_iam_policy.manage_network_interface.arn, aws_iam_policy.read_identity_users_stream.arn, ] } data "aws_iam_policy_document" "read_identity_users_stream" { statement { effect = "Allow" actions = [ "dynamodb:GetRecords", "dynamodb:GetShardIterator", "dynamodb:DescribeStream", "dynamodb:ListStreams", ] resources = [ - module.shared.dynamodb_tables["identity-users"].arn, module.shared.dynamodb_tables["identity-users"].stream_arn, "${module.shared.dynamodb_tables["identity-users"].arn}/stream/*", + module.shared.dynamodb_tables["identity-reserved-usernames"].stream_arn, + "${module.shared.dynamodb_tables["identity-reserved-usernames"].arn}/stream/*", ] } } resource "aws_iam_policy" "read_identity_users_stream" { name = "read-identity-users-stream" path = "/" description = "IAM policy for managing identity-users stream" policy = data.aws_iam_policy_document.read_identity_users_stream.json } data "aws_iam_policy_document" "manage_cloudwatch_logs" { statement { effect = "Allow" actions = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ] resources = ["arn:aws:logs:*:*:*"] } } resource "aws_iam_policy" "manage_cloudwatch_logs" { name = "manage-cloudwatch-logs" path = "/" description = "IAM policy for managing cloudwatch logs" policy = data.aws_iam_policy_document.manage_cloudwatch_logs.json } data "aws_iam_policy_document" "manage_network_interface" { statement { effect = "Allow" actions = [ "ec2:CreateNetworkInterface", "ec2:DescribeNetworkInterfaces", "ec2:DeleteNetworkInterface" ] resources = ["*"] } } resource "aws_iam_policy" "manage_network_interface" { name = "manage-network-interface" path = "/" description = "IAM policy for managing network interfaces" policy = data.aws_iam_policy_document.manage_network_interface.json } resource "aws_iam_role_policy_attachment" "AWSLambdaVPCAccessExecutionRole" { role = aws_iam_role.search_index_lambda.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" } data "aws_iam_policy_document" "opensearch_domain_access" { statement { effect = "Allow" principals { type = "*" identifiers = ["${module.shared.search_index_lambda.arn}"] } actions = [ "es:ESHttpHead", "es:ESHttpPost", "es:ESHttpGet", "es:ESHttpDelete", "es:ESHttpPut", ] resources = ["${module.shared.opensearch_domain_identity.arn}/*"] } } resource "aws_opensearch_domain_policy" "opensearch_domain_access" { domain_name = module.shared.opensearch_domain_identity.domain_name access_policies = data.aws_iam_policy_document.opensearch_domain_access.json }