Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-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 |

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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/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)
```
Expand Down Expand Up @@ -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/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/aws-ec2-vault-cluster/`:

```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)
- `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.

## Troubleshooting

| Symptom | Action |
Expand Down
Empty file removed aws/.gitkeep
Empty file.
14 changes: 14 additions & 0 deletions aws/aws-ec2-vault-cluster/.nullstone/module.yml
Original file line number Diff line number Diff line change
@@ -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
107 changes: 107 additions & 0 deletions aws/aws-ec2-vault-cluster/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions aws/aws-ec2-vault-cluster/aws.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
provider "aws" {
default_tags {
tags = local.tags
}
}
29 changes: 29 additions & 0 deletions aws/aws-ec2-vault-cluster/connections.tf
Original file line number Diff line number Diff line change
@@ -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"
}
101 changes: 101 additions & 0 deletions aws/aws-ec2-vault-cluster/iam.tf
Original file line number Diff line number Diff line change
@@ -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
}
30 changes: 30 additions & 0 deletions aws/aws-ec2-vault-cluster/nullstone.tf
Original file line number Diff line number Diff line change
@@ -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}"
}
29 changes: 29 additions & 0 deletions aws/aws-ec2-vault-cluster/outputs.tf
Original file line number Diff line number Diff line change
@@ -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."
}
11 changes: 11 additions & 0 deletions aws/aws-ec2-vault-cluster/secrets.tf
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading