Skip to content

Commit 5548405

Browse files
sjenningTaylorMutchvarshaprasad96
authored
feat(credentials): add provider credential storage drivers (#2437)
* feat(credentials): add provider credential storage drivers Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * fix(credentials): harden credential update handling Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * fix(credentials): harden credential driver security, correctness, and performance Address review findings from the credential storage drivers PR: - Route additional_credentials through the driver on refresh to prevent silent data loss for multi-credential providers (e.g. AWS STS) - Clean up stored credential handles on CAS failure during refresh to prevent orphaned secrets in external backends - Enforce namespace validation in the Kubernetes Secrets driver to prevent cross-namespace credential access when allow_reference_namespace is not enabled - Cache Vault Kubernetes auth tokens with 80% TTL to avoid re-authenticating on every credential operation - Parallelize resolve_credentials in all three drivers using try_join_all for faster sandbox startup - Add existingSecret support for the KEK Secret to fix helm template/GitOps workflows where lookup returns empty and regenerates the key - Document RBAC blast radius for the Kubernetes Secrets credential driver and recommend a dedicated namespace Signed-off-by: Varsha Prasad <varshaprasad96@gmail.com> Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com> * fix(credentials): add optimistic concurrency, fix thundering herd, parallelize operations Use resourceVersion optimistic concurrency with retry loop for K8s Secret ownership checks to prevent TOCTOU races. Switch Vault token cache from RwLock to Mutex with double-check pattern to prevent thundering herd on cache miss. Parallelize credential store and delete operations across independent keys using try_join_all. Signed-off-by: Varsha Prasad <varshaprasad96@gmail.com> Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com> * fix(credentials): handle partial failures, add delete retry, consolidate cleanup Replace try_join_all with join_all in credential store/delete operations to handle partial failures — successfully-stored handles are cleaned up when another key fails. Add retry loop with conflict detection to db-credstore delete_credential, matching the K8s driver pattern. Consolidate 4 manual cleanup_pre_stored_provider_credentials call sites into a single error handler using an async block. Remove inconsistent .trim() from db-credstore validate_handle_owner. Signed-off-by: Varsha Prasad <varshaprasad96@gmail.com> Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com> * fix(credentials): fix retry loop guard and remove unprotected validation Remove attempt-count guard from 409/Aborted match arms in retry loops so the post-loop Status::aborted error is reachable after exhausting retries. Previously, last-attempt conflicts fell through to the catch-all error arm, producing misleading Status::unavailable errors. Remove duplicate validation calls that ran after prepare_provider_credential_update but outside the cleanup-protected async block, which would leak pre-stored handles on failure. Signed-off-by: Varsha Prasad <varshaprasad96@gmail.com> Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com> * fix(credentials): add workspace/provider UUID to credential backend paths Include workspace and provider ID in credential backend object paths to ensure cross-workspace uniqueness and prevent credential collision (GATOR-1806c9be-01). - Updated credential driver proto to include workspace and provider_id fields - Modified Vault driver to include workspace/provider_id in managed_secret_path - Modified Kubernetes Secrets driver to include workspace/provider_id in credential_owner_id and managed_secret_name - Updated all credential runtime calls to pass workspace/provider_id - Updated tests to use the new signatures This prevents two workspaces sharing the same external credential store from colliding on provider names, which was a critical security issue (CWE-639). * fix(credentials): preserve provider-level expiration for handle-backed credentials Compute effective expiration from both provider and driver values using the earliest non-zero timestamp and skip expired values before insertion (GATOR-1806c9be-02). - Modified resolve_provider_handles to check provider credential_expires_at_ms - Skip expired credentials during resolution instead of returning them - Use effective expiration (min of provider and driver) in resolution results - Fix inference.rs to preserve earliest expiration when merging This ensures handle-backed credentials respect the same expiration semantics as inline credentials. * fix(credentials): stage refresh changes under new handles before validation Stage credential replacements under new immutable handles instead of reusing existing handles to prevent overwriting committed values before validation/CAS (GATOR-1806c9be-03). - Stage credentials with empty existing_handles map to force new handle creation - Validate and CAS before the new values are committed to backend storage - Delete old handles only after successful CAS - On CAS failure, delete only the newly staged handles - This prevents CWE-362/CWE-367 race conditions where failed refreshes could still modify or delete the active credential The fix ensures that a rejected refresh cannot modify the backend object still referenced by the committed provider record. * fix(credentials): add timeouts to credential driver RPCs Apply configured timeouts to both startup capability negotiation and runtime RPCs to prevent indefinite hangs (GATOR-1806c9be-05). - Add DEFAULT_CREDENTIAL_DRIVER_RPC_TIMEOUT_SECS constant (30s) - Apply timeout to GetCapabilities during startup connection - Apply timeout to all runtime RPCs (store, delete, resolve) - Use tokio::time::timeout to bound the entire GetCapabilities operation during startup, not just the socket connection - Return contextual deadline errors on timeout This prevents a faulty or overloaded driver from hanging gateway operations indefinitely. * fix(credentials): fix test to use consistent workspace/provider identity The Kubernetes auth Vault resolve test was constructing a managed path with test-workspace/test-provider-id but sending default/prov-123 in the request, causing validation to reject the request (GATOR-18e32351-01). - Update test to use test-workspace and test-provider-id in the request to match the logical_path construction - This ensures the test exercises the intended code path and validates Kubernetes auth resolution properly The test now passes and correctly validates identity enforcement. * fix(credentials): use unique staging ID for refresh to avoid overwrites Stage refresh replacements under genuinely distinct immutable handles using a unique staging ID to prevent overwriting committed values (GATOR-1806c9be-03). - Generate a unique staging ID using UUID for each refresh operation - Use this staging ID when storing credentials instead of the real provider ID - Pass the same staging ID during cleanup on failure to delete only staged objects - This ensures deterministic paths (Vault) and object names (K8s) don't collide with the committed provider's credentials The fix prevents failed refreshes from silently replacing active credentials or breaking providers by deleting still-referenced backend objects. * fix(credentials): wrap credential driver RPCs in local timeouts Add local tokio::time::timeout wrappers around credential driver RPCs to bound non-compliant or stalled UDS peers (GATOR-1806c9be-05). - Wrap StoreCredential, DeleteCredential, and ResolveCredentials in local timeouts - Return contextual deadline_exceeded errors when timeouts occur - Keep existing gRPC timeout metadata for compliant implementations - GetCapabilities during startup was already wrapped in previous commit This ensures a faulty local driver cannot hang gateway operations indefinitely, even if it accepts the connection but never responds to the RPC. * fix(credentials): preserve ownership for staged refreshes Signed-off-by: Seth Jennings <sjenning@redhat.com> * fix(credentials): bound startup capability probe Signed-off-by: Seth Jennings <sjenning@redhat.com> * test(provider): authenticate credential handler requests Signed-off-by: Seth Jennings <sjenning@redhat.com> * fix(ci): grant actions read to credential driver e2e Signed-off-by: Seth Jennings <sjenning@redhat.com> --------- Signed-off-by: Taylor Mutch <taylormutch@gmail.com> Signed-off-by: Varsha Prasad <varshaprasad96@gmail.com> Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com> Signed-off-by: Seth Jennings <sjenning@redhat.com> Co-authored-by: Taylor Mutch <taylormutch@gmail.com> Co-authored-by: Varsha Prasad Narsing <varshaprasad96@gmail.com>
1 parent 8c7dd14 commit 5548405

60 files changed

Lines changed: 10167 additions & 159 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/debug-openshell-cluster/SKILL.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,15 @@ release. Look for failed installs, unexpected values, missing namespace, wrong
233233
image tag, TLS settings that do not match the registered endpoint, and
234234
scheduling failures.
235235

236+
When no external credential driver is enabled, the Helm chart uses the
237+
gateway's default encrypted database credential storage. The chart creates a
238+
retained Kubernetes Secret for the shared KEK, injects it into gateway pods, and
239+
stores encrypted credential envelopes in the OpenShell database. For
240+
`workload.kind=deployment` or multi-replica gateways, confirm
241+
`server.externalDbSecret` points at a shared database. A render/install error
242+
mentioning `server.credentialDrivers` means the values selected multiple
243+
external credential backends.
244+
236245
For HA or PostgreSQL-backed installs, also check the external database Secret
237246
referenced by `server.externalDbSecret` and the PostgreSQL workload if the test
238247
or operator deployed one in-cluster:

‎.github/workflows/branch-e2e.yml‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
run_core_e2e: ${{ steps.labels.outputs.run_core_e2e }}
2727
run_gpu_e2e: ${{ steps.labels.outputs.run_gpu_e2e }}
2828
run_kubernetes_ha_e2e: ${{ steps.labels.outputs.run_kubernetes_ha_e2e }}
29+
run_kubernetes_credential_drivers_e2e: ${{ steps.labels.outputs.run_kubernetes_credential_drivers_e2e }}
2930
run_any_e2e: ${{ steps.labels.outputs.run_any_e2e }}
3031
steps:
3132
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -44,6 +45,7 @@ jobs:
4445
run_core_e2e="$(jq -r 'index("test:e2e") != null' <<< "$LABELS_JSON")"
4546
run_gpu_e2e="$(jq -r 'index("test:e2e-gpu") != null' <<< "$LABELS_JSON")"
4647
run_kubernetes_ha_e2e="$(jq -r 'index("test:e2e-kubernetes") != null' <<< "$LABELS_JSON")"
48+
run_kubernetes_credential_drivers_e2e="$(jq -r 'index("test:e2e-kubernetes") != null' <<< "$LABELS_JSON")"
4749
;;
4850
merge_group)
4951
# Merge groups have no PR labels. When GPU E2E is required as documented
@@ -52,14 +54,16 @@ jobs:
5254
run_core_e2e=true
5355
run_gpu_e2e=true
5456
run_kubernetes_ha_e2e=false
57+
run_kubernetes_credential_drivers_e2e=false
5558
;;
5659
*)
5760
run_core_e2e=true
5861
run_gpu_e2e=true
5962
run_kubernetes_ha_e2e=true
63+
run_kubernetes_credential_drivers_e2e=true
6064
;;
6165
esac
62-
if [ "$run_core_e2e" = "true" ] || [ "$run_gpu_e2e" = "true" ] || [ "$run_kubernetes_ha_e2e" = "true" ]; then
66+
if [ "$run_core_e2e" = "true" ] || [ "$run_gpu_e2e" = "true" ] || [ "$run_kubernetes_ha_e2e" = "true" ] || [ "$run_kubernetes_credential_drivers_e2e" = "true" ]; then
6367
run_any_e2e=true
6468
else
6569
run_any_e2e=false
@@ -68,6 +72,7 @@ jobs:
6872
echo "run_core_e2e=$run_core_e2e"
6973
echo "run_gpu_e2e=$run_gpu_e2e"
7074
echo "run_kubernetes_ha_e2e=$run_kubernetes_ha_e2e"
75+
echo "run_kubernetes_credential_drivers_e2e=$run_kubernetes_credential_drivers_e2e"
7176
echo "run_any_e2e=$run_any_e2e"
7277
} >> "$GITHUB_OUTPUT"
7378
@@ -192,6 +197,19 @@ jobs:
192197
external-postgres-secret: openshell-ha-pg
193198
cli-artifact-prefix: rust-binary-cli
194199

