Skip to main content
Version: v0.25.0 (Latest)

Cloud KMS providers

Every tenant starts with the software KMS provider default, which tenant onboarding creates. Keys that must live in Azure Key Vault, Azure Managed HSM or AWS KMS need a cloud provider next to it. There are two ways to get one. They produce the same runtime provider, but ownership, how the credential is supplied, and how the provider is changed afterwards all differ.

A managed provider is declared by the operator in the deployment configuration, through environment variables on Docker Compose or Helm values on Kubernetes. The platform creates it at startup in the platform tenant and offers it to the tenants you name. Its coordinates and credential never pass through a browser or an API call, and the API refuses to change it. Use this when the operator runs the vault and the provider belongs to the installation.

A tenant-defined provider is created at runtime through the Admin Console or the Platform Config API, either by a tenant administrator for their own tenant or by a platform operator on the platform tenant, who can then share it. Use this when a tenant brings its own vault, or when a platform provider has to be added without redeploying.

Managed (deployment-declared)Tenant-defined (Admin Console or API)
Who sets it upPlatform operator, at deploy timeTenant administrator, or platform operator for a shared provider
WhereEnvironment variables and platform config (Compose), Helm values (Kubernetes)Resources > KMS > Providers, or POST /api/platform/config/v1/tenants/{tenantId}/kms/resources
Owning tenantThe platform tenantThe tenant that creates it
Credential sourceA deployment secret source: an environment variable on Compose, a Kubernetes Secret on HelmA write-only value stored by the product (PRODUCT_MANAGED), or a reference into the tenant's own secret store (TENANT_SUPPLIED)
Reaches tenantsOffered to the listed tenants; each tenant enables itAvailable to the owning tenant; a platform provider reaches tenants once the operator shares it
Changing itChange the configuration and restart the platform service. The API refuses credential, reference, detach and retire requests; only its sharing can be adjustedAdmin Console or API, with conditional writes on resourceVersion

Both paths need the same switch. secret-management.authority.tenant-policy.allow-tenant-managed-providers publishes the Azure Key Vault and AWS KMS offerings. While it is false, only the software offering exists: a declared provider stops the platform at startup, and Add KMS offers no cloud technology.

DeploymentSetting
Docker ComposeSECRET_MANAGEMENT_AUTHORITY_TENANT_POLICY_ALLOW_TENANT_MANAGED_PROVIDERS=true (the Compose default)
HelmsecretManagement.authority.allowTenantManagedProviders: true (the chart default is false)

Three meanings of "managed"​

The word appears in three places that are easy to mix up.

A managed provider, as used on this page, is one the deployment declares. The resource records it as declaredByDeployment: true.

Credential ownership is a property of every cloud KMS resource. PRODUCT_MANAGED means the product stores the credential, either from a deployment secret source or from a one-time write-only value. TENANT_SUPPLIED means the resource holds only a reference into the tenant's own secret store. A managed provider is always PRODUCT_MANAGED. A tenant-defined provider can be either.

Key lifecycle is a property of each key and certificate reference, not of the provider. PLATFORM_MANAGED keys were generated or imported through EDK, and deleting one deletes the provider object. EXTERNALLY_MANAGED keys were registered by reference, and deleting one only removes the EDK reference. A managed provider can hold externally managed keys, and a tenant-defined provider can hold platform-managed ones.

Managed Azure Key Vault provider​

Prepare Azure​

Create or pick the vault or Managed HSM, and a Microsoft Entra application registration with a client secret. Collect the vault URI, the Entra directory (tenant) id, and the application's client id. Give the application a data-plane role on the vault that covers what EDK will do: Key Vault Crypto User is enough to sign with keys that already exist, and Key Vault Crypto Officer is needed if EDK generates keys. Missing roles show up as a failed validation or a failed signature, not as a configuration error at startup.

The provider authenticates with the client secret. Managed identity and workload identity are not supported.

