Skip to content

Key rotation

This page shows how to change each secret that Sluice uses.

Example

Goal: give a client a new API key while the old key still works.

Store the hash of the new key in a file that only the owner can write:

sluice apikey hash --pepper key-rotation-demo-pepper --id report-bot-2026b --material new-secret \
  > /tmp/sluice-key-rotation/report-bot-2026b.hash

chmod 600 /tmp/sluice-key-rotation/report-bot-2026b.hash

Add a second entry to the subject binding. A secret file needs an absolute path. The example uses /tmp/sluice-key-rotation:

apiVersion: sluice.bino.bi/v1alpha1
kind: SubjectBinding
metadata:
  name: report-bot
spec:
  apiKeys:
    - id: "report-bot-2026a"
      hashRef: "secret://file//tmp/sluice-key-rotation/report-bot-2026a.hash"
      groups: ["agents"]
    # The new key. It needs its own id.
    - id: "report-bot-2026b"
      hashRef: "secret://file//tmp/sluice-key-rotation/report-bot-2026b.hash"
      groups: ["agents"]

Reload:

curl -s -X POST http://localhost:9091/admin/reload \
  -H "Authorization: Bearer demo-admin-token-change-me"
{
  "ok": true,
  "digest": "<digest>"
}

Both keys work now. The new key:

curl -s http://localhost:8080/v1/query \
  -H "X-Api-Key: report-bot-2026b.new-secret" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT id, name FROM shop.main.customers ORDER BY id"}'
{
  "query_id": "<query-id>",
  "columns": ["id", "name"],
  "rows": [
    [1, "Alice"],
    [2, "Bob"]
  ],
  "row_count": 2,
  "truncated": false
}

When the client uses the new key, remove the old entry and reload. Then the old key gets this answer:

curl -s http://localhost:8080/v1/query \
  -H "X-Api-Key: report-bot-2026a.old-secret" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT id, name FROM shop.main.customers ORDER BY id"}'
{
  "code": "ERR_UNAUTHORIZED",
  "message": "authentication required"
}

What happened: Sluice reads every hashRef again on a reload. The new key still works.

Secrets

Secret Key New value works What to know
API key apiKeys[].hashRef After a reload (secret://file/) Use two entries with two ids, as in the example
Pepper identity.apiKeyPepper After a restart Every key hash changes. First compute all hashes again with sluice apikey hash
JWT key (RS, ES) JWKS of the issuer On first use Publish the new key next to the old one. A removed key works until jwksCacheTtl ends
JWT secret (HS) hmacSecretRef After a reload (secret://file/) One secret per issuer. Old tokens fail at once
Audit genesis audit.file.genesis Only in an empty audit directory Set a new audit.file.path too, then restart. Verify each directory with its own --anchor
Admin token admin.token After a restart /metrics needs the same token
Data source credentials credentialsRef, tokenRef, headersRef After a restart Keep the old credential valid at the source until then
Column mask salt and fpe key saltRef, keyRef After a reload (secret://file/) The same input gives a new masked value

Sluice reads a secret://env/ value from its own process environment. A new or changed variable needs a restart.

Limits

  • Sluice cuts spaces and line ends at the end of the pepper and the genesis. sluice apikey hash --pepper secret://... does the same.
  • Without a reload, Sluice can use an old mask salt for up to 10 minutes.
  • With claims.subjectId, the audit log does not show which key a client used.

See also