200+
kubernetes-credential-drivers-e2e:
201+
needs: [pr_metadata, build-gateway, build-supervisor]
202+
if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_kubernetes_credential_drivers_e2e == 'true'
203+
permissions:
204+
actions: read
205+
contents: read
206+
packages: read
207+
uses: ./.github/workflows/e2e-kubernetes-test.yml
208+
with:
209+
image-tag: ${{ github.sha }}
210+
job-name: Kubernetes Credential Drivers E2E
211+
e2e-task: e2e:kubernetes:credential-drivers
212+
195213
core-e2e-result:
196214
name: Core E2E result
197215
needs: [pr_metadata, build-gateway, build-supervisor, build-cli, build-driver-vm-linux, e2e, kubernetes-e2e]
@@ -282,3 +300,30 @@ jobs:
282300
fi
283301
done
284302
exit "$failed"
303+
304+
kubernetes-credential-drivers-e2e-result:
305+
name: Kubernetes Credential Drivers E2E result
306+
needs: [pr_metadata, build-gateway, build-supervisor, kubernetes-credential-drivers-e2e]
307+
if: always() && needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_kubernetes_credential_drivers_e2e == 'true'
308+
runs-on: ubuntu-latest
309+
steps:
310+
- name: Verify Kubernetes credential drivers E2E jobs
311+
env:
312+
BUILD_GATEWAY_RESULT: ${{ needs.build-gateway.result }}
313+
BUILD_SUPERVISOR_RESULT: ${{ needs.build-supervisor.result }}
314+
KUBERNETES_CREDENTIAL_DRIVERS_E2E_RESULT: ${{ needs.kubernetes-credential-drivers-e2e.result }}
315+
run: |
316+
set -euo pipefail
317+
failed=0
318+
for item in \
319+
"build-gateway:$BUILD_GATEWAY_RESULT" \
320+
"build-supervisor:$BUILD_SUPERVISOR_RESULT" \
321+
"kubernetes-credential-drivers-e2e:$KUBERNETES_CREDENTIAL_DRIVERS_E2E_RESULT"; do
322+
name="${item%%:*}"
323+
result="${item#*:}"
324+
if [ "$result" != "success" ]; then
325+
echo "::error::$name concluded $result"
326+
failed=1
327+
fi
328+
done
329+
exit "$failed"

