ChainLaunch

Deploy Fabric and Besu Networks with AWS KMS — Self-Hosted with LocalStack

Deploy Fabric and Besu Networks with AWS KMS — Self-Hosted with LocalStack

TL;DR: Use ChainLaunch and Terraform to deploy both Hyperledger Fabric and Besu networks with AWS KMS as the key provider. Start with LocalStack for free local development, then swap to real AWS KMS for production — no code changes required.

Enterprise blockchain deployments demand more than a Docker Compose file and some private keys stored on disk. Compliance teams want HSM-backed key management. Security teams want audit trails. And operations teams want infrastructure-as-code they can review in a pull request.

This guide walks through deploying both a Hyperledger Fabric channel and a Besu QBFT network using AWS KMS for all cryptographic keys — self-hosted on your own infrastructure. We start with LocalStack so you can follow along without an AWS account, then show exactly what changes when you move to production.

What You'll Build

By the end of this guide, you'll have:

  • LocalStack running locally as an AWS KMS substitute
  • ChainLaunch managing your blockchain infrastructure
  • A Fabric network with 1 peer org, 1 orderer org, and a channel — all keys in KMS
  • A Besu network with 4 QBFT validators — all secp256k1 keys in KMS
  • Terraform code that works identically against LocalStack and real AWS
graph TB subgraph LocalStack / AWS KMS[AWS KMS] end subgraph ChainLaunch CL[ChainLaunch Server] CL --> KMS end subgraph Fabric Network Peer0[Peer - Org1] Orderer0[Orderer - OrdererOrg] end subgraph Besu Network V1[Validator 1] V2[Validator 2] V3[Validator 3] V4[Validator 4] end CL --> Peer0 CL --> Orderer0 CL --> V1 CL --> V2 CL --> V3 CL --> V4

Prerequisites

  • Docker installed and running (for LocalStack and blockchain nodes)
  • ChainLaunch Pro installed (chainlaunch serve on port 8100)
  • Terraform 1.5+ installed
  • curl for quick API checks

No AWS account needed — we use LocalStack for the entire tutorial.

Step 1: Spin Up LocalStack

LocalStack provides a fully functional AWS KMS API on your laptop. One command:

docker run -d \
  --name localstack \
  -p 4566:4566 \
  -e SERVICES=kms \
  -e DEFAULT_REGION=us-east-1 \
  localstack/localstack:latest

Verify it's running:

curl -s http://localhost:4566/_localstack/health | python3 -m json.tool

You should see "kms": "running" in the output. That's it — you now have a local KMS service that behaves like the real thing.

Quick test — create a key to confirm KMS is working:

aws --endpoint-url=http://localhost:4566 kms create-key \
  --key-usage SIGN_VERIFY \
  --key-spec ECC_SECG_P256K1 \
  --region us-east-1 \
  --no-cli-pager

If you don't have the AWS CLI installed, skip this — ChainLaunch will create keys through its own API.

Step 2: Start ChainLaunch

Start ChainLaunch Pro in development mode:

chainlaunch serve --port 8100 --data ~/.chainlaunch --db blockchain-kms.db --dev

Verify:

curl -s -u admin:admin123 http://localhost:8100/api/v1/key-providers | python3 -m json.tool

You should see the default DATABASE provider. We'll add KMS next.

Step 3: Configure the AWS KMS Provider

Create a main.tf file:

terraform {
  required_providers {
    chainlaunch = {
      source = "kfsoftware/chainlaunch"
    }
  }
}
 
provider "chainlaunch" {
  url      = "http://localhost:8100"
  username = "admin"
  password = "admin123"
}
 
# ============================================================================
# AWS KMS PROVIDER (LocalStack)
# ============================================================================
 
resource "chainlaunch_key_provider" "kms" {
  name       = "localstack-kms"
  type       = "AWS_KMS"
  is_default = true
 
  aws_kms_config = {
    operation             = "IMPORT"
    aws_region            = "us-east-1"
    aws_access_key_id     = "test"
    aws_secret_access_key = "test"
    endpoint_url          = "http://localhost:4566"
    kms_key_alias_prefix  = "chainlaunch/"
  }
}

Apply it:

terraform init
terraform apply -auto-approve

Option B: Via curl

