Cloud KMS
forze[kms-aws], forze[kms-gcp], and forze[kms-yc] each supply a
KeyManagementPort backed by a managed key service, so the key-encryption key
never leaves the KMS — reach for one when you want
envelope encryption without running Vault.
Install¶
uv add 'forze[kms-aws]'
uv add 'forze[kms-gcp]'
uv add 'forze[kms-yc]'
Wire it¶
Build the client and register its deps module — that publishes the client so the
lifecycle step can open it. CryptoDepsModule composes the keyring over the
adapter and registers KeyManagementDepKey itself:
from forze.application.contracts.crypto import KeyRef, StaticKeyDirectory
from forze.application.execution import CryptoDepsModule, DepsRegistry, LifecyclePlan
from forze_kms.aws import (
AwsKmsClient,
AwsKmsDepsModule,
AwsKmsKeyManagement,
awskms_lifecycle_step,
)
kms = AwsKmsClient()
deps = DepsRegistry.from_modules(
AwsKmsDepsModule(client=kms),
CryptoDepsModule(
kms=AwsKmsKeyManagement(client=kms),
directory=StaticKeyDirectory(KeyRef(key_id="alias/app-kek")),
),
)
lifecycle = LifecyclePlan.from_steps(awskms_lifecycle_step(region_name="eu-central-1"))
from forze.application.contracts.crypto import KeyRef, StaticKeyDirectory
from forze.application.execution import CryptoDepsModule, DepsRegistry, LifecyclePlan
from forze_kms.gcp import (
GcpKmsClient,
GcpKmsDepsModule,
GcpKmsKeyManagement,
gcpkms_lifecycle_step,
)
kms = GcpKmsClient()
key = "projects/acme/locations/europe-west1/keyRings/app/cryptoKeys/app-kek"
deps = DepsRegistry.from_modules(
GcpKmsDepsModule(client=kms),
CryptoDepsModule(
kms=GcpKmsKeyManagement(client=kms),
directory=StaticKeyDirectory(KeyRef(key_id=key)),
),
)
lifecycle = LifecyclePlan.from_steps(gcpkms_lifecycle_step())
from forze.application.contracts.crypto import KeyRef, StaticKeyDirectory
from forze.application.execution import CryptoDepsModule, DepsRegistry, LifecyclePlan
from forze_kms.yc import (
YcKmsClient,
YcKmsDepsModule,
YcKmsKeyManagement,
yckms_lifecycle_step,
)
kms = YcKmsClient()
deps = DepsRegistry.from_modules(
YcKmsDepsModule(client=kms),
CryptoDepsModule(
kms=YcKmsKeyManagement(client=kms),
directory=StaticKeyDirectory(KeyRef(key_id="abjq…")),
),
)
lifecycle = LifecyclePlan.from_steps(yckms_lifecycle_step())
Leave key_management unset on the KMS deps module: CryptoDepsModule already
registers the port, and registering it twice is a conflicting dependency.
Per-tenant keys¶
Give each tenant its own KEK, and create it when the tenant is onboarded. A provisioner resolves through the same directory the keyring encrypts through, so the provisioned key and the encrypt-path key can never drift:
from forze.application.contracts.crypto import TenantTemplateKeyDirectory
from forze_kms.aws import AwsKmsTenantProvisioner
# KMS mints the CMK id, so a tenant's key is addressed by a caller-chosen alias.
directory = TenantTemplateKeyDirectory(
template="alias/tenant-{tenant_id}",
default_key_id="alias/shared-kek",
)
provisioner = AwsKmsTenantProvisioner(client=kms, directory=directory)
from forze.application.contracts.crypto import TenantTemplateKeyDirectory
from forze_kms.gcp import GcpKmsTenantProvisioner
ring = "projects/acme/locations/europe-west1/keyRings/app"
directory = TenantTemplateKeyDirectory(
template=f"{ring}/cryptoKeys/tenant-{{tenant_id}}",
default_key_id=f"{ring}/cryptoKeys/shared-kek",
)
# The key ring is shared and long-lived; only the CryptoKey is per-tenant.
provisioner = GcpKmsTenantProvisioner(client=kms, directory=directory)
from forze_kms.yc import YcKmsKeyDirectory, YcKmsTenantProvisioner
# Yandex Cloud mints the key id, so a template cannot address a tenant's key —
# this directory looks it up by the name the provisioner creates.
directory = YcKmsKeyDirectory(client=kms, folder_id="b1g…", template="tenant-{tenant_id}")
provisioner = YcKmsTenantProvisioner(client=kms, directory=directory)
Pass the provisioner to TenancyDepsModule(tenant_provisioner=…) — alongside a
schema or bucket provisioner via CompositeTenantProvisioner — so onboarding a
tenant readies every backend at once. Provisioning is idempotent, so a retried
onboarding is safe.
What it provides¶
| Contract | Implementation | Dep key |
|---|---|---|
| Key management (envelope encryption) | AwsKmsKeyManagement · GcpKmsKeyManagement · YcKmsKeyManagement |
KeyManagementDepKey (registered by CryptoDepsModule) |
| Per-tenant KEK provisioning | AwsKmsTenantProvisioner · GcpKmsTenantProvisioner · YcKmsTenantProvisioner |
via TenantProvisionerPort |
| Key directory (Yandex Cloud only) | YcKmsKeyDirectory |
passed to CryptoDepsModule(directory=…) |
| Raw client | AwsKmsClient · GcpKmsClient · YcKmsClient |
AwsKmsClientDepKey · GcpKmsClientDepKey · YcKmsClientDepKey |
What a KeyRef.key_id names, per provider:
| Provider | Extra | key_id |
Credentials when unset |
|---|---|---|---|
| AWS | forze[kms-aws] |
a CMK id, ARN, or alias/<name> |
the botocore chain (env, profile, instance role) |
| Google Cloud | forze[kms-gcp] |
a CryptoKey resource name (projects/…/cryptoKeys/…) |
application-default credentials |
| Yandex Cloud | forze[kms-yc] |
a symmetric key id | the instance metadata service |
Notes¶
- Credentials are implicit by default — each lifecycle step falls back to its
platform's ambient chain (the table above). Pass them explicitly when you must:
access_key_id/secret_access_keyonawskms_lifecycle_step,credentialsongcpkms_lifecycle_step, andiam_token/oauth_token/service_account_keyonyckms_lifecycle_step. - Rotation is transparent. A wrapped data key is decryptable by the KMS without
being told which version sealed it, so rotating the KEK never orphans data: new
writes wrap under the new version while old ciphertext still decrypts. Nothing to
migrate, no re-encrypt sweep — see Searchable fields and rotation
for the one case that does need a re-index. Pointing a tenant at a
different
key_idis a migration, not a rotation — it needs a previous-key read overlap and a re-encrypt sweep; see Replacing a key. - Data-key length is
dek_byteson the adapter — 32 bytes (AES-256) by default, matching the keyring's AEAD; 16 selects AES-128. - Teardown is opt-in and never immediate.
deprovisiondoes nothing unless you setallow_deletion=True— destroying a KEK makes every value wrapped under it unrecoverable. Even then the platform protects you: AWS drops the alias and schedules the CMK for deletion afterpending_window_days(7–30, so you can cancel); Google Cloud cannot delete a CryptoKey at all, so the provisioner destroys its versions and the empty key resource remains; Yandex Cloud deletes the key outright. - Google Cloud KMS has no data-key API. The adapter mints the data key itself
from the framework's CSPRNG entropy seam and wraps it with
Encrypt; AWS and Yandex Cloud use their nativeGenerateDataKey. The envelope is identical either way. - The Yandex Cloud SDK is blocking. Calls are driven off the event loop, so a KMS round-trip never stalls the runtime.
- Each client needs its lifecycle step — the deps module only registers an already-constructed client; it doesn't open it.