Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Kubernetes

Three patterns for pulling ScaiVault secrets into Kubernetes pods, from simplest to most flexible.

Patterns at a glance#

Pattern When to use Trade-offs
Init container Most common. Secrets needed at process startup. Restart-needed for rotation.
Sidecar Long-running services with rotation. More moving parts; reload signal needed.
CSI driver Multiple secrets, mountpoint semantics, no app changes. Cluster-wide install; opinionated mount layout.

For dynamic secrets (per-pod DB credentials), the sidecar pattern is the right answer — it can renew and revoke leases over the pod's lifetime.

Authentication#

Workload Identity is the modern path. Map a Kubernetes service account to a ScaiKey identity via OIDC token exchange:

yaml
1
2
3
4
5
6
7
apiVersion: v1
kind: ServiceAccount
metadata:
  name: billing-prod
  namespace: billing
  annotations:
    scaikey.scailabs.ai/role: "sa:billing-prod"

The pod's projected service-account token is exchanged for a ScaiVault bearer token at startup via POST /v1/auth/exchange. No long-lived secret material in cluster.

If your cluster doesn't have OIDC configured for ScaiKey yet, fall back to a static client-credentials secret stored in a Kubernetes Secret. The exchange flow is preferred long-term.

Pattern 1: init container#

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: apps/v1
kind: Deployment
metadata:
  name: billing
spec:
  template:
    spec:
      serviceAccountName: billing-prod
      initContainers:
      - name: fetch-secrets
        image: scailabs/scaivault-init:1.0
        env:
        - name: SCAIVAULT_URL
          value: https://scaivault.scailabs.ai
        - name: SCAIVAULT_TOKEN_FILE
          value: /var/run/secrets/scaivault/token
        - name: SECRETS
          value: |
            environments/production/billing/database -> /run/secrets/db.json
            environments/production/billing/stripe   -> /run/secrets/stripe.json
        volumeMounts:
        - {name: secrets, mountPath: /run/secrets}
        - {name: sv-token, mountPath: /var/run/secrets/scaivault, readOnly: true}
      containers:
      - name: app
        image: acme/billing:1.0
        volumeMounts:
        - {name: secrets, mountPath: /run/secrets, readOnly: true}
      volumes:
      - {name: secrets, emptyDir: {medium: Memory}}
      - name: sv-token
        projected:
          sources:
          - serviceAccountToken:
              audience: scaivault
              expirationSeconds: 3600
              path: token

emptyDir.medium: Memory keeps the secrets off disk. projected serviceAccountToken gives the init container a freshly-projected ScaiKey-compatible JWT.

The init container does:

  1. Read its SA token.
  2. Exchange for a ScaiVault bearer token via /v1/auth/exchange.
  3. Read each secret listed in $SECRETS.
  4. Write to the indicated file in the shared emptyDir.
  5. Exit 0.

App reads from /run/secrets/db.json like any local file. No SDK in the app needed.

Rotation with init containers#

Restart the pods periodically. A CronJob:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: batch/v1
kind: CronJob
metadata:
  name: rolling-restart-billing
spec:
  schedule: "0 3 * * *"   # daily at 03:00 UTC
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: deployment-restarter
          containers:
          - name: kubectl
            image: bitnami/kubectl:1.30
            command: ["kubectl", "rollout", "restart", "deployment/billing"]
          restartPolicy: OnFailure

Crude but works. For finer-grained rotation, use the sidecar pattern.

Pattern 2: sidecar#

