If you're using AWS KMS for blockchain transaction signing, every call to kms:Sign is an unrestricted operation. Any process with the right IAM permissions can sign any transaction to any address for any amount.
This tutorial shows you how to add a policy evaluation layer in front of your KMS signing operations — so every transaction is checked against rules (value limits, address allowlists, velocity caps) before the signature is produced. If the policy says no, the key never signs.
We'll use Gate, an open-source pre-signature enforcement SDK, in SHADOW mode — meaning it evaluates every transaction and logs the decision, but never blocks. Zero risk to your existing operations.
What you'll build
Your App → Gate SDK (evaluate) → Policy Decision → KMS Sign (only if ALLOW)
↓
Signed receipt (HMAC + RSA)
Decision log (DynamoDB)
Every signing request gets:
- A policy evaluation (denylist, value threshold, velocity, signer authorization)
- A cryptographically signed receipt (verifiable by third parties)
- A decision log entry (queryable audit trail)
In SHADOW mode, the SDK never blocks — it returns ALLOW for everything but logs WOULD_BLOCK when policy violations are detected. You see what Gate would catch without any production risk.
Prerequisites
- Node.js 18+
- An AWS account with a KMS asymmetric signing key (secp256k1 or ECDSA P-256)
- A Gate tenant (free trial at blockintelai.com)
Step 1: Install
npm install blockintel-gate-sdk @aws-sdk/client-kms uuid
Step 2: Initialize the Gate client
import { createGateClient } from 'blockintel-gate-sdk';
const gate = createGateClient({
apiUrl: 'https://gate-hotpath.blockintelai.com',
tenantId: process.env.GATE_TENANT_ID,
apiKey: process.env.GATE_API_KEY,
environment: 'production',
signerId: 'treasury-signer-1',
onConnectionFailure: 'FAIL_OPEN', // SHADOW mode default — never block
});
FAIL_OPEN means that if Gate itself is unreachable, your signing operations continue uninterrupted. Gate is never a Tier-0 dependency unless you want it to be.
Step 3: Wrap your signing function
Here's a typical KMS signing function without Gate:
import { KMSClient, SignCommand } from '@aws-sdk/client-kms';
import { v4 as uuidv4 } from 'uuid';
const kms = new KMSClient({ region: 'us-east-1' });
// ❌ Before: unrestricted signing
async function signTransaction(txHash: Buffer, keyId: string) {
const result = await kms.send(new SignCommand({
KeyId: keyId,
Message: txHash,
MessageType: 'DIGEST',
SigningAlgorithm: 'ECDSA_SHA_256',
}));
return result.Signature;
}
Here's the same function with Gate evaluation:
// ✅ After: every signing operation evaluated against policy
async function signTransactionWithGate(
txHash: Buffer,
keyId: string,
toAddress: string,
valueWei: string,
valueUsd: number,
) {
// Step 1: Ask Gate if this transaction should proceed
const decision = await gate.evaluate({
requestId: uuidv4(),
txIntent: {
toAddress,
value: valueWei,
valueUsd,
network: 'ethereum',
networkFamily: 'EVM',
chainId: 1,
from: '0xYourWalletAddress',
},
signingContext: {
signerId: 'treasury-signer-1',
actorPrincipal: 'ops@yourcompany.com',
},
});
// Step 2: In SHADOW mode, decision is always ALLOW
// But decision.reasonCodes tells you what WOULD have been blocked
if (decision.reasonCodes?.length > 0) {
console.warn(`[GATE] Would block: ${decision.reasonCodes.join(', ')}`);
}
// Step 3: Receipt is signed and logged regardless
console.log(`[GATE] Receipt: ${decision.receiptSignature}`);
// Step 4: Sign with KMS (always proceeds in SHADOW mode)
const result = await kms.send(new SignCommand({
KeyId: keyId,
Message: txHash,
MessageType: 'DIGEST',
SigningAlgorithm: 'ECDSA_SHA_256',
}));
return {
signature: result.Signature,
gateDecision: decision.decision,
gateReceipt: decision.receiptSignature,
wouldBlock: decision.reasonCodes,
};
}
That's it. Every KMS signing operation now produces a policy decision and a signed receipt. In SHADOW mode, nothing is blocked — you're collecting data.
Step 4: Set up policies
Log into the Gate Console and create policies for your tenant. Common starting rules:
| Rule | What it does | Example |
|---|---|---|
| Denylist | Blocks transactions to known malicious addresses | OFAC SDN list, known exploit contracts |
| Destination Allowlist | Only allow transfers to approved addresses | Your hot wallets, exchange deposit addresses |
| Value Threshold | Block transactions above a USD amount | Max $100,000 per transaction |
| Velocity Limit | Cap transaction volume in a rolling window | Max $500,000 per 10 minutes |
| Signer Allowlist | Only approved signing identities can request |
treasury-signer-1, ops-signer-2
|
| New Destination | Flag first-time destination addresses | Log or require step-up approval |
After creating rules, compile and publish a policy snapshot. The hot path loads the snapshot and evaluates every gate.evaluate() call against it.
Step 5: Verify it works
// Test: normal transaction (should be ALLOW)
const normalTx = await signTransactionWithGate(
Buffer.from('abc123', 'hex'),
'arn:aws:kms:us-east-1:123456789:key/your-key-id',
'0x742d35Cc6634c0532925a3b844Bc9e7595916DA2', // Known good address
'1000000000000000000', // 1 ETH
2500, // $2,500
);
console.log(normalTx.gateDecision); // "ALLOW"
console.log(normalTx.wouldBlock); // []
// Test: denylisted address (should be WOULD_BLOCK in SHADOW)
const blockedTx = await signTransactionWithGate(
Buffer.from('def456', 'hex'),
'arn:aws:kms:us-east-1:123456789:key/your-key-id',
'0xba5ed7f51d2a0cfeef2ca243af1188bad4f1cb03', // Known exploit address
'50000000000000000000', // 50 ETH
125000, // $125,000
);
console.log(blockedTx.gateDecision); // "ALLOW" (SHADOW mode override)
console.log(blockedTx.wouldBlock); // ["DENYLIST"]
In SHADOW mode, both transactions succeed. But the second one logs WOULD_BLOCK with reason DENYLIST. You now have visibility into what Gate would catch — without changing your production behavior.
What you get after 14 days
Let Gate run in SHADOW mode for two weeks. You'll accumulate:
- A decision log — every signing operation evaluated, with policy verdicts
- A block rate — what percentage of transactions would have been blocked by your policy
- A max-loss estimate — if a signing key were compromised, how much could an attacker drain in 24 hours under your current policy (this is the Bot Blast Radius metric)
- Signed receipts — cryptographically signed records of every decision, verifiable by third parties (useful for insurance, compliance, audits)
This data is visible in the Gate Console dashboard. If you're working with a custody insurance carrier, the BBR report and evidence bundle are exactly what underwriters need to evaluate your signing controls.
Going from SHADOW to enforcement
When you're ready to enforce (not just monitor), the transition is one config change:
const gate = createGateClient({
// ... same config
onConnectionFailure: 'FAIL_CLOSED', // Now: block on policy violation
});
At SOFT_ENFORCE, the SDK throws BlockIntelBlockedError when a policy violation is detected. Your code catches it and decides what to do.
At HARD_KMS_GATEWAY, Gate removes kms:Sign from your application's IAM role entirely. The only path to a signature is through Gate's policy evaluation. Even a fully compromised application cannot sign a transaction that violates policy — because the application no longer has the IAM permission to call KMS directly.
That's the enforcement ladder: SHADOW (monitor) → SOFT_ENFORCE (SDK blocks) → HARD_KMS_GATEWAY (IAM blocks). Each step is reversible. Each step produces more evidence.
Python
The same flow works with the Python SDK:
pip install gate-sdk boto3
from gate_sdk import GateClient
gate = GateClient(
base_url="https://gate-hotpath.blockintelai.com",
tenant_id=os.environ["GATE_TENANT_ID"],
api_key=os.environ["GATE_API_KEY"],
environment="production",
signer_id="treasury-signer-1",
)
decision = gate.evaluate(
request_id=str(uuid4()),
to_address="0x742d35Cc6634c0532925a3b844Bc9e7595916DA2",
value="1000000000000000000",
value_usd=2500,
network="ethereum",
network_family="EVM",
chain_id=1,
from_address="0xYourWalletAddress",
signer_id="treasury-signer-1",
actor_principal="ops@yourcompany.com",
)
print(f"Decision: {decision.decision}")
print(f"Receipt: {decision.receipt_signature}")
Note: the Python SDK's default timeout is 50ms (vs 15s in TypeScript) — it's optimized for hot-path latency. If you see circuit breaker trips, check your control plane p99 before increasing the timeout.
Links
- Gate SDK (npm):
npm install blockintel-gate-sdk - Gate SDK (PyPI):
pip install gate-sdk - Documentation: blockintelai.com/docs
- Console: gate.blockintelai.com
Built by BlockIntel. Gate is pre-signature policy enforcement for crypto custody — we stop unauthorized transactions before they're signed, not after.
Top comments (0)