diff --git a/.github/workflows/enos-run.yml b/.github/workflows/enos-run.yml index cd1c3e10f..99b0d4207 100644 --- a/.github/workflows/enos-run.yml +++ b/.github/workflows/enos-run.yml @@ -96,7 +96,6 @@ jobs: ENOS_VAR_local_boundary_src_dir: ${{ github.workspace }} ENOS_VAR_local_boundary_ui_src_dir: ./support/src/boundary-ui ENOS_VAR_crt_bundle_path: ./support/boundary.zip - ENOS_VAR_tfc_api_token: ${{ secrets.TF_API_TOKEN }} ENOS_VAR_test_email: ${{ secrets.SERVICE_USER_EMAIL }} ENOS_VAR_boundary_edition: ${{ inputs.edition }} ENOS_VAR_boundary_docker_image_name: ${{ inputs.docker-image-name }} diff --git a/enos/README.md b/enos/README.md index 45f12443f..bda267d87 100644 --- a/enos/README.md +++ b/enos/README.md @@ -13,17 +13,12 @@ for further information regarding installation, execution or composing Enos scen ## Requirements * AWS access. HashiCorp Boundary developers should use Doormat. * Terraform >= 1.0 -* Enos >= v0.0.10. You can [install it from a release channel](https://github.com/hashicorp/Enos-Docs/blob/main/installation.md) or use `make tools` install it into `$GOBIN`. -* Access to the QTI org in Terraform Cloud. HashiCorp Boundary developers can - access this token in 1Password or request their own in #team-quality on slack. +* Enos >= v0.0.28 (`brew tap hashicorp/tap && brew update && brew install hashicorp/tap/enos`) * An SSH keypair in the AWS region you wish to run the scenario. You can use doormat to login to the AWS console to create or upload an existing keypair. * Boundary installed locally. `make install` will put it in `$GOPATH/bin`, which you can use with the `local_boundary_dir` variable, e.g. `local_boundary_dir = /Users//.go/bin`. -* For the Bats CLI UI scenarios, you'll need `bats`, `jq` and a valid keychain - configured. Windows and macOS will use the system keychains by default. If - you're using Linux it will default to [pass](https://www.passwordstore.org/). ## Scenarios Variables In CI, each scenario is executed via Github Actions and has been configured using @@ -33,7 +28,6 @@ For local execution you can specify all the required variables using environment variables, or you can update `enos.vars.hcl` with values and uncomment the lines. Variables that are required: -- `tfc_api_token` - `aws_ssh_private_key_path` - `aws_ssh_keypair_name` - `enos_user` diff --git a/enos/enos-modules.hcl b/enos/enos-modules.hcl index f43140774..d2b53ba11 100644 --- a/enos/enos-modules.hcl +++ b/enos/enos-modules.hcl @@ -104,7 +104,7 @@ module "aws_target" { } module "vault" { - source = "app.terraform.io/hashicorp-qti/aws-vault/enos" + source = "./modules/aws_vault" project_name = "qti-enos-boundary" environment = var.environment diff --git a/enos/enos-scenario-e2e-aws-base-with-vault.hcl b/enos/enos-scenario-e2e-aws-base-with-vault.hcl index 073b60fe1..8083d89cb 100644 --- a/enos/enos-scenario-e2e-aws-base-with-vault.hcl +++ b/enos/enos-scenario-e2e-aws-base-with-vault.hcl @@ -161,6 +161,7 @@ scenario "e2e_aws_base_with_vault" { vault_addr = step.create_vault_cluster.instance_public_ips[0] vault_root_token = step.create_vault_cluster.vault_root_token aws_region = var.aws_region + max_page_size = step.create_boundary_cluster.max_page_size } } diff --git a/enos/enos-variables.hcl b/enos/enos-variables.hcl index 14832c864..82b586d3d 100644 --- a/enos/enos-variables.hcl +++ b/enos/enos-variables.hcl @@ -119,11 +119,6 @@ variable "boundary_install_dir" { default = "/opt/boundary/bin" } -variable "tfc_api_token" { - description = "The Terraform Cloud QTI Organization API token." - type = string -} - variable "vault_instance_type" { description = "Instance type for test target nodes" type = string diff --git a/enos/enos.hcl b/enos/enos.hcl index 3483a10cc..5bd1d82a5 100644 --- a/enos/enos.hcl +++ b/enos/enos.hcl @@ -3,10 +3,6 @@ terraform_cli "default" { plugin_cache_dir = abspath("./terraform-plugin-cache") - - credentials "app.terraform.io" { - token = var.tfc_api_token - } } terraform "default" { @@ -14,7 +10,7 @@ terraform "default" { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } aws = { diff --git a/enos/enos.vars.hcl b/enos/enos.vars.hcl index 87210362f..d0c5979c3 100644 --- a/enos/enos.vars.hcl +++ b/enos/enos.vars.hcl @@ -19,11 +19,6 @@ // the workflow will be used automatically in the CI. // enos_user = "enos" -// The app.terraform.io API token for the QTI organization. There is a shared -// token for the Boundary team in 1Password, or you can request access to the -// organization by reaching out to the members of #team-quality. -// tfc_api_token = "xxxxxxxx.atlasv1.xxx...." - // The directory that contains the copy of boundary you want to local execution // from. `make install` should install it into the $GOBIN, which is usually // similar to what is listed below. diff --git a/enos/modules/aws_boundary/main.tf b/enos/modules/aws_boundary/main.tf index e25624c17..e962e966b 100644 --- a/enos/modules/aws_boundary/main.tf +++ b/enos/modules/aws_boundary/main.tf @@ -6,7 +6,7 @@ terraform { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" version = ">= 0.3.25" } } diff --git a/enos/modules/aws_target/main.tf b/enos/modules/aws_target/main.tf index 5f99d8a83..053583707 100644 --- a/enos/modules/aws_target/main.tf +++ b/enos/modules/aws_target/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/aws_vault/iam.tf b/enos/modules/aws_vault/iam.tf new file mode 100644 index 000000000..7eb568082 --- /dev/null +++ b/enos/modules/aws_vault/iam.tf @@ -0,0 +1,52 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +data "aws_iam_policy_document" "vault_instance_role" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +data "aws_iam_policy_document" "vault_profile" { + statement { + resources = ["*"] + + actions = [ + "ec2:DescribeInstances", + "secretsmanager:*" + ] + } + + statement { + resources = [var.kms_key_arn] + + actions = [ + "kms:DescribeKey", + "kms:ListKeys", + "kms:Encrypt", + "kms:Decrypt", + "kms:GenerateDataKey" + ] + } +} + +resource "aws_iam_role" "vault_instance_role" { + name = "vault_instance_role-${random_string.cluster_id.result}" + assume_role_policy = data.aws_iam_policy_document.vault_instance_role.json +} + +resource "aws_iam_instance_profile" "vault_profile" { + name = "vault_instance_profile-${random_string.cluster_id.result}" + role = aws_iam_role.vault_instance_role.name +} + +resource "aws_iam_role_policy" "vault_policy" { + name = "vault_policy-${random_string.cluster_id.result}" + role = aws_iam_role.vault_instance_role.id + policy = data.aws_iam_policy_document.vault_profile.json +} diff --git a/enos/modules/aws_vault/infra.tf b/enos/modules/aws_vault/infra.tf new file mode 100644 index 000000000..ddcb05f08 --- /dev/null +++ b/enos/modules/aws_vault/infra.tf @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +data "aws_vpc" "infra" { + id = var.vpc_id +} + +data "aws_subnets" "infra" { + filter { + name = "vpc-id" + values = [var.vpc_id] + } +} + +data "aws_caller_identity" "current" {} + +data "aws_kms_key" "kms_key" { + key_id = var.kms_key_arn +} diff --git a/enos/modules/aws_vault/locals.tf b/enos/modules/aws_vault/locals.tf new file mode 100644 index 000000000..81a94507f --- /dev/null +++ b/enos/modules/aws_vault/locals.tf @@ -0,0 +1,56 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +locals { + consul_bin_path = "${var.consul_install_dir}/consul" + key_shares = { + "awskms" = null + "shamir" = 5 + } + key_threshold = { + "awskms" = null + "shamir" = 3 + } + followers = toset(slice(local.instances, 1, length(local.instances))) + instances = [for idx in range(var.instance_count) : tostring(idx)] + leader = toset(slice(local.instances, 0, 1)) + name_suffix = "${var.project_name}-${var.environment}" + recovery_shares = { + "awskms" = 5 + "shamir" = null + } + recovery_threshold = { + "awskms" = 3 + "shamir" = null + } + seal = { + "awskms" = { + type = "awskms" + attributes = { + kms_key_id = var.kms_key_arn + } + } + "shamir" = { + type = "shamir" + attributes = null + } + } + storage_config = [for idx in local.vault_instances : (var.storage_backend == "raft" ? + merge( + { + node_id = "${var.vault_node_prefix}_${idx}" + }, + var.storage_backend_addl_config + ) : + { + address = "127.0.0.1:8500" + path = "vault" + }) + ] + vault_bin_path = "${var.vault_install_dir}/vault" + vault_cluster_tag = coalesce(var.vault_cluster_tag, "vault-server-${random_string.cluster_id.result}") + vault_instances = toset(local.instances) + audit_device_file_path = "/var/log/vault/vault_audit.log" + vault_service_user = "vault" + enable_audit_device = var.enable_file_audit_device && var.vault_init +} diff --git a/enos/modules/aws_vault/main.tf b/enos/modules/aws_vault/main.tf new file mode 100644 index 000000000..40b0ebdc6 --- /dev/null +++ b/enos/modules/aws_vault/main.tf @@ -0,0 +1,21 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +terraform { + required_providers { + enos = { + source = "registry.terraform.io/hashicorp-forge/enos" + version = ">= 0.4.2" + } + } +} + +data "enos_environment" "localhost" {} + +resource "random_string" "cluster_id" { + length = 8 + lower = true + upper = false + numeric = false + special = false +} diff --git a/enos/modules/aws_vault/outputs.tf b/enos/modules/aws_vault/outputs.tf new file mode 100644 index 000000000..9ad96e38b --- /dev/null +++ b/enos/modules/aws_vault/outputs.tf @@ -0,0 +1,78 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +output "instance_ids" { + description = "IDs of Vault instances" + value = [for instance in aws_instance.vault_instance : instance.id] +} + +output "instance_public_ips" { + description = "Public IPs of Vault instances" + value = [for instance in aws_instance.vault_instance : instance.public_ip] +} + +output "instance_private_ips" { + description = "Private IPs of Vault instances" + value = [for instance in aws_instance.vault_instance : instance.private_ip] +} + +output "key_id" { + value = data.aws_kms_key.kms_key.id +} + +output "vault_instances" { + description = "The vault cluster instances that were created" + + value = { + for instance in aws_instance.vault_instance : instance.id => { + public_ip = instance.public_ip + private_ip = instance.private_ip + } + } +} + +output "vault_root_token" { + value = coalesce(var.vault_root_token, try(enos_vault_init.leader[0].root_token, null), "none") +} + +output "vault_unseal_keys_b64" { + value = try(enos_vault_init.leader[0].unseal_keys_b64, []) +} + +output "vault_unseal_keys_hex" { + value = try(enos_vault_init.leader[0].unseal_keys_hex, null) +} + +output "vault_unseal_shares" { + value = try(enos_vault_init.leader[0].unseal_keys_shares, -1) +} + +output "vault_unseal_threshold" { + value = try(enos_vault_init.leader[0].unseal_keys_threshold, -1) +} + +output "vault_recovery_keys_b64" { + value = try(enos_vault_init.leader[0].recovery_keys_b64, []) +} + +output "vault_recovery_keys_hex" { + value = try(enos_vault_init.leader[0].recovery_keys_hex, []) +} + +output "vault_recovery_key_shares" { + value = try(enos_vault_init.leader[0].recovery_keys_shares, -1) +} + +output "vault_recovery_threshold" { + value = try(enos_vault_init.leader[0].recovery_keys_threshold, -1) +} + +output "vault_cluster_tag" { + description = "Cluster tag for Vault cluster" + value = local.vault_cluster_tag +} + +output "audit_device_log_file_path" { + description = "The path of the file audit device path, if enabled" + value = var.enable_file_audit_device ? local.audit_device_file_path : "no audit devices enabled" +} diff --git a/enos/modules/aws_vault/scripts/create_audit_log_dir.sh b/enos/modules/aws_vault/scripts/create_audit_log_dir.sh new file mode 100755 index 000000000..1d6f18c01 --- /dev/null +++ b/enos/modules/aws_vault/scripts/create_audit_log_dir.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +set -eux + +LOG_DIR=$(dirname "$LOG_FILE_PATH") + +function retry { + local retries=$1 + shift + local count=0 + + until "$@"; do + exit=$? + wait=10 + count=$((count + 1)) + + if [ "$count" -lt "$retries" ]; then + sleep "$wait" + else + return "$exit" + fi + done + + return 0 +} + +# Strangely, sometimes the service user has not been created yet. This retry awaits the creation of the user +retry 7 id -a "$SERVICE_USER" + +sudo mkdir -p "$LOG_DIR" +sudo chown -R "$SERVICE_USER":"$SERVICE_USER" "$LOG_DIR" diff --git a/enos/modules/aws_vault/scripts/enable_audit_logging.sh b/enos/modules/aws_vault/scripts/enable_audit_logging.sh new file mode 100644 index 000000000..92fb23bef --- /dev/null +++ b/enos/modules/aws_vault/scripts/enable_audit_logging.sh @@ -0,0 +1,7 @@ +#!/bin/env sh +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +set -eux + +$VAULT_BIN_PATH audit enable file file_path="$LOG_FILE_PATH" diff --git a/enos/modules/aws_vault/security-groups.tf b/enos/modules/aws_vault/security-groups.tf new file mode 100644 index 000000000..b96d33106 --- /dev/null +++ b/enos/modules/aws_vault/security-groups.tf @@ -0,0 +1,74 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +resource "aws_security_group" "enos_vault_sg" { + name = "vault-sg-${random_string.cluster_id.result}" + description = "SSH and Vault Traffic" + vpc_id = var.vpc_id + + # SSH + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.localhost.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + } + + # Vault traffic + ingress { + from_port = 8200 + to_port = 8201 + protocol = "tcp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.localhost.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + formatlist("%s/32", var.sg_additional_ips), + ]) + } + + # Consul Agent traffic + ingress { + from_port = 8301 + to_port = 8301 + protocol = "tcp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.localhost.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + } + + ingress { + from_port = 8301 + to_port = 8301 + protocol = "udp" + cidr_blocks = flatten([ + formatlist("%s/32", data.enos_environment.localhost.public_ipv4_addresses), + join(",", data.aws_vpc.infra.cidr_block_associations.*.cidr_block), + ]) + } + + # Internal Traffic + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + self = true + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge( + var.common_tags, + { + Name = "${local.name_suffix}-vault-sg" + }, + ) +} diff --git a/enos/modules/aws_vault/templates/install-dependencies.sh b/enos/modules/aws_vault/templates/install-dependencies.sh new file mode 100755 index 000000000..1f6bc1c5b --- /dev/null +++ b/enos/modules/aws_vault/templates/install-dependencies.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +set -ex -o pipefail + +deps="${dependencies}" + +if [ "$deps" == "" ] +then + echo "No dependencies to install." + exit 0 +fi + +function retry { + local retries=$1 + shift + local count=0 + + until "$@"; do + exit=$? + wait=$((2 ** count)) + count=$((count + 1)) + if [ "$count" -lt "$retries" ]; then + sleep "$wait" + else + return "$exit" + fi + done + + return 0 +} + +echo "Installing Dependencies: $deps" +if [ -f /etc/debian_version ]; then + # Make sure cloud-init is not modifying our sources list while we're trying + # to install. + retry 7 grep ec2 /etc/apt/sources.list + + cd /tmp + retry 5 sudo apt update + retry 5 sudo apt install -y $${deps[@]} +else + cd /tmp + retry 7 sudo yum -y install $${deps[@]} +fi diff --git a/enos/modules/aws_vault/templates/vault-write-license.sh b/enos/modules/aws_vault/templates/vault-write-license.sh new file mode 100755 index 000000000..7187c35b9 --- /dev/null +++ b/enos/modules/aws_vault/templates/vault-write-license.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +license='${vault_license}' +if test $license = "none"; then + exit 0 +fi + +function retry { + local retries=$1 + shift + local count=0 + + until "$@"; do + exit=$? + wait=$((2 ** count)) + count=$((count + 1)) + + if [ "$count" -lt "$retries" ]; then + sleep "$wait" + else + return "$exit" + fi + done + + return 0 +} + +export VAULT_ADDR=http://localhost:8200 +export VAULT_TOKEN='${vault_root_token}' + +# Temporary hack until we can make the unseal resource handle legacy license +# setting. If we're running 1.8 and above then we shouldn't try to set a license. +ver=$(${vault_bin_path} version) +if [[ "$(echo "$ver" |awk '{print $2}' |awk -F'.' '{print $2}')" -ge 8 ]]; then + exit 0 +fi + +retry 5 ${vault_bin_path} write /sys/license text="$license" diff --git a/enos/modules/aws_vault/variables.tf b/enos/modules/aws_vault/variables.tf new file mode 100644 index 000000000..2dd409803 --- /dev/null +++ b/enos/modules/aws_vault/variables.tf @@ -0,0 +1,255 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +variable "ami_id" { + description = "AMI from enos-infra" + type = string +} + +variable "common_tags" { + description = "Tags to set for all resources" + type = map(string) + default = { "Project" : "Enos" } +} + +variable "consul_cluster_tag" { + type = string + description = "cluster tag for consul cluster" + default = null +} + +variable "consul_data_dir" { + type = string + description = "The directory where the consul will store data" + default = "/opt/consul/data" +} + +variable "consul_install_dir" { + type = string + description = "The directory where the consul binary will be installed" + default = "/opt/consul/bin" +} + +variable "consul_license" { + type = string + description = "The license to use for Consul Enterprise" + default = null +} + +variable "consul_log_dir" { + type = string + description = "The directory where the consul will write log output" + default = "/var/log/consul.d" +} + +variable "consul_log_level" { + type = string + description = "The consul service log level" + default = "INFO" +} + +variable "consul_release" { + type = object({ + version = string + edition = string + }) + description = "Consul release version and edition to install from releases.hashicorp.com" + default = { + version = "1.15.3" + edition = "oss" + } +} + +variable "dependencies_to_install" { + type = list(string) + description = "A list of dependencies to install" + default = [] +} + +variable "environment" { + description = "Name of the environment." + type = string +} + +variable "instance_count" { + description = "Number of EC2 instances in each subnet" + type = number + default = 3 +} + +variable "instance_type" { + description = "EC2 Instance" + type = string + default = "t2.micro" +} + +variable "kms_key_arn" { + type = string + description = "ARN of KMS Key from enos-infra" + default = null +} + +variable "manage_service" { + type = bool + description = "Manage the service users and systemd" + default = true +} + +variable "project_name" { + description = "Name of the project." + type = string +} + +variable "sg_additional_ips" { + description = "A list of additional IPs to allow by Vault security groups (use with caution)" + type = list(string) + default = [] +} + +variable "ssh_aws_keypair" { + description = "SSH keypair used to connect to EC2 instances" + type = string +} + +variable "storage_backend" { + type = string + description = "The type of Vault storage backend which will be used" + default = "raft" + + validation { + condition = contains(["raft", "consul"], var.storage_backend) + error_message = "The \"storage_backend\" must be one of: [raft|consul]." + } +} + +variable "storage_backend_addl_config" { + type = map(any) + description = "A set of key value pairs to inject into the storage block" + default = {} +} + +variable "unseal_method" { + type = string + description = "The method by which to unseal the Vault cluster" + default = "awskms" + + validation { + condition = contains(["awskms", "shamir"], var.unseal_method) + error_message = "The unseal_method must be one of 'awskms or 'shamir'." + } +} + +variable "vault_artifactory_release" { + type = object({ + username = string + token = string + url = string + sha256 = string + }) + description = "Vault release version and edition to install from artifactory.hashicorp.engineering" + default = null +} + +variable "vault_cluster_tag" { + type = string + description = "Cluster tag for Vault cluster" + default = null +} + +variable "vault_config_dir" { + type = string + description = "The directory to use for Vault configuration" + default = "/etc/vault.d" +} + +variable "vault_init" { + type = bool + description = "Initialize the Vault cluster" + default = true +} + +variable "vault_install_dir" { + type = string + description = "The directory where the vault binary will be installed" + default = "/opt/vault/bin" +} + +variable "vault_license" { + type = string + sensitive = true + description = "vault license" + default = null +} + +variable "vault_local_artifact_path" { + type = string + description = "The path to a locally built vault artifact to install" + default = null +} + +variable "vault_log_dir" { + type = string + description = "The directory to use for Vault logs" + default = "/var/log/vault.d" +} + +variable "vault_log_level" { + type = string + description = "The vault service log level" + default = "info" + + validation { + condition = contains(["trace", "debug", "info", "warn", "error"], var.vault_log_level) + error_message = "The vault_log_level must be one of 'trace', 'debug', 'info', 'warn', or 'error'." + } +} + +variable "vault_node_prefix" { + type = string + description = "The vault node prefix" + default = "node" +} + +variable "vault_release" { + type = object({ + version = string + edition = string + }) + description = "Vault release version and edition to install from releases.hashicorp.com" + default = null +} + +variable "vault_root_token" { + type = string + description = "Vault root token" + default = null +} + +variable "vault_unseal_when_no_init" { + type = bool + description = "Unseal the Vault manually even if we're not initializing it" + default = false +} + +variable "vault_unseal_keys" { + type = list(string) + description = "Vault unseal keys to use for shamir unseal, usually only for already initialized clusters" + default = null +} + +variable "vpc_id" { + description = "VPC ID from enos-infra" + type = string +} + +variable "vault_environment" { + description = "Optional environment variables to set when running the vault service" + type = map(string) + default = null +} + +variable "enable_file_audit_device" { + description = "If true the file audit device will be enabled at the path /var/log/vault/vault_audit.log" + type = bool + default = true +} diff --git a/enos/modules/aws_vault/vault-instances.tf b/enos/modules/aws_vault/vault-instances.tf new file mode 100644 index 000000000..864ebf93c --- /dev/null +++ b/enos/modules/aws_vault/vault-instances.tf @@ -0,0 +1,341 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +resource "aws_instance" "vault_instance" { + for_each = local.vault_instances + ami = var.ami_id + instance_type = var.instance_type + vpc_security_group_ids = [aws_security_group.enos_vault_sg.id] + subnet_id = tolist(data.aws_subnets.infra.ids)[each.key % length(data.aws_subnets.infra.ids)] + key_name = var.ssh_aws_keypair + iam_instance_profile = aws_iam_instance_profile.vault_profile.name + tags = merge( + var.common_tags, + { + Name = "${local.name_suffix}-vault-${var.vault_node_prefix}-${each.key}" + Type = local.vault_cluster_tag + }, + ) +} + +resource "enos_remote_exec" "install_dependencies" { + depends_on = [aws_instance.vault_instance] + for_each = toset([ + for idx in local.vault_instances : idx + if length(var.dependencies_to_install) > 0 + ]) + + content = templatefile("${path.module}/templates/install-dependencies.sh", { + dependencies = join(" ", var.dependencies_to_install) + }) + + transport = { + ssh = { + host = aws_instance.vault_instance[each.value].public_ip + } + } +} + +resource "enos_bundle_install" "consul" { + for_each = { + for idx, instance in aws_instance.vault_instance : idx => instance + if var.storage_backend == "consul" + } + + destination = var.consul_install_dir + release = merge(var.consul_release, { product = "consul" }) + + transport = { + ssh = { + host = each.value.public_ip + } + } +} + +resource "enos_bundle_install" "vault" { + for_each = aws_instance.vault_instance + + destination = var.vault_install_dir + release = var.vault_release == null ? var.vault_release : merge({ product = "vault" }, var.vault_release) + artifactory = var.vault_artifactory_release + path = var.vault_local_artifact_path + + transport = { + ssh = { + host = each.value.public_ip + } + } +} + +resource "enos_consul_start" "consul" { + for_each = enos_bundle_install.consul + + bin_path = local.consul_bin_path + data_dir = var.consul_data_dir + config = { + data_dir = var.consul_data_dir + datacenter = "dc1" + retry_join = ["provider=aws tag_key=Type tag_value=${var.consul_cluster_tag}"] + server = false + bootstrap_expect = 0 + license = var.consul_license + log_level = var.consul_log_level + log_file = var.consul_log_dir + } + license = var.consul_license + unit_name = "consul" + username = "consul" + + transport = { + ssh = { + host = aws_instance.vault_instance[each.key].public_ip + } + } +} + +resource "enos_vault_start" "leader" { + depends_on = [ + enos_consul_start.consul, + enos_bundle_install.vault, + ] + for_each = local.leader + + bin_path = local.vault_bin_path + config_dir = var.vault_config_dir + manage_service = var.manage_service + config = { + api_addr = "http://${aws_instance.vault_instance[each.key].private_ip}:8200" + cluster_addr = "http://${aws_instance.vault_instance[each.key].private_ip}:8201" + cluster_name = local.vault_cluster_tag + listener = { + type = "tcp" + attributes = { + address = "0.0.0.0:8200" + tls_disable = "true" + } + } + log_level = var.vault_log_level + storage = { + type = var.storage_backend + attributes = ({ for key, value in local.storage_config[each.key] : key => value }) + } + seal = local.seal[var.unseal_method] + ui = true + } + license = var.vault_license + unit_name = "vault" + username = local.vault_service_user + + environment = var.vault_environment + + transport = { + ssh = { + host = aws_instance.vault_instance[each.key].public_ip + } + } +} + +resource "enos_vault_start" "followers" { + depends_on = [ + enos_vault_start.leader, + ] + for_each = local.followers + + bin_path = local.vault_bin_path + config_dir = var.vault_config_dir + manage_service = var.manage_service + config = { + api_addr = "http://${aws_instance.vault_instance[each.key].private_ip}:8200" + cluster_addr = "http://${aws_instance.vault_instance[each.key].private_ip}:8201" + cluster_name = local.vault_cluster_tag + log_level = var.vault_log_level + listener = { + type = "tcp" + attributes = { + address = "0.0.0.0:8200" + tls_disable = "true" + } + } + storage = { + type = var.storage_backend + attributes = { for key, value in local.storage_config[each.key] : key => value } + } + seal = local.seal[var.unseal_method] + ui = true + } + license = var.vault_license + unit_name = "vault" + username = local.vault_service_user + + environment = var.vault_environment + + transport = { + ssh = { + host = aws_instance.vault_instance[each.key].public_ip + } + } +} + +resource "enos_vault_init" "leader" { + depends_on = [enos_vault_start.followers] + for_each = toset([ + for idx in local.leader : idx + if var.vault_init + ]) + + bin_path = local.vault_bin_path + vault_addr = enos_vault_start.leader[0].config.api_addr + + key_shares = local.key_shares[var.unseal_method] + key_threshold = local.key_threshold[var.unseal_method] + + recovery_shares = local.recovery_shares[var.unseal_method] + recovery_threshold = local.recovery_threshold[var.unseal_method] + + transport = { + ssh = { + host = aws_instance.vault_instance[0].public_ip + } + } +} + +resource "enos_vault_unseal" "leader" { + depends_on = [ + enos_vault_start.followers, + enos_vault_init.leader, + ] + for_each = enos_vault_init.leader // only unseal the leader if we initialized it + bin_path = local.vault_bin_path + vault_addr = enos_vault_start.leader[each.key].config.api_addr + seal_type = var.unseal_method + unseal_keys = var.unseal_method != "shamir" ? null : coalesce(var.vault_unseal_keys, enos_vault_init.leader[0].unseal_keys_hex) + + transport = { + ssh = { + host = aws_instance.vault_instance[0].public_ip + } + } +} + +# We need to ensure that the directory used for audit logs is present and accessible to the vault +# user on all nodes, since logging will only happen on the leader. +resource "enos_remote_exec" "create_audit_log_dir" { + depends_on = [ + enos_vault_unseal.leader, + ] + for_each = toset([ + for idx in local.vault_instances : idx + if var.enable_file_audit_device + ]) + + environment = { + LOG_FILE_PATH = local.audit_device_file_path + SERVICE_USER = local.vault_service_user + } + + scripts = [abspath("${path.module}/scripts/create_audit_log_dir.sh")] + + transport = { + ssh = { + host = aws_instance.vault_instance[each.value].public_ip + } + } +} + +resource "enos_remote_exec" "init_audit_device" { + depends_on = [ + enos_remote_exec.create_audit_log_dir, + enos_vault_unseal.leader, + ] + for_each = toset([ + for idx in local.leader : idx + if local.enable_audit_device + ]) + + environment = { + VAULT_TOKEN = enos_vault_init.leader[each.key].root_token + VAULT_ADDR = "http://127.0.0.1:8200" + VAULT_BIN_PATH = local.vault_bin_path + LOG_FILE_PATH = local.audit_device_file_path + SERVICE_USER = local.vault_service_user + } + + scripts = [ + abspath("${path.module}/scripts/enable_audit_logging.sh"), + ] + + transport = { + ssh = { + host = aws_instance.vault_instance[each.key].public_ip + } + } +} + +resource "enos_vault_unseal" "followers" { + depends_on = [ + enos_vault_init.leader, + enos_vault_unseal.leader, + ] + // Only unseal followers if we're not using an auto-unseal method and we've + // initialized the cluster + for_each = { + for idx, follower in local.followers : idx => follower + if var.unseal_method == "shamir" && var.vault_init + } + bin_path = local.vault_bin_path + vault_addr = enos_vault_start.followers[each.key].config.api_addr + seal_type = var.unseal_method + unseal_keys = var.unseal_method != "shamir" ? null : coalesce(var.vault_unseal_keys, enos_vault_init.leader[0].unseal_keys_hex) + + transport = { + ssh = { + host = aws_instance.vault_instance[0].public_ip + } + } +} + +// Unseal everything when we're not initializing and unseal when not init is set. +// This flag is intended to handle cases when you're adding additional nodes to +// an existing cluster to use auto-pilot to upgrade them and they're not using +// an auto-unseal method. +resource "enos_vault_unseal" "when_vault_unseal_when_no_init_is_set" { + depends_on = [enos_vault_start.followers] + for_each = toset([ + for idx in local.instances : idx + if var.vault_unseal_when_no_init && !var.vault_init + ]) + + bin_path = local.vault_bin_path + vault_addr = "http://localhost:8200" + seal_type = var.unseal_method + unseal_keys = coalesce( + var.vault_unseal_keys, + try(enos_vault_init.leader[0].unseal_keys_hex, null), + ) + + transport = { + ssh = { + host = aws_instance.vault_instance[each.key].public_ip + } + } +} + +resource "enos_remote_exec" "vault_write_license" { + count = var.vault_init ? 1 : 0 + depends_on = [ + enos_vault_unseal.leader, + enos_vault_unseal.when_vault_unseal_when_no_init_is_set, + ] + + content = templatefile("${path.module}/templates/vault-write-license.sh", { + vault_bin_path = local.vault_bin_path, + vault_root_token = coalesce(var.vault_root_token, try(enos_vault_init.leader[0].root_token, null), "none") + vault_license = coalesce(var.vault_license, "none") + }) + + transport = { + ssh = { + host = aws_instance.vault_instance[0].public_ip + } + } +} diff --git a/enos/modules/aws_worker/main.tf b/enos/modules/aws_worker/main.tf index f6f4f59b6..b262af584 100644 --- a/enos/modules/aws_worker/main.tf +++ b/enos/modules/aws_worker/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/binary_finder/main.tf b/enos/modules/binary_finder/main.tf index 58ebadbc1..2024ef6e0 100644 --- a/enos/modules/binary_finder/main.tf +++ b/enos/modules/binary_finder/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/build_boundary_docker_crt/main.tf b/enos/modules/build_boundary_docker_crt/main.tf index eb16ed4da..1f27ee604 100644 --- a/enos/modules/build_boundary_docker_crt/main.tf +++ b/enos/modules/build_boundary_docker_crt/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/build_boundary_docker_local/main.tf b/enos/modules/build_boundary_docker_local/main.tf index 6370c397c..6fffebdc9 100644 --- a/enos/modules/build_boundary_docker_local/main.tf +++ b/enos/modules/build_boundary_docker_local/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/build_local/main.tf b/enos/modules/build_local/main.tf index 34ba734af..4b451235e 100644 --- a/enos/modules/build_local/main.tf +++ b/enos/modules/build_local/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/docker_boundary/main.tf b/enos/modules/docker_boundary/main.tf index d1a87ad6b..3a81a73d1 100644 --- a/enos/modules/docker_boundary/main.tf +++ b/enos/modules/docker_boundary/main.tf @@ -14,7 +14,7 @@ terraform { } enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/docker_boundary_cmd/main.tf b/enos/modules/docker_boundary_cmd/main.tf index 8a403e194..fb945a0df 100644 --- a/enos/modules/docker_boundary_cmd/main.tf +++ b/enos/modules/docker_boundary_cmd/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/docker_check_health/main.tf b/enos/modules/docker_check_health/main.tf index 1741c74eb..357e659a2 100644 --- a/enos/modules/docker_check_health/main.tf +++ b/enos/modules/docker_check_health/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/docker_ldap/main.tf b/enos/modules/docker_ldap/main.tf index c970a7baa..43bdba4cd 100644 --- a/enos/modules/docker_ldap/main.tf +++ b/enos/modules/docker_ldap/main.tf @@ -9,7 +9,7 @@ terraform { } enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/docker_minio/main.tf b/enos/modules/docker_minio/main.tf index ef23d92c4..ca9c25f66 100644 --- a/enos/modules/docker_minio/main.tf +++ b/enos/modules/docker_minio/main.tf @@ -9,7 +9,7 @@ terraform { } enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/docker_openssh_server/main.tf b/enos/modules/docker_openssh_server/main.tf index 3a2891b37..e78a92ff0 100644 --- a/enos/modules/docker_openssh_server/main.tf +++ b/enos/modules/docker_openssh_server/main.tf @@ -14,7 +14,7 @@ terraform { } enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/docker_openssh_server_ca_key/main.tf b/enos/modules/docker_openssh_server_ca_key/main.tf index 03c989345..9285271c1 100644 --- a/enos/modules/docker_openssh_server_ca_key/main.tf +++ b/enos/modules/docker_openssh_server_ca_key/main.tf @@ -14,7 +14,7 @@ terraform { } enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/docker_postgres/main.tf b/enos/modules/docker_postgres/main.tf index cde66a3a5..071308190 100644 --- a/enos/modules/docker_postgres/main.tf +++ b/enos/modules/docker_postgres/main.tf @@ -9,7 +9,7 @@ terraform { } enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/docker_vault/main.tf b/enos/modules/docker_vault/main.tf index b9266a60c..cc09d4ad5 100644 --- a/enos/modules/docker_vault/main.tf +++ b/enos/modules/docker_vault/main.tf @@ -14,7 +14,7 @@ terraform { } enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/docker_worker/main.tf b/enos/modules/docker_worker/main.tf index d059e939f..93fdc7b86 100644 --- a/enos/modules/docker_worker/main.tf +++ b/enos/modules/docker_worker/main.tf @@ -14,7 +14,7 @@ terraform { } enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/test_e2e/main.tf b/enos/modules/test_e2e/main.tf index 4ae9e8821..237a2059c 100644 --- a/enos/modules/test_e2e/main.tf +++ b/enos/modules/test_e2e/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/test_e2e_docker/main.tf b/enos/modules/test_e2e_docker/main.tf index be20e7fc8..8bd7b0f0d 100644 --- a/enos/modules/test_e2e_docker/main.tf +++ b/enos/modules/test_e2e_docker/main.tf @@ -9,7 +9,7 @@ terraform { } enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } } diff --git a/enos/modules/test_e2e_ui/main.tf b/enos/modules/test_e2e_ui/main.tf index 6cc757bc6..1eacf4c09 100644 --- a/enos/modules/test_e2e_ui/main.tf +++ b/enos/modules/test_e2e_ui/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { enos = { - source = "app.terraform.io/hashicorp-qti/enos" + source = "registry.terraform.io/hashicorp-forge/enos" } } }