curl -s -u admin:admin123 http://localhost:8100/api/v1/key-providers \
  -H "Content-Type: application/json" \
  -d '{
    "name": "localstack-kms",
    "type": "AWS_KMS",
    "isDefault": 1,
    "config": {
      "awsKms": {
        "operation": "IMPORT",
        "awsRegion": "us-east-1",
        "awsAccessKeyId": "test",
        "awsSecretAccessKey": "test",
        "endpointUrl": "http://localhost:4566",
        "kmsKeyAliasPrefix": "chainlaunch/"
      }
    }
  }' | python3 -m json.tool

Check the provider status:

curl -s -u admin:admin123 http://localhost:8100/api/v1/key-providers/2/awskms/status | python3 -m json.tool

You should see "kms_reachable": true and "kms_status": "available".

Step 4: Deploy the Fabric Network

Now add the Fabric resources to your Terraform configuration. Append to main.tf:

# ============================================================================
# FABRIC: ORGANIZATIONS
# ============================================================================
 
resource "chainlaunch_fabric_organization" "org1" {
  msp_id      = "Org1MSP"
  description = "Peer organization with KMS-backed keys"
  provider_id = tonumber(chainlaunch_key_provider.kms.id)
}
 
resource "chainlaunch_fabric_organization" "orderer_org" {
  msp_id      = "OrdererMSP"
  description = "Orderer organization with KMS-backed keys"
  provider_id = tonumber(chainlaunch_key_provider.kms.id)
}
 
# ============================================================================
# FABRIC: NODES
# ============================================================================
 
resource "chainlaunch_fabric_peer" "peer0_org1" {
  name            = "peer0-org1"
  organization_id = chainlaunch_fabric_organization.org1.id
  msp_id          = "Org1MSP"
  mode            = "service"
  version         = "2.5.12"
 
  external_endpoint         = "localhost:7051"
  listen_address            = "0.0.0.0:7051"
  chaincode_address         = "0.0.0.0:7052"
  events_address            = "0.0.0.0:7053"
  operations_listen_address = "0.0.0.0:9443"
  domain_names              = ["peer0-org1", "localhost"]
}
 
resource "chainlaunch_fabric_orderer" "orderer0" {
  name            = "orderer0"
  organization_id = chainlaunch_fabric_organization.orderer_org.id
  msp_id          = "OrdererMSP"
  mode            = "service"
  version         = "2.5.12"
 
  external_endpoint         = "localhost:7050"
  listen_address            = "0.0.0.0:7050"
  admin_address             = "0.0.0.0:7055"
  operations_listen_address = "0.0.0.0:8443"
  domain_names              = ["orderer0", "localhost"]
}
 
# ============================================================================
# FABRIC: CHANNEL
# ============================================================================
 
resource "chainlaunch_fabric_network" "mychannel" {
  name        = "mychannel"
  description = "Channel with KMS-backed identities"
 
  peer_organizations = [
    {
      id       = chainlaunch_fabric_organization.org1.id
      node_ids = [chainlaunch_fabric_peer.peer0_org1.id]
    }
  ]
 
  orderer_organizations = [
    {
      id       = chainlaunch_fabric_organization.orderer_org.id
      node_ids = [chainlaunch_fabric_orderer.orderer0.id]
    }
  ]
 
  consensus_type = "etcdraft"
}
 
# Join nodes to channel
resource "chainlaunch_fabric_join_node" "peer0_join" {
  network_id = chainlaunch_fabric_network.mychannel.id
  node_id    = chainlaunch_fabric_peer.peer0_org1.id
  role       = "peer"
}
 
resource "chainlaunch_fabric_join_node" "orderer0_join" {
  network_id = chainlaunch_fabric_network.mychannel.id
  node_id    = chainlaunch_fabric_orderer.orderer0.id
  role       = "orderer"
}
 
# Set anchor peer
resource "chainlaunch_fabric_anchor_peers" "org1_anchors" {
  network_id      = chainlaunch_fabric_network.mychannel.id
  organization_id = chainlaunch_fabric_organization.org1.id
  anchor_peer_ids = [chainlaunch_fabric_peer.peer0_org1.id]
 
  depends_on = [chainlaunch_fabric_join_node.peer0_join]
}

Apply:

terraform apply -auto-approve

Terraform will create organizations (generating signing keys and TLS keys in KMS), provision nodes, create the channel, and join all nodes to it.

Step 5: Deploy the Besu Network

