External KMS (KMIP) Configuration
This guide explains how to connect your DS3 Gateway to an external KMS using the KMIP protocol (1.4). It covers the configuration that the Kubernetes gateway operator (gateway-operator) applies to enable per-tenant encryption with your own KMS in production.
External KMS encryption is currently experimental. When enabled, objects uploaded to the Swarm are encrypted with keys stored in the KMS. Once encrypted:
- All Gateways that serve the same tenant must share the same KMS configuration (same KMIP server, same tenant key seeds). A Gateway without the correct keys cannot decrypt objects uploaded by another Gateway.
- If you decommission a Gateway or migrate to a new one, the new Gateway must be configured with the same KMS and the same tenant keys — otherwise existing objects become unreadable.
- If you lose access to the KMS or misconfigure any Gateway, objects encrypted under this mode become permanently unreadable.
Do not enable this feature unless you have a robust KMS backup and disaster recovery plan in place, and all Gateways serving the tenant are configured identically.
Encryption Modes Overview
The DS3 Gateway encrypts objects before uploading them to the Swarm. Three key-source modes are available:
| Mode | Description | Use case |
|---|---|---|
single-key | One encryption key for all tenants | Dev, test, simple deployments |
local-multi-keys | Per-tenant keys read from a local file inside the Secret | Small-scale, air-gapped |
remote-multi-keys | Keys fetched from the Coordinator, with a KMIP KMS fallback for private tenants | Production with external KMS |
This document covers remote-multi-keys with an external KMIP-compliant KMS.
Architecture
The DS3 Gateway loads its encryption configuration from two Kubernetes resources that the gateway operator injects as volumes. The configuration lives under the crypto JSON key in both:
- ConfigMap (
/opt/config/application.json) — typically the encryption mode (mode) and the refresh interval (update_period). - Secret (
/opt/secret/application.json) — typically the KMS connection parameters (kms_config), TLS certificates, and key blobs.
There is no strict rule enforced by the code — gods3 merges both files at startup, so you can split fields by sensitivity. The examples below follow the common convention: ConfigMap for non-sensitive settings, Secret for credentials and keys.
Key resolution flow in remote-multi-keys mode:
- The DS3 Gateway polls the Coordinator for per-tenant configurations.
- Public tenants (
zero_knowledge: false) carry their key seed directly in the Coordinator response — no external KMS call. - Private tenants (
zero_knowledge: true) go through this chain:- The Gateway checks a local file in the Secret (
tenant_keys). - If no key found there, it queries the external KMS via KMIP 1.4.
- If the KMS has no key for that tenant either, HTTP 500 is returned and the upload/download fails.
- The Gateway checks a local file in the Secret (
Prerequisites
- A KMS server supporting KMIP 1.4.
- A TLS client certificate + private key for mTLS authentication to the KMS.
- The KMS must contain one Managed Object (type
Secret Data) per tenant, keyed by tenant UUID as the object'sNameattribute. - A Coordinator instance serving tenant configurations (required for
remote-multi-keysmode).
Step 1: Configure the ConfigMap (non-sensitive)
Add the encryption mode and refresh interval to your Gateway's ConfigMap. The gateway operator mounts this file at /opt/config/application.json — the default name is gods3-config, referenced by spec.gods3.applicationConfigMapName in the Gateway CR.
- New deployment — create the ConfigMap
- Existing Gateway — add/update the crypto section
# gods3-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: gods3-config
namespace: <namespace>
data:
application.json: |
{
"crypto": {
"mode": "remote-multi-keys",
"update_period": "1h"
}
}
Merge with your existing config: the YAML above shows only the new crypto section. Your real file must include all your existing sections (cache, redis, etc.) alongside this one — add the crypto block to your current application.json content, do not replace it.
The gateway reads application.json as raw JSON. If you deploy via the Cubbit Helm chart, you write YAML values (e.g. config.crypto.mode: remote-multi-keys) and the chart converts them to JSON automatically. When managing the ConfigMap directly, the file content must be valid JSON.
kubectl apply -f gods3-config.yaml
Or, for a quick setup:
kubectl create configmap gods3-config \
--namespace <namespace> \
--from-literal=application.json='{
"crypto": {
"mode": "remote-multi-keys",
"update_period": "1h"
}
}'
If you already have a ConfigMap (e.g. with smart_data_placement, redis, and other sections), do not replace it. Only the crypto block is new — keep everything else.
-
Export your current
application.json:kubectl get configmap gods3-config -n <namespace> -o jsonpath='{.data.application\.json}' > application.json -
Edit
application.json— insert thecryptoblock alongside your existing sections. Only thecryptopart is new:{
"crypto": {
"mode": "remote-multi-keys",
"update_period": "1h"
}
}Your existing sections (
smart_data_placement,redis, etc.) stay exactly where they are — just add thecryptoobject next to them. -
Apply the updated ConfigMap:
kubectl create configmap gods3-config \
--namespace <namespace> \
--from-file=application.json=./application.json \
--dry-run=client -o yaml | kubectl apply -f -
Only the application.json data key changes — the ConfigMap metadata and any other keys remain untouched.
Alternatively, for small edits:
kubectl edit configmap gods3-config -n <namespace>
Then add the crypto section inside the existing data.application.json content.
| Field | Always required | Description |
|---|---|---|
mode | no — defaults to single-key | Set to "remote-multi-keys" |
update_period | yes for multi-keys (local-multi-keys and remote-multi-keys) | How often the DS3 Gateway refreshes keys from the Coordinator or KMS. Format: Go duration string (e.g. "30s", "5m", "1h"). Without this value the gateway panics at startup in multi-keys mode. |
Step 2: Configure the Secret (sensitive)
Add the KMS connection parameters and the encryption key blob to your Gateway's Secret. The gateway operator mounts this file at /opt/secret/application.json — the default name is gods3-service, referenced by spec.gods3.applicationSecretName in the Gateway CR.
The Secret holds the KMS connection details. Both live under the crypto JSON key — the DS3 Gateway reads this file and merges it with the ConfigMap values at startup.
- New deployment — create the Secret
- Existing Gateway — add/update the KMS config
# gods3-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: gods3-service
namespace: <namespace>
stringData:
application.json: |
{
"crypto": {
"kms_config": {
"kmip": {
"address": "kms.example.com:5696",
"major_version": 1,
"minor_version": 4
},
"tls": {
"key": "-----BEGIN PRIVATE KEY-----\n<your-private-key>\n-----END PRIVATE KEY-----",
"cert": "-----BEGIN CERTIFICATE-----\n<your-cert>\n-----END CERTIFICATE-----",
"cipher_suites": [4865, 4866, 4867],
"insecure_skip_verify": false
}
}
}
}
stringData is write-only — Kubernetes does not return the values on read. Store the source file securely; never commit plain-text secrets to git.
Apply it:
kubectl apply -f gods3-secret.yaml
Or, for a quick setup:
kubectl create secret generic gods3-service \
--namespace <namespace> \
--from-literal=application.json='{
"crypto": {
"kms_config": {
"kmip": { "address": "kms.example.com:5696", "major_version": 1, "minor_version": 4 },
"tls": {
"key": "-----BEGIN PRIVATE KEY-----\n<your-private-key>\n-----END PRIVATE KEY-----",
"cert": "-----BEGIN CERTIFICATE-----\n<your-cert>\n-----END CERTIFICATE-----",
"cipher_suites": [4865, 4866, 4867],
"insecure_skip_verify": false
}
}
}
}'
If your Secret already contains other keys (e.g. ds3-secret-header-value, metrics-secret-header-value), do not replace them. Only the application.json key is new — or, if it already exists, merge the crypto block into it.
Since a Secret is a map of named keys — not a single JSON file — there are two approaches:
Option A — single application.json key (recommended when the Secret already uses this key):
-
Export the current
application.json:kubectl get secret gods3-service -n <namespace> -o jsonpath='{.data.application\.json}' | base64 -d > application.json -
Edit
application.json— add thecryptoblock alongside any existing content:{
"crypto": {
"kms_config": {
"kmip": { "address": "kms.example.com:5696", "major_version": 1, "minor_version": 4 },
"tls": { "key": "...", "cert": "...", "cipher_suites": [4865, 4866, 4867], "insecure_skip_verify": false }
}
}
}Your existing JSON content stays untouched — just insert the
cryptoobject next to it. -
Apply the updated Secret:
kubectl create secret generic gods3-service \
--namespace <namespace> \
--from-file=application.json=./application.json \
--dry-run=client -o yaml | kubectl apply -f -
Option B — add application.json as a new key (if the Secret does not have it yet):
-
Export the current Secret manifest:
kubectl get secret gods3-service -n <namespace> -o yaml > gods3-service-current.yaml -
Edit
gods3-service-current.yaml— add anapplication.jsonentry underdata(the value must be base64-encoded):apiVersion: v1
kind: Secret
metadata:
name: gods3-service
namespace: <namespace>
data:
ds3-secret-header-value: <existing-base64>
metrics-secret-header-value: <existing-base64>
application.json: <base64-of-your-json> -
Apply:
kubectl apply -f gods3-service-current.yaml
Only the application.json key (mounted at /opt/secret/application.json) is read by the DS3 Gateway. Other keys in the Secret are ignored by the crypto layer.
Field reference
| Field | Required | Description |
|---|---|---|
kms_config.kmip.address | yes | Hostname and port of the KMIP server (e.g. "kms.example.com:5696"). |
kms_config.kmip.address | yes | Hostname and port of the KMIP server (e.g. "kms.example.com:5696"). |
kms_config.kmip.major_version | yes | Must be 1. |
kms_config.kmip.minor_version | yes | Must be 4. Only KMIP 1.4 is supported. |
kms_config.tls.key | yes | PEM-encoded TLS client private key. The \n characters must be literal newlines in the JSON string. |
kms_config.tls.cert | yes | PEM-encoded TLS client certificate. Same newline requirement. |
kms_config.tls.cipher_suites | no | List of allowed TLS cipher suites as uint16 values matching Go's crypto/tls constants. If omitted, Go uses a safe default list. |
kms_config.tls.insecure_skip_verify | no | Set to true only for testing — skips server certificate chain verification. Never use in production. |
Step 3: Apply the Gateway Custom Resource
The gateway operator mounts the ConfigMap and Secret into the DS3 Gateway pod automatically. You only need to point to them (or accept the defaults):
- New deployment
- Existing Gateway — add/update crypto references
# gateway.yaml
apiVersion: gateway.cubbit.io/v1alpha1
kind: Gateway
metadata:
name: my-gateway
namespace: <namespace>
spec:
gods3:
enabled: true
# These are the defaults — omit them if you use the standard names:
applicationConfigMapName: gods3-config
applicationSecretName: gods3-service
If you already have a Gateway resource, edit it:
kubectl edit gateway my-gateway -n <namespace>
Ensure the spec.gods3 section includes (or already has) these two fields:
spec:
gods3:
enabled: true
applicationConfigMapName: gods3-config
applicationSecretName: gods3-service
If your ConfigMap or Secret has a different name, replace the value accordingly. The rest of the Gateway spec (console, additionalEnvs, etc.) stays untouched.
Apply it:
kubectl apply -f gateway.yaml
Note: The deprecated
spec.gods3.keysSeedConfigurationfield is ignored in current versions. All crypto configuration flows through the ConfigMap and Secret above. The gateway operator logs a deprecation warning ifkeysSeedConfigurationis populated, but no error is raised.
Step 4: Prepare the KMS (KMIP)
Your KMS must expose per-tenant keys as KMIP Managed Objects with the following attributes:
| KMIP Attribute | Value |
|---|---|
| Object Type | Secret Data |
Name (NameValue) | The tenant UUID exactly as the Coordinator returns it (e.g., "67f1adf3-08e5-4eea-94e3-e611f263c87b") |
| Name Type | Uninterpreted Text String |
| Key Value | Base64-encoded seed value (will be used as input to SHA-256 for X25519 key derivation) |
Important rules:
- Each tenant UUID must map to at most one Managed Object. Duplicates cause undefined behaviour.
- Only tenants with
zero_knowledge: truein the Coordinator will trigger a KMS lookup. - Tenants with
zero_knowledge: falsecarry their key in the Coordinator's response — the KMS is not called for them. - If a
zero_knowledge: truetenant has a key in the local Secret file (tenant_keyssection), the file takes precedence over the KMS.
Testing with PyKMIP (development)
For testing, you can run a PyKMIP server. Cubbit maintains a forked version pre-configured for the DS3 Gateway. The server listens on port 5696 and can be seeded with test tenant keys via a startup script. Refer to the Cubbit development documentation for the exact deployment manifests.
Step 5: Verify
Check the DS3 Gateway pod logs
kubectl logs -n <namespace> -l app=<gateway-name>-gods3 -c gods3 | grep -E 'keys|kms|kmip'
A healthy startup shows:
"Starting Keys Client update routine"
Check the gateway operator logs
kubectl logs -n <namespace> -l app=gateway-operator -c manager | grep gods3
The ConfigMap and Secret volumes should be mounted correctly — you'll see the ConfigMap and Secret being wired into the deployment.
Simulate an upload (optional)
Upload an object through the S3 API. If keys are resolved correctly, the payload is encrypted and uploaded to the Swarm.
If the KMS is unreachable during the periodic key refresh, you will see this in the logs:
"periodical update: failed to get key for tenant ..."
This does not affect in-flight operations — the Gateway keeps the last successfully loaded keys. If no key was ever loaded for a tenant (first start, KMS never reachable), uploads for that tenant return HTTP 500.
Troubleshooting
All symptoms below appear in the DS3 Gateway pod logs (kubectl logs -n <namespace> -l app=<gateway-name>-gods3 -c gods3).
| Symptom (from pod logs) | Likely cause | Fix |
|---|---|---|
kms config is required | kms_config is absent or empty in the Secret | Add kms_config with the required fields |
missing required configuration | One of kmip.address, tls.cert, or tls.key is empty | Fill in all required fields |
key not found for private tenantID ... | No KMIP Managed Object with that tenant name | Add the tenant key to the KMS |
error writing kms request / error reading kms response | Network issue, firewall, TLS handshake failure | Check network connectivity, TLS cert validity, KMIP server logs |
error decoding kms response / error decoding kms response payload | KMS returned an unexpected message format | Verify the KMS is KMIP 1.4 compliant and the response structure matches what the DS3 Gateway expects |
invalid crypto.mode value | Typo in the mode field | Use one of: single-key, local-multi-keys, remote-multi-keys |
unsupported KMIP version, use 1.4 | major_version/minor_version not set to 1 and 4 | Correct the values in the Secret |
What you do NOT need to do
- ❌ Do not set
TENANT_ENCRYPTION_KEY_MODE— it is no longer used. - ❌ Do not populate
spec.gods3.keysSeedConfigurationon the Gateway CR — it is deprecated. - ❌ Do not restart the DS3 Gateway after updating keys in the KMS — keys are hot-reloaded every
update_period.
Reference: key derivation
The DS3 Gateway uses X25519 (Curve25519) + AES-256-CTR hybrid encryption (ECIES). The key derivation chain is:
KMIP key value (base64)
↓ base64 decode
raw seed (32 bytes)
↓ SHA-256
private key (32 bytes)
↓ X25519 ScalarBaseMult
public key (32 bytes) ← kept in the DS3 Gateway's in-memory key store, scoped per tenant
During encryption, an ephemeral keypair is generated per payload, an ECDH shared secret is derived, and the actual AES key is obtained by hashing that shared secret. This ensures per-payload forward secrecy.
Key format requirements
| Property | Value |
|---|---|
| Algorithm | Raw symmetric seed (any cryptographically secure random) |
| Min decoded length | 32 bytes (256 bits) |
| Recommended length | 48 bytes (384 bits) — extra entropy for future-proofing |
| Encoding in KMIP | Base64 |
| KMIP Object Type | Secret Data |
| Pre-generated | No — the Gateway derives the X25519 keypair from the seed |
To generate a compliant seed:
openssl rand -base64 48
If the KMS becomes unavailable
The Gateway caches resolved keys in memory. If the KMS goes down:
- In-flight uploads/downloads → continue uninterrupted — keys already loaded at startup or from the last successful refresh are still valid.
- New tenant requests for tenants whose keys have never been loaded → fail with HTTP 500 until the KMS is back.
- Gateway logs → show
"periodical update: failed to get key for tenant ..."at everyupdate_period. - Automatic recovery → the Gateway retries on every refresh cycle; no manual intervention needed once the KMS is restored.
Backup and disaster recovery
- KMIP key seeds are the single source of truth for private tenants. Ensure your KMS has its own backup/DR strategy (replication, snapshots, cross-region).
- Cached payloads that were encrypted with a key that is no longer available become unreadable. Objects stored in the Swarm are unaffected.
Production checklist
- KMIP server is reachable from the DS3 Gateway pod (check network policies, firewall rules)
- TLS client cert is valid and signed by a CA the KMS trusts
-
insecure_skip_verifyisfalsein production -
update_periodis set to a reasonable interval (1h recommended) - Tenant
zero_knowledgeflags are correctly set in the Coordinator - Each tenant that should use the KMS has a KMIP Managed Object with the matching UUID as the name
- The Secret is stored in a secure location (Vault, ExternalSecret, etc.) — never committed to git