A lightweight, tenant-aware Key Management Service built in Go. miniKMS provides envelope encryption, a 3-level PKI hierarchy, and tamper-evident audit logging over gRPC.
Traditional secret management tools like HashiCorp Vault handle storage but don't provide fine-grained, per-tenant encryption key isolation. miniKMS fills this gap by offering:
- Per-tenant key derivation — each tenant gets an isolated master key derived via HKDF, with no shared key material between tenants
- Per-scope DEKs — within a tenant, each scope (application, service, environment) gets its own data encryption key
- Envelope encryption — data is encrypted with a DEK, and the DEK is encrypted with the tenant's master key, enabling key rotation without re-encrypting all data
- Zero plaintext at rest — all DEKs are stored encrypted; the root key is held only in memory
- Secret management backends — encrypt secrets before storing them in Vault, databases, or object storage
- Multi-tenant SaaS — isolate encryption keys per customer/organization with automatic key provisioning
- BYOK augmentation — layer server-side KMS encryption on top of client-side (BYOK/PKI) encryption for defense-in-depth
- Certificate management — issue and manage certificates with a built-in 3-level PKI hierarchy
- Compliance & audit — hash-chained audit logs provide tamper-evident proof of all cryptographic operations
Root Key (256-bit, loaded from env at startup)
└─ Tenant Master Key (derived via HKDF-SHA256 per tenant_id)
└─ Scope DEK (random AES-256 key, encrypted with tenant master key)
└─ Data (encrypted with scope DEK using AES-256-GCM)
- Root Key: A 256-bit hex-encoded key provided via
MINIKMS_ROOT_KEYenv var. Never persisted to disk. - Tenant Master Key: Deterministically derived from the root key using HKDF with configurable salt. No database storage needed.
- Scope DEK: Random AES-256 key generated per scope. Stored encrypted (wrapped by tenant master key) in PostgreSQL with version tracking.
graph TD
RK[Root Key<br/>256-bit, in-memory only] -->|HKDF-SHA256| TMK[Tenant Master Key<br/>per org_id, deterministic]
TMK -->|AES-256-GCM wrap| DEK[Scope DEK<br/>per app_id, stored encrypted]
DEK -->|AES-256-GCM + AAD| ED[Encrypted Data]
RK -->|Shamir 3-of-5| KES[Key Escrow Shares<br/>disaster recovery]
style RK fill:#e74c3c,color:#fff
style TMK fill:#e67e22,color:#fff
style DEK fill:#2ecc71,color:#fff
style ED fill:#3498db,color:#fff
style KES fill:#9b59b6,color:#fff
graph TD
ROOT[Root CA<br/>P-384, self-signed<br/>MaxPathLen: 1] -->|signs| ORG[Org Intermediate CA<br/>P-384, MaxPathLen: 0<br/>OID: org_id]
ORG -->|signs| MEMBER[Member Certificate<br/>P-256, IsCA: false<br/>ExtKeyUsage: ClientAuth<br/>OID: member_id, org_id, role]
ORG -->|issues| CRL[CRL<br/>Certificate Revocation List]
CRL -.->|revokes| MEMBER
style ROOT fill:#e74c3c,color:#fff
style ORG fill:#e67e22,color:#fff
style MEMBER fill:#2ecc71,color:#fff
style CRL fill:#95a5a6,color:#fff
sequenceDiagram
participant C as Client
participant KMS as KMSService
participant DEK as AppDEKManager
participant DB as PostgreSQL
participant OKM as OrgKeyManager
participant RKH as RootKeyHolder
participant Crypto as crypto
C->>KMS: Encrypt(tenant_id, scope_id, plaintext, aad)
KMS->>DEK: GetOrCreateDEK(tenant_id, scope_id)
DEK->>DB: GetActiveKeyVersion()
alt No active DEK
DEK->>Crypto: GenerateKey()
DEK->>OKM: DeriveOrgKey(tenant_id)
OKM->>RKH: GetKey()
OKM->>Crypto: DeriveOrgMasterKey(rootKey, orgID)
DEK->>Crypto: Encrypt(orgKey, dek, aad)
DEK->>DB: CreateKeyVersion(encryptedDEK)
end
DEK-->>KMS: plaintext DEK, keyVersionID
KMS->>Crypto: Encrypt(dek, plaintext, aad)
KMS->>DEK: IncrementAndCheckRotation(keyVersionID)
KMS-->>C: base64(ciphertext), keyVersionID
| Service | Description |
|---|---|
KMSService |
Encrypt, Decrypt, BatchEncrypt, BatchDecrypt — core envelope encryption |
PKIService |
3-level certificate hierarchy: Root CA → Org Intermediate CA → Member Certificate |
AuditService |
Hash-chained tamper-evident audit log queries |
| Primitive | Usage |
|---|---|
| AES-256-GCM | Data encryption (DEK → ciphertext) and DEK wrapping |
| HKDF-SHA256 | Root key → tenant master key derivation |
| Shamir Secret Sharing | Key escrow for disaster recovery (GF(256) arithmetic) |
| X.509 / ECDSA | PKI certificate issuance and validation |
All configuration is via environment variables:
| Variable | Required | Default | Description |
|---|---|---|---|
MINIKMS_ROOT_KEY |
Conditional | — | 256-bit hex-encoded root encryption key |
MINIKMS_ROOT_KEY_FILE |
Conditional | — | Path to a mounted 256-bit hex-encoded root key; recommended for production |
MINIKMS_ROOT_CA_CERT |
Conditional | — | Shared Root CA certificate PEM |
MINIKMS_ROOT_CA_CERT_FILE |
Conditional | — | Path to the shared Root CA certificate PEM; recommended for production |
MINIKMS_ROOT_CA_KEY |
Conditional | — | Shared P-384 Root CA private key PEM |
MINIKMS_ROOT_CA_KEY_FILE |
Conditional | — | Path to the shared P-384 Root CA private key PEM; recommended for production |
MINIKMS_SESSION_SIGNING_KEY |
Conditional | — | P-256 private key in PKCS#8 or SEC1 PEM format |
MINIKMS_SESSION_SIGNING_KEY_FILE |
Conditional | — | Path to a mounted P-256 PEM private key; recommended for production |
MINIKMS_TLS_CA_FILE |
Required if TLS | — | Client CA PEM. Required whenever MINIKMS_TLS_ENABLED=true (mTLS) |
MINIKMS_DB_URL |
Yes | — | PostgreSQL connection string |
MINIKMS_REDIS_URL |
Yes | — | Redis connection string (DEK caching) |
MINIKMS_GRPC_ADDR |
No | 0.0.0.0:50051 |
gRPC listen address |
MINIKMS_HKDF_SALT |
No | envsync-minikms-v1 |
HKDF salt for key derivation |
MINIKMS_TLS_ENABLED |
No | false |
Enable TLS for gRPC |
MINIKMS_TLS_CERT_FILE |
No | — | TLS certificate file path |
MINIKMS_TLS_KEY_FILE |
No | — | TLS private key file path |
Exactly one of MINIKMS_SESSION_SIGNING_KEY and
MINIKMS_SESSION_SIGNING_KEY_FILE must be configured. All miniKMS replicas must
use the same key so that sessions remain valid across restarts and can be
verified by every replica.
Exactly one value or file source must also be configured for the root encryption key, Root CA certificate, and Root CA private key. Every replica must receive the same material. See the Kubernetes HA deployment guide.
- Go 1.24+
- PostgreSQL 15+
- Redis 7+
Run the migration to create the required tables:
psql $MINIKMS_DB_URL -f migrations/001_initial_schema.sql
psql $MINIKMS_DB_URL -f migrations/002_vault_storage.sql
psql $MINIKMS_DB_URL -f migrations/003_escrow_recovery.sql
psql $MINIKMS_DB_URL -f migrations/004_multi_replica_ha.sqlThis creates the key, token, certificate, revocation, audit, vault, and versioned
key_escrow_sets / key_escrow_shares storage used by miniKMS.
For root and Org CA disaster recovery, enable the audited Shamir escrow flow and
follow the escrow recovery runbook. The
minikms-escrow admin CLI exports owner-only custodian packages, enforces the
configured K-of-N threshold, and creates a fresh share generation after every
successful recovery.
# Set required env vars
export MINIKMS_ROOT_KEY="$(openssl rand -hex 32)"
openssl ecparam -name secp384r1 -genkey -noout -out ./root-ca-key.pem
openssl req -new -x509 -key ./root-ca-key.pem -sha384 -days 3650 \
-subj "/CN=miniKMS Root CA/O=EnvSync" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:1" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-out ./root-ca-cert.pem
chmod 600 ./root-ca-key.pem
export MINIKMS_ROOT_CA_CERT_FILE="./root-ca-cert.pem"
export MINIKMS_ROOT_CA_KEY_FILE="./root-ca-key.pem"
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ./session-signing-key.pem
chmod 600 ./session-signing-key.pem
export MINIKMS_SESSION_SIGNING_KEY_FILE="./session-signing-key.pem"
export MINIKMS_DB_URL="postgres://user:pass@localhost:5432/minikms?sslmode=disable"
export MINIKMS_REDIS_URL="redis://localhost:6379/0"
# Build and run
make build
make runThe session signing key is a long-lived ES256 private key. miniKMS does not generate an ephemeral fallback: startup fails if the key is missing, invalid, not P-256, or configured through both supported sources.
Generate the key once:
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \
-out session-signing-key.pem
chmod 600 session-signing-key.pemStore the PEM in a secret manager and mount it read-only into each miniKMS replica. For example, a container can use:
services:
minikms:
environment:
MINIKMS_SESSION_SIGNING_KEY_FILE: /run/secrets/minikms-session-signing-key
secrets:
- minikms-session-signing-key
secrets:
minikms-session-signing-key:
file: ./session-signing-key.pemKubernetes deployments should mount the same Secret into every pod and point
MINIKMS_SESSION_SIGNING_KEY_FILE at the mounted file. Do not commit the key,
include it in images, or print it in logs. Back up the secret securely: losing
or replacing it invalidates all active sessions.
Direct PEM injection through MINIKMS_SESSION_SIGNING_KEY is supported for
secret-injection platforms that provide environment values, but a mounted file
avoids exposing the private key through environment inspection.
Signing-key rotation is not automatic. Replacing the configured key immediately
invalidates existing sessions. A future zero-downtime rotation mechanism should
add JWT kid headers and retain the previous verification key for at least the
maximum session lifetime.
# Build the image
make docker-build
# Run with docker-compose (includes PostgreSQL and Redis)
docker compose up minikmsThe Dockerfile uses a multi-stage build (Go 1.24 builder → Alpine 3.21 runtime) and exposes port 50051.
miniKMS is included in the project's docker-compose.yaml:
minikms:
build:
context: .
dockerfile: Dockerfile
ports:
- "50051:50051"
environment:
MINIKMS_DB_URL: postgres://user:pass@postgres:5432/minikms?sslmode=disable
MINIKMS_REDIS_URL: redis://redis:6379/1
MINIKMS_ROOT_KEY: ${MINIKMS_ROOT_KEY}
MINIKMS_SESSION_SIGNING_KEY_FILE: /run/secrets/minikms-session-signing-key
MINIKMS_GRPC_ADDR: 0.0.0.0:50051
depends_on:
- postgres
- redisEncrypt a plaintext value with the scope's DEK. Automatically provisions a DEK if one doesn't exist for the tenant/scope pair.
rpc Encrypt(EncryptRequest) returns (EncryptResponse);
message EncryptRequest {
string tenant_id = 1; // Tenant identifier
string scope_id = 2; // Scope/namespace identifier
bytes plaintext = 3;
string aad = 4; // Additional Authenticated Data
}
message EncryptResponse {
bytes ciphertext = 1;
string key_version_id = 2;
}rpc Decrypt(DecryptRequest) returns (DecryptResponse);
message DecryptRequest {
string tenant_id = 1;
string scope_id = 2;
bytes ciphertext = 3;
string aad = 4;
string key_version_id = 5;
}Encrypt or decrypt multiple items in a single call for better throughput.
3-level certificate hierarchy:
- Root CA — self-signed, long-lived
- Org Intermediate CA — per-tenant, signed by Root CA
- Member Certificate — per-user/service, signed by Org CA
Query hash-chained audit log entries. Each entry contains:
entry_hash = SHA256(previous_hash || timestamp || action || actor_id || details)
This provides tamper-evidence: modifying any historical entry breaks the hash chain.
# Build
make build
# Run tests
make test
# Regenerate protobuf code
make proto
# Build Docker image
make docker-buildminikms/
├── api/proto/minikms/v1/ # Protobuf service definitions
│ ├── kms.proto # KMS encrypt/decrypt service
│ ├── pki.proto # PKI certificate service
│ └── audit.proto # Audit log service
├── cmd/minikms/ # Application entrypoint
│ └── main.go
├── internal/
│ ├── config/ # Environment-based configuration
│ ├── crypto/ # Cryptographic primitives
│ │ ├── aes256gcm.go # AES-256-GCM encrypt/decrypt
│ │ ├── hkdf.go # HKDF key derivation
│ │ └── shamir.go # Shamir Secret Sharing (GF(256))
│ ├── keys/ # Key management
│ │ ├── root_key.go # Singleton root key holder
│ │ ├── org_key.go # HKDF-derived tenant master keys
│ │ └── app_dek.go # Per-scope DEK lifecycle
│ ├── service/ # gRPC service implementations
│ │ ├── kms_service.go # KMS encrypt/decrypt handlers
│ │ └── key_service.go # Key provisioning handlers
│ └── store/ # Data access layer
│ └── postgres.go # PostgreSQL key_versions queries
├── migrations/
│ └── 001_initial_schema.sql
├── Dockerfile
├── Makefile
└── go.mod
- The root key is the most sensitive piece of data. It should be injected via a secure mechanism (e.g., Kubernetes secrets, cloud KMS unsealing) and never written to disk or logs.
- HKDF salt should be consistent across deployments. Changing it will make existing tenant master keys underivable.
- Key rotation: New DEK versions can be created per scope. Old versions remain available for decryption via
key_version_id. Encrypt operations always use the latest version. - Shamir Secret Sharing enables splitting the root key into N shares with a threshold of K for disaster recovery.