Skip to content

Commit f20ab82

Browse files
committed
fix(keycloak): require a persistent DEK set for the KMS proxy
Without encryption.deksetSecretName the proxy mints a new DEK set on every start and keeps it only in memory. After any restart of the proxy pod it can no longer decrypt the rows it wrote before, so the default configuration loses access to all encrypted user data on the first rollout, drain or OOM. Refuse to render the proxy without a DEK set unless encryption.fields is "disabled". Passthrough keeps a way for the static backend to get its KEK Secret created before the DEK set is minted with it. Assisted-by: LLM Signed-off-by: Andrei Kvapil <[email protected]>
1 parent cd427a2 commit f20ab82

3 files changed

Lines changed: 89 additions & 10 deletions

File tree

‎packages/system/keycloak/templates/proxy.yaml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@
5555
{{- if and (gt $replicas 1) (not .Values.encryption.deksetSecretName) }}
5656
{{- fail "encryption.replicas > 1 requires encryption.deksetSecretName (a shared wrapped DEK set); otherwise replicas would encrypt deterministic columns under different keys" }}
5757
{{- end }}
58+
{{- /*
59+
Without KKP_DEKSET_FILE the proxy mints a fresh DEK set on every start and
60+
keeps it only in memory, so after any restart (rollout, drain, OOM) it can
61+
no longer decrypt what it wrote before. Refuse to encrypt without a
62+
persistent DEK set. fields=disabled is pure passthrough and needs no key: it
63+
is how the static backend gets its KEK Secret created before the operator
64+
mints a DEK set with it.
65+
*/}}
66+
{{- if and (ne (.Values.encryption.fields | default "") "disabled") (not .Values.encryption.deksetSecretName) }}
67+
{{- fail "encryption.deksetSecretName is required unless encryption.fields is \"disabled\": without a persistent DEK set the proxy mints a new key on every start and cannot decrypt data written before a restart. Mint one with the proxy's `backfill generate-dekset`, store it in a Secret and set encryption.deksetSecretName" }}
68+
{{- end }}
5869
{{- if eq $backend "static" }}
5970
{{- /*
6071
Static KMS: a self-generated 32-byte KEK kept in a Secret. Generated once

‎packages/system/keycloak/tests/encryption_test.yaml‎

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ tests:
4141
template: templates/proxy.yaml
4242
set:
4343
encryption.enabled: true
44+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
4445
_cluster:
4546
root-host: example.org
4647
cluster-domain: cozy.local
@@ -55,6 +56,7 @@ tests:
5556
value: Deployment
5657
set:
5758
encryption.enabled: true
59+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
5860
_cluster:
5961
root-host: example.org
6062
cluster-domain: cozy.local
@@ -83,6 +85,7 @@ tests:
8385
value: Secret
8486
set:
8587
encryption.enabled: true
88+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
8689
_cluster:
8790
root-host: example.org
8891
cluster-domain: cozy.local
@@ -180,6 +183,7 @@ tests:
180183
value: Service
181184
set:
182185
encryption.enabled: true
186+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
183187
_cluster:
184188
root-host: example.org
185189
cluster-domain: cozy.local
@@ -198,6 +202,7 @@ tests:
198202
value: StatefulSet
199203
set:
200204
encryption.enabled: true
205+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
201206
_cluster:
202207
root-host: example.org
203208
cluster-domain: cozy.local
@@ -220,6 +225,7 @@ tests:
220225
value: Deployment
221226
set:
222227
encryption.enabled: true
228+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
223229
encryption.kms.backend: vault-transit
224230
encryption.kms.vault.address: https://vault.vault.svc:8200
225231
encryption.kms.vault.keyName: keycloak
@@ -245,6 +251,7 @@ tests:
245251
template: templates/proxy.yaml
246252
set:
247253
encryption.enabled: true
254+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
248255
encryption.kms.backend: vault-transit
249256
encryption.kms.vault.address: https://vault.vault.svc:8200
250257
encryption.kms.vault.keyName: keycloak
@@ -261,6 +268,7 @@ tests:
261268
template: templates/proxy.yaml
262269
set:
263270
encryption.enabled: true
271+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
264272
encryption.kms.backend: vault
265273
_cluster:
266274
root-host: example.org
@@ -273,6 +281,7 @@ tests:
273281
template: templates/proxy.yaml
274282
set:
275283
encryption.enabled: true
284+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
276285
encryption.kms.backend: vault-transit
277286
encryption.kms.vault.keyName: keycloak
278287
encryption.kms.vault.tokenSecretName: keycloak-vault-token
@@ -287,6 +296,7 @@ tests:
287296
template: templates/proxy.yaml
288297
set:
289298
encryption.enabled: true
299+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
290300
encryption.kms.backend: vault-transit
291301
encryption.kms.vault.address: https://vault.vault.svc:8200
292302
encryption.kms.vault.tokenSecretName: keycloak-vault-token
@@ -301,6 +311,7 @@ tests:
301311
template: templates/proxy.yaml
302312
set:
303313
encryption.enabled: true
314+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
304315
encryption.kms.backend: vault-transit
305316
encryption.kms.vault.address: https://vault.vault.svc:8200
306317
encryption.kms.vault.keyName: keycloak
@@ -318,6 +329,7 @@ tests:
318329
value: Deployment
319330
set:
320331
encryption.enabled: true
332+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
321333
encryption.kms.backend: vault-transit
322334
encryption.kms.vault.address: https://vault.vault.svc:8200
323335
encryption.kms.vault.keyName: keycloak
@@ -361,6 +373,7 @@ tests:
361373
template: templates/proxy.yaml
362374
set:
363375
encryption.enabled: true
376+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
364377
encryption.kms.backend: vault-transit
365378
encryption.kms.vault.address: https://vault.vault.svc:8200
366379
encryption.kms.vault.keyName: keycloak
@@ -379,6 +392,7 @@ tests:
379392
value: Deployment
380393
set:
381394
encryption.enabled: true
395+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
382396
encryption.kms.backend: vault-transit
383397
encryption.kms.vault.address: https://vault.vault.svc:8200
384398
encryption.kms.vault.keyName: keycloak
@@ -414,6 +428,7 @@ tests:
414428
template: templates/proxy.yaml
415429
set:
416430
encryption.enabled: true
431+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
417432
encryption.kms.backend: vault-transit
418433
encryption.kms.vault.address: https://vault.vault.svc:8200
419434
encryption.kms.vault.keyName: keycloak
@@ -429,6 +444,7 @@ tests:
429444
template: templates/proxy.yaml
430445
set:
431446
encryption.enabled: true
447+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
432448
encryption.kms.backend: vault-transit
433449
encryption.kms.vault.address: https://vault.vault.svc:8200
434450
encryption.kms.vault.keyName: keycloak
@@ -447,6 +463,7 @@ tests:
447463
value: ServiceAccount
448464
set:
449465
encryption.enabled: true
466+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
450467
_cluster:
451468
root-host: example.org
452469
cluster-domain: cozy.local
@@ -465,6 +482,7 @@ tests:
465482
value: ServiceAccount
466483
set:
467484
encryption.enabled: true
485+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
468486
encryption.kms.backend: vault-transit
469487
encryption.kms.vault.address: https://vault.vault.svc:8200
470488
encryption.kms.vault.keyName: keycloak
@@ -485,6 +503,7 @@ tests:
485503
value: Deployment
486504
set:
487505
encryption.enabled: true
506+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
488507
_cluster:
489508
root-host: example.org
490509
cluster-domain: cozy.local
@@ -503,6 +522,7 @@ tests:
503522
value: Deployment
504523
set:
505524
encryption.enabled: true
525+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
506526
encryption.kms.backend: vault-transit
507527
encryption.kms.vault.address: https://vault.vault.svc:8200
508528
encryption.kms.vault.keyName: keycloak
@@ -516,13 +536,39 @@ tests:
516536
path: spec.template.spec.automountServiceAccountToken
517537
value: true
518538

519-
- it: mints an ephemeral DEK (no dekset) by default
539+
- it: refuses to encrypt without a persistent DEK set
540+
# Without KKP_DEKSET_FILE the proxy mints a new DEK on every start and
541+
# cannot decrypt rows written before a restart.
542+
template: templates/proxy.yaml
543+
set:
544+
encryption.enabled: true
545+
_cluster:
546+
root-host: example.org
547+
cluster-domain: cozy.local
548+
asserts:
549+
- failedTemplate:
550+
errorPattern: "encryption.deksetSecretName is required unless encryption.fields is"
551+
552+
- it: refuses to encrypt only emails without a persistent DEK set
553+
template: templates/proxy.yaml
554+
set:
555+
encryption.enabled: true
556+
encryption.fields: email-only
557+
_cluster:
558+
root-host: example.org
559+
cluster-domain: cozy.local
560+
asserts:
561+
- failedTemplate:
562+
errorPattern: "encryption.deksetSecretName is required unless encryption.fields is"
563+
564+
- it: runs as a passthrough without a DEK set when fields are disabled
520565
template: templates/proxy.yaml
521566
documentSelector:
522567
path: kind
523568
value: Deployment
524569
set:
525570
encryption.enabled: true
571+
encryption.fields: disabled
526572
_cluster:
527573
root-host: example.org
528574
cluster-domain: cozy.local
@@ -534,6 +580,11 @@ tests:
534580
- equal:
535581
path: spec.template.spec.containers[0].name
536582
value: proxy
583+
- contains:
584+
path: spec.template.spec.containers[0].env
585+
content:
586+
name: KKP_FIELDS
587+
value: disabled
537588
- notContains:
538589
path: spec.template.spec.containers[0].env
539590
any: true
@@ -585,6 +636,7 @@ tests:
585636
set:
586637
encryption.enabled: true
587638
encryption.replicas: 2
639+
encryption.fields: disabled
588640
_cluster:
589641
root-host: example.org
590642
cluster-domain: cozy.local
@@ -631,6 +683,7 @@ tests:
631683
- it: trusts a private Vault CA supplied inline as caBundle
632684
set:
633685
encryption.enabled: true
686+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
634687
encryption.kms.backend: vault-transit
635688
encryption.kms.vault.address: https://vault.example
636689
encryption.kms.vault.keyName: keycloak
@@ -705,6 +758,7 @@ tests:
705758
- it: mounts a pre-seeded CA Secret when caSecretName is set
706759
set:
707760
encryption.enabled: true
761+
encryption.fields: disabled
708762
encryption.kms.backend: vault-transit
709763
encryption.kms.vault.address: https://vault.example
710764
encryption.kms.vault.keyName: keycloak
@@ -763,6 +817,7 @@ tests:
763817
- it: fails when both caBundle and caSecretName are set
764818
set:
765819
encryption.enabled: true
820+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
766821
encryption.kms.backend: vault-transit
767822
encryption.kms.vault.address: https://vault.example
768823
encryption.kms.vault.keyName: keycloak
@@ -781,6 +836,7 @@ tests:
781836
- it: leaves the proxy on the system trust store when no CA is configured
782837
set:
783838
encryption.enabled: true
839+
encryption.fields: disabled
784840
encryption.kms.backend: vault-transit
785841
encryption.kms.vault.address: https://vault.example
786842
encryption.kms.vault.keyName: keycloak
@@ -811,6 +867,7 @@ tests:
811867
- it: ignores a pre-seeded Vault CA Secret under the static backend
812868
set:
813869
encryption.enabled: true
870+
encryption.fields: disabled
814871
encryption.kms.vault.caSecretName: vault-ca
815872
_cluster:
816873
root-host: example.org
@@ -833,6 +890,7 @@ tests:
833890
- it: ignores an inline Vault CA bundle under the static backend
834891
set:
835892
encryption.enabled: true
893+
encryption.fields: disabled
836894
encryption.kms.vault.caBundle: PEM
837895
_cluster:
838896
root-host: example.org
@@ -854,6 +912,7 @@ tests:
854912
- it: does not enforce Vault CA mutual exclusion under the static backend
855913
set:
856914
encryption.enabled: true
915+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
857916
encryption.kms.vault.caSecretName: vault-ca
858917
encryption.kms.vault.caBundle: PEM
859918
_cluster:
@@ -868,6 +927,7 @@ tests:
868927
- it: rolls the pods when a chart-owned caBundle is rotated
869928
set:
870929
encryption.enabled: true
930+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
871931
encryption.kms.backend: vault-transit
872932
encryption.kms.vault.address: https://vault.example
873933
encryption.kms.vault.keyName: keycloak
@@ -893,6 +953,7 @@ tests:
893953
- it: rejects a caBundle that is not a PEM certificate bundle
894954
set:
895955
encryption.enabled: true
956+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
896957
encryption.kms.backend: vault-transit
897958
encryption.kms.vault.address: https://vault.example
898959
encryption.kms.vault.keyName: keycloak
@@ -910,6 +971,7 @@ tests:
910971
- it: reads a pre-seeded bundle from a custom caSecretKey
911972
set:
912973
encryption.enabled: true
974+
encryption.deksetSecretName: keycloak-kms-proxy-dekset
913975
encryption.kms.backend: vault-transit
914976
encryption.kms.vault.address: https://vault.example
915977
encryption.kms.vault.keyName: keycloak

‎packages/system/keycloak/values.yaml‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,17 @@ extraEnv: []
9292
# Project: https://github.com/cozystack/keycloak-kms-proxy
9393
#
9494
# OPERATIONAL CAVEATS — read before enabling:
95-
# * Availability downgrade. The proxy runs as a single replica (HA needs a
96-
# shared wrapped DEK set, tracked as a follow-up). Keycloak itself runs HA
97-
# (2 replicas); with encryption on, both replicas funnel their DB traffic
98-
# through this one pod, so any proxy restart — node drain, rollout, image
99-
# pull, OOM — briefly drops the database connection for the entire IdP.
95+
# * Availability downgrade. The proxy runs as a single replica by default.
96+
# Keycloak itself runs HA (2 replicas); with encryption on, both replicas
97+
# funnel their DB traffic through this one pod, so any proxy restart — node
98+
# drain, rollout, image pull, OOM — briefly drops the database connection
99+
# for the entire IdP. Set encryption.replicas to run more than one.
100+
# * A persistent DEK set is required. Unless fields is "disabled", the chart
101+
# refuses to render without deksetSecretName: without it the proxy would
102+
# mint a new DEK on every start and lose access to everything it encrypted
103+
# before a restart. With kms.backend=static, deploy once with
104+
# fields: disabled (passthrough) so the KEK Secret exists, mint the DEK set
105+
# with that KEK, then set deksetSecretName and switch fields on.
100106
# * Existing data is NOT migrated automatically. Flipping enabled:true on a
101107
# populated database leaves the pre-existing PII as plaintext; logins that
102108
# match on the deterministic columns (username/email) then fail until the
@@ -124,10 +130,10 @@ encryption:
124130
# DEK set) so every replica encrypts deterministic columns under one key.
125131
replicas: 1
126132
# Existing Secret holding the wrapped DEK set (dekset.json) minted by the
127-
# proxy's backfill tool (`cmd/backfill generate-dekset`). REQUIRED to migrate
128-
# an existing populated database — the proxy must reuse the DEK the backfill
129-
# encrypted rows with — and to run more than one replica. When empty the proxy
130-
# mints an ephemeral DEK at startup, which is only safe on a fresh, empty DB.
133+
# proxy's backfill tool (`cmd/backfill generate-dekset`). REQUIRED unless
134+
# fields is "disabled": the proxy must load the same DEKs on every start to
135+
# read what it encrypted before, and the backfill and every replica must
136+
# share them. Back it up together with the KEK.
131137
deksetSecretName: ""
132138
deksetSecretKey: dekset.json
133139
kms:

0 commit comments

Comments
 (0)