‎.github/workflows/e2e-kubernetes-test.yml‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ on:
3737
required: false
3838
type: string
3939
default: "v0.5.0"
40+
e2e-task:
41+
description: "mise task to run for the Kubernetes e2e job"
42+
required: false
43+
type: string
44+
default: "e2e:kubernetes"
4045
mise-version:
4146
description: "mise version to install on the bare Kubernetes e2e runner"
4247
required: false
@@ -130,12 +135,13 @@ jobs:
130135
kind load image-archive "$archive" --name "$KIND_CLUSTER_NAME"
131136
done
132137
133-
- name: Run Kubernetes E2E (Rust smoke)
138+
- name: Run Kubernetes E2E
134139
env:
135140
AGENT_SANDBOX_VERSION: ${{ inputs.agent-sandbox-version }}
136141
OPENSHELL_E2E_KUBE_CONTEXT: kind-${{ env.KIND_CLUSTER_NAME }}
137142
OPENSHELL_E2E_KUBE_EXTRA_VALUES: ${{ inputs.extra-helm-values }}
138143
OPENSHELL_E2E_KUBE_EXTERNAL_POSTGRES_SECRET: ${{ inputs.external-postgres-secret }}
139144
IMAGE_TAG: ${{ inputs.image-tag }}
140145
OPENSHELL_REGISTRY: ghcr.io/nvidia/openshell
141-
run: mise run --no-deps --skip-deps e2e:kubernetes
146+
E2E_TASK: ${{ inputs.e2e-task }}
147+
run: mise run --no-deps --skip-deps "$E2E_TASK"

