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 up | Platform operator, at deploy time | Tenant administrator, or platform operator for a shared provider |
| Where | Environment variables and platform config (Compose), Helm values (Kubernetes) | Resources > KMS > Providers, or POST /api/platform/config/v1/tenants/{tenantId}/kms/resources |
| Owning tenant | The platform tenant | The tenant that creates it |
| Credential source | A deployment secret source: an environment variable on Compose, a Kubernetes Secret on Helm | A write-only value stored by the product (PRODUCT_MANAGED), or a reference into the tenant's own secret store (TENANT_SUPPLIED) |
| Reaches tenants | Offered to the listed tenants; each tenant enables it | Available to the owning tenant; a platform provider reaches tenants once the operator shares it |
| Changing it | Change the configuration and restart the platform service. The API refuses credential, reference, detach and retire requests; only its sharing can be adjusted | Admin 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.
| Deployment | Setting |
|---|---|
| Docker Compose | SECRET_MANAGEMENT_AUTHORITY_TENANT_POLICY_ALLOW_TENANT_MANAGED_PROVIDERS=true (the Compose default) |
| Helm | secretManagement.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.
| Key | Required | Meaning |
|---|---|---|
kind | Yes | AZURE_KEY_VAULT. A blank value switches the declaration off. |
display-name | Yes | Label shown in the console. It is fixed once the resource exists. |
vault-uri | Yes | HTTPS vault or Managed HSM URI, for example https://contoso-signing.vault.azure.net/. |
tenant-id | Yes | Microsoft Entra directory id. |
client-id | Yes | Application (client) id of the Entra app registration. |
hsm-type | Yes | KEYVAULT or MANAGED_HSM. |
application-id | No | Provider application label. Defaults to the provider id. This is not the Entra client id. |
credential.secret-id | Yes | Opaque id (sec_ followed by 16 to 128 letters, digits, _ or -) of the client secret in a deployment secret source. |
sharing.fulfillment | No | SHARED_INSTANCE (default): tenants use this provider and its credential. TEMPLATE: each tenant gets its own copy and must supply its own credential. |
sharing.tenants | No | * 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-default | No | true 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:
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:
| Key | Required | Meaning |
|---|---|---|
kind | Yes | AWS_KMS. A blank value switches the declaration off. |
display-name | Yes | Label shown in the console. It is fixed once the resource exists. |
region | Yes | AWS region of the KMS endpoint, for example eu-west-1. |
access-key-id | Yes | Access key id of the IAM identity, for example AKIAIOSFODNN7EXAMPLE. It identifies the key pair and is not secret. |
endpoint-url | No | HTTPS endpoint override, for example a VPC endpoint. Defaults to the regional KMS endpoint. |
application-id | No | Provider application label. Defaults to the provider id. |
credential.secret-id | Yes | Opaque id of the secret access key in a deployment secret source. |
sharing.* | No | Same 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:
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.
Related
KMS container, Secrets and KMS providers, KMS providers reference, Platform Config API, KMS API