From 91d301f44c612a1e46e09b2a5e3b7ea028a92226 Mon Sep 17 00:00:00 2001 From: David-282 Date: Thu, 28 May 2026 11:47:34 +0100 Subject: [PATCH] feature(devops): add AWS KMS provisioning terraform script #issue 412 --- .github/workflows/terraform-kms.yml | 62 +++++ infra/terraform/kms/QA_STEPS.md | 115 +++++++++ infra/terraform/kms/main.tf | 251 +++++++++++++++++++ infra/terraform/kms/outputs.tf | 28 +++ infra/terraform/kms/terraform.tfvars.example | 36 +++ infra/terraform/kms/variables.tf | 109 ++++++++ 6 files changed, 601 insertions(+) create mode 100644 .github/workflows/terraform-kms.yml create mode 100644 infra/terraform/kms/QA_STEPS.md create mode 100644 infra/terraform/kms/main.tf create mode 100644 infra/terraform/kms/outputs.tf create mode 100644 infra/terraform/kms/terraform.tfvars.example create mode 100644 infra/terraform/kms/variables.tf diff --git a/.github/workflows/terraform-kms.yml b/.github/workflows/terraform-kms.yml new file mode 100644 index 0000000..d130693 --- /dev/null +++ b/.github/workflows/terraform-kms.yml @@ -0,0 +1,62 @@ +# ============================================================================== +# .github/workflows/terraform-kms.yml +# CI pipeline for AnchorPoint KMS Terraform — Issue #412 +# +# Runs on every pull request that touches infra/terraform/** +# Stages: fmt → validate → checkov → plan +# ============================================================================== + +name: Terraform KMS CI + +on: + pull_request: + paths: + - 'infra/terraform/**' + +jobs: + terraform-kms: + name: Lint, Validate, Scan, Plan + runs-on: ubuntu-latest + + defaults: + run: + working-directory: infra/terraform/kms + + steps: + # ── Checkout ───────────────────────────────────────────────────────────── + - name: Checkout + uses: actions/checkout@v4 + + # ── Terraform setup ─────────────────────────────────────────────────────── + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "1.5.7" + + # ── Format check ───────────────────────────────────────────────────────── + - name: Terraform Format Check + run: terraform fmt -check + + # ── Init (no backend — dry run only) ───────────────────────────────────── + - name: Terraform Init + run: terraform init -backend=false + + # ── Validate ───────────────────────────────────────────────────────────── + - name: Terraform Validate + run: terraform validate + + # ── Security scan ───────────────────────────────────────────────────────── + - name: Checkov Security Scan + uses: bridgecrewio/checkov-action@v12 + with: + directory: infra/terraform/kms + framework: terraform + soft_fail: false + + # ── Plan (dry run against testnet vars) ────────────────────────────────── + - name: Terraform Plan + run: | + terraform plan \ + -backend=false \ + -var="environment=testnet" \ + -var="key_alias=anchorpoint-stellar-keys" diff --git a/infra/terraform/kms/QA_STEPS.md b/infra/terraform/kms/QA_STEPS.md new file mode 100644 index 0000000..ae6a3d6 --- /dev/null +++ b/infra/terraform/kms/QA_STEPS.md @@ -0,0 +1,115 @@ +# KMS Manual QA Steps — Issue #412 + +## Pre-requisites +- AWS CLI configured with sufficient permissions +- Terraform >= 1.0.0 installed +- `checkov` installed (`pip install checkov`) + +--- + +## Step 1 — Initialize and validate + +```bash +cd infra/terraform/kms + +terraform init +terraform fmt -check +terraform validate +``` + +--- + +## Step 2 — Security scan + +```bash +checkov -d . --framework terraform +``` + +Expected passes: +- `CKV_AWS_7` — Key rotation enabled +- `CKV_AWS_109` — Key policy does not use wildcard principal +- `CKV_AWS_149` — No plaintext secrets in config + +--- + +## Step 3 — Plan + +```bash +cp terraform.tfvars.example terraform.tfvars +# Fill in real ARNs in terraform.tfvars + +terraform plan -var-file=terraform.tfvars +``` + +Expected output: +``` +Plan: 3 to add, 0 to change, 0 to destroy. + + aws_kms_key.anchorpoint_stellar_keys + + aws_kms_alias.anchorpoint_stellar_keys + + aws_ssm_parameter.kms_key_arn +``` + +--- + +## Step 4 — Apply + +```bash +terraform apply -var-file=terraform.tfvars +``` + +--- + +## Step 5 — Verify key rotation is enabled + +```bash +KEY_ID=$(terraform output -raw key_id) + +aws kms get-key-rotation-status --key-id "$KEY_ID" +``` + +Expected: +```json +{ "KeyRotationEnabled": true } +``` + +--- + +## Step 6 — Encrypt/decrypt round-trip + +```bash +ALIAS=$(terraform output -raw alias_name) + +# Encrypt a test string +CIPHER=$(aws kms encrypt \ + --key-id "$ALIAS" \ + --plaintext "anchorpoint-test" \ + --query CiphertextBlob \ + --output text) + +# Decrypt it back — should return "anchorpoint-test" +aws kms decrypt \ + --ciphertext-blob fileb://<(echo "$CIPHER" | base64 -d) \ + --query Plaintext \ + --output text | base64 -d +``` + +--- + +## Step 7 — Verify SSM parameter was written + +```bash +SSM_PATH=$(terraform output -raw ssm_parameter_name) + +aws ssm get-parameter --name "$SSM_PATH" --query Parameter.Value --output text +``` + +Expected: the full KMS key ARN. + +--- + +## Step 8 — Teardown (testnet only) + +```bash +terraform destroy -var-file=terraform.tfvars +# Key enters 10-day pending-deletion window. +``` diff --git a/infra/terraform/kms/main.tf b/infra/terraform/kms/main.tf new file mode 100644 index 0000000..3af1d5f --- /dev/null +++ b/infra/terraform/kms/main.tf @@ -0,0 +1,251 @@ +# ============================================================================== +# AWS KMS Provisioning for AnchorPoint — Issue #412 +# ============================================================================== +# Provisions a symmetric CMK (AES-256) that encrypts Stellar provider signing +# keypairs used in SEP-10 authentication and SEP-24 transaction flows. +# +# Security invariant (ref: IMPLEMENTATION_SUMMARY.md): +# Provider private keys are NEVER written to any persistent store in plaintext. +# This key is the single encryption root for all Stellar signing key material. +# +# WARNING: Destroying this key without first rotating the Stellar signing keypair +# will permanently break SEP-10 authentication for the anchor. +# Always rotate the keypair BEFORE scheduling key deletion. +# ============================================================================== + +terraform { + required_version = ">= 1.0.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0.0, < 6.0.0" + } + } +} + +# ------------------------------------------------------------------------------ +# Data Sources +# ------------------------------------------------------------------------------ + +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +# ------------------------------------------------------------------------------ +# KMS Key Policy +# +# Four explicit principal groups — no wildcard principals: +# +# 1. Root account — mandatory AWS requirement, full admin +# 2. key_administrator_arns — DevOps/CI lifecycle management only +# 3. api_server_role_arn — AnchorPoint API server: encrypt/decrypt only +# 4. worker_role_arn — BullMQ worker process: encrypt/decrypt only +# Kept separate from the API role because the +# worker runs as its own independent process and +# decrypts Stellar signing keys during settlement +# and contract-call job execution. Bundling both +# into one principal would violate least privilege +# if either process is compromised independently. +# 5. CloudTrail (optional) — log delivery encryption +# ------------------------------------------------------------------------------ + +data "aws_iam_policy_document" "kms_policy" { + + # ── 1. Root admin (mandatory AWS requirement) ──────────────────────────────── + statement { + sid = "EnableRootAdministration" + effect = "Allow" + + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] + } + + actions = ["kms:*"] + resources = ["*"] + } + + # ── 2. Key administrators (DevOps / CI roles) ──────────────────────────────── + # Can manage key lifecycle (rotate, disable, delete) but cannot encrypt/decrypt. + dynamic "statement" { + for_each = length(var.key_administrator_arns) > 0 ? [1] : [] + + content { + sid = "AllowKeyAdministrators" + effect = "Allow" + + principals { + type = "AWS" + identifiers = var.key_administrator_arns + } + + actions = [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:TagResource", + "kms:UntagResource", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + ] + + resources = ["*"] + } + } + + # ── 3. API server role — encrypt/decrypt only ──────────────────────────────── + # The AnchorPoint Node.js API uses this to encrypt provider keys on ingestion + # and decrypt them during SEP-10 challenge generation and SEP-24 flows. + dynamic "statement" { + for_each = var.api_server_role_arn != "" ? [1] : [] + + content { + sid = "AllowAPIServerUsage" + effect = "Allow" + + principals { + type = "AWS" + identifiers = [var.api_server_role_arn] + } + + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ] + + resources = ["*"] + } + } + + # ── 4. BullMQ worker role — encrypt/decrypt only ───────────────────────────── + # Separate principal from the API server role (see policy header comment above). + dynamic "statement" { + for_each = var.worker_role_arn != "" ? [1] : [] + + content { + sid = "AllowWorkerUsage" + effect = "Allow" + + principals { + type = "AWS" + identifiers = [var.worker_role_arn] + } + + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ] + + resources = ["*"] + } + } + + # ── 5. CloudTrail log encryption (optional) ────────────────────────────────── + dynamic "statement" { + for_each = var.enable_cloudtrail_encryption ? [1] : [] + + content { + sid = "AllowCloudTrailEncryption" + effect = "Allow" + + principals { + type = "Service" + identifiers = ["cloudtrail.amazonaws.com"] + } + + actions = [ + "kms:GenerateDataKey*", + "kms:DescribeKey", + ] + + resources = ["*"] + + condition { + test = "StringLike" + variable = "kms:EncryptionContext:aws:cloudtrail:arn" + values = ["arn:aws:cloudtrail:*:${data.aws_caller_identity.current.account_id}:trail/*"] + } + } + } +} + +# ------------------------------------------------------------------------------ +# KMS Customer Managed Key +# +# Symmetric AES-256 key — matches the SYMMETRIC_DEFAULT algorithm expected by +# @aws-sdk/client-kms v3 used in key-management.service.ts. +# ------------------------------------------------------------------------------ + +resource "aws_kms_key" "anchorpoint_stellar_keys" { + description = var.description + key_usage = "ENCRYPT_DECRYPT" + customer_master_key_spec = "SYMMETRIC_DEFAULT" + + # Automatically rotates the backing key material every ~365 days. + # Key IDs and aliases remain stable — no application config change required. + enable_key_rotation = true + + # 10-day deletion window for testnet: long enough to recover from an + # accidental destroy, short enough for teardown cycles. + # For mainnet, raise this to 30 and add prevent_destroy = true. + deletion_window_in_days = var.deletion_window_in_days + + multi_region = var.multi_region + + policy = data.aws_iam_policy_document.kms_policy.json + + tags = { + Name = "anchorpoint-stellar-keys-${var.environment}" + Environment = var.environment + Project = "AnchorPoint" + } +} + +# ------------------------------------------------------------------------------ +# KMS Alias +# +# Application code references the alias, not the raw key ID. This means key +# rotation or replacement is transparent to the backend service. +# Format: alias// +# e.g. alias/testnet/anchorpoint-stellar-keys +# ------------------------------------------------------------------------------ + +resource "aws_kms_alias" "anchorpoint_stellar_keys" { + name = "alias/${var.environment}/${var.key_alias}" + target_key_id = aws_kms_key.anchorpoint_stellar_keys.key_id +} + +# ------------------------------------------------------------------------------ +# SSM Parameter Store — AWS_KMS_KEY_ARN +# +# Writes the provisioned key ARN into SSM so the backend reads it automatically +# at startup via KEY_MANAGEMENT_BACKEND=aws-kms in key-management.service.ts. +# Eliminates manual copy-paste from terraform output into .env files. +# +# Path: /anchorpoint//AWS_KMS_KEY_ARN +# ------------------------------------------------------------------------------ + +resource "aws_ssm_parameter" "kms_key_arn" { + name = "/anchorpoint/${var.environment}/AWS_KMS_KEY_ARN" + type = "String" + value = aws_kms_key.anchorpoint_stellar_keys.arn + description = "KMS key ARN for AnchorPoint ${var.environment} — maps to AWS_KMS_KEY_ARN in backend config" + + tags = { + Environment = var.environment + Project = "AnchorPoint" + } +} diff --git a/infra/terraform/kms/outputs.tf b/infra/terraform/kms/outputs.tf new file mode 100644 index 0000000..1c8e2b3 --- /dev/null +++ b/infra/terraform/kms/outputs.tf @@ -0,0 +1,28 @@ +# ============================================================================== +# outputs.tf — AnchorPoint KMS — Issue #412 +# ============================================================================== + +output "key_id" { + description = "The globally unique identifier of the KMS key. Safe to log." + value = aws_kms_key.anchorpoint_stellar_keys.key_id +} + +output "key_arn" { + description = "The ARN of the KMS key. Use this in IAM policies and SDK calls." + value = aws_kms_key.anchorpoint_stellar_keys.arn +} + +output "alias_name" { + description = "The full alias name (e.g. alias/testnet/anchorpoint-stellar-keys). Reference this in backend config instead of the raw key ARN." + value = aws_kms_alias.anchorpoint_stellar_keys.name +} + +output "alias_arn" { + description = "The ARN of the KMS alias." + value = aws_kms_alias.anchorpoint_stellar_keys.arn +} + +output "ssm_parameter_name" { + description = "SSM parameter path where AWS_KMS_KEY_ARN is stored for backend consumption." + value = aws_ssm_parameter.kms_key_arn.name +} diff --git a/infra/terraform/kms/terraform.tfvars.example b/infra/terraform/kms/terraform.tfvars.example new file mode 100644 index 0000000..b311e51 --- /dev/null +++ b/infra/terraform/kms/terraform.tfvars.example @@ -0,0 +1,36 @@ +# ============================================================================== +# terraform.tfvars.example — AnchorPoint KMS — Issue #412 +# +# !! DO NOT COMMIT terraform.tfvars !! +# This file is a safe-to-commit template. Copy it to terraform.tfvars and +# fill in real values before running plan/apply. +# +# terraform.tfvars is gitignored — it contains real IAM ARNs. +# ============================================================================== + +environment = "testnet" + +key_alias = "anchorpoint-stellar-keys" + +# IAM roles that can rotate, disable, and delete the key. +# Typically your CI/CD deploy role and a DevOps admin role. +key_administrator_arns = [ + "arn:aws:iam::YOUR_ACCOUNT_ID:role/anchorpoint-devops-admin", + "arn:aws:iam::YOUR_ACCOUNT_ID:role/anchorpoint-cicd-role", +] + +# IAM role attached to the AnchorPoint Node.js API runtime (ECS task / EC2). +api_server_role_arn = "arn:aws:iam::YOUR_ACCOUNT_ID:role/anchorpoint-api-server-role" + +# IAM role attached to the BullMQ worker process. +# Must be a separate role from api_server_role_arn. +worker_role_arn = "arn:aws:iam::YOUR_ACCOUNT_ID:role/anchorpoint-worker-role" + +# Set to true if you have a CloudTrail trail in this account. +enable_cloudtrail_encryption = false + +# Cannot be changed after key creation. +multi_region = false + +# 10-day window for testnet. Raise to 30 for mainnet. +deletion_window_in_days = 10 diff --git a/infra/terraform/kms/variables.tf b/infra/terraform/kms/variables.tf new file mode 100644 index 0000000..1753c55 --- /dev/null +++ b/infra/terraform/kms/variables.tf @@ -0,0 +1,109 @@ +# ============================================================================== +# variables.tf — AnchorPoint KMS — Issue #412 +# ============================================================================== + +# ------------------------------------------------------------------------------ +# Required +# ------------------------------------------------------------------------------ + +variable "environment" { + description = "Deployment environment. Controls alias prefix and SSM path." + type = string + + validation { + condition = contains(["testnet", "staging", "mainnet"], var.environment) + error_message = "environment must be one of: testnet, staging, mainnet." + } +} + +variable "key_alias" { + description = <<-EOT + Short, lowercase, hyphenated key alias. + Combined with environment to form: alias// + Recommended value: "anchorpoint-stellar-keys" + EOT + type = string + + validation { + condition = can(regex("^[a-z0-9-]+$", var.key_alias)) + error_message = "key_alias must be lowercase alphanumeric with hyphens only." + } +} + +# ------------------------------------------------------------------------------ +# IAM principals — key policy +# ------------------------------------------------------------------------------ + +variable "key_administrator_arns" { + description = <<-EOT + IAM ARNs (roles/users) granted key administration rights. + Typically: DevOps admin role and CI/CD deploy role. + These principals can rotate/disable/delete the key but NOT encrypt/decrypt. + EOT + type = list(string) + default = [] +} + +variable "api_server_role_arn" { + description = <<-EOT + IAM role ARN attached to the AnchorPoint Node.js API runtime + (ECS task role or EC2 instance profile). + Granted encrypt/decrypt only — no admin rights. + EOT + type = string + default = "" +} + +variable "worker_role_arn" { + description = <<-EOT + IAM role ARN attached to the BullMQ worker process. + Must be separate from api_server_role_arn — the worker runs as an + independent process and decrypts Stellar signing keys during + settlement and contract-call job execution. + Granted encrypt/decrypt only — no admin rights. + EOT + type = string + default = "" +} + +# ------------------------------------------------------------------------------ +# Feature flags +# ------------------------------------------------------------------------------ + +variable "enable_cloudtrail_encryption" { + description = "When true, adds a key policy statement allowing CloudTrail to encrypt log delivery." + type = bool + default = false +} + +variable "multi_region" { + description = "When true, creates a multi-region primary key. Cannot be changed after creation." + type = bool + default = false +} + +variable "deletion_window_in_days" { + description = <<-EOT + Waiting period before a scheduled key deletion takes effect (7–30 days). + Testnet default: 10 days — long enough to recover from an accidental destroy, + short enough for teardown cycles. + Mainnet recommendation: 30 days. + EOT + type = number + default = 10 + + validation { + condition = var.deletion_window_in_days >= 7 && var.deletion_window_in_days <= 30 + error_message = "deletion_window_in_days must be between 7 and 30." + } +} + +# ------------------------------------------------------------------------------ +# Metadata +# ------------------------------------------------------------------------------ + +variable "description" { + description = "Human-readable description attached to the KMS key." + type = string + default = "AnchorPoint KMS CMK — encrypts Stellar provider signing keypairs" +}