The platform service reaches the vault over HTTPS on port 443. If the vault uses a private endpoint, add the vault host and its private address range to the secret-management egress allowlist (secretManagement.egress.privateEndpointAllowlist in Helm).

Each Azure key or certificate a tenant uses through a shared provider must carry the tag sphereon-tenant-id=<tenant id>. Sharing the provider does not give every tenant every key in the vault. See Azure KMS, BYOK, and BYOC for how the tag is checked.

Configuration keys​

A declared provider lives under secret-management.authority.platform-providers.<providerId> in the platform service configuration. The <providerId> segment is the permanent runtime provider id that applications send when they sign. It must be 3 to 64 characters of lowercase letters, digits and hyphens, starting with a letter.

KeyRequiredMeaning
kindYesAZURE_KEY_VAULT. A blank value switches the declaration off.
display-nameYesLabel shown in the console. It is fixed once the resource exists.
vault-uriYesHTTPS vault or Managed HSM URI, for example https://contoso-signing.vault.azure.net/.
tenant-idYesMicrosoft Entra directory id.
client-idYesApplication (client) id of the Entra app registration.
hsm-typeYesKEYVAULT or MANAGED_HSM.
application-idNoProvider application label. Defaults to the provider id. This is not the Entra client id.
credential.secret-idYesOpaque id (sec_ followed by 16 to 128 letters, digits, _ or -) of the client secret in a deployment secret source.
sharing.fulfillmentNoSHARED_INSTANCE (default): tenants use this provider and its credential. TEMPLATE: each tenant gets its own copy and must supply its own credential.
sharing.tenantsNo* for every customer tenant, including tenants created later, or a comma-separated list of tenant ids. When absent, the provider is offered to nobody until an operator shares it.
sharing.suggested-defaultNotrue makes this the tenant's default provider while the tenant has made no choice of its own. Default false.

