From fc5c1d24f59f1405cf093d7c325119041fd88a8f Mon Sep 17 00:00:00 2001 From: Amit Biswas Date: Thu, 3 Sep 2026 04:42:53 -0500 Subject: [PATCH 1/2] Add aws-vault-ec2-cluster connections, IAM, and security groups. This is the Nullstone AWS module skeleton so a workspace can attach VPC, snapshot S3, and unseal KMS before ASG and NLB work. --- CHANGELOG.md | 2 + README.md | 34 ++++++++++-- aws/.gitkeep | 0 aws/.nullstone/module.yml | 14 +++++ aws/.terraform.lock.hcl | 107 ++++++++++++++++++++++++++++++++++++++ aws/aws.tf | 9 ++++ aws/connections.tf | 34 ++++++++++++ aws/iam.tf | 101 +++++++++++++++++++++++++++++++++++ aws/nullstone.tf | 30 +++++++++++ aws/outputs.tf | 29 +++++++++++ aws/secrets.tf | 11 ++++ aws/security.tf | 83 +++++++++++++++++++++++++++++ aws/variables.tf | 27 ++++++++++ 13 files changed, 476 insertions(+), 5 deletions(-) delete mode 100644 aws/.gitkeep create mode 100644 aws/.nullstone/module.yml create mode 100644 aws/.terraform.lock.hcl create mode 100644 aws/aws.tf create mode 100644 aws/connections.tf create mode 100644 aws/iam.tf create mode 100644 aws/nullstone.tf create mode 100644 aws/outputs.tf create mode 100644 aws/secrets.tf create mode 100644 aws/security.tf create mode 100644 aws/variables.tf diff --git a/CHANGELOG.md b/CHANGELOG.md index e23ce31..f8be592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,4 @@ # 0.1.0 (Unreleased) * Initial release +* `aws/`: Nullstone module `aws-vault-ec2-cluster`. Connections for VPC, S3 snapshots, and unseal KMS. IAM instance role, SM tokens (init/provisioning/operator), node and NLB security groups. No ASG yet. +* README Testing: how to `tofu fmt` / `init` / `validate` `aws/`, and what Nullstone connections are required for plan. diff --git a/README.md b/README.md index 7514a75..8cca840 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Applications share one Vault. They do not see each other's secrets. Isolation is | Target | Role | Status | |---|---|---| | `local/` | Docker Compose | Implemented | -| `aws/` | AWS | Not implemented | +| `aws/` | `aws-vault-ec2-cluster` | Connections, IAM, Secrets Manager tokens, security groups. ASG/NLB not built yet. | | `gcp/` | GCP | Not implemented | | `azure/` | Azure | Not implemented | @@ -46,10 +46,10 @@ Implemented: Not implemented: -- AWS, GCP, Azure, Kubernetes -- Production KMS auto-unseal +- AWS ASG, NLB, AMI, user-data, and `bootstrap aws` +- GCP, Azure, Kubernetes +- Production KMS auto-unseal on a running cluster - TLS, multi-node Raft, DR replication -- OpenTofu / Terraform Local unseal submits Shamir shares (5 shares, threshold 3) for laptop use. It is not AWS KMS, Cloud KMS, or Azure Key Vault auto-unseal. @@ -82,7 +82,7 @@ vault-cluster/ ├── cmd/ Go app entrypoints (vault-utils CLI) ├── internal/ Go libraries, policy templates, lint fixtures ├── local/ Compose target, snapshots -├── aws/ Nullstone Terraform module (not yet implemented) +├── aws/ Nullstone module `aws-vault-ec2-cluster` (IAM/SM/SG; no ASG yet) ├── gcp/ Nullstone Terraform module (not yet implemented) └── azure/ Nullstone Terraform module (not yet implemented) ``` @@ -318,6 +318,30 @@ In `local/`, `TestLocalComposeStatic` lints `compose.yml` (digest pins, no dev m Denials must be HTTP 403. A 404 is a different failure. +### AWS module (`aws/`) + +`go test` does not cover this directory. The current slice is OpenTofu only (connections, IAM, Secrets Manager, security groups). There is no Docker or live-AWS test in CI. + +From `aws/`: + +```bash +tofu fmt -check +tofu init -backend=false +tofu validate +``` + +`tofu plan` and `tofu apply` need a Nullstone workspace plus AWS credentials. Without them, plan fails with `no nullstone workspace 0/0/0` and missing AWS credentials. That is expected. Do not `tofu apply` from this repo unless you intend to create IAM, Secrets Manager, and security groups. + +To plan against real connections: + +1. Install the Nullstone CLI (`ns`). +2. In an AWS Nullstone stack, attach this module and connect: + - `network` → `network/aws/vpc` (same VPC pattern as Nullstone EC2 apps) + - `s3_bucket` → `datastore/aws/s3` (snapshot bucket) + - `unseal_key` → `datastore/aws/kms` (dedicated unseal key, not the bucket SSE key) +3. Run workspace preview/plan in Nullstone so `ns_connection` outputs resolve. +4. In the plan, expect an IAM role + instance profile, SSM attach, inline IAM policy, three Secrets Manager secrets (`init` / `provisioning` / `operator`), and two security groups (NLB + nodes) with the 8200/8201/8210 rules. No ASG, NLB, or launch template yet. + ## Troubleshooting | Symptom | Action | diff --git a/aws/.gitkeep b/aws/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/aws/.nullstone/module.yml b/aws/.nullstone/module.yml new file mode 100644 index 0000000..67aee72 --- /dev/null +++ b/aws/.nullstone/module.yml @@ -0,0 +1,14 @@ +org_name: nullstone +name: aws-vault-ec2-cluster +friendly_name: Vault EC2 Cluster +description: Self-hosted Vault CE on an EC2 Auto Scaling Group with an internal NLB. +category: cluster +subcategory: "" +provider_types: + - aws +platform: vault +subplatform: ec2 +type: "" +appCategories: [] +is_public: false +tool_name: opentofu diff --git a/aws/.terraform.lock.hcl b/aws/.terraform.lock.hcl new file mode 100644 index 0000000..c518e6b --- /dev/null +++ b/aws/.terraform.lock.hcl @@ -0,0 +1,107 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/aws" { + version = "6.62.0" + hashes = [ + "h1:5x/4mMlIqSyeMJQ8tD0A+FTNynpdHW8IA7F2zqrgpwU=", + "h1:Etlx1tUvlB7sqFWPciSn9Yz1g50ctM+dGlyVMpu720I=", + "h1:FBnaqFZDf3DdVb4SVpCPoT+TZQUSChNmmDViEGPV1rw=", + "h1:MRIBAtFWiQAyo4kpbBUtIvxlSmpQ9Eel0nLMYCb6MH4=", + "h1:MeMP80kzq1meAJQ0l+kU6KVW1S+4EhIRSy6q3L++LPU=", + "h1:N8W8KgcjlG1kb9mPapfieH/vYzyNrJBsb4RS0axwg5Y=", + "h1:OB5obEZKuaX4gQ7DYxYvAMaf4I6v2keeKOPfOpFMkgI=", + "h1:WnhkO4yQc0QvVgx/xJFI+UbM1y8BC8yvApoDUlhb5XY=", + "h1:fN9PvxrT2/rAiT86Hen0vicyMbKXAZG6VC/TEP9ZpD8=", + "h1:kRrdLje5ab/tmObt9lOKOi2KCyIB8B3xZCKhcfdS2K0=", + "h1:l5VASLhVAOCr2Q+7ywGqWb+JSEIJ5UjilOMBjFMdQ7U=", + "h1:mugvZRK3/kSysUmpt664tMNpOZtGbQd6nGKaM4p3kwo=", + "h1:nY4ct0BTUQ7se8gbnzXsPCN2tiP4mZsGnEw2juSpXNs=", + "h1:qRFHk1ksyfMj2KOrs9pEntzagM+Tzqm+qznJfxVrJj0=", + "h1:szVx4GPJr2hJt9t82VilK2Gu/N9WR/KbdeLZ9wb0Nek=", + "zh:072d542e40ca0b8c5e081c9f834e3e41aa2ad31c01219522f314685d5c482823", + "zh:0b2643473f5bb154d724e64e75632814d74acda5fd81b6488d0ed13f413152b2", + "zh:108b3e175886e45c4e955f1ef323aa849dfd82fc450108238be2ab7c0d1f0c2e", + "zh:3586ca03a5d07e40a67b01b7fdebc6f7636a42c2a2f6ab88635ffca8f6928dfe", + "zh:47bd626153ee94e6b45533c7c02f579dc6d586d6d9580e03a040643f647be50b", + "zh:49aafbe6f665b3c4c97522905fd09229f2758a782c7cfc508280607030134ef8", + "zh:6e3397227c3f8f3becc04f95ebcb977d7012f025c9921f9ef5e603fe6d5da665", + "zh:70f4478658a13eeb57725e3b424329494a0ffbd78ac2734effea4e60d802cfa2", + "zh:774d4357447f94a5d4981c7569f85cd140ac577974174c5e7b477d834c6bfb43", + "zh:7971ef7bc532ab0edb3ee739ac3010b46321ec8541f873956a20dabbd6a0f189", + "zh:8ec18fba906468a9e6ce15329aa098dfb45ab1b9dca446a8fc2079a0d6ef590c", + "zh:98328661edd4dfc4e4782fd03a7010854218d3fbf35435837ac33e7ee8aafe7e", + "zh:9cbbf5033116f72749ebd4cf9add6aadf7e9e4bc7704eec86a8f4c139c16c120", + "zh:9faf944474f9233ddbb3606bc495570d4b952fea21f09c28676f9847d619e50f", + "zh:d0636d6de8a7b0b0bcc2add80556ee7118981d738998d4772558b9de0df99af1", + ] +} + +provider "registry.opentofu.org/hashicorp/random" { + version = "3.9.0" + hashes = [ + "h1:8EQU5KSxezcjo/phRSe69rDOI0lk4pSaggj7FsskYp8=", + "h1:Lw9im2VBBJQ3RyAbHPQ0rcvcmmcZWm3x+kIOpN+Tv9s=", + "h1:U8KXqGCoNI9/guYbTvzgdtVk3fRthoG0UXwm1JoEpIs=", + "h1:YXaVd4p6qXPPVaxIBaIDNXmBwT02ZqDn0qD+tYpw8sA=", + "h1:cOpc03fphEt/G9Rfc4jLL/fW0D7tgvlXqiDKPF4vuww=", + "h1:g09RR7T1xWkeGrZwWvWMT9ncJrFGr1k3CBD585UmO7w=", + "h1:gGDdPPibmw2EWROx+sh1RGLjR5+nPwZyrf6/N9jXfeM=", + "h1:haE7/nXCOhXKP4oXeEnER3t5CaVQWqujz4nBnpeTUv4=", + "h1:ieSVpfZS2lKuMr05ph0QsOVpCzg7uk3cgKBaXR+Ikug=", + "h1:ig2s1IS9IzehorRjvVAnKIsUUj8fkgyxct1L/kswcc4=", + "h1:j3lS+ZEERFnoab8t1ppDrScGVP/cgWbzlCrEYKTCXYw=", + "h1:lxezrKmOiQIySHAM+os8qLVq7hqufDr8h3Hpzvsk+78=", + "h1:lzRqBJAG+NETxHbEZUJ/YP3RMEjZBinTX7VmgH3lw60=", + "h1:tdSNWK5ApqUsgbdYieyeYLTu6nIZUV3hR1oFqUfAuGo=", + "h1:xedet8yH/zI2CfdxsGlK0nlFWc/Bp61yrWsEa3fHB8g=", + "zh:03f1114cc20b8913523735ab76e0f0a2b16ce13c92923a53304bf85f07fc0dbc", + "zh:105b678ee72322a3067f105d7e05e940f6143238f377f6e87ff4ec909246ac2a", + "zh:55f3bbf13ea18cbace61a706566a80f25f33fe2b1780b6f3d7b582af2a05b6d2", + "zh:63adf996db48f082f7a6351eb485e219cd88795fc71e6ec60a837263ab0d2cb1", + "zh:7e99550738a4e3cc68b8a467714b0d69371025fe95e3326d5323d026d55653e9", + "zh:8342b54af3a18a37e075eeae61be57f4de2ba71b35d95c5075d402dd2c1f289d", + "zh:83ee18e32ac9dd5fc91298554b7c4cfa4c3a1db50f4c797945637cc93c0844ae", + "zh:993ecc0adbf6bd535a59fbc9b735d8c33950e6f6eb5e621d750da9b71d65d80a", + "zh:ad722bc59d4edbf1415e827fc007c0efe6e0e9462d5568bae20b34be1058a261", + "zh:ae9448e1f87b2f9a6c5197a0e9862162ec6b137cb3a3835e11522995d8939e7c", + "zh:bc9cdd3aac784f759125c6627f6f6416e8726a1c184eb9cf3e55b9edbc94c627", + "zh:c8e35b89572ba1c40a9b20022e033a3395fb8d42e7604d50c900f193ba10382e", + "zh:e2deaa8a9975ef81d9f62baed12c41286918b0a10908e0e031f13f69a3b730a1", + "zh:ee39707557210a0ab1098aa357d2cdfe502e5a312d0dbdffb09d08facc4d3fc5", + "zh:f81afe4eb63e8aa9e0ea71be6c990f0dc69cb360e7191c0742a991f4a5081b64", + ] +} + +provider "registry.opentofu.org/nullstone-io/ns" { + version = "0.11.1" + constraints = "~> 0.11.0" + hashes = [ + "h1:6BxUpd3+1TLtEsj54HBS4nOkR9FicIuuXEVDPCByF7Q=", + "h1:71TqF9V72ZFWQf/RFzarQgxgKglL4VvPaOuwb/xBc7k=", + "h1:EYaDXEvkg6WmXCw47d4Tijcm9rlOw3DYkGF05wx0H3o=", + "h1:HaGl+RgDBjpxvxrxbuUhDiuwMkmc+IFf/jDQW8tQY1c=", + "h1:PRRORu4Y+QfYbNvjIEiaQXlQ9veksGKAWHm0Por7ZHk=", + "h1:Q45Qrs0APQjOr+HhvnxB49f4wPE04/Pq5ukoq8uUZp4=", + "h1:ShGG1d+/n+cwFvvncUjjzh64Hqc3TQdJxuy7YKfP1yo=", + "h1:TRhYX38Zlx/32PbScJyI3mKmIPp8xXDkUSCGZCRQNNQ=", + "h1:YrKu51K7Bae6Mj/CFN9MX08ITrgjcdH7MchfL0tqv9M=", + "h1:cTiiEMOG2BDPmWv4VtpGV+nRnGTgs7GYMUS4gR9slvk=", + "h1:f1sN6fJ4vDYh0AiPl3TKT+0qBxvdxzmX+ZbGiNg4h3U=", + "h1:mR6frDhMROvg82IrNOXeZUywhMRLHByK1am8G4SOBxg=", + "h1:o/Hd6E8+H1/+Haa96SjdI2JGN1jN0yPi79JxBZplIY0=", + "zh:2e7aa2baf793e68155ce2e3d1bf17f2fcbb1d33b0986edf8ac37f35c09e0b217", + "zh:50e49ef17f8edc2c65a434a46994326740a1975ebf28aeea0964cd2dcfbaeed8", + "zh:56c1d3ad611599e1946992bc78f7172f4f5774b38e08c9f3384988cfc844374a", + "zh:7fe50367ad319f9e5b07aa925cfa337f98c079d119a48e87f6df2c9772297a68", + "zh:8947b5f6360b414f3f9f4dc56d2efe6bea981b24ae8c0d76a7b3191d47512897", + "zh:95f5581d67edbf0923332eaa95805a8388bd48b34d8f8a714c66985b7dc66449", + "zh:a666ee46523be51cb9445d0fece4441776bbcb7c161cfa374a6195448ed005d8", + "zh:ad17d9f33d1f69c025de5b803a85fe0d937113d4cac22c74fb814a862e3fb750", + "zh:b3541c9fee4b362f30ba50cc1daf74d910fae1942e71252f306711ec22d33415", + "zh:bf68295cbb945321453f51537f0ed1dbeedeb274e600672d53f6f43f953b51b0", + "zh:ce56a788e0cd05cfb094e9834b64bbf51fb570d2626b19e60540b8d56842db70", + "zh:d577d8aed3d4389231a21fa3fbca2433a98930f05e54c82ff030279e9871090f", + "zh:ede865f1859ffe2788bc0f15ec45de337fece8247e752a0e817a0ad5c85b8031", + ] +} diff --git a/aws/aws.tf b/aws/aws.tf new file mode 100644 index 0000000..645bfe8 --- /dev/null +++ b/aws/aws.tf @@ -0,0 +1,9 @@ +provider "aws" { + default_tags { + tags = local.tags + } +} + +data "aws_region" "this" {} + +data "aws_caller_identity" "this" {} diff --git a/aws/connections.tf b/aws/connections.tf new file mode 100644 index 0000000..c07bc75 --- /dev/null +++ b/aws/connections.tf @@ -0,0 +1,34 @@ +data "ns_connection" "network" { + name = "network" + contract = "network/aws/vpc" +} + +data "ns_connection" "s3_bucket" { + name = "s3_bucket" + contract = "datastore/aws/s3" +} + +data "ns_connection" "unseal_key" { + name = "unseal_key" + contract = "datastore/aws/kms" +} + +locals { + vpc_id = data.ns_connection.network.outputs.vpc_id + private_subnet_ids = data.ns_connection.network.outputs.private_subnet_ids + + snapshot_bucket_name = data.ns_connection.s3_bucket.outputs.db_hostname + snapshot_bucket_arn = data.ns_connection.s3_bucket.outputs.db_arn + snapshot_bucket_url = "${data.ns_connection.s3_bucket.outputs.db_protocol}://${data.ns_connection.s3_bucket.outputs.db_hostname}" + snapshot_kms_key_arn = try(data.ns_connection.s3_bucket.outputs.kms_key_arn, "") + + unseal_kms_key_arn = data.ns_connection.unseal_key.outputs.kms_key_arn + unseal_kms_key_id = data.ns_connection.unseal_key.outputs.kms_key_id + + vpc_cidr = data.ns_connection.network.outputs.vpc_cidr + + vault_api_port = 8200 + vault_cluster_port = 8201 + vault_health_port = 8210 + snapshot_prefix = "vault-snapshots" +} diff --git a/aws/iam.tf b/aws/iam.tf new file mode 100644 index 0000000..7e1cdfc --- /dev/null +++ b/aws/iam.tf @@ -0,0 +1,101 @@ +data "aws_iam_policy_document" "assume" { + statement { + effect = "Allow" + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "this" { + name = local.resource_name + assume_role_policy = data.aws_iam_policy_document.assume.json + tags = local.tags +} + +resource "aws_iam_instance_profile" "this" { + name = local.resource_name + role = aws_iam_role.this.name + tags = local.tags +} + +resource "aws_iam_role_policy_attachment" "ssm" { + role = aws_iam_role.this.name + policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" +} + +data "aws_iam_policy_document" "this" { + statement { + sid = "UnsealKey" + effect = "Allow" + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:DescribeKey", + ] + resources = [local.unseal_kms_key_arn] + } + + dynamic "statement" { + for_each = local.snapshot_kms_key_arn == "" ? [] : [local.snapshot_kms_key_arn] + content { + sid = "SnapshotKey" + effect = "Allow" + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:DescribeKey", + ] + resources = [statement.value] + } + } + + statement { + sid = "SnapshotList" + effect = "Allow" + actions = [ + "s3:ListBucket", + ] + resources = [local.snapshot_bucket_arn] + condition { + test = "StringLike" + variable = "s3:prefix" + values = [local.snapshot_prefix, "${local.snapshot_prefix}/*"] + } + } + + statement { + sid = "SnapshotObjects" + effect = "Allow" + actions = [ + "s3:GetObject", + "s3:PutObject", + ] + resources = ["${local.snapshot_bucket_arn}/${local.snapshot_prefix}/*"] + } + + statement { + sid = "PlatformTokens" + effect = "Allow" + actions = [ + "secretsmanager:GetSecretValue", + "secretsmanager:PutSecretValue", + ] + resources = [for s in aws_secretsmanager_secret.platform : s.arn] + } + + statement { + sid = "RaftJoin" + effect = "Allow" + actions = ["ec2:DescribeInstances"] + resources = ["*"] + } +} + +resource "aws_iam_role_policy" "this" { + name = local.resource_name + role = aws_iam_role.this.id + policy = data.aws_iam_policy_document.this.json +} diff --git a/aws/nullstone.tf b/aws/nullstone.tf new file mode 100644 index 0000000..a66bc88 --- /dev/null +++ b/aws/nullstone.tf @@ -0,0 +1,30 @@ +terraform { + required_providers { + ns = { + source = "nullstone-io/ns" + version = "~> 0.11.0" + } + aws = { + source = "hashicorp/aws" + } + random = { + source = "hashicorp/random" + } + } +} + +data "ns_workspace" "this" {} + +resource "random_string" "resource_suffix" { + length = 5 + lower = true + upper = false + numeric = false + special = false +} + +locals { + tags = data.ns_workspace.this.aws_tags + block_name = data.ns_workspace.this.block_name + resource_name = "${data.ns_workspace.this.block_ref}-${random_string.resource_suffix.result}" +} diff --git a/aws/outputs.tf b/aws/outputs.tf new file mode 100644 index 0000000..cb07f79 --- /dev/null +++ b/aws/outputs.tf @@ -0,0 +1,29 @@ +output "role_name" { + value = aws_iam_role.this.name + description = "string ||| IAM role name for Vault EC2 instances." +} + +output "instance_profile_name" { + value = aws_iam_instance_profile.this.name + description = "string ||| Instance profile name for the launch template." +} + +output "security_group_id" { + value = aws_security_group.nodes.id + description = "string ||| Security group attached to Vault nodes." +} + +output "nlb_security_group_id" { + value = aws_security_group.nlb.id + description = "string ||| Security group attached to the internal NLB." +} + +output "operator_secret_arn" { + value = aws_secretsmanager_secret.platform["operator"].arn + description = "string ||| Secrets Manager ARN for the operator token." +} + +output "provisioning_secret_arn" { + value = aws_secretsmanager_secret.platform["provisioning"].arn + description = "string ||| Secrets Manager ARN for the provisioning token." +} diff --git a/aws/secrets.tf b/aws/secrets.tf new file mode 100644 index 0000000..1eca7c8 --- /dev/null +++ b/aws/secrets.tf @@ -0,0 +1,11 @@ +locals { + platform_secret_names = toset(["init", "provisioning", "operator"]) +} + +resource "aws_secretsmanager_secret" "platform" { + for_each = local.platform_secret_names + + name_prefix = "${local.block_name}/vault/${each.key}/" + recovery_window_in_days = 0 + tags = local.tags +} diff --git a/aws/security.tf b/aws/security.tf new file mode 100644 index 0000000..7717b4e --- /dev/null +++ b/aws/security.tf @@ -0,0 +1,83 @@ +resource "aws_security_group" "nlb" { + name = "${local.resource_name}/nlb" + vpc_id = local.vpc_id + tags = merge(local.tags, { Name = "${local.resource_name}/nlb" }) +} + +resource "aws_security_group" "nodes" { + name = "${local.resource_name}/nodes" + vpc_id = local.vpc_id + tags = merge(local.tags, { Name = "${local.resource_name}/nodes" }) +} + +resource "aws_security_group_rule" "nlb_api_from_vpc" { + security_group_id = aws_security_group.nlb.id + type = "ingress" + protocol = "tcp" + from_port = local.vault_api_port + to_port = local.vault_api_port + cidr_blocks = [local.vpc_cidr] +} + +resource "aws_security_group_rule" "nlb_to_api" { + security_group_id = aws_security_group.nlb.id + type = "egress" + protocol = "tcp" + from_port = local.vault_api_port + to_port = local.vault_api_port + source_security_group_id = aws_security_group.nodes.id +} + +resource "aws_security_group_rule" "nlb_to_health" { + security_group_id = aws_security_group.nlb.id + type = "egress" + protocol = "tcp" + from_port = local.vault_health_port + to_port = local.vault_health_port + source_security_group_id = aws_security_group.nodes.id +} + +resource "aws_security_group_rule" "nodes_api_from_nlb" { + security_group_id = aws_security_group.nodes.id + type = "ingress" + protocol = "tcp" + from_port = local.vault_api_port + to_port = local.vault_api_port + source_security_group_id = aws_security_group.nlb.id +} + +resource "aws_security_group_rule" "nodes_health_from_nlb" { + security_group_id = aws_security_group.nodes.id + type = "ingress" + protocol = "tcp" + from_port = local.vault_health_port + to_port = local.vault_health_port + source_security_group_id = aws_security_group.nlb.id +} + +resource "aws_security_group_rule" "nodes_raft" { + security_group_id = aws_security_group.nodes.id + type = "ingress" + protocol = "tcp" + from_port = local.vault_cluster_port + to_port = local.vault_cluster_port + source_security_group_id = aws_security_group.nodes.id +} + +resource "aws_security_group_rule" "nodes_https" { + security_group_id = aws_security_group.nodes.id + type = "egress" + protocol = "tcp" + from_port = 443 + to_port = 443 + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "nodes_raft_egress" { + security_group_id = aws_security_group.nodes.id + type = "egress" + protocol = "tcp" + from_port = local.vault_cluster_port + to_port = local.vault_cluster_port + source_security_group_id = aws_security_group.nodes.id +} diff --git a/aws/variables.tf b/aws/variables.tf new file mode 100644 index 0000000..e2c77da --- /dev/null +++ b/aws/variables.tf @@ -0,0 +1,27 @@ +variable "cluster_size" { + type = number + default = 1 + description = "Vault nodes. 1 is non-prod (no quorum). 3 is prod/staging (one per AZ)." + + validation { + condition = contains([1, 3], var.cluster_size) + error_message = "cluster_size must be 1 or 3." + } +} + +variable "instance_type" { + type = string + default = "t4g.micro" + description = "EC2 type. Default is the cheap non-prod size." +} + +variable "backup_schedule" { + type = string + default = "off" + description = "Raft snapshot cadence to the connected S3 bucket: hourly, daily, or off." + + validation { + condition = contains(["hourly", "daily", "off"], var.backup_schedule) + error_message = "backup_schedule must be hourly, daily, or off." + } +} From f0110b7620885a14983d6df5303207e349a8e29c Mon Sep 17 00:00:00 2001 From: Amit Biswas Date: Fri, 4 Sep 2026 08:13:15 -0400 Subject: [PATCH 2/2] Address review: rename and relocate aws-ec2-vault-cluster. Move the module under aws/aws-ec2-vault-cluster so naming matches other Nullstone AWS modules, and apply the connection and variable comments. --- CHANGELOG.md | 2 -- README.md | 10 +++--- aws/.nullstone/module.yml | 14 -------- .../.nullstone/module.yml | 14 ++++++++ .../.terraform.lock.hcl | 0 aws/aws-ec2-vault-cluster/aws.tf | 5 +++ aws/aws-ec2-vault-cluster/connections.tf | 29 ++++++++++++++++ aws/{ => aws-ec2-vault-cluster}/iam.tf | 0 aws/{ => aws-ec2-vault-cluster}/nullstone.tf | 0 aws/{ => aws-ec2-vault-cluster}/outputs.tf | 0 aws/{ => aws-ec2-vault-cluster}/secrets.tf | 0 aws/{ => aws-ec2-vault-cluster}/security.tf | 0 aws/aws-ec2-vault-cluster/variables.tf | 33 ++++++++++++++++++ aws/aws.tf | 9 ----- aws/connections.tf | 34 ------------------- aws/variables.tf | 27 --------------- 16 files changed, 86 insertions(+), 91 deletions(-) delete mode 100644 aws/.nullstone/module.yml create mode 100644 aws/aws-ec2-vault-cluster/.nullstone/module.yml rename aws/{ => aws-ec2-vault-cluster}/.terraform.lock.hcl (100%) create mode 100644 aws/aws-ec2-vault-cluster/aws.tf create mode 100644 aws/aws-ec2-vault-cluster/connections.tf rename aws/{ => aws-ec2-vault-cluster}/iam.tf (100%) rename aws/{ => aws-ec2-vault-cluster}/nullstone.tf (100%) rename aws/{ => aws-ec2-vault-cluster}/outputs.tf (100%) rename aws/{ => aws-ec2-vault-cluster}/secrets.tf (100%) rename aws/{ => aws-ec2-vault-cluster}/security.tf (100%) create mode 100644 aws/aws-ec2-vault-cluster/variables.tf delete mode 100644 aws/aws.tf delete mode 100644 aws/connections.tf delete mode 100644 aws/variables.tf diff --git a/CHANGELOG.md b/CHANGELOG.md index f8be592..e23ce31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,2 @@ # 0.1.0 (Unreleased) * Initial release -* `aws/`: Nullstone module `aws-vault-ec2-cluster`. Connections for VPC, S3 snapshots, and unseal KMS. IAM instance role, SM tokens (init/provisioning/operator), node and NLB security groups. No ASG yet. -* README Testing: how to `tofu fmt` / `init` / `validate` `aws/`, and what Nullstone connections are required for plan. diff --git a/README.md b/README.md index 8cca840..053f698 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Applications share one Vault. They do not see each other's secrets. Isolation is | Target | Role | Status | |---|---|---| | `local/` | Docker Compose | Implemented | -| `aws/` | `aws-vault-ec2-cluster` | Connections, IAM, Secrets Manager tokens, security groups. ASG/NLB not built yet. | +| `aws/aws-ec2-vault-cluster/` | `aws-ec2-vault-cluster` | Connections, IAM, Secrets Manager tokens, security groups. ASG/NLB not built yet. | | `gcp/` | GCP | Not implemented | | `azure/` | Azure | Not implemented | @@ -82,7 +82,7 @@ vault-cluster/ ├── cmd/ Go app entrypoints (vault-utils CLI) ├── internal/ Go libraries, policy templates, lint fixtures ├── local/ Compose target, snapshots -├── aws/ Nullstone module `aws-vault-ec2-cluster` (IAM/SM/SG; no ASG yet) +├── aws/aws-ec2-vault-cluster/ Nullstone module (IAM/SM/SG; no ASG yet) ├── gcp/ Nullstone Terraform module (not yet implemented) └── azure/ Nullstone Terraform module (not yet implemented) ``` @@ -318,11 +318,11 @@ In `local/`, `TestLocalComposeStatic` lints `compose.yml` (digest pins, no dev m Denials must be HTTP 403. A 404 is a different failure. -### AWS module (`aws/`) +### AWS module (`aws/aws-ec2-vault-cluster/`) `go test` does not cover this directory. The current slice is OpenTofu only (connections, IAM, Secrets Manager, security groups). There is no Docker or live-AWS test in CI. -From `aws/`: +From `aws/aws-ec2-vault-cluster/`: ```bash tofu fmt -check @@ -337,7 +337,7 @@ To plan against real connections: 1. Install the Nullstone CLI (`ns`). 2. In an AWS Nullstone stack, attach this module and connect: - `network` → `network/aws/vpc` (same VPC pattern as Nullstone EC2 apps) - - `s3_bucket` → `datastore/aws/s3` (snapshot bucket) + - `snapshots_bucket` → `datastore/aws/s3` (snapshot bucket) - `unseal_key` → `datastore/aws/kms` (dedicated unseal key, not the bucket SSE key) 3. Run workspace preview/plan in Nullstone so `ns_connection` outputs resolve. 4. In the plan, expect an IAM role + instance profile, SSM attach, inline IAM policy, three Secrets Manager secrets (`init` / `provisioning` / `operator`), and two security groups (NLB + nodes) with the 8200/8201/8210 rules. No ASG, NLB, or launch template yet. diff --git a/aws/.nullstone/module.yml b/aws/.nullstone/module.yml deleted file mode 100644 index 67aee72..0000000 --- a/aws/.nullstone/module.yml +++ /dev/null @@ -1,14 +0,0 @@ -org_name: nullstone -name: aws-vault-ec2-cluster -friendly_name: Vault EC2 Cluster -description: Self-hosted Vault CE on an EC2 Auto Scaling Group with an internal NLB. -category: cluster -subcategory: "" -provider_types: - - aws -platform: vault -subplatform: ec2 -type: "" -appCategories: [] -is_public: false -tool_name: opentofu diff --git a/aws/aws-ec2-vault-cluster/.nullstone/module.yml b/aws/aws-ec2-vault-cluster/.nullstone/module.yml new file mode 100644 index 0000000..a9985e7 --- /dev/null +++ b/aws/aws-ec2-vault-cluster/.nullstone/module.yml @@ -0,0 +1,14 @@ +org_name: nullstone +name: aws-ec2-vault-cluster +friendly_name: Vault Cluster (AWS EC2) +description: Self-hosted Vault cluster running on an EC2 Auto Scaling Group +category: datastore +subcategory: "" +provider_types: + - aws +platform: vault +subplatform: ec2 +type: "" +appCategories: [] +is_public: true +tool_name: opentofu diff --git a/aws/.terraform.lock.hcl b/aws/aws-ec2-vault-cluster/.terraform.lock.hcl similarity index 100% rename from aws/.terraform.lock.hcl rename to aws/aws-ec2-vault-cluster/.terraform.lock.hcl diff --git a/aws/aws-ec2-vault-cluster/aws.tf b/aws/aws-ec2-vault-cluster/aws.tf new file mode 100644 index 0000000..1644b4c --- /dev/null +++ b/aws/aws-ec2-vault-cluster/aws.tf @@ -0,0 +1,5 @@ +provider "aws" { + default_tags { + tags = local.tags + } +} diff --git a/aws/aws-ec2-vault-cluster/connections.tf b/aws/aws-ec2-vault-cluster/connections.tf new file mode 100644 index 0000000..2f9f0c8 --- /dev/null +++ b/aws/aws-ec2-vault-cluster/connections.tf @@ -0,0 +1,29 @@ +data "ns_connection" "network" { + name = "network" + contract = "network/aws/vpc" +} + +data "ns_connection" "snapshots_bucket" { + name = "snapshots_bucket" + contract = "datastore/aws/s3" +} + +data "ns_connection" "unseal_key" { + name = "unseal_key" + contract = "datastore/aws/kms" +} + +locals { + vpc_id = data.ns_connection.network.outputs.vpc_id + vpc_cidr = data.ns_connection.network.outputs.vpc_cidr + + snapshot_bucket_arn = data.ns_connection.snapshots_bucket.outputs.db_arn + snapshot_kms_key_arn = try(data.ns_connection.snapshots_bucket.outputs.kms_key_arn, "") + + unseal_kms_key_arn = data.ns_connection.unseal_key.outputs.kms_key_arn + + vault_api_port = 8200 + vault_cluster_port = 8201 + vault_health_port = 8210 + snapshot_prefix = "vault-snapshots" +} diff --git a/aws/iam.tf b/aws/aws-ec2-vault-cluster/iam.tf similarity index 100% rename from aws/iam.tf rename to aws/aws-ec2-vault-cluster/iam.tf diff --git a/aws/nullstone.tf b/aws/aws-ec2-vault-cluster/nullstone.tf similarity index 100% rename from aws/nullstone.tf rename to aws/aws-ec2-vault-cluster/nullstone.tf diff --git a/aws/outputs.tf b/aws/aws-ec2-vault-cluster/outputs.tf similarity index 100% rename from aws/outputs.tf rename to aws/aws-ec2-vault-cluster/outputs.tf diff --git a/aws/secrets.tf b/aws/aws-ec2-vault-cluster/secrets.tf similarity index 100% rename from aws/secrets.tf rename to aws/aws-ec2-vault-cluster/secrets.tf diff --git a/aws/security.tf b/aws/aws-ec2-vault-cluster/security.tf similarity index 100% rename from aws/security.tf rename to aws/aws-ec2-vault-cluster/security.tf diff --git a/aws/aws-ec2-vault-cluster/variables.tf b/aws/aws-ec2-vault-cluster/variables.tf new file mode 100644 index 0000000..896fe22 --- /dev/null +++ b/aws/aws-ec2-vault-cluster/variables.tf @@ -0,0 +1,33 @@ +variable "cluster_size" { + type = number + default = 1 + description = <= 1 && var.cluster_size % 2 == 1 + error_message = "cluster_size must be an odd number of instances (1, 3, 5, 7, ...)." + } +} + +variable "instance_type" { + type = string + default = "t4g.micro" + description = <