‎.github/workflows/e2e-label-help.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
status_summary="The matching required CI gate status on this PR will flip green automatically once the run finishes."
5252
;;
5353
test:e2e-kubernetes)
54-
suite_summary="Kubernetes HA E2E"
54+
suite_summary="Kubernetes HA and credential-driver E2E"
5555
build_summary="gateway and supervisor images"
5656
status_summary="This is an optional proof-of-life suite; failures are visible in the workflow run but do not publish a required CI gate status."
5757
;;

‎AGENTS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ These pipelines connect skills into end-to-end workflows. Individual skill files
4242
| `crates/openshell-sdk/` | Shared client SDK | Async Rust gateway client (gRPC transport, TLS, OIDC refresh, edge tunnel); consumed by CLI, TUI, and `@openshell/sdk` |
4343
| `crates/openshell-providers/` | Provider management | Credential provider backends |
4444
| `crates/openshell-tui/` | Terminal UI | Ratatui-based dashboard for monitoring |
45+
| `crates/openshell-driver-kubernetes-secrets/` | Kubernetes Secrets credential driver | In-process `CredentialDriver` backend for OpenShell-managed K8s Secret storage |
46+
| `crates/openshell-driver-vault/` | Vault credential driver | In-process `CredentialDriver` backend for Vault-compatible KV storage |
4547
| `crates/openshell-driver-kubernetes/` | Kubernetes compute driver | In-process `ComputeDriver` backend for K8s sandbox pods |
4648
| `crates/openshell-driver-docker/` | Docker compute driver | In-process `ComputeDriver` backend for local Docker sandbox containers |
4749
| `crates/openshell-driver-podman/` | Podman compute driver | In-process `ComputeDriver` backend for local Podman sandbox containers |