Values must be literal after interpolation. ${env:...} placeholders resolve normally. A value that still contains ${ after resolution, including a ${secret:...} reference, fails startup, and so does a client secret placed directly in the configuration.

Docker Compose​

The Compose bundle in the deployment repository already contains a declaration with the provider id azure-shared-signing, wired to environment variables and switched off by default. To switch it on, set these variables in the Compose environment (.env or your shell) and start or recreate the platform service.

SECRET_MANAGEMENT_AUTHORITY_TENANT_POLICY_ALLOW_TENANT_MANAGED_PROVIDERS=true
EDK_PLATFORM_KMS_AZURE_KIND=AZURE_KEY_VAULT
EDK_PLATFORM_KMS_AZURE_DISPLAY_NAME="Platform Azure Key Vault"
EDK_PLATFORM_KMS_AZURE_VAULT_URI=https://contoso-signing.vault.azure.net/
EDK_PLATFORM_KMS_AZURE_TENANT_ID=<entra-directory-id>
EDK_PLATFORM_KMS_AZURE_CLIENT_ID=<entra-application-client-id>
EDK_PLATFORM_KMS_AZURE_CLIENT_SECRET=<entra-client-secret>
EDK_PLATFORM_KMS_AZURE_HSM_TYPE=KEYVAULT
EDK_PLATFORM_KMS_AZURE_SHARED_TENANTS=*
EDK_SECRET_MANAGEMENT_ENVIRONMENT_MANIFEST=./config/secret-management-environment.azure.manifest
EDK_SECRET_MANAGEMENT_ENVIRONMENT_MANIFEST_SHA256=sha256:69e4ebaa9ac18cc7f8ffd2d3796e904204d8d9d81388d096b091ce0177248a86

The client secret travels through the environment deployment source rather than through configuration. That source reads a manifest file that maps opaque secret ids to environment variable names, and the declaration's credential.secret-id must appear in it. The bundled Azure manifest is the standard manifest plus one line:

sec_platform_azure_kms_client_secret_01=EDK_PLATFORM_KMS_AZURE_CLIENT_SECRET

The platform verifies the mounted manifest against EDK_SECRET_MANAGEMENT_ENVIRONMENT_MANIFEST_SHA256 and refuses to start on a mismatch. The digest above belongs to the bundled Azure manifest. If you add secret ids of your own, compute a new digest with the script under Manifest digests.

To declare a second provider, or to use a provider id other than azure-shared-signing, add another block under secret-management.authority.platform-providers in the platform configuration file, following the same pattern with your own ${env:...} variables and secret id, and add that secret id to the manifest.

Kubernetes with Helm​

The chart renders secretManagement.authority.platformProviders into the platform configuration. On Kubernetes the client secret comes from a Kubernetes Secret through the chart's Kubernetes mount source, so no secret value appears in the values file.

Create the Secret in the release namespace with your secret tooling. The key name is up to you.

kubectl create secret generic edk-platform-kms-credentials \
--namespace edk \
--from-literal=azure-client-secret='<entra-client-secret>'

Then declare the provider and map its secret id to that key:

secretManagement:
authority:
allowTenantManagedProviders: true
platformProviders:
azure-shared-signing:
kind: AZURE_KEY_VAULT
displayName: Platform Azure Key Vault
vaultUri: https://contoso-signing.vault.azure.net/
tenantId: <entra-directory-id>
clientId: <entra-application-client-id>
hsmType: KEYVAULT
credentialSecretId: sec_platform_azure_kms_client_secret_01
sharing:
fulfillment: SHARED_INSTANCE
tenants: "*"
suggestedDefault: false
kubernetesMount:
enabled: true
existingSecret: edk-platform-kms-credentials
manifest:
sec_platform_azure_kms_client_secret_01: azure-client-secret
manifestSha256: sha256:fcd51ea870bcc566aba7ab126c1709465fc84d9699177bf20728bfab5dc534d7

manifestSha256 covers the secret ids and the Secret key names, not the secret values, so the digest above is correct for exactly this mapping. Recompute it whenever you add an entry or rename a key. The chart refuses to render when a declared provider's credentialSecretId is missing from kubernetesMount.manifest, when allowTenantManagedProviders is false, or when the topology is not distributed.

What happens at startup​

On every start the platform service compares the declarations with what the platform tenant holds.

A new declaration creates a resource owned by the platform tenant, binds its credential from the deployment source, and offers the provider according to sharing. When the declaration already matches the stored resource, nothing is written. Changing the vault URI, directory id, client id or HSM type rewrites the stored coordinates on the next start.

Startup stops with an error in three cases: the credential's secret id is not in any deployment source manifest, the Azure offering is not published because allow-tenant-managed-providers is false, or a provider with the same id already exists that was created through the API rather than declared. In the last case retire that resource or pick another provider id.

The tenant list is merged with what is already stored. A tenant you added to the offer by hand stays on it, and a tenant you withdrew stays withdrawn even when sharing.tenants is *. fulfillment and suggested-default always follow the declaration.

A request that would change a declared provider's credential or coordinates, or detach or retire it, fails with 409 and error code INVALID_STATE, and the message names the configuration key to change instead. An API change would be overwritten on the next start, so the configuration is the only place such a change holds.

Removing a declaration from the configuration leaves the resource in place, because keys may still reference it. To stop tenants using it, withdraw the offer by writing an empty tenant list with setKmsResourceSharing, after the tenants have moved their default and their keys to another provider.

Tenants enable the offer​

An offered provider does not appear in a tenant's runtime on its own. The tenant enables it, either on Resources > KMS > Providers or through the API, and can then make it the default for new keys:

Loading example...
Loading example...

After that the tenant generates or registers keys against the provider id through the tenant KMS API, exactly as for any other provider. A SHARED_INSTANCE provider asks the tenant for no credential, and its configuration stays hidden from the tenant.

Rotate the client secret​

Create the new client secret in Entra, put the new value in the environment variable or the Kubernetes Secret, and restart the platform service. The secret id and the manifest stay the same, so the digest does not change. Remove the old Entra secret only after a signature with the new one has succeeded.

Managed AWS KMS provider​

Prepare AWS​

Create an IAM user, or pick an existing one, and an access key for it. The provider signs every AWS request with that access key id and its secret access key. Instance roles, IAM roles for service accounts and the default credential chain are not used.

Grant the identity the KMS actions EDK will perform. Signing with keys that already exist needs kms:Sign, kms:Verify, kms:GetPublicKey and kms:DescribeKey, plus kms:ListResourceTags when the provider is shared, because tenant ownership is read from key tags. Letting EDK generate keys adds kms:CreateKey and kms:CreateAlias, deleting generated keys adds kms:ScheduleKeyDeletion and kms:DeleteAlias, and encryption uses kms:Encrypt and kms:Decrypt. Scope the policy to the keys or aliases the installation owns.

Each existing AWS key a tenant registers through a shared provider must carry the tag sphereon-tenant-id=<tenant id>. AWS KMS has no certificate objects, so a certificate chain for an AWS key is registered as stored public material.

Configuration keys​

The declaration uses the same block under secret-management.authority.platform-providers.<providerId> as Azure, with the AWS fields:

KeyRequiredMeaning
kindYesAWS_KMS. A blank value switches the declaration off.
display-nameYesLabel shown in the console. It is fixed once the resource exists.
regionYesAWS region of the KMS endpoint, for example eu-west-1.
access-key-idYesAccess key id of the IAM identity, for example AKIAIOSFODNN7EXAMPLE. It identifies the key pair and is not secret.
endpoint-urlNoHTTPS endpoint override, for example a VPC endpoint. Defaults to the regional KMS endpoint.
application-idNoProvider application label. Defaults to the provider id.
credential.secret-idYesOpaque id of the secret access key in a deployment secret source.
sharing.*NoSame as for Azure: fulfillment, tenants, suggested-default.

Docker Compose​

The Compose bundle carries a second, switched-off declaration with the provider id aws-shared-signing. Set these variables and start or recreate the platform service:

SECRET_MANAGEMENT_AUTHORITY_TENANT_POLICY_ALLOW_TENANT_MANAGED_PROVIDERS=true
EDK_PLATFORM_KMS_AWS_KIND=AWS_KMS
EDK_PLATFORM_KMS_AWS_DISPLAY_NAME="Platform AWS KMS"
EDK_PLATFORM_KMS_AWS_REGION=eu-west-1
EDK_PLATFORM_KMS_AWS_ACCESS_KEY_ID=<aws-access-key-id>
EDK_PLATFORM_KMS_AWS_SECRET_ACCESS_KEY=<aws-secret-access-key>
EDK_PLATFORM_KMS_AWS_SHARED_TENANTS=*
EDK_SECRET_MANAGEMENT_ENVIRONMENT_MANIFEST=./config/secret-management-environment.aws.manifest
EDK_SECRET_MANAGEMENT_ENVIRONMENT_MANIFEST_SHA256=sha256:6b78f7ffed1dcdd91136ab4a4f708dcc851ac09c0ec0fefd4ddfd67057b20051

The bundled AWS manifest is the standard manifest plus sec_platform_aws_kms_secret_access_key_01=EDK_PLATFORM_KMS_AWS_SECRET_ACCESS_KEY, and the digest above belongs to it. To run the Azure and AWS declarations together, mount one manifest that contains both lines and compute its digest with the script under Manifest digests.

Kubernetes with Helm​

Store the secret access key in a Kubernetes Secret, then declare the provider:

kubectl create secret generic edk-platform-kms-credentials \
--namespace edk \
--from-literal=aws-secret-access-key='<aws-secret-access-key>'
secretManagement:
authority:
allowTenantManagedProviders: true
platformProviders:
aws-shared-signing:
kind: AWS_KMS
displayName: Platform AWS KMS
region: eu-west-1
accessKeyId: <aws-access-key-id>
credentialSecretId: sec_platform_aws_kms_secret_access_key_01
sharing:
fulfillment: SHARED_INSTANCE
tenants: "*"
kubernetesMount:
enabled: true
existingSecret: edk-platform-kms-credentials
manifest:
sec_platform_aws_kms_secret_access_key_01: aws-secret-access-key
manifestSha256: sha256:<digest of this manifest>

Compute manifestSha256 with the script under Manifest digests in kubernetes-mount mode. When Azure and AWS share one Secret, list both secret ids in the same manifest and hash them together.

Startup, the tenant enable step and the 409 behaviour are the same as for Azure. To rotate, create a new access key for the IAM identity, update both access-key-id and the secret value, restart the platform service, and deactivate the old key once a signature with the new one has succeeded. Changing access-key-id rewrites the stored coordinates on that start.

Tenant-defined providers in the Admin Console or API​

A tenant administrator opens Resources > KMS > Providers and chooses Add KMS. The wizard asks for the technology, the credential ownership, the permanent provider id, a display name, and the technology fields: vault URI, directory id, client id and HSM type for Azure, or region, access key id and an optional endpoint for AWS. It then writes the credential (the Azure client secret or the AWS secret access key), validates the connection and shows a review.

The same sequence through the Platform Config API is create, attach the credential, validate:

Loading example...
Loading example...
Loading example...
Loading example...

For AWS the create request's configuration carries region, accessKeyId and optionally endpointUrl; the secret access key goes only into the credential request.

Credential ownership decides the attach request. With PRODUCT_MANAGED you send the secret once, the product stores it, and it is never returned. With TENANT_SUPPLIED you send only a reference into the tenant's own secret store together with its provider assignment id, and never the value.

A platform operator can do the same on the platform tenant and then share the resource with SHARED_INSTANCE or TEMPLATE. Azure KMS, BYOK, and BYOC walks through that lifecycle, key and certificate registration, and removal.

Environment variable names​

Any configuration key can also be set as an environment variable. The name is the key in upper case with every . and - replaced by _. Single and double underscores are treated the same, so SECRET_MANAGEMENT_AUTHORITY_TENANT_POLICY_ALLOW_TENANT_MANAGED_PROVIDERS and SECRET_MANAGEMENT__AUTHORITY__TENANT_POLICY__ALLOW_TENANT_MANAGED_PROVIDERS set the same key.

Provider declarations are the exception. Their provider id is part of the key path, and an environment variable name cannot preserve the hyphens in an id such as azure-shared-signing. Declare providers in the platform configuration file or in Helm values, and use ${env:NAME} placeholders for the values you want to supply from the environment, as the Compose bundle does.

Manifest digests​

Both deployment secret sources check their manifest against a SHA-256 digest before the platform starts. The digest is computed over the entries sorted by secret id. For each entry it hashes the secret id, a zero byte, and the SHA-256 of a per-source value: an empty string for the environment source, and the Secret key name for the Kubernetes mount source. Secret values are never part of it.

This script prints the digest for either source:

import hashlib, sys

mode, path = sys.argv[1], sys.argv[2] # mode: environment or kubernetes-mount
entries = {}
for line in open(path, encoding="utf-8"):
line = line.strip()
if line and not line.startswith("#"):
secret_id, value = line.split("=", 1)
entries[secret_id.strip()] = value.strip()

digest = hashlib.sha256()
for secret_id in sorted(entries):
value = b"" if mode == "environment" else entries[secret_id].encode("utf-8")
digest.update(secret_id.encode("utf-8") + b"\0" + hashlib.sha256(value).digest())
print("sha256:" + digest.hexdigest())

For the Kubernetes mount source, write the kubernetesMount.manifest map to a file first, one secret-id=secret-key line per entry.

KMS container, Secrets and KMS providers, KMS providers reference, Platform Config API, KMS API