The sidecar runs alongside the app, fetches secrets, and refreshes them on a schedule or on event. The app reloads when the file changes.

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
spec:
  containers:
  - name: app
    image: acme/billing:1.0
    volumeMounts:
    - {name: secrets, mountPath: /run/secrets, readOnly: true}
  - name: scaivault-sidecar
    image: scailabs/scaivault-sidecar:1.0
    env:
    - {name: SCAIVAULT_URL, value: https://scaivault.scailabs.ai}
    - name: WATCH_PATHS
      value: |
        environments/production/billing/database -> /run/secrets/db.json
        environments/production/billing/stripe   -> /run/secrets/stripe.json
    - {name: REFRESH_INTERVAL, value: "5m"}
    - {name: RELOAD_SIGNAL, value: "SIGHUP"}
    - {name: RELOAD_TARGET, value: "billing"}
    volumeMounts:
    - {name: secrets, mountPath: /run/secrets}
    - {name: sv-token, mountPath: /var/run/secrets/scaivault, readOnly: true}
  shareProcessNamespace: true   # for signal delivery
  volumes:
  - {name: secrets, emptyDir: {medium: Memory}}
  - name: sv-token
    projected:
      sources:
      - serviceAccountToken:
          audience: scaivault
          expirationSeconds: 3600
          path: token

The sidecar polls every REFRESH_INTERVAL, compares the fetched value against the current file, and on change writes the new value + sends SIGHUP to the app process. App needs to handle the signal (or watch the file with inotify).

For event-driven refresh instead of polling, subscribe the sidecar to secret.rotated for the configured paths and refresh on event. Lower latency but adds network dependency.

Sidecar with dynamic credentials#

For Postgres dynamic creds, the sidecar generates the lease and renews it:

yaml
1
2
3
4
5
6
7
- name: scaivault-sidecar
  image: scailabs/scaivault-sidecar:1.0
  env:
  - name: DYNAMIC
    value: |
      engine=postgres-prod role=readonly ttl=2h -> /run/secrets/db.json
  - {name: SCAIVAULT_URL, value: https://scaivault.scailabs.ai}

On startup: generate, write. Periodically: renew. On shutdown: revoke. The pod gets a fresh DB user on each restart and revokes it on exit.

Pattern 3: CSI driver#

The Secret Store CSI driver mounts secrets as volumes. Install the ScaiVault provider:

bash
1
2
3
helm repo add scailabs https://charts.scailabs.ai
helm install scaivault-csi scailabs/secrets-store-csi-driver-scaivault \
  --namespace kube-system

Then a SecretProviderClass:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: billing-secrets
  namespace: billing
spec:
  provider: scaivault
  parameters:
    base_url: https://scaivault.scailabs.ai
    audience: scaivault
    objects: |
      - path: environments/production/billing/database
        file: db.json
      - path: environments/production/billing/stripe
        file: stripe.json
        field: secret_key

The field selector pulls one field instead of the whole JSON.

Pod consuming it:

yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
spec:
  serviceAccountName: billing-prod
  containers:
  - name: app
    image: acme/billing:1.0
    volumeMounts:
    - name: secrets
      mountPath: /run/secrets
      readOnly: true
  volumes:
  - name: secrets
    csi:
      driver: secrets-store.csi.k8s.io
      readOnly: true
      volumeAttributes:
        secretProviderClass: billing-secrets

CSI handles auth via the service account's projected token automatically. Secrets appear as files at /run/secrets/db.json etc. Rotation: enable the driver's rotation reconciler (--enable-secret-rotation=true) and it polls and updates the files at the configured interval.

Common questions#

"Can I sync ScaiVault into native Kubernetes Secrets?" Yes, via syncSecret on the CSI driver's SecretProviderClass. The driver materializes the values into a Kubernetes Secret object. Useful for things that expect to read from native Secrets (some Helm charts, third-party operators). Less recommended for application code — the indirection adds a refresh problem.

"What about secrets that need to exist before the pod starts?" Use a Job that runs before the deployment rolls out, has access to the right ScaiKey identity, and writes a native Kubernetes Secret. ArgoCD can sequence this via sync waves.

"How do I rotate the kubelet's projected token?" You don't. Kubelet refreshes projected service-account tokens automatically; ScaiVault auth-exchange handles short-lived tokens by re-exchanging on each call. Don't cache exchanged tokens longer than ~50 minutes.

"Should the init container exit cleanly if ScaiVault is down?" Default behavior: fail. The pod doesn't start. Better than starting with stale or missing credentials. Override with FAIL_OPEN=true if you have an explicit fallback path.

What's next#

Updated 2026-05-17 13:26:50 View source (.md) rev 1