‎CI.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ Three opt-in labels enable the long-running E2E suites:
1818
suites in `Branch E2E Checks`
1919
- `test:e2e-gpu` runs GPU E2E in `Branch E2E Checks`
2020
- `test:e2e-kubernetes` runs Kubernetes E2E with the HA Helm overlay
21-
(`replicaCount: 2` and bundled PostgreSQL) in `Branch E2E Checks`
21+
(`replicaCount: 2` and bundled PostgreSQL) and the credential-driver suite
22+
(Kubernetes Secrets plus Vault) in `Branch E2E Checks`
2223

2324
When multiple labels are present, `Branch E2E Checks` builds the shared gateway and supervisor images once, builds one CLI artifact per runner architecture, builds the Linux VM driver artifact once, and fans out all enabled suites in parallel. Docker, Podman, GPU, Rust, Python, MCP, and VM E2E jobs reuse the matching prebuilt gateway and CLI binaries instead of compiling additional debug binaries in each job; Kubernetes E2E consumes the gateway image directly and reuses the prebuilt CLI. VM E2E also reuses the prebuilt VM driver artifact and falls back to local VM-driver/runtime preparation for local runs or workflow invocations that omit the artifact.
24-
The `OpenShell / E2E` and `OpenShell / GPU E2E` required statuses are evaluated from separate suite result jobs inside that workflow. `test:e2e-kubernetes` is optional while HA behavior is under active iteration: failures are visible in the workflow run but do not publish a required CI gate status.
25+
The `OpenShell / E2E` and `OpenShell / GPU E2E` required statuses are evaluated from separate suite result jobs inside that workflow. `test:e2e-kubernetes` is optional while Kubernetes HA and credential-driver behavior are under active iteration: failures are visible in the workflow run but do not publish a required CI gate status.
2526

2627
The GitHub ruleset should require the `OpenShell / ...` statuses published by `Required CI Gates`, not the push-triggered workflow jobs directly.
2728

