diff --git a/locals.tf b/locals.tf index ca438da2..a972f1d7 100644 --- a/locals.tf +++ b/locals.tf @@ -38,6 +38,8 @@ locals { selected_database = ( var.enable_aurora && var.db_use_mtls ? error("Both enable_aurora and db_use_mtls cannot be true.") : + var.enable_aurora && var.postgres_enable_iam_auth ? error("Both enable_aurora and postgres_enable_iam_auth cannot be true.") : + var.db_use_mtls && var.postgres_enable_iam_auth ? error("Both db_use_mtls and postgres_enable_iam_auth cannot be true.") : var.enable_aurora ? local.aurora_database : var.db_use_mtls ? local.mtls_database : var.enable_edb ? local.enterprise_db : diff --git a/main.tf b/main.tf index 3de95ca7..08959c37 100644 --- a/main.tf +++ b/main.tf @@ -45,6 +45,16 @@ module "service_accounts" { postgres_client_key_secret_id = var.postgres_client_key_secret_id postgres_ca_certificate_secret_id = var.postgres_ca_certificate_secret_id vm_key_secret_id = var.vm_key_secret_id + redis_enable_iam_auth = var.redis_enable_iam_auth + postgres_enable_iam_auth = var.postgres_enable_iam_auth && !var.postgres_use_password_auth + db_username = var.db_username + db_iam_username = var.db_iam_username != null ? var.db_iam_username : "" + db_identifier = local.enable_database_module ? module.database[0].identifier : ( + var.enable_aurora ? module.aurora_database[0].identifier : "" + ) + db_resource_id = local.enable_database_module ? module.database[0].dbi_resource_id : ( + var.enable_aurora ? module.aurora_database[0].dbi_resource_id : "" + ) } # ----------------------------------------------------------------------------- @@ -94,6 +104,7 @@ module "redis" { redis_encryption_in_transit = var.redis_encryption_in_transit redis_encryption_at_rest = var.redis_encryption_at_rest redis_use_password_auth = var.redis_use_password_auth + redis_enable_iam_auth = var.redis_enable_iam_auth redis_port = var.redis_encryption_in_transit ? "6380" : "6379" } @@ -161,21 +172,22 @@ module "database" { source = "./modules/database" count = local.enable_database_module ? 1 : 0 - db_size = var.db_size - db_backup_retention = var.db_backup_retention - db_backup_window = var.db_backup_window - db_name = var.db_name - db_parameters = var.db_parameters - db_username = var.db_username - engine_version = var.postgres_engine_version - friendly_name_prefix = var.friendly_name_prefix - network_id = local.network_id - network_private_subnet_cidrs = var.network_private_subnet_cidrs - network_subnets_private = local.network_private_subnets - tfe_instance_sg = module.vm.tfe_instance_sg - kms_key_arn = local.kms_key_arn - allow_major_version_upgrade = var.allow_major_version_upgrade - allow_multiple_azs = var.allow_multiple_azs + db_size = var.db_size + db_backup_retention = var.db_backup_retention + db_backup_window = var.db_backup_window + db_name = var.db_name + db_parameters = var.db_parameters + db_username = var.db_username + engine_version = var.postgres_engine_version + friendly_name_prefix = var.friendly_name_prefix + network_id = local.network_id + network_private_subnet_cidrs = var.network_private_subnet_cidrs + network_subnets_private = local.network_private_subnets + tfe_instance_sg = module.vm.tfe_instance_sg + kms_key_arn = local.kms_key_arn + allow_major_version_upgrade = var.allow_major_version_upgrade + allow_multiple_azs = var.allow_multiple_azs + enable_iam_database_authentication = var.postgres_enable_iam_auth && !var.postgres_use_password_auth } # ----------------------------------------------------------------------------- @@ -253,7 +265,7 @@ module "aurora_database" { # Docker Compose File Config for TFE on instance(s) using Flexible Deployment Options # ------------------------------------------------------------------------------------ module "runtime_container_engine_config" { - source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/runtime_container_engine_config?ref=main" + source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/runtime_container_engine_config?ref=pravi-postgres-passwordless" count = var.is_replicated_deployment ? 0 : 1 tfe_license = var.hc_license @@ -286,15 +298,18 @@ module "runtime_container_engine_config" { iact_time_limit = var.iact_subnet_time_limit run_pipeline_image = var.run_pipeline_image - database_name = local.database.name - database_user = local.database.username - database_password = local.database.password - database_host = local.database.endpoint - database_parameters = local.database.parameters - database_use_mtls = var.db_use_mtls - database_ca_cert_file = "/etc/ssl/private/terraform-enterprise/postgres/ca.crt" - database_client_cert_file = "/etc/ssl/private/terraform-enterprise/postgres/cert.crt" - database_client_key_file = "/etc/ssl/private/terraform-enterprise/postgres/key.key" + database_name = local.database.name + database_user = var.postgres_enable_iam_auth && var.db_iam_username != null ? var.db_iam_username : local.database.username + database_password = var.postgres_use_password_auth ? local.database.password : "" + database_host = local.database.endpoint + database_parameters = local.database.parameters + database_use_mtls = var.db_use_mtls + database_ca_cert_file = "/etc/ssl/private/terraform-enterprise/postgres/ca.crt" + database_client_cert_file = "/etc/ssl/private/terraform-enterprise/postgres/cert.crt" + database_client_key_file = "/etc/ssl/private/terraform-enterprise/postgres/key.key" + # Enable PostgreSQL IAM authentication for isolated testing + database_passwordless_aws_use_iam = var.postgres_enable_iam_auth && !var.postgres_use_password_auth + database_passwordless_aws_region = var.postgres_enable_iam_auth && !var.postgres_use_password_auth ? data.aws_region.current.name : "" explorer_database_name = local.explorer_database.name explorer_database_user = local.explorer_database.username @@ -312,21 +327,21 @@ module "runtime_container_engine_config" { s3_server_side_encryption_kms_key_id = local.kms_key_arn s3_use_instance_profile = var.aws_access_key_id == null ? "1" : "0" - redis_host = local.redis.hostname - redis_user = local.redis.username - redis_password = local.redis.password - redis_use_tls = local.redis.use_tls - redis_use_auth = local.redis.use_password_auth - redis_use_sentinel = var.enable_redis_sentinel - redis_sentinel_hosts = local.redis.sentinel_hosts - redis_sentinel_leader_name = local.redis.sentinel_leader - redis_sentinel_user = local.redis.sentinel_username - redis_sentinel_password = local.redis.sentinel_password - redis_use_mtls = var.enable_redis_mtls - enable_sentinel_mtls = var.enable_sentinel_mtls - redis_ca_cert_path = "/etc/ssl/private/terraform-enterprise/redis/cacert.pem" - redis_client_cert_path = "/etc/ssl/private/terraform-enterprise/redis/cert.pem" - redis_client_key_path = "/etc/ssl/private/terraform-enterprise/redis/key.pem" + redis_host = local.redis.hostname + redis_user = local.redis.username + redis_password = var.redis_use_password_auth ? local.redis.password : "" + redis_use_tls = local.redis.use_tls + redis_use_auth = local.redis.use_password_auth || (var.redis_enable_iam_auth && !var.redis_use_password_auth) + redis_use_sentinel = var.enable_redis_sentinel + redis_sentinel_hosts = local.redis.sentinel_hosts + redis_sentinel_leader_name = local.redis.sentinel_leader + redis_sentinel_user = local.redis.sentinel_username + redis_sentinel_password = var.redis_use_password_auth ? local.redis.sentinel_password : "" + redis_use_mtls = var.enable_redis_mtls + enable_sentinel_mtls = var.enable_sentinel_mtls + redis_ca_cert_path = "/etc/ssl/private/terraform-enterprise/redis/cacert.pem" + redis_client_cert_path = "/etc/ssl/private/terraform-enterprise/redis/cert.pem" + redis_client_key_path = "/etc/ssl/private/terraform-enterprise/redis/key.pem" trusted_proxies = local.trusted_proxies @@ -343,7 +358,7 @@ module "runtime_container_engine_config" { # AWS cloud init used to install and configure TFE on instance(s) using Flexible Deployment Options # -------------------------------------------------------------------------------------------------- module "tfe_init_fdo" { - source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/tfe_init?ref=main" + source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/tfe_init?ref=pravi-postgres-passwordless" count = var.is_replicated_deployment ? 0 : 1 cloud = "aws" @@ -388,7 +403,7 @@ module "tfe_init_fdo" { # TFE and Replicated settings to pass to the tfe_init_replicated module for replicated deployment # -------------------------------------------------------------------------------------------- module "settings" { - source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/settings?ref=main" + source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/settings?ref=pravi-postgres-passwordless" count = var.is_replicated_deployment ? 1 : 0 # TFE Base Configuration @@ -450,7 +465,7 @@ module "settings" { # AWS user data / cloud init used to install and configure TFE on instance(s) # ----------------------------------------------------------------------------- module "tfe_init_replicated" { - source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/tfe_init_replicated?ref=main" + source = "git::https://github.com/hashicorp/terraform-random-tfe-utility//modules/tfe_init_replicated?ref=pravi-postgres-passwordless" count = var.is_replicated_deployment ? 1 : 0 # TFE & Replicated Configuration data diff --git a/modules/database/main.tf b/modules/database/main.tf index 84ca0292..e6f6899b 100644 --- a/modules/database/main.tf +++ b/modules/database/main.tf @@ -83,4 +83,7 @@ resource "aws_db_instance" "postgresql" { kms_key_id = var.kms_key_arn storage_type = "gp2" vpc_security_group_ids = [aws_security_group.postgresql.id] + + # Enable IAM database authentication if requested + iam_database_authentication_enabled = var.enable_iam_database_authentication } diff --git a/modules/database/variables.tf b/modules/database/variables.tf index 5361416e..fbd55bba 100644 --- a/modules/database/variables.tf +++ b/modules/database/variables.tf @@ -77,3 +77,9 @@ variable "allow_multiple_azs" { description = "Determine Amazon RDS Postgres deployment strategy." default = true } + +variable "enable_iam_database_authentication" { + type = bool + description = "Enable IAM database authentication for the RDS instance." + default = false +} diff --git a/modules/postgres-passwordless/data.tf b/modules/postgres-passwordless/data.tf new file mode 100644 index 00000000..d8814054 --- /dev/null +++ b/modules/postgres-passwordless/data.tf @@ -0,0 +1,23 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +data "aws_ami" "ubuntu" { + most_recent = true + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + owners = ["099720109477"] # Canonical +} + +data "aws_route53_zone" "postgres_zone" { + name = var.domain_name + private_zone = false +} \ No newline at end of file diff --git a/modules/postgres-passwordless/files/fetch_cert_and_start_server.sh b/modules/postgres-passwordless/files/fetch_cert_and_start_server.sh new file mode 100644 index 00000000..b65b9fd4 --- /dev/null +++ b/modules/postgres-passwordless/files/fetch_cert_and_start_server.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +set -eu pipefail + +apt-get update -y && apt-get install -y docker.io postgresql-client openssl unzip jq +systemctl enable --now docker +usermod -aG docker ubuntu + +curl -sS --noproxy '*' "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m | grep -q 'arm\|aarch' && echo 'aarch64' || echo 'x86_64').zip" -o "awscliv2.zip" > /dev/null 2>&1 +unzip -q awscliv2.zip > /dev/null 2>&1 +./aws/install > /dev/null 2>&1 +rm -rf aws awscliv2.zip > /dev/null 2>&1 + +# For passwordless postgres, we start with basic configuration +# IAM authentication will be handled at the RDS level +docker run -d \ + --name postgres \ + -p 5432:5432 \ + -e POSTGRES_USER="$POSTGRES_USER" \ + -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \ + -e POSTGRES_DB="$POSTGRES_DB" \ + postgres:16 + +# Wait until PostgreSQL is up +echo "Waiting for PostgreSQL to become ready..." +timeout=180 +start=$(date +%s) +while ! docker exec postgres pg_isready -U "$POSTGRES_USER" >/dev/null 2>&1; do + sleep 1 + [[ $(( $(date +%s) - start )) -gt $timeout ]] && echo "Timeout waiting for PostgreSQL" && docker logs postgres && exit 1 +done + +echo "PostgreSQL with passwordless authentication is fully up and running." \ No newline at end of file diff --git a/modules/postgres-passwordless/main.tf b/modules/postgres-passwordless/main.tf new file mode 100644 index 00000000..56d239e2 --- /dev/null +++ b/modules/postgres-passwordless/main.tf @@ -0,0 +1,113 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# This module provisions a PostgreSQL instance with passwordless authentication (IAM or similar) +# Adapted from database-mtls module, but without client certs/keys and with passwordless config + +resource "random_string" "postgres_db_password" { + length = 128 + special = true + override_special = "#$%&*" +} + +resource "aws_route53_record" "postgres_db_dns" { + zone_id = data.aws_route53_zone.postgres_zone.zone_id + name = "${var.friendly_name_prefix}-postgres-passwordless" + type = "A" + ttl = 300 + + records = [aws_instance.postgres_db_instance.public_ip] +} + +resource "aws_security_group" "postgres_db_sg" { + description = "The security group of the PostgreSQL deployment for TFE." + name = "${var.friendly_name_prefix}-postgres-passwordless" + vpc_id = var.network_id +} + +resource "aws_security_group_rule" "postgres_db_ingress" { + security_group_id = aws_security_group.postgres_db_sg.id + type = "ingress" + from_port = 5432 + to_port = 5432 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "postgres_db_ssh_ingress" { + security_group_id = aws_security_group.postgres_db_sg.id + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "postgres_db_egress" { + security_group_id = aws_security_group.postgres_db_sg.id + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_instance" "postgres_db_instance" { + ami = data.aws_ami.ubuntu.id + instance_type = "m5.xlarge" + associate_public_ip_address = true + vpc_security_group_ids = [aws_security_group.postgres_db_sg.id] + iam_instance_profile = var.aws_iam_instance_profile + key_name = aws_key_pair.ec2_key.key_name + subnet_id = var.network_public_subnets[0] + root_block_device { + volume_type = "gp3" + volume_size = 100 + delete_on_termination = true + encrypted = true + } + + tags = { + Name = "Terraform-Postgres-Passwordless" + } +} + +resource "local_file" "postgres_db_private_key" { + content = tls_private_key.postgres_db_ssh_key.private_key_pem + filename = "${path.module}/${var.friendly_name_prefix}-ec2-postgres-key.pem" + file_permission = "0600" +} + +resource "tls_private_key" "postgres_db_ssh_key" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "aws_key_pair" "ec2_key" { + key_name = "${var.friendly_name_prefix}-ec2-postgres-key" + public_key = tls_private_key.postgres_db_ssh_key.public_key_openssh +} + +resource "null_resource" "postgres_db_server_start" { + depends_on = [aws_route53_record.postgres_db_dns] + + connection { + type = "ssh" + user = "ubuntu" + private_key = tls_private_key.postgres_db_ssh_key.private_key_pem + host = aws_route53_record.postgres_db_dns.fqdn + } + + provisioner "file" { + source = "${path.module}/files/fetch_cert_and_start_server.sh" + destination = "/home/ubuntu/fetch_cert_and_start_server.sh" + } + + provisioner "remote-exec" { + inline = [ + "sleep 60", + "chmod +x /home/ubuntu/fetch_cert_and_start_server.sh", + "sudo POSTGRES_PASSWORD='${random_string.postgres_db_password.result}' POSTGRES_USER=${var.db_username} POSTGRES_DB=${var.db_name} /home/ubuntu/fetch_cert_and_start_server.sh" + ] + } +} diff --git a/modules/postgres-passwordless/outputs.tf b/modules/postgres-passwordless/outputs.tf new file mode 100644 index 00000000..3f0543ac --- /dev/null +++ b/modules/postgres-passwordless/outputs.tf @@ -0,0 +1,45 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +output "endpoint" { + description = "The connection endpoint of the PostgreSQL instance in address:port format." + value = aws_route53_record.postgres_db_dns.fqdn +} + +output "name" { + description = "The name of the PostgreSQL instance." + value = var.db_name +} + +output "password" { + description = "The password of the main PostgreSQL user." + value = random_string.postgres_db_password.result + sensitive = true +} + +output "username" { + description = "The name of the main PostgreSQL user." + value = var.db_username +} + +output "parameters" { + description = "PostgreSQL server parameters for the connection URI." + value = var.db_parameters +} + +# Legacy outputs for backward compatibility +output "postgres_db_endpoint" { + description = "The endpoint of the PostgreSQL instance." + value = aws_route53_record.postgres_db_dns.fqdn +} + +output "postgres_db_sg_id" { + description = "The security group ID for the PostgreSQL instance." + value = aws_security_group.postgres_db_sg.id +} + +output "postgres_db_password" { + description = "The password for the PostgreSQL instance." + value = random_string.postgres_db_password.result + sensitive = true +} diff --git a/modules/postgres-passwordless/service_accounts/data.tf b/modules/postgres-passwordless/service_accounts/data.tf new file mode 100644 index 00000000..7bd8c303 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/data.tf @@ -0,0 +1,14 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +data "aws_iam_instance_profile" "existing_instance_profile" { + count = var.existing_iam_instance_profile_name != null ? 1 : 0 + + name = var.existing_iam_instance_profile_name +} + +data "aws_iam_role" "existing_instance_role" { + count = var.existing_iam_instance_role_name != null ? 1 : 0 + + name = var.existing_iam_instance_role_name +} \ No newline at end of file diff --git a/modules/postgres-passwordless/service_accounts/locals.tf b/modules/postgres-passwordless/service_accounts/locals.tf new file mode 100644 index 00000000..b5ed7979 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/locals.tf @@ -0,0 +1,7 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + iam_instance_role = try(data.aws_iam_role.existing_instance_role[0], aws_iam_role.instance_role[0]) + iam_instance_profile = try(data.aws_iam_instance_profile.existing_instance_profile[0], aws_iam_instance_profile.postgres_passwordless[0]) +} \ No newline at end of file diff --git a/modules/postgres-passwordless/service_accounts/main.tf b/modules/postgres-passwordless/service_accounts/main.tf new file mode 100644 index 00000000..5575a2c5 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/main.tf @@ -0,0 +1,122 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_iam_instance_profile" "postgres_passwordless" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + name_prefix = "${var.friendly_name_prefix}-postgres-passwordless" + role = local.iam_instance_role.name +} + +resource "aws_iam_role" "instance_role" { + count = var.existing_iam_instance_role_name == null ? 1 : 0 + + name_prefix = "${var.friendly_name_prefix}-postgres-passwordless" + assume_role_policy = data.aws_iam_policy_document.instance_role[0].json +} + +data "aws_iam_policy_document" "instance_role" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + statement { + effect = "Allow" + actions = [ + "sts:AssumeRole", + ] + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +# RDS IAM authentication policy for passwordless database access +resource "aws_iam_role_policy" "rds_iam_auth" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + policy = data.aws_iam_policy_document.rds_iam_auth[0].json + role = local.iam_instance_role.id + + name = "${var.friendly_name_prefix}-postgres-passwordless-rds-auth" +} + +data "aws_iam_policy_document" "rds_iam_auth" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + statement { + actions = [ + "rds-db:connect" + ] + effect = "Allow" + resources = [ + "arn:aws:rds-db:*:*:dbuser:${var.db_instance_identifier}/${var.db_username}" + ] + sid = "AllowRDSIAMAuthentication" + } +} + +# Basic EC2 and CloudWatch permissions +resource "aws_iam_role_policy" "basic_permissions" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + policy = data.aws_iam_policy_document.basic_permissions[0].json + role = local.iam_instance_role.id + + name = "${var.friendly_name_prefix}-postgres-passwordless-basic" +} + +data "aws_iam_policy_document" "basic_permissions" { + count = var.existing_iam_instance_profile_name == null ? 1 : 0 + + statement { + actions = [ + "ec2:DescribeInstances", + "ec2:DescribeTags", + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ] + effect = "Allow" + resources = ["*"] + sid = "AllowBasicEC2AndCloudWatchAccess" + } +} + +# This will allow you to add any additional policies you may need, regardless +# of whether you're using an existing role and instance profile. +resource "aws_iam_role_policy_attachment" "misc" { + for_each = var.iam_role_policy_arns + + role = local.iam_instance_role.name + policy_arn = each.value +} + +resource "aws_iam_role_policy_attachment" "kms_policy" { + count = var.existing_iam_instance_profile_name == null && var.kms_key_arn != null ? 1 : 0 + + role = local.iam_instance_role.name + policy_arn = aws_iam_policy.kms_policy[0].arn +} + +resource "aws_iam_policy" "kms_policy" { + count = var.existing_iam_instance_profile_name == null && var.kms_key_arn != null ? 1 : 0 + + name = "${var.friendly_name_prefix}-postgres-passwordless-kms" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey", + ] + Effect = "Allow" + Resource = var.kms_key_arn + }, + ] + }) +} \ No newline at end of file diff --git a/modules/postgres-passwordless/service_accounts/outputs.tf b/modules/postgres-passwordless/service_accounts/outputs.tf new file mode 100644 index 00000000..9fb46fd0 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/outputs.tf @@ -0,0 +1,22 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +output "iam_instance_profile" { + description = "The IAM instance profile that will be attached to the PostgreSQL EC2 instance." + value = local.iam_instance_profile +} + +output "iam_instance_profile_name" { + description = "The name of the IAM instance profile that will be attached to the PostgreSQL EC2 instance." + value = local.iam_instance_profile.name +} + +output "iam_role" { + description = "The IAM role associated with the PostgreSQL EC2 instance." + value = local.iam_instance_role +} + +output "iam_role_name" { + description = "The name of the IAM role associated with the PostgreSQL EC2 instance." + value = local.iam_instance_role.name +} \ No newline at end of file diff --git a/modules/postgres-passwordless/service_accounts/variables.tf b/modules/postgres-passwordless/service_accounts/variables.tf new file mode 100644 index 00000000..faafd759 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/variables.tf @@ -0,0 +1,41 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "existing_iam_instance_profile_name" { + description = "The IAM instance profile to be attached to the PostgreSQL EC2 instance. Leave the value null to create a new one." + type = string + default = null +} + +variable "existing_iam_instance_role_name" { + type = string + description = "The IAM role to associate with the instance profile. To create a new role, this value should be null." + default = null +} + +variable "friendly_name_prefix" { + type = string + description = "(Required) Friendly name prefix used for tagging and naming AWS resources." +} + +variable "iam_role_policy_arns" { + default = [] + description = "A set of Amazon Resource Names of IAM role policies to be attached to the PostgreSQL IAM role." + type = set(string) +} + +variable "kms_key_arn" { + type = string + description = "KMS key arn for AWS KMS Customer managed key. Set to null if not using KMS." + default = null +} + +variable "db_instance_identifier" { + type = string + description = "The RDS instance identifier for IAM authentication. Used in the RDS IAM policy." +} + +variable "db_username" { + type = string + description = "The database username for IAM authentication. Used in the RDS IAM policy." +} \ No newline at end of file diff --git a/modules/postgres-passwordless/service_accounts/versions.tf b/modules/postgres-passwordless/service_accounts/versions.tf new file mode 100644 index 00000000..f1371cf2 --- /dev/null +++ b/modules/postgres-passwordless/service_accounts/versions.tf @@ -0,0 +1,12 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_version = ">= 1.0.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +} \ No newline at end of file diff --git a/modules/postgres-passwordless/variables.tf b/modules/postgres-passwordless/variables.tf new file mode 100644 index 00000000..43129ca6 --- /dev/null +++ b/modules/postgres-passwordless/variables.tf @@ -0,0 +1,44 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "domain_name" { + description = "The name of the Route 53 Hosted Zone in which a record will be created." + type = string +} + +variable "db_name" { + type = string + description = "PostgreSQL instance name. No special characters." +} + +variable "db_username" { + type = string + description = "PostgreSQL instance username. No special characters." +} + +variable "db_parameters" { + type = string + description = "PostgreSQL server parameters for the connection URI. Used to configure the PostgreSQL connection (e.g. sslmode=require)." + default = "" +} + +variable "network_id" { + description = "The identity of the VPC in which the security group attached to the PostgreSQL instance will be deployed." + type = string +} + +variable "network_public_subnets" { + default = [] + description = "A list of the identities of the public subnetworks in which resources will be deployed." + type = list(string) +} + +variable "friendly_name_prefix" { + type = string + description = "(Required) Friendly name prefix used for tagging and naming AWS resources." +} + +variable "aws_iam_instance_profile" { + description = "The AWS IAM instance profile name to be attached to the instance." + type = string +} diff --git a/modules/postgres-passwordless/versions.tf b/modules/postgres-passwordless/versions.tf new file mode 100644 index 00000000..18efc9b7 --- /dev/null +++ b/modules/postgres-passwordless/versions.tf @@ -0,0 +1,28 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_version = ">= 1.0.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 3.0" + } + local = { + source = "hashicorp/local" + version = ">= 2.0" + } + null = { + source = "hashicorp/null" + version = ">= 3.0" + } + } +} diff --git a/modules/redis/variables.tf b/modules/redis/variables.tf index db99b9f1..fc1d2123 100644 --- a/modules/redis/variables.tf +++ b/modules/redis/variables.tf @@ -82,3 +82,9 @@ variable "redis_use_password_auth" { type = bool description = "Determine if a password is required for Redis." } + +variable "redis_enable_iam_auth" { + type = bool + description = "Whether to enable IAM authentication for Redis. Used for passwordless authentication." + default = false +} diff --git a/modules/service_accounts/main.tf b/modules/service_accounts/main.tf index d7c7b8ed..d76c05e9 100644 --- a/modules/service_accounts/main.tf +++ b/modules/service_accounts/main.tf @@ -114,3 +114,38 @@ resource "aws_iam_policy" "kms_policy" { ] }) } + +# PostgreSQL IAM authentication policy +resource "aws_iam_policy" "postgres_iam_policy" { + count = var.postgres_enable_iam_auth ? 1 : 0 + + name_prefix = "${var.friendly_name_prefix}-tfe-postgres-iam" + description = "IAM policy for PostgreSQL database authentication" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "rds-db:connect" + ] + Resource = [ + "arn:aws:rds-db:*:*:dbuser:${var.db_identifier}/${var.db_iam_username}" + ] + } + ] + }) + + tags = { + Name = "${var.friendly_name_prefix}-tfe-postgres-iam-policy" + } +} + +# Attach PostgreSQL IAM policy to the instance role +resource "aws_iam_role_policy_attachment" "postgres_iam_policy_attachment" { + count = var.postgres_enable_iam_auth && var.existing_iam_instance_profile_name == null ? 1 : 0 + + role = local.iam_instance_role.name + policy_arn = aws_iam_policy.postgres_iam_policy[0].arn +} diff --git a/modules/service_accounts/outputs.tf b/modules/service_accounts/outputs.tf index c0841629..f140aab3 100644 --- a/modules/service_accounts/outputs.tf +++ b/modules/service_accounts/outputs.tf @@ -12,3 +12,8 @@ output "iam_role" { description = "The IAM role associated with the instance profile." } + +output "postgres_iam_policy_arn" { + value = try(aws_iam_policy.postgres_iam_policy[0].arn, "") + description = "The ARN of the PostgreSQL IAM authentication policy, if created." +} diff --git a/modules/service_accounts/variables.tf b/modules/service_accounts/variables.tf index 9fa39644..76f06e6f 100644 --- a/modules/service_accounts/variables.tf +++ b/modules/service_accounts/variables.tf @@ -98,3 +98,40 @@ variable "postgres_client_key_secret_id" { default = null description = "The secrets manager secret ID of the Base64 & PEM encoded private key for postgres." } + +# IAM Authentication variables +variable "redis_enable_iam_auth" { + type = bool + default = false + description = "Whether to enable IAM authentication for Redis." +} + +variable "postgres_enable_iam_auth" { + type = bool + default = false + description = "Whether to enable IAM authentication for PostgreSQL." +} + +variable "db_username" { + type = string + default = "" + description = "The master username for the database." +} + +variable "db_iam_username" { + type = string + default = "" + description = "The IAM username for database authentication." +} + +variable "db_identifier" { + type = string + default = "" + description = "The database identifier." +} + +variable "db_resource_id" { + type = string + default = "" + description = "The database resource ID." +} diff --git a/outputs.tf b/outputs.tf index fc0e7af0..f530e7fc 100644 --- a/outputs.tf +++ b/outputs.tf @@ -90,3 +90,31 @@ output "s3_bucket" { value = local.object_storage.s3_bucket description = "S3 bucket name" } + +# Database outputs for PostgreSQL passwordless authentication +output "database_endpoint" { + value = local.enable_database_module ? module.database[0].endpoint : (var.enable_aurora ? module.aurora_database[0].endpoint : "") + description = "The PostgreSQL database endpoint." +} + +output "database_name" { + value = local.enable_database_module ? module.database[0].name : (var.enable_aurora ? module.aurora_database[0].name : "") + description = "The PostgreSQL database name." +} + +output "database_username" { + value = local.enable_database_module ? module.database[0].username : (var.enable_aurora ? module.aurora_database[0].username : "") + description = "The PostgreSQL database username." + sensitive = true +} + +output "database_password" { + value = local.enable_database_module ? module.database[0].password : (var.enable_aurora ? module.aurora_database[0].password : "") + description = "The PostgreSQL database password." + sensitive = true +} + +output "postgres_iam_policy_arn" { + value = module.service_accounts.postgres_iam_policy_arn + description = "The ARN of the PostgreSQL IAM authentication policy, if created." +} diff --git a/variables.tf b/variables.tf index 3b901ef1..2333e019 100644 --- a/variables.tf +++ b/variables.tf @@ -208,6 +208,12 @@ variable "sentinel_leader" { description = "The name of the Redis Sentinel leader" } +variable "redis_enable_iam_auth" { + type = bool + description = "Whether to enable IAM authentication for Redis. Used for passwordless authentication." + default = false +} + # Postgres # -------- variable "db_name" { @@ -222,6 +228,12 @@ variable "db_username" { description = "PostgreSQL instance username. No special characters." } +variable "db_iam_username" { + default = null + type = string + description = "PostgreSQL IAM username for TFE connection when IAM auth is enabled. If null, uses db_username. No special characters." +} + variable "db_backup_retention" { type = number description = "The days to retain backups for. Must be between 0 and 35" @@ -270,6 +282,18 @@ variable "db_use_mtls" { default = false } +variable "postgres_enable_iam_auth" { + type = bool + description = "Whether to enable IAM authentication for PostgreSQL. Used for passwordless authentication." + default = false +} + +variable "postgres_use_password_auth" { + type = bool + description = "Whether to use password authentication for PostgreSQL. Set to false for passwordless authentication." + default = true +} + variable "postgres_ca_certificate_secret_id" { type = string description = "The secrets manager secret ID of the Base64 & PEM encoded certificate for postgres."