Besu uses secp256k1 keys for validator identities — the same curve Ethereum uses. Append to main.tf:

# ============================================================================
# BESU: VALIDATOR KEYS (secp256k1 via KMS)
# ============================================================================
 
resource "chainlaunch_key" "besu_validators" {
  count = 4
 
  name        = "besu-validator-${count.index}"
  algorithm   = "EC"
  curve       = "secp256k1"
  provider_id = tonumber(chainlaunch_key_provider.kms.id)
}
 
# ============================================================================
# BESU: NETWORK (QBFT Consensus)
# ============================================================================
 
resource "chainlaunch_besu_network" "mainnet" {
  name            = "besu-kms-network"
  description     = "Besu QBFT network with KMS-backed validator keys"
  chain_id        = 1337
  consensus       = "qbft"
  block_period    = 5
  epoch_length    = 30000
  request_timeout = 10
 
  initial_validator_key_ids = [
    for key in chainlaunch_key.besu_validators : tonumber(key.id)
  ]
}
 
# ============================================================================
# BESU: VALIDATOR NODES
# ============================================================================
 
resource "chainlaunch_besu_node" "validators" {
  count = 4
 
  name       = "besu-validator-${count.index}"
  network_id = tonumber(chainlaunch_besu_network.mainnet.id)
  key_id     = tonumber(chainlaunch_key.besu_validators[count.index].id)
  mode       = "service"
  version    = "24.5.1"
 
  external_ip = "127.0.0.1"
  internal_ip = "127.0.0.1"
  p2p_host    = "0.0.0.0"
  p2p_port    = 30303 + count.index
  rpc_host    = "0.0.0.0"
  rpc_port    = 8545 + count.index
 
  metrics_enabled = true
 
  depends_on = [chainlaunch_besu_network.mainnet]
}
 
# ============================================================================
# OUTPUTS
# ============================================================================
 
output "fabric_channel_id" {
  value = chainlaunch_fabric_network.mychannel.id
}
 
output "besu_network_id" {
  value = chainlaunch_besu_network.mainnet.id
}
 
output "besu_rpc_endpoints" {
  value = [
    for node in chainlaunch_besu_node.validators :
    "http://127.0.0.1:${node.rpc_port}"
  ]
}

Apply:

terraform apply -auto-approve

This creates 4 secp256k1 keys in KMS, generates the QBFT genesis block with those validator addresses, and starts 4 Besu nodes.

Step 6: Verify Everything Works

Check Fabric

# List nodes
curl -s -u admin:admin123 http://localhost:8100/api/v1/nodes | python3 -m json.tool
 
# Check channel
curl -s -u admin:admin123 http://localhost:8100/api/v1/networks | python3 -m json.tool

Check Besu

# Query the first validator's RPC
curl -s -X POST http://localhost:8545 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
 
# Check peers are connected
curl -s -X POST http://localhost:8545 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"net_peerCount","params":[],"id":1}'

Verify Keys Are in KMS

# List keys managed by ChainLaunch
curl -s -u admin:admin123 http://localhost:8100/api/v1/keys | python3 -m json.tool
 
# Check KMS directly via LocalStack
aws --endpoint-url=http://localhost:4566 kms list-aliases \
  --region us-east-1 --no-cli-pager

You should see aliases prefixed with chainlaunch/ — these are the keys backing your blockchain nodes.

Moving to Production: What Changes?

When you're ready for real AWS KMS, the only thing that changes is the provider configuration. Everything else — organizations, nodes, networks, channels — stays identical.

Option 1: Switch to IAM Role (EC2 / EKS)

If ChainLaunch runs on an EC2 instance or EKS pod, remove the static credentials entirely:

resource "chainlaunch_key_provider" "kms" {
  name       = "production-kms"
  type       = "AWS_KMS"
  is_default = true
 
  aws_kms_config = {
    operation            = "IMPORT"
    aws_region           = "us-east-1"
    kms_key_alias_prefix = "chainlaunch/"
    # No credentials — AWS SDK uses instance role automatically
  }
}

Option 2: Switch to AssumeRole (cross-account)

For cross-account access or least-privilege:

resource "chainlaunch_key_provider" "kms" {
  name       = "production-kms"
  type       = "AWS_KMS"
  is_default = true
 
  aws_kms_config = {
    operation            = "IMPORT"
    aws_region           = "us-east-1"
    assume_role_arn      = "arn:aws:iam::123456789012:role/ChainLaunchKMSRole"
    external_id          = "chainlaunch-prod-001"
    kms_key_alias_prefix = "chainlaunch/"
  }
}

Option 3: Update an existing provider

Already running with static credentials and want to switch to AssumeRole? Just update the Terraform config and apply — the provider supports in-place updates:

# Before: static credentials
aws_kms_config = {
  operation             = "IMPORT"
  aws_region            = "us-east-1"
  aws_access_key_id     = "AKIA..."
  aws_secret_access_key = "..."
}
 
# After: switch to AssumeRole
aws_kms_config = {
  operation       = "IMPORT"
  aws_region      = "us-east-1"
  assume_role_arn = "arn:aws:iam::123456789012:role/ChainLaunchKMSRole"
  external_id     = "chainlaunch-prod-001"
}
terraform apply

No node restarts. No key re-generation. The auth method changes and ChainLaunch verifies connectivity.

Required IAM Policy

Whichever auth method you choose, the IAM principal needs these KMS permissions:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "kms:CreateKey",
        "kms:CreateAlias",
        "kms:DeleteAlias",
        "kms:DescribeKey",
        "kms:GetPublicKey",
        "kms:ListAliases",
        "kms:ListKeys",
        "kms:Sign",
        "kms:Verify",
        "kms:TagResource",
        "kms:ScheduleKeyDeletion"
      ],
      "Resource": "*"
    }
  ]
}

Why AWS KMS for Blockchain Keys?

Using AWS KMS (or LocalStack for development) instead of storing keys in a database gives you:

Compliance and audit: Every key operation is logged in CloudTrail. You can prove who created, used, or deleted a key and when.

Key isolation: Private keys never leave the KMS boundary. Signing operations happen inside KMS — ChainLaunch sends the data to sign and gets back the signature, but never holds the raw private key material.

Access control: IAM policies control who can use which keys. You can restrict validator key usage to specific roles or services.

Durability: KMS keys are replicated across multiple AZs. No single point of failure for your blockchain identity material.

Rotation readiness: When your compliance team asks about key rotation, you have an answer: KMS supports automatic key rotation for symmetric keys, and ChainLaunch can orchestrate certificate renewal for asymmetric keys.

Clean Up

Destroy everything in reverse:

terraform destroy -auto-approve

Stop LocalStack:

docker rm -f localstack

Frequently Asked Questions

Can I use the same KMS provider for both Fabric and Besu?

Yes. A single chainlaunch_key_provider of type AWS_KMS can store keys for both platforms. Fabric uses EC (P-256/P-384) keys for signing and TLS, while Besu uses EC (secp256k1) keys for validator identities. KMS supports both curve families.

Does LocalStack support all KMS operations ChainLaunch needs?

Yes. LocalStack Pro and Community both support CreateKey, GetPublicKey, Sign, CreateAlias, and the other operations ChainLaunch uses. The Community edition is sufficient for development.

What happens if KMS is temporarily unavailable?

Existing blockchain nodes continue operating — they only need KMS for signing operations. If KMS is down, new transactions can't be signed, but the network continues to process previously signed transactions. ChainLaunch retries KMS operations with exponential backoff.

Can I migrate from database keys to KMS keys?

Not in-place — you'd create new keys in KMS and create new nodes using those keys. For Fabric, this means creating new organizations. For Besu, you'd need to add new validators and remove old ones through the QBFT governance process.

How much does AWS KMS cost for blockchain?

KMS charges $1/month per key plus $0.03 per 10,000 API calls. A typical 4-validator Besu network with 4 Fabric nodes uses around 8-12 keys, costing roughly $8-12/month for key storage plus negligible API call costs. Far less than the operational cost of managing keys yourself.


David Viejo is the founder of ChainLaunch and a Hyperledger Foundation contributor. He created the Bevel Operator Fabric project and has been building blockchain infrastructure tooling since 2020.

Related Articles

Ready to Transform Your Blockchain Workflow?

Deploy Fabric & Besu in minutes, not weeks. AI-powered chaincode, real-time monitoring, and enterprise security with Vault.

ChainLaunch Pro: $60,000/year   Includes premium support, unlimited networks, advanced AI tools, and priority updates.

Questions? Contact us at support@chainlaunch.dev