@@ -135,7 +136,7 @@ The bot's full administrator documentation is internal to NVIDIA. The only comma
135136
| File | Role |
136137
|---|---|
137138
| `.github/workflows/branch-checks.yml` | Required non-E2E checks. Triggers on `push: pull-request/[0-9]+` for PR mirrors and `merge_group` for queued merges. |
138-
| `.github/workflows/branch-e2e.yml` | Standard, GPU, and Kubernetes HA E2E. PR mirror pushes use `test:e2e`, `test:e2e-gpu`, and `test:e2e-kubernetes` labels; merge groups run core and GPU E2E. |
139+
| `.github/workflows/branch-e2e.yml` | Standard, GPU, Kubernetes HA, and Kubernetes credential-driver E2E. PR mirror pushes use `test:e2e`, `test:e2e-gpu`, and `test:e2e-kubernetes` labels; merge groups run core and GPU E2E. |
139140
| `.github/workflows/helm-lint.yml` | Helm chart validation. PR mirror pushes skip lint jobs unless Helm inputs changed; merge groups always validate Helm because they represent the final integration state. |
140141
| `.github/actions/pr-gate/action.yml` | Composite action that resolves PR metadata and verifies the required label is set for PR mirror pushes. Non-push events are allowed through. |
141142
| `.github/actions/pr-merge-base/action.yml` | Composite action that resolves and fetches the merge-base commit for `pull-request/<N>` push workflows. |

‎Cargo.lock‎

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ sha2 = "0.10"
104104
rand = "0.9"
105105
jsonwebtoken = "9"
106106
getrandom = "0.3"
107+
ring = "0.17"
107108
spiffe = { version = "0.15", default-features = false, features = ["workload-api-jwt", "tracing"] }
108109

109110
# Filesystem embedding

‎TESTING.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ Suites:
151151
- Docker suite (`--features e2e-docker`) - common suite plus Docker-only coverage such as Dockerfile image builds, Docker preflight checks, and managed Docker gateway resume.
152152
- Docker GPU suite (`--features e2e-docker-gpu`) - Docker suite plus GPU sandbox smoke coverage.
153153
- VM suite (`--features e2e-vm`) - runs e2e tests on a VM.
154+
- Kubernetes credential-driver suite (`--features e2e-kubernetes-credential-drivers`) - targeted Kubernetes Secrets and Vault provider credential storage coverage.
154155

155156
GPU device-selection tests compare OpenShell sandboxes against a plain Docker or
156157
Podman container that requests `--device nvidia.com/gpu=all`. The probe image
@@ -180,6 +181,14 @@ Run the VM-backed Rust CLI e2e suite:
180181
mise run e2e:vm
181182
```
182183

184+
Run the targeted Kubernetes credential-driver e2e suite. This deploys an
185+
OpenBao fixture for the Vault-compatible driver path and validates Kubernetes
186+
Secrets and Vault storage backends one at a time:
187+
188+
```shell
189+
mise run e2e:kubernetes:credential-drivers
190+
```
191+
183192
Run a single test directly with cargo:
184193

185194
```shell
@@ -210,3 +219,4 @@ The harness (`e2e/rust/src/harness/`) provides:
210219
| `OPENSHELL_GATEWAY` | Override active gateway name for E2E tests |
211220
| `OPENSHELL_GATEWAY_ENDPOINT` | Run E2E tests against an existing plaintext HTTP gateway endpoint |
212221
| `OPENSHELL_E2E_DRIVER` | Driver name exported by the e2e gateway wrapper (`docker`, `podman`, or `vm`) |
222+
| `OPENSHELL_E2E_CREDENTIAL_DRIVERS` | Enables the Kubernetes credential-driver fixture path in `e2e/with-kube-gateway.sh` |

‎architecture/gateway.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,13 @@ keeps only the current injectable credential values and optional per-credential
305305
expiry timestamps. A refresh normally mints one credential, but a strategy may
306306
co-mint several (AWS STS mints the access key, secret key, and session token in
307307
one call); the refresh state pins the resolved set of env keys it owns so
308-
collision checks reserve all of them before the first mint.
308+
collision checks reserve all of them before the first mint. Provider records
309+
keep inline credential values only for legacy records created before credential
310+
driver storage. New provider writes keep driver-owned credential handles. When
311+
no external credential driver is configured, gateways use server-owned encrypted
312+
database credential storage for defense in depth. Multi-replica deployments can
313+
use that default with a shared database and shared key-encryption key, or opt
314+
into an external backend such as Vault or Kubernetes Secrets.
309315

310316
### Optimistic Concurrency (CAS)
311317

0 commit comments

Comments
 (0)