diff --git a/examples/vault-consul-ami/tls/agent.hcl b/examples/vault-consul-ami/tls/agent.hcl new file mode 100644 index 00000000..0277c0a4 --- /dev/null +++ b/examples/vault-consul-ami/tls/agent.hcl @@ -0,0 +1,5 @@ + acl = { + enabled = true, + default_policy = "deny", + enable_token_persistence = true + } diff --git a/examples/vault-consul-ami/vault-consul.json b/examples/vault-consul-ami/vault-consul.json index 34fc05d0..7429edfd 100644 --- a/examples/vault-consul-ami/vault-consul.json +++ b/examples/vault-consul-ami/vault-consul.json @@ -2,9 +2,9 @@ "min_packer_version": "0.12.0", "variables": { "aws_region": "us-east-1", - "vault_version": "1.1.0", + "vault_version": "1.2.3", "consul_module_version": "v0.7.3", - "consul_version": "1.5.3", + "consul_version": "1.6.1", "consul_download_url": "{{env `CONSUL_DOWNLOAD_URL`}}", "vault_download_url": "{{env `VAULT_DOWNLOAD_URL`}}", "install_auth_signing_script": "true", @@ -87,6 +87,10 @@ " /tmp/terraform-aws-vault/modules/install-vault/install-vault --version {{user `vault_version`}};", "fi" ] + },{ + "type": "file", + "source": "{{template_dir}}/tls/agent.hcl", + "destination": "/tmp/agent.hcl" },{ "type": "file", "source": "{{template_dir}}/auth/sign-request.py", @@ -149,7 +153,13 @@ " /tmp/terraform-aws-consul/modules/install-consul/install-consul --download-url {{user `consul_download_url`}};", "else", " /tmp/terraform-aws-consul/modules/install-consul/install-consul --version {{user `consul_version`}};", - "fi" + "fi", + "sudo cp /opt/vault/tls/ca.crt.pem /opt/consul/tls/ca/ca.crt.pem", + "sudo cp /opt/vault/tls/vault.crt.pem /opt/consul/tls/vault.crt.pem", + "sudo cp /opt/vault/tls/vault.key.pem /opt/consul/tls/vault.key.pem", + "sudo chown -R consul:consul /opt/consul/tls/", + "sudo chmod -R 600 /opt/consul/tls", + "sudo chmod 700 /opt/consul/tls" ], "pause_before": "30s" },{ diff --git a/examples/vault-prod-ready/README.md b/examples/vault-prod-ready/README.md new file mode 100644 index 00000000..22a45418 --- /dev/null +++ b/examples/vault-prod-ready/README.md @@ -0,0 +1,175 @@ +# Vault auto unseal cluster private with kms and acl for consul with encryption. PROD READY + +This example combines the following: Vault auto unseal, Vault private cluster, and Consul with encryption and also adds a few items that we expect in a production Vault deployment, including: +* Secure instances using security groups and remove [0.0.0.0/0] +* Enable ACL on consul and pass the token in a secure way using AWS Secrets manager +* Enable encryption on Consul (based on https://github.com/hashicorp/terraform-aws-consul/tree/master/examples/example-with-encryption) +* Auto generate gossip_encryption_key and store it in AWS Secrets manager +* Only allow ssh from Vpn server +* Creates an ELB in front of Consul instances +* Creates an ELB in front of Vault instances +* Removed the block for deploying cluster in default VPC and availability zones, this needs to be set explicitly to enforce security + +This example use Terragrunt to solve some dependencies but can by used with terraform also. + +Thanks @mkrull for the ACL token provision + +# HOWTO +* 1 - Run terragrunt +* 2 - run `vault operator init` on one of the vault servers +* 3 - To login in consul look for master token under aws secrets manager + +# Vault auto unseal example + +This folder shows an example of Terraform code that deploys a [Vault][vault] cluster +in AWS with [auto unseal][auto_unseal]. Auto unseal is a Vault feature +that automatically [unseals][seal] each node in the cluster at boot using [Amazon KMS][kms]. +Without auto unseal, Vault operators are expected to manually unseal each Vault node +after it boots, a cumbersome process that typically requires multiple Vault operators +to each enter a Vault master key shard. + +This example creates a private Vault cluster that is accessible only from within +the VPC within the AWS account in which it resides, or other VPCs that are peered +with the Vault VPC. The Vault cluster uses [Consul][consul] as the storage backend, +so this example also deploys a separate Consul server cluster using the +[consul-cluster module][consul_cluster] from the Consul AWS Module. Each of the +servers in this example has [Dnsmasq][dnsmasq] installed (via the [install-dnsmasq module][dnsmasq_module]) +or [setup-systemd-resolved][setup_systemd_resolved] (in the case of Ubuntu 18.04) +which allows them to use the Consul server cluster for service discovery and thereby +access Vault via DNS using the domain name `vault.service.consul`. + +For more info on how the Vault cluster works, check out the [vault-cluster][vault_cluster] +documentation. + +**Billing Warning**: Every time you create a KMS key, you're charged $1 for the month, +even if you immediately delete it. + + Private Vault Cluster Example + +This folder shows an example of Terraform code to deploy a [Vault](https://www.vaultproject.io/) cluster in +[AWS](https://aws.amazon.com/) using the [vault-cluster module](https://github.com/hashicorp/terraform-aws-vault/tree/master/modules/vault-cluster). The Vault cluster uses +[Consul](https://www.consul.io/) as a storage backend, so this example also deploys a separate Consul server cluster +using the [consul-cluster module](https://github.com/hashicorp/terraform-aws-consul/tree/master/modules/consul-cluster) +from the Consul AWS Module. + +This example creates a private Vault cluster, which is private in the sense that the EC2 Instances are not fronted by a +load balancer, as is the case in the [Vault Public Example](/examples/root-example). Keep in mind that if the Vault +nodes are deployed to public subnets (i.e. subnets that have a route to the public Internet), this "private" cluster will +still be accessible from the public Internet. + +Each of the servers in this example has [Dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) installed (via the +[install-dnsmasq module](https://github.com/hashicorp/terraform-aws-consul/tree/master/modules/install-dnsmasq)) or +[setup-systemd-resolved](https://github.com/hashicorp/terraform-aws-consul/tree/master/modules/setup-systemd-resolved) +(in the case Ubuntu of 18.04) +which allows it to use the Consul server cluster for service discovery and thereby access Vault via DNS using the +domain name `vault.service.consul`. For an example of a Vault cluster +that is publicly accessible, see [the root example](https://github.com/hashicorp/terraform-aws-vault/tree/master/examples/root-example). + +![Vault architecture](https://github.com/hashicorp/terraform-aws-vault/blob/master/_docs/architecture.png?raw=true) + +You will need to create an [Amazon Machine Image (AMI)](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html) +that has Vault and Consul installed, which you can do using the [vault-consul-ami example](https://github.com/hashicorp/terraform-aws-vault/tree/master/examples/vault-consul-ami)). + +For more info on how the Vault cluster works, check out the [vault-cluster](https://github.com/hashicorp/terraform-aws-vault/tree/master/modules/vault-cluster) documentation. + +**Note**: To keep this example as simple to deploy and test as possible, it deploys the Vault cluster into your default +VPC and default subnets, all of which are publicly accessible. This is OK for learning and experimenting, but for +production usage, we strongly recommend deploying the Vault cluster into the private subnets of a custom VPC. + +### Quick start + +1. `git clone` this repo to your computer. +1. Build a Vault and Consul AMI. See the [vault-consul-ami example][vault_consul_ami] + documentation for instructions. Don't forget to set the variable `vault_download_url` + with the url of the enterprise version of Vault if you wish to use Vault Enterprise. + Make sure to note down the ID of the AMI. +1. Install [Terraform][terraform]. +1. [Create an AWS KMS key][key_creation]. Take note of the key alias. +1. Open `vars.tf`, set the environment variables specified at the top of the file, + and fill in any other variables that don't have a default. Put the AMI ID you + previously took note into the `ami_id` variable and the KMS key alias into + `auto_unseal_kms_key_alias`. +1. Run `terraform init`. +1. Run `terraform apply`. +1. Run the [vault-examples-helper.sh script][examples_helper] to + print out the IP addresses of the Vault server and some example commands you + can run to interact with the cluster: `../vault-examples-helper/vault-examples-helper.sh`. +1. Ssh to an instance in the vault cluster and run `vault operator init` to initialize + the cluster, then `vault status` to check that it is unsealed. If you ssh to a + different node in the cluster, you might have to restart Vault first with + `sudo systemctl restart vault.service` so it will rejoin the cluster and unseal. + To avoid doing that, you can start your cluster with initially just one node and + start the server, then change the `vault_cluster_size` variable back to 3 and and + run `terraform apply again`. The new nodes will join the cluster already unsealed + in this case. + +### Seal + +All data stored by Vault is encrypted with a Master Key which is not stored anywhere +and Vault only ever keeps in memory. When Vault first boots, it does not have the +Master Key in memory, and therefore it can access its storage, but it cannot decrypt +its own data. So you can't really do anything apart from unsealing it or checking +the server status. While Vault is at this state, we say it is "sealed". + +Since vault uses [Shamir's Secret Sharing][shamir], which splits the master key into +pieces, running `vault operator unseal ` adds piece by piece until there +are enough parts to reconstruct the master key. This is done on different machines in the +vault cluster for better security. When Vault is unsealed and it has the recreated +master key in memory, it can then be used to read the stored decryption keys, which +can decrypt the data, and then you can start performing other operations on Vault. +Vault remains unsealed until it reboots or until someone manually reseals it. + +### Auto-unseal + +Vault has a feature that allows automatic unsealing via Amazon KMS. It +allows operators to delegate the unsealing process to AWS, which is useful for failure +situations where the server has to restart and then it will be already unsealed or +for the creation of ephemeral clusters. This process uses an AWS KMS key as +a [seal wrap][seal_wrap] mechanism: it encrypts and decrypts Vault's master key +(and it does so with the whole key, replacing the Shamir's Secret Sharing method). + +This feature is enabled by adding a `awskms` stanza at Vault's configuration. This +module takes this into consideration on the [`run-vault`][run_vault] binary, allowing +you to pass the following flags to it: + * `--enable-auto-unseal`: Enables the AWS KMS Auto-unseal feature and adds the `awskms` + stanza to the configuration + * `--auto-unseal-kms-key-id`: The key id of the AWS KMS key to be used + * `--auto-unseal-region`: The AWS region where the KMS key lives + +In this example, like in other examples, we execute `run-vault` at the [`user-data` +script][user_data], which runs on boot for every node in the Vault cluster. The +`key-id` is passed to this script by Terraform, after Terraform reads this value from a +data source through the key alias. This means that the AWS key has to be previously +manually created and we are using Terraform just to find this resource, not to +create it. It is important to notice that AWS KMS keys have a [cost][kms_pricing] +per month per key, as well as an API usage cost. + +``` +data "aws_kms_alias" "vault-example" { + name = "alias/${var.auto_unseal_kms_key_alias}" +} +``` + +If you wish to use Vault Enterprise, you still need to apply your Vault +Enterprise License to the cluster with `vault write /sys/license "text=$LICENSE_KEY_TEXT"`. + +[ami]: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html +[auto_unseal]: https://www.vaultproject.io/docs/enterprise/auto-unseal/index.html +[consul_cluster]: https://github.com/hashicorp/terraform-aws-consul/tree/master/modules/consul-cluster +[consul]: https://www.consul.io/ +[dnsmasq_module]: https://github.com/hashicorp/terraform-aws-consul/tree/master/modules/install-dnsmasq +[dnsmasq]: http://www.thekelleys.org.uk/dnsmasq/doc.html +[setup_systemd_resolved]: https://github.com/hashicorp/terraform-aws-consul/tree/master/modules/setup-systemd-resolved +[examples_helper]: https://github.com/hashicorp/terraform-aws-vault/tree/master/examples/vault-examples-helper/vault-examples-helper.sh +[key_creation]: https://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html +[kms]: https://aws.amazon.com/kms/ +[kms_pricing]: https://aws.amazon.com/kms/pricing/ +[run_vault]: https://github.com/hashicorp/terraform-aws-vault/tree/master/modules/run-vault +[seal_wrap]: https://www.vaultproject.io/docs/enterprise/sealwrap/index.html +[seal]: https://www.vaultproject.io/docs/concepts/seal.html +[shamir]: https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing +[terraform]: https://www.terraform.io/ +[user_data]: https://github.com/hashicorp/terraform-aws-vault/tree/master/examples/vault-auto-unseal/user-data-vault.sh +[vault_cluster]: https://github.com/hashicorp/terraform-aws-vault/tree/master/modules/vault-cluster +[vault_consul_ami]: https://github.com/hashicorp/terraform-aws-vault/tree/master/examples/vault-consul-ami +[vault]: https://www.vaultproject.io/ diff --git a/examples/vault-prod-ready/main.tf b/examples/vault-prod-ready/main.tf new file mode 100644 index 00000000..3bf22f0a --- /dev/null +++ b/examples/vault-prod-ready/main.tf @@ -0,0 +1,534 @@ +# ---------------------------------------------------------------------------------------------------------------------- +# REQUIRE A SPECIFIC TERRAFORM VERSION OR HIGHER +# This module has been updated with 0.12 syntax, which means it is no longer compatible with any versions below 0.12. +# ---------------------------------------------------------------------------------------------------------------------- +terraform { + required_version = ">= 0.12" +} + +data "aws_kms_alias" "vault-example" { + name = "alias/${var.auto_unseal_kms_key_alias}" +} + +# ---------------------------------------------------------------------------------------------------------------------- +# SECURITY GROUPS +# ---------------------------------------------------------------------------------------------------------------------- +# Only allow vault to be accessed from the OpenVPN server +resource "aws_security_group_rule" "allow_vault_inbound_ssh" { + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + source_security_group_id = var.security_group_id + security_group_id = module.vault_cluster.security_group_id +} + +# Only allow consul to be accessed from the OpenVPN server +resource "aws_security_group_rule" "allow_consul_inbound_ssh" { + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + source_security_group_id = var.security_group_id + security_group_id = module.consul_cluster.security_group_id +} + +# Only allow vault to be accessed from the OpenVPN server +resource "aws_security_group_rule" "allow_vault_inbound_http" { + type = "ingress" + from_port = 8200 + to_port = 8200 + protocol = "tcp" + source_security_group_id = var.security_group_id + security_group_id = module.vault_cluster.security_group_id +} +# Only allow vault to be accessed from the OpenVPN server +resource "aws_security_group_rule" "allow_consul_inbound_http" { + type = "ingress" + from_port = 8500 + to_port = 8500 + protocol = "tcp" + source_security_group_id = var.security_group_id + security_group_id = module.consul_cluster.security_group_id +} +resource "aws_security_group_rule" "allow_vault_inbound_http_from_elb" { + type = "ingress" + from_port = 8200 + to_port = 8200 + protocol = "tcp" + source_security_group_id = aws_security_group.elb_vault.id + security_group_id = module.vault_cluster.security_group_id +} +resource "aws_security_group_rule" "allow_consul_inbound_http_from_elb" { + type = "ingress" + from_port = 8500 + to_port = 8500 + protocol = "tcp" + source_security_group_id = aws_security_group.elb_consul.id + security_group_id = module.consul_cluster.security_group_id +} + +# --------------------------------------------------------------------------------------------------------------------- +# DEPLOY THE VAULT SERVER CLUSTER +# --------------------------------------------------------------------------------------------------------------------- + +module "vault_cluster" { + # When using these modules in your own templates, you will need to use a Git URL with a ref attribute that pins you + # to a specific version of the modules, such as the following example: + source = "github.com/hashicorp/terraform-aws-vault.git//modules/vault-cluster?ref=v0.13.3" + + cluster_name = var.vault_cluster_name + cluster_size = var.vault_cluster_size + instance_type = var.vault_instance_type + + ami_id = var.ami_id + user_data = data.template_file.user_data_vault_cluster.rendered + + vpc_id = var.vpc_id +# subnet_ids = var.vpc_subnet_ids + + # This setting will create the AWS policy that allows the vault cluster to + # access KMS and use this key for encryption and decryption + enable_auto_unseal = true + + auto_unseal_kms_key_arn = data.aws_kms_alias.vault-example.target_key_arn + + # To make testing easier, we allow requests from any IP address here but in a production deployment, we *strongly* + # recommend you limit this to the IP address ranges of known, trusted servers inside your VPC. + + allowed_ssh_cidr_blocks = [] + allowed_inbound_cidr_blocks = [] + allowed_inbound_security_group_ids = [] + allowed_inbound_security_group_count = 0 + ssh_key_name = var.ssh_key_name +} + +# --------------------------------------------------------------------------------------------------------------------- +# ATTACH IAM POLICIES FOR CONSUL +# To allow our Vault servers to automatically discover the Consul servers, we need to give them the IAM permissions from +# the Consul AWS Module's consul-iam-policies module. +# --------------------------------------------------------------------------------------------------------------------- + +module "consul_iam_policies_servers" { + source = "github.com/hashicorp/terraform-aws-consul.git//modules/consul-iam-policies?ref=v0.7.0" + + iam_role_id = module.vault_cluster.iam_role_id +} + +# --------------------------------------------------------------------------------------------------------------------- +# GIVE SSH-GRUNT PERMISSIONS TO TALK TO IAM +# We add an IAM policy to Jenkins that allows ssh-grunt to make API calls to IAM to fetch IAM user and group data. +# --------------------------------------------------------------------------------------------------------------------- + +module "ssh_grunt_policies" { + source = "git::git@github.com:gruntwork-io/module-security.git//modules/iam-policies?ref=v0.19.1" + + aws_account_id = var.aws_account_id + + # ssh-grunt is an automated app, so we can't use MFA with it + iam_policy_should_require_mfa = false + trust_policy_should_require_mfa = false + + # Since our IAM users are defined in a separate AWS account, we need to give ssh-grunt permission to make API calls to + # that account. + allow_access_to_other_account_arns = [var.external_account_ssh_grunt_role_arn] +} + +resource "aws_iam_role_policy" "ssh_grunt_permissions_vault" { + name = "ssh-grunt-permissions" + role = module.vault_cluster.iam_role_id + policy = module.ssh_grunt_policies.allow_access_to_other_accounts[0] +} + +resource "aws_iam_role_policy" "ssh_grunt_permissions_consul" { + name = "ssh-grunt-permissions" + role = module.consul_cluster.iam_role_id + policy = module.ssh_grunt_policies.allow_access_to_other_accounts[0] +} + +# --------------------------------------------------------------------------------------------------------------------- +# ADD IAM POLICY THAT ALLOWS CLOUDWATCH LOG AGGREGATION +# --------------------------------------------------------------------------------------------------------------------- + +module "cloudwatch_log_aggregation" { + source = "git::git@github.com:gruntwork-io/module-aws-monitoring.git//modules/logs/cloudwatch-log-aggregation-iam-policy?ref=v0.14.0" + name_prefix = "cloudwatch_consul_vault-${var.vault_cluster_name}" +} + +resource "aws_iam_policy_attachment" "attach_cloudwatch_consul_log_aggregation_policy" { + name = "attach-cloudwatch-log-aggregation-policy" + roles = [module.consul_cluster.iam_role_id] + policy_arn = module.cloudwatch_log_aggregation.cloudwatch_log_aggregation_policy_arn +} +resource "aws_iam_policy_attachment" "attach_cloudwatch_vault_log_aggregation_policy" { + name = "attach-cloudwatch-log-aggregation-policy" + roles = [module.vault_cluster.iam_role_id] + policy_arn = module.cloudwatch_log_aggregation.cloudwatch_log_aggregation_policy_arn +} +# --------------------------------------------------------------------------------------------------------------------- +# ADD IAM POLICY THAT ALLOWS READING AND WRITING CLOUDWATCH METRICS +# --------------------------------------------------------------------------------------------------------------------- + +module "cloudwatch_metrics" { + source = "git::git@github.com:gruntwork-io/module-aws-monitoring.git//modules/metrics/cloudwatch-custom-metrics-iam-policy?ref=v0.14.0" + name_prefix = "cloudwatch_consul_vault-${var.consul_cluster_name}" +} + +resource "aws_iam_policy_attachment" "attach_cloudwatch_consul_metrics_policy" { + name = "attach-cloudwatch-log-aggregation-policy" + roles = [module.consul_cluster.iam_role_id] + policy_arn = module.cloudwatch_log_aggregation.cloudwatch_log_aggregation_policy_arn +} +resource "aws_iam_policy_attachment" "attach_cloudwatch_vault_metrics_policy" { + name = "attach-cloudwatch-log-aggregation-policy" + roles = [module.vault_cluster.iam_role_id] + policy_arn = module.cloudwatch_log_aggregation.cloudwatch_log_aggregation_policy_arn +} + +# --------------------------------------------------------------------------------------------------------------------- +# THE USER DATA SCRIPT THAT WILL RUN ON EACH VAULT SERVER WHEN IT'S BOOTING +# This script will configure and start Vault +# --------------------------------------------------------------------------------------------------------------------- + +data "template_file" "user_data_vault_cluster" { + template = file("${path.module}/user-data-vault.sh") + + vars = { + consul_cluster_tag_key = var.consul_cluster_tag_key + consul_cluster_tag_value = var.consul_cluster_name + kms_key_id = data.aws_kms_alias.vault-example.target_key_id + aws_region = var.aws_region + enable_gossip_encryption = var.enable_gossip_encryption + gossip_encryption_key = aws_secretsmanager_secret_version.gossip_encryption_key.secret_string + enable_rpc_encryption = var.enable_rpc_encryption + ca_path = var.ca_path + cert_file_path = var.cert_file_path + key_file_path = var.key_file_path + consul_token_secret = aws_secretsmanager_secret.consul_token.name + } +} + +# --------------------------------------------------------------------------------------------------------------------- +# PERMIT CONSUL SPECIFIC TRAFFIC IN VAULT CLUSTER +# To allow our Vault servers consul agents to communicate with other consul agents and participate in the LAN gossip, +# we open up the consul specific protocols and ports for consul traffic +# --------------------------------------------------------------------------------------------------------------------- + +module "security_group_rules" { + source = "github.com/hashicorp/terraform-aws-consul.git//modules/consul-client-security-group-rules?ref=v0.7.0" + + security_group_id = module.vault_cluster.security_group_id + + # To make testing easier, we allow requests from any IP address here but in a production deployment, we *strongly* + # recommend you limit this to the IP address ranges of known, trusted servers inside your VPC. + + allowed_inbound_cidr_blocks = [] + allowed_inbound_security_group_ids = [] +} + +# --------------------------------------------------------------------------------------------------------------------- +# DEPLOY THE CONSUL SERVER CLUSTER +# --------------------------------------------------------------------------------------------------------------------- + +module "consul_cluster" { + source = "github.com/hashicorp/terraform-aws-consul.git//modules/consul-cluster?ref=v0.7.0" + + cluster_name = var.consul_cluster_name + cluster_size = var.consul_cluster_size + instance_type = var.consul_instance_type + + # The EC2 Instances will use these tags to automatically discover each other and form a cluster + cluster_tag_key = var.consul_cluster_tag_key + cluster_tag_value = var.consul_cluster_name + + ami_id = var.ami_id + user_data = data.template_file.user_data_consul.rendered + + vpc_id = var.vpc_id +# subnet_ids = var.vpc_subnet_ids + + # To make testing easier, we allow Consul and SSH requests from any IP address here but in a production + # deployment, we strongly recommend you limit this to the IP address ranges of known, trusted servers inside your VPC. + + allowed_ssh_cidr_blocks = [] + allowed_inbound_cidr_blocks = var.vpc_cidr_blocks +# https://github.com/hashicorp/terraform-aws-vault/pull/115/files allow vault servers by default #115 + allowed_inbound_security_group_count = 1 + allowed_inbound_security_group_ids = [module.vault_cluster.security_group_id] + ssh_key_name = var.ssh_key_name +} + +# --------------------------------------------------------------------------------------------------------------------- +# THE USER DATA SCRIPT THAT WILL RUN ON EACH CONSUL SERVER WHEN IT'S BOOTING +# This script will configure and start Consul +# --------------------------------------------------------------------------------------------------------------------- + +data "template_file" "user_data_consul" { + template = file("${path.module}/user-data-consul.sh") + + vars = { + consul_cluster_tag_key = var.consul_cluster_tag_key + consul_cluster_tag_value = var.consul_cluster_name + aws_region = var.aws_region + ca_path = var.ca_path + cert_file_path = var.cert_file_path + key_file_path = var.key_file_path + enable_gossip_encryption = var.enable_gossip_encryption + gossip_encryption_key = aws_secretsmanager_secret_version.gossip_encryption_key.secret_string + enable_rpc_encryption = var.enable_rpc_encryption + consul_token_secret = aws_secretsmanager_secret.consul_token.name + } +} + +#Add load balancers and dns +# --------------------------------------------------------------------------------------------------------------------- +# CREATE AN ELB TO PERFORM HEALTH CHECKS ON CONSUL +# Use an ELB for health checks. This is useful for doing zero-downtime deployments and making sure that failed nodes +# are automatically replaced. We also use it to expose the management UI. +# --------------------------------------------------------------------------------------------------------------------- + +resource "aws_elb" "load_balancer_consul" { + name = var.consul_cluster_name +# subnets = var.vpc_subnet_ids + security_groups = [aws_security_group.elb_consul.id] + internal = true + + connection_draining = true + connection_draining_timeout = 60 + + # Perform TCP health checks on Consul's client port. + health_check { + target = "TCP:8500" + interval = 30 + timeout = 10 + healthy_threshold = 2 + unhealthy_threshold = 2 + } + + # The ELB can be used to reach the management interface + listener { + instance_port = 8500 + instance_protocol = "http" + lb_port = 8500 + lb_protocol = "http" + } +} + +# --------------------------------------------------------------------------------------------------------------------- +# CREATE A SECURITY GROUP THAT CONTROLS WHAT TRAFFIC CAN GO IN AND OUT OF THE ELB OF CONSUL +# --------------------------------------------------------------------------------------------------------------------- + +resource "aws_security_group" "elb_consul" { + name = var.consul_cluster_name + vpc_id = var.vpc_id +} + +resource "aws_security_group_rule" "allow_all_outbound_consul" { + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.elb_consul.id +} + +resource "aws_security_group_rule" "allow_consul_inbound_from_elb" { + type = "ingress" + from_port = 8500 + to_port = 8500 + protocol = "tcp" + security_group_id = aws_security_group.elb_consul.id + source_security_group_id = module.consul_cluster.security_group_id +} + +resource "aws_security_group_rule" "allow_openvpntoconsul_inbound_from_elb" { + type = "ingress" + from_port = 8500 + to_port = 8500 + protocol = "tcp" + security_group_id = aws_security_group.elb_consul.id + source_security_group_id = var.security_group_id +} +## Allow clients to connect from within the provided security group +resource "aws_security_group_rule" "allow_consul_inbound_from_subnets" { + type = "ingress" + from_port = 8500 + to_port = 8500 + protocol = "tcp" + security_group_id = aws_security_group.elb_consul.id + cidr_blocks = var.vpc_cidr_blocks +} +#Assign ASG to ELB +resource "aws_autoscaling_attachment" "elb_consul" { + autoscaling_group_name = module.consul_cluster.asg_name + elb = var.consul_cluster_name +} + +# --------------------------------------------------------------------------------------------------------------------- +# CREATE AN ELB TO PERFORM HEALTH CHECKS ON VAULT +# Use an ELB for health checks. This is useful for doing zero-downtime deployments and making sure that failed nodes +# are automatically replaced. We also use it to expose the management UI. +# --------------------------------------------------------------------------------------------------------------------- + +resource "aws_elb" "load_balancer_vault" { + name = var.vault_cluster_name +# subnets = var.vpc_subnet_ids + security_groups = [aws_security_group.elb_vault.id] + internal = true + connection_draining = true + connection_draining_timeout = 60 + + # Perform TCP health checks on Consul's client port. + health_check { + target = "TCP:8200" + interval = 30 + timeout = 10 + healthy_threshold = 2 + unhealthy_threshold = 2 + } + + # The ELB can be used to reach the management interface + listener { + instance_port = 8200 + instance_protocol = "http" + lb_port = 8200 + lb_protocol = "http" + } +} + +# --------------------------------------------------------------------------------------------------------------------- +# CREATE A SECURITY GROUP THAT CONTROLS WHAT TRAFFIC CAN GO IN AND OUT OF THE ELB OF VAULT +# --------------------------------------------------------------------------------------------------------------------- + +resource "aws_security_group" "elb_vault" { + name = var.vault_cluster_name + vpc_id = var.vpc_id +} + +resource "aws_security_group_rule" "allow_all_outbound_vault" { + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.elb_vault.id +} + +resource "aws_security_group_rule" "allow_vault_inbound_from_elb" { + type = "ingress" + from_port = 8200 + to_port = 8200 + protocol = "tcp" + security_group_id = aws_security_group.elb_vault.id + source_security_group_id = module.vault_cluster.security_group_id +} + +resource "aws_security_group_rule" "allow_openvpntovault_inbound_from_elb" { + type = "ingress" + from_port = 8200 + to_port = 8200 + protocol = "tcp" + security_group_id = aws_security_group.elb_vault.id + source_security_group_id = var.security_group_id +} +## Allow clients to connect from within the provided security group +resource "aws_security_group_rule" "allow_vault_inbound_from_subnets" { + type = "ingress" + from_port = 8200 + to_port = 8200 + protocol = "tcp" + security_group_id = aws_security_group.elb_vault.id + cidr_blocks = var.vpc_cidr_blocks +} +#Assign ASG to ELB +resource "aws_autoscaling_attachment" "elb_vault" { + autoscaling_group_name = module.vault_cluster.asg_name + elb = var.vault_cluster_name +} + +# Secret that holds the Consul master token +resource "aws_secretsmanager_secret" "consul_token" { + name_prefix = "${var.consul_cluster_name}-token" +} + +# Random uuid used as master token +resource "random_uuid" "consul_token" {} + +# Secret version updated with the random uuid +resource "aws_secretsmanager_secret_version" "consul_token" { + secret_id = aws_secretsmanager_secret.consul_token.id + secret_string = random_uuid.consul_token.result +} + +# Secret that holds the gossip encryption key +resource "aws_secretsmanager_secret" "gossip_encryption_key" { + name_prefix = "gossip_encryption_key" +} + +# Random uuid used as gossip encryption key +resource "random_string" "gossip_encryption_key" { + length = 32 + +} + +# Secret version updated with the random uuid +resource "aws_secretsmanager_secret_version" "gossip_encryption_key" { + secret_id = aws_secretsmanager_secret.gossip_encryption_key.id + secret_string = base64encode(random_string.gossip_encryption_key.result) +} + +# Policy to allow Consul to write the consul_token secret and gossip encryption key +resource "aws_iam_policy" "secretsmanager_get_token" { + name = var.consul_cluster_name + policy = < >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1 + +# These variables are passed in via Terraform template interpolation +#/opt/consul/bin/run-consul --server --cluster-tag-key "${consul_cluster_tag_key}" --cluster-tag-value "${consul_cluster_tag_value}" + +#From encryption example + +# These variables are passed in via Terraform template interplation +if [[ "${enable_gossip_encryption}" == "true" && ! -z "${gossip_encryption_key}" ]]; then + # Note that setting the encryption key in plain text here means that it will be readable from the Terraform state file + # and/or the EC2 API/console. We're doing this for simplicity, but in a real production environment you should pass an + # encrypted key to Terraform and decrypt it before passing it to run-consul with something like KMS. + gossip_encryption_configuration="--enable-gossip-encryption --gossip-encryption-key ${gossip_encryption_key}" +fi + +if [[ "${enable_rpc_encryption}" == "true" && ! -z "${ca_path}" && ! -z "${cert_file_path}" && ! -z "${key_file_path}" ]]; then + rpc_encryption_configuration="--enable-rpc-encryption --ca-path ${ca_path} --cert-file-path ${cert_file_path} --key-file-path ${key_file_path}" +fi + +# Create acl config including the master token from AWS SecretsManager +TOKEN=$(aws secretsmanager --region "${aws_region}" get-secret-value --secret-id ${consul_token_secret} | jq -r .SecretString) +echo '{"acl": {"tokens": {"master": "'$TOKEN'"}}}' > /opt/consul/config/acl.json + +/opt/consul/bin/run-consul --server --datacenter "${consul_cluster_tag_key}" --cluster-tag-key "${consul_cluster_tag_key}" --cluster-tag-value "${consul_cluster_tag_value}" $gossip_encryption_configuration $rpc_encryption_configuration diff --git a/examples/vault-prod-ready/user-data-vault.sh b/examples/vault-prod-ready/user-data-vault.sh new file mode 100644 index 00000000..9cf8d354 --- /dev/null +++ b/examples/vault-prod-ready/user-data-vault.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# This script is meant to be run in the User Data of each EC2 Instance while it's booting. The script uses the +# run-consul script to configure and start Consul in client mode and then the run-vault script to configure +# the auto unsealing on server init + +set -e + +# Send the log output from this script to user-data.log, syslog, and the console +# From: https://alestic.com/2010/12/ec2-user-data-output/ +exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1 + +# The Packer template puts the TLS certs in these file paths +readonly VAULT_TLS_CERT_FILE="/opt/vault/tls/vault.crt.pem" +readonly VAULT_TLS_KEY_FILE="/opt/vault/tls/vault.key.pem" + +# The variables below are filled in via Terraform interpolation + +#/opt/consul/bin/run-consul --client --cluster-tag-key "${consul_cluster_tag_key}" --cluster-tag-value "${consul_cluster_tag_value}" +# These variables are passed in via Terraform template interplation +if [[ "${enable_gossip_encryption}" == "true" && ! -z "${gossip_encryption_key}" ]]; then + # Note that setting the encryption key in plain text here means that it will be readable from the Terraform state file + # and/or the EC2 API/console. We're doing this for simplicity, but in a real production environment you should pass an + # encrypted key to Terraform and decrypt it before passing it to run-consul with something like KMS. + gossip_encryption_configuration="--enable-gossip-encryption --gossip-encryption-key ${gossip_encryption_key}" +fi + +if [[ "${enable_rpc_encryption}" == "true" && ! -z "${ca_path}" && ! -z "${cert_file_path}" && ! -z "${key_file_path}" ]]; then + rpc_encryption_configuration="--enable-rpc-encryption --ca-path ${ca_path} --cert-file-path ${cert_file_path} --key-file-path ${key_file_path}" +fi + +# Create acl config including the master token from AWS SecretsManager +TOKEN=$(aws secretsmanager --region "${aws_region}" get-secret-value --secret-id ${consul_token_secret} | jq -r .SecretString) + +/opt/consul/bin/run-consul --client --datacenter "${consul_cluster_tag_key}" --cluster-tag-key "${consul_cluster_tag_key}" --cluster-tag-value "${consul_cluster_tag_value}" $gossip_encryption_configuration $rpc_encryption_configuration + +/opt/vault/bin/run-vault \ + --tls-cert-file "$VAULT_TLS_CERT_FILE" \ + --tls-key-file "$VAULT_TLS_KEY_FILE" \ + --enable-auto-unseal \ + --auto-unseal-kms-key-id "${kms_key_id}" \ + --auto-unseal-kms-key-region "${aws_region}" + +# Inject token into the consul storage block and restart vault to pick it up +sed -i '/storage "consul"/a token = "'$TOKEN'"' /opt/vault/config/default.hcl +systemctl restart vault + +#echo --tls-cert-file "$VAULT_TLS_CERT_FILE" --tls-key-file "$VAULT_TLS_KEY_FILE" --enable-auto-unseal --auto-unseal-kms-key-id "${kms_key_id}" --auto-unseal-kms-key-region "${aws_region}" > /tmp/log + +# When you ssh to one of the instances in the vault cluster and initialize the server +# You will notice it will now boot unsealed +# /opt/vault/bin/vault operator init +# /opt/vault/bin/vault status +# +# If the enterprise license isn't applied, it will however reseal after 30 minutes +# This is how you apply the license, please note that the VAULT_TOKEN environment +# variable needs to be set with the root token obtained when you initialized the server +# /opt/vault/bin/vault write /sys/license "text=" diff --git a/examples/vault-prod-ready/variables.tf b/examples/vault-prod-ready/variables.tf new file mode 100644 index 00000000..d25cadd1 --- /dev/null +++ b/examples/vault-prod-ready/variables.tf @@ -0,0 +1,140 @@ + +# --------------------------------------------------------------------------------------------------------------------- +# ENVIRONMENT VARIABLES +# Define these secrets as environment variables +# --------------------------------------------------------------------------------------------------------------------- + +# AWS_ACCESS_KEY_ID +# AWS_SECRET_ACCESS_KEY +# AWS_DEFAULT_REGION + +# --------------------------------------------------------------------------------------------------------------------- +# REQUIRED PARAMETERS +# You must provide a value for each of these parameters. +# --------------------------------------------------------------------------------------------------------------------- + +variable "ami_id" { + description = "The ID of the AMI to run in the cluster. This should be an AMI built from the Packer template under examples/vault-consul-ami/vault-consul.json." + type = string + default = null +} + +variable "ssh_key_name" { + description = "The name of an EC2 Key Pair that can be used to SSH to the EC2 Instances in this cluster. Set to an empty string to not associate a Key Pair." + type = string +} + +variable "auto_unseal_kms_key_alias" { + description = "The alias of AWS KMS key used for encryption and decryption" + type = string +} + +# --------------------------------------------------------------------------------------------------------------------- +# OPTIONAL PARAMETERS +# These parameters have reasonable defaults. +# --------------------------------------------------------------------------------------------------------------------- + +variable "vault_cluster_name" { + description = "What to name the Vault server cluster and all of its associated resources" + type = string + default = "vault-example" +} + +variable "consul_cluster_name" { + description = "What to name the Consul server cluster and all of its associated resources" + type = string + default = "consul-example" +} + +variable "auth_server_name" { + description = "What to name the server authenticating to vault" + type = string + default = "auth-example" +} + +variable "vault_cluster_size" { + description = "The number of Vault server nodes to deploy. We strongly recommend using 3 or 5." + type = number + default = 3 +} + +variable "consul_cluster_size" { + description = "The number of Consul server nodes to deploy. We strongly recommend using 3 or 5." + type = number + default = 3 +} + +variable "vault_instance_type" { + description = "The type of EC2 Instance to run in the Vault ASG" + type = string + default = "t2.micro" +} + +variable "consul_instance_type" { + description = "The type of EC2 Instance to run in the Consul ASG" + type = string + default = "t2.nano" +} + +variable "consul_cluster_tag_key" { + description = "The tag the Consul EC2 Instances will look for to automatically discover each other and form a cluster." + type = string + default = "consul-servers" +} + +variable "vpc_id" { + description = "The ID of the VPC to deploy into. Leave an empty string to use the Default VPC in this region." + type = string + default = null +} + +#variable "vpc_subnet_ids" { +# description = "Subnets for Consul and Vault" +# type = list(string) +#} + +variable "security_group_id" { + description = "Security group ID of VPN Server" + type = string +} + +variable "external_account_ssh_grunt_role_arn" { + description = "Since our IAM users are defined in a separate AWS account, this variable is used to specify the ARN of an IAM role that allows ssh-grunt to retrieve IAM group and public SSH key info from that account." + type = string +} + +## +# --------------------------------------------------------------------------------------------------------------------- +# Encryption +# --------------------------------------------------------------------------------------------------------------------- + +variable "enable_gossip_encryption" { + description = "Encrypt gossip traffic between nodes. Must also specify encryption key." + type = bool + default = false +} + +variable "enable_rpc_encryption" { + description = "Encrypt RPC traffic between nodes. Must also specify TLS certificates and keys." + type = bool + default = false +} + +variable "ca_path" { + description = "Path to the directory of CA files used to verify outgoing connections." + type = string + default = "/opt/consul/tls/ca" +} + +variable "cert_file_path" { + description = "Path to the certificate file used to verify incoming connections." + type = string + default = "/opt/consul/tls/consul.crt.pem" +} + +variable "key_file_path" { + description = "Path to the certificate key used to verify incoming connections." + type = string + default = "/opt/consul/tls/consul.key.pem" +} +