IAM-enforced KMS emulator — Test encryption AND permissions locally, fail like production would.
A production-grade KMS implementation with optional pre-flight IAM enforcement. Unlike standard emulators that allow everything, this can deny unauthorized cryptographic operations using production-style IAM authorization policies.
Dual protocol support: Native gRPC + REST/HTTP. Real encryption: AES-256-GCM (not mocked). No GCP credentials required.
Most KMS emulators skip authorization. This one can enforce production-style IAM authorization policies using the IAM Emulator as a control plane.
| Approach | Example | When | Behavior |
|---|---|---|---|
| Mock | Standard emulators | Never | Always allows |
| Observer | Post-execution analysis | After | Records what you used |
| Control Plane | Blackwell (this) | Before | Denies unauthorized |
Pre-flight enforcement catches permission bugs in development/CI, not production.
Before Blackwell, "GCP Hermetic Testing" was essentially impossible.
Google's official emulators have a critical flaw: they ignore authorization. Your tests pass locally because the emulator allows everything, then fail in production when IAM denies the request.
The two bad options:
- Fake Auth - Emulator ignores permissions (fast, but catches zero IAM bugs)
- Staging Leak - Call real GCP IAM API (hermetic seal broken, tests become flaky)
Blackwell closes the hermetic seal:
With IAM enforcement enabled, your tests:
- Fail for the same authorization reasons production would (
PermissionDeniederrors) - Run completely offline (no network, no GCP credentials)
- Execute deterministically (0ms IAM propagation delay vs 1-60s in real GCP)
This is true hermetic testing - all dependencies sealed inside the boundary, no external leaks.
Scope note: IAM enforcement in this emulator is scoped for CI authorization testing. It validates authorization intent and high-risk KMS operations (encrypt, decrypt, destroy, policy changes), not full GCP IAM semantic parity.
- Off (default) - No IAM checks, fast iteration
- Permissive - Enforce when IAM available, allow on connectivity errors (fail-open)
- Strict - Always enforce, deny on connectivity errors (fail-closed, CI-ready)
The Security Paradox:
"A test that cannot fail due to a permission error is a test that has not fully validated the code's production readiness."
Use strict mode in CI to catch IAM bugs before deployment, not during Friday night incidents.
Standalone - Run independently for KMS-only testing:
server-dual
# Single service, no IAM enforcement (mode=off)With IAM Enforcement - Run standalone with IAM checks:
# Start IAM emulator first
cd ../gcp-iam-emulator && ./bin/server --config policy.yaml
# Start KMS with enforcement
IAM_MODE=strict IAM_EMULATOR_HOST=localhost:8080 server-dual
# Now requires valid permissions for encrypt/decrypt operationsOrchestrated Ecosystem - Use with GCP IAM Control Plane for multi-service testing:
gcp-emulator start
# KMS + Secret Manager + IAM emulator
# Single policy file, unified authorizationChoose standalone for simple workflows, IAM-enforced for production-like testing.
- Dual Protocol Support - Native gRPC + REST/HTTP APIs (choose what fits your workflow)
- SDK Compatible - Drop-in replacement for official
cloud.google.com/go/kms(gRPC) - curl Friendly - Full REST API with JSON, test from any language or terminal
- Real Encryption - AES-256-GCM for symmetric encryption (not mocked)
- Key Versioning - Rotation, primary version switching, state transitions
- Pre-Flight Authorization - Checks permissions before cryptographic operations
- Deterministic Policy Evaluation - Uses IAM Emulator control plane for decisions
- Three Modes - Off (default), Permissive (fail-open), Strict (fail-closed)
- Production Semantics - Same permission names as real GCP (
cloudkms.cryptoKeys.encrypt) - Fail Like Production - Catch permission bugs in CI, not production
- No GCP Credentials - Works entirely offline without authentication
- Fast & Lightweight - In-memory storage, starts in milliseconds
- Docker Support - Pre-built containers (gRPC-only, REST-only, or dual)
- Thread-Safe - Concurrent access with proper synchronization
CreateKeyRing- Create new keyringsGetKeyRing- Retrieve keyring metadataListKeyRings- List all keyringsCreateCryptoKey- Create encryption/decryption keysGetCryptoKey- Retrieve key metadataListCryptoKeys- List all keys in a keyringUpdateCryptoKey- Update key metadata (labels)
CreateCryptoKeyVersion- Create new key versions for rotationGetCryptoKeyVersion- Get specific version detailsListCryptoKeyVersions- List all versions of a keyUpdateCryptoKeyPrimaryVersion- Switch to a different key versionUpdateCryptoKeyVersion- Update version state (enable/disable)DestroyCryptoKeyVersion- Schedule version for destruction
Encrypt- Encrypt data with a crypto key (AES-256-GCM)Decrypt- Decrypt data with a crypto key (works with any enabled version)
EC_SIGN_SECP256K1_SHA256— secp256k1 ECDSA signing viaAsymmetricSign(SHA-256 digest) andGetPublicKey(PEM)
# Create the keyring first, then:
curl -X POST "http://localhost:8080/v1/projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys?cryptoKeyId=secp256k1-signer" \
-H "Content-Type: application/json" \
-d '{
"purpose": "ASYMMETRIC_SIGN",
"versionTemplate": {
"algorithm": "EC_SIGN_SECP256K1_SHA256",
"protectionLevel": "SOFTWARE"
}
}'PENDING_GENERATION → ENABLED → DISABLED → DESTROY_SCHEDULED → DESTROYED
↑ ↓
└──────────┘
- Key lifecycle (RestoreCryptoKeyVersion)
- Asymmetric operations (AsymmetricSign, AsymmetricDecrypt, GetPublicKey)
- MAC operations (MacSign, MacVerify)
- Import/Export (ImportCryptoKeyVersion, CreateImportJob, etc.)
- Raw operations (RawEncrypt, RawDecrypt, Decapsulate)
- Random generation (GenerateRandomBytes)
Current coverage: 14 of ~26 methods (54%) - complete key management + lifecycle
Three server variants available:
| Variant | Protocols | Use Case | Install Command |
|---|---|---|---|
server |
gRPC only | SDK users, fastest startup | go install .../cmd/server@latest |
server-rest |
REST/HTTP | curl, scripts, any language | go install .../cmd/server-rest@latest |
server-dual |
Both gRPC + REST | Maximum flexibility | go install .../cmd/server-dual@latest |
# gRPC only (recommended for SDK users)
go install github.com/blackwell-systems/gcp-kms-emulator/cmd/server@latest
# REST API only
go install github.com/blackwell-systems/gcp-kms-emulator/cmd/server-rest@latest
# Both protocols
go install github.com/blackwell-systems/gcp-kms-emulator/cmd/server-dual@latestgRPC server:
# Start on default port 9090
server
# Custom port
server --port 8080REST server:
# Start on default ports (gRPC: 9090, HTTP: 8080)
server-rest
# Custom ports
server-rest --grpc-port 9090 --http-port 8080Dual protocol server:
# Start both protocols (gRPC: 9090, HTTP: 8080)
server-dual
# Custom ports
server-dual --grpc-port 9090 --http-port 8080package main
import (
"context"
"fmt"
kms "cloud.google.com/go/kms/apiv1"
"google.golang.org/api/option"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
ctx := context.Background()
// Connect to emulator instead of real GCP
conn, _ := grpc.NewClient(
"localhost:9090",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
client, _ := kms.NewKeyManagementClient(ctx, option.WithGRPCConn(conn))
defer client.Close()
// Use client normally - API is identical to real GCP
// ...
}Start REST server:
server-rest
# HTTP gateway listening at :8080Create a keyring:
curl -X POST "http://localhost:8080/v1/projects/my-project/locations/global/keyRings?keyRingId=my-keyring"Create a crypto key:
curl -X POST "http://localhost:8080/v1/projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys?cryptoKeyId=my-key" \
-H "Content-Type: application/json" \
-d '{"purpose":"ENCRYPT_DECRYPT"}'Encrypt data:
curl -X POST "http://localhost:8080/v1/projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys/my-key:encrypt" \
-H "Content-Type: application/json" \
-d '{"plaintext":"'$(echo -n "my-secret-data" | base64)'"}'Decrypt data:
curl -X POST "http://localhost:8080/v1/projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys/my-key:decrypt" \
-H "Content-Type: application/json" \
-d '{"ciphertext":"<base64-ciphertext>"}'REST API matches GCP's official REST endpoints - same paths, same JSON format, same behavior.
The KMS emulator supports optional permission checks using the GCP IAM Emulator.
Environment Variables:
IAM_MODE- Controls permission enforcement (default:off)off- No permission checks (legacy behavior)permissive- Check permissions, fail-open on connectivity errorsstrict- Check permissions, fail-closed on connectivity errors (for CI)
IAM_EMULATOR_HOST- IAM emulator address (default:localhost:8080)
Without IAM (default):
# No permission checks - all operations succeed
serverWith IAM (permissive mode):
# Start IAM emulator first
iam-emulator
# Start KMS with IAM checks (fail-open)
IAM_MODE=permissive IAM_EMULATOR_HOST=localhost:8080 serverWith IAM (strict mode for CI):
# All operations require valid permissions
IAM_MODE=strict IAM_EMULATOR_HOST=localhost:8080 serverSpecify the calling principal for permission checks:
gRPC:
ctx := metadata.AppendToOutgoingContext(ctx, "x-emulator-principal", "user:admin@example.com")
resp, err := client.CreateKeyRing(ctx, req)REST:
curl -H "X-Emulator-Principal: user:admin@example.com" \
-X POST "http://localhost:8080/v1/projects/my-project/locations/global/keyRings?keyRingId=my-keyring"KMS operations map to GCP IAM permissions:
| Operation | Permission | Resource |
|---|---|---|
| CreateKeyRing | cloudkms.keyRings.create |
Parent location |
| GetKeyRing | cloudkms.keyRings.get |
KeyRing |
| ListKeyRings | cloudkms.keyRings.list |
Parent location |
| CreateCryptoKey | cloudkms.cryptoKeys.create |
Parent keyring |
| GetCryptoKey | cloudkms.cryptoKeys.get |
CryptoKey |
| UpdateCryptoKey | cloudkms.cryptoKeys.update |
CryptoKey |
| ListCryptoKeys | cloudkms.cryptoKeys.list |
Parent keyring |
| Encrypt | cloudkms.cryptoKeys.encrypt |
CryptoKey |
| Decrypt | cloudkms.cryptoKeys.decrypt |
CryptoKey |
| CreateCryptoKeyVersion | cloudkms.cryptoKeyVersions.create |
Parent cryptokey |
| GetCryptoKeyVersion | cloudkms.cryptoKeyVersions.get |
CryptoKeyVersion |
| UpdateCryptoKeyVersion | cloudkms.cryptoKeyVersions.update |
CryptoKeyVersion |
| ListCryptoKeyVersions | cloudkms.cryptoKeyVersions.list |
Parent cryptokey |
| UpdateCryptoKeyPrimaryVersion | cloudkms.cryptoKeys.update |
CryptoKey |
| DestroyCryptoKeyVersion | cloudkms.cryptoKeyVersions.destroy |
CryptoKeyVersion |
| Scenario | off |
permissive |
strict |
|---|---|---|---|
| No IAM emulator | Allow | Allow | Deny |
| IAM unavailable | Allow | Allow | Deny |
| No principal | Allow | Deny | Deny |
| Permission denied | Allow | Deny | Deny |
Use off for local dev, permissive for integration tests, strict for CI.
IAM enforcement in this emulator is deliberately scoped for authorization testing, not comprehensive permission modeling. The underlying IAM emulator checks a small set of built-in roles (primitives + Secret Manager + KMS) plus unlimited custom role definitions to catch the bugs that actually break production: missing permissions, wrong role assignments, and unauthorized destructive operations (destroy, setIamPolicy, disable). This curated-first approach catches 95% of real-world authorization bugs while maintaining hermetic execution (no GCP credentials required), deterministic behavior (0ms propagation delay vs 1-60s in real GCP), and zero maintenance burden from tracking GCP's evolving role catalog. If you need to test additional permissions, define them explicitly in the IAM emulator's policy.yaml as custom roles — this explicit approach is simpler, more reliable, and avoids the catalog staleness problem that plagues comprehensive IAM emulation. We optimize for authorization failures that matter, not theoretical IAM completeness.
# Build all variants
make docker
# Or build individually
docker build --build-arg VARIANT=grpc -t kms-emulator:grpc . # gRPC only (default)
docker build --build-arg VARIANT=rest -t kms-emulator:rest . # REST only
docker build --build-arg VARIANT=dual -t kms-emulator:dual . # Both protocolsgRPC only:
docker run -p 9090:9090 gcp-kms-emulator:grpcREST only:
docker run -p 8080:8080 gcp-kms-emulator:restDual protocol:
docker run -p 9090:9090 -p 8080:8080 gcp-kms-emulator:dualGitHub Actions:
services:
gcp-kms:
image: gcp-kms-emulator:dual
ports:
- 9090:9090
- 8080:8080Docker Compose:
services:
gcp-kms:
image: gcp-kms-emulator:dual
ports:
- "9090:9090" # gRPC
- "8080:8080" # REST- Local Development - Test KMS encryption without cloud access
- CI/CD Pipelines - Fast integration tests without GCP credentials
- Unit Testing - Deterministic encryption behavior
- Security Testing - Validate encryption workflows
- Cost Reduction - Avoid GCP API charges during development
Maintained by Dayna Blackwell — founder of Blackwell Systems, building reference infrastructure for cloud-native development.
- GCP IAM Control Plane - CLI to orchestrate the Local IAM Control Plane (this emulator + IAM + others)
- GCP IAM Emulator - Policy engine (the brain) for IAM enforcement
- GCP Secret Manager Emulator - IAM-enforced Secret Manager data plane
- gcp-emulator-auth - Enforcement proxy library (the guard)
If you're using this KMS emulator — in CI, locally, or in a test harness — I'd love to hear how you're using it.
- What crypto bugs did you catch? (unauthorized encrypt/decrypt, key version issues, IAM policy problems)
- Are you using real cryptographic operations? (testing with actual key material, or mocking crypto)
- How are you managing keys? (versioning, rotation, destruction workflows)
- What's still friction? (missing algorithms, IAM integration complexity, key import limitations)
Open an issue, start a discussion, or reach out directly:
This helps shape the roadmap and ensures the project stays aligned with real-world needs.
Apache 2.0 - See LICENSE for details.