From 3c36c57065f31765d5f22debb8a2ec050a2b6260 Mon Sep 17 00:00:00 2001 From: EdmondDantes21 Date: Wed, 12 Aug 2026 10:02:44 +0200 Subject: [PATCH 1/2] feat: generate only authn's RSA private key, not a shared JWT secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit authn now signs RS256 JWTs with an RSA private key and publishes the public key itself via JWKS; consumers no longer need a copy of any key material. secret.yaml generates ONLY the private key (Secret authn-jwt-signing-key, key private.pem, matching authn chart defaults jwt.signKeySecretName/ signKeySecretKey) instead of the old shared jwt-sign-key HMAC secret — a second Secret holding the public key would be one more object to keep in sync and would pin verifiers to a keypair only a redeploy of all of them could rotate. `lookup` still reuses an existing key across re-renders so reconciles never rotate it. --- chart/templates/secret.yaml | 31 +++++++++++------- chart/values.schema.json | 64 +++++++++++++++++++++++++++++++------ docs/overview.md | 11 +++++-- docs/usage.md | 5 +-- 4 files changed, 86 insertions(+), 25 deletions(-) diff --git a/chart/templates/secret.yaml b/chart/templates/secret.yaml index e244552..5cb4a6a 100644 --- a/chart/templates/secret.yaml +++ b/chart/templates/secret.yaml @@ -1,21 +1,29 @@ {{/* - JWT signing key — replaces the installer's `create-jwt-sign-key` object step. - `lookup` reuses an existing key on re-render so reconciles never rotate it; - only generated on first create. + authn signs JWTs asymmetrically (RS256) with the RSA PRIVATE key in Secret + `authn-jwt-signing-key`, key `private.pem` (matching authn chart defaults + jwt.signKeySecretName/signKeySecretKey). + + ONLY the private key is created here. Consumers that verify authn's tokens + (snowplow et al.) read the PUBLIC key from authn's JWKS endpoint, + GET /.well-known/jwks.json, which authn derives from this private key at + startup. + + `lookup` reuses an existing key on re-render so reconciles never rotate it; a + fresh key is generated only on first create. */}} {{- if and ((include "inst.featureEnabled" (list $ "portal")) | trim) (not .Values.bootstrap.coreProvider.enabled) }} {{- $ns := .Values.namespaces.krateo }} -{{- $existing := lookup "v1" "Secret" $ns "jwt-sign-key" }} -{{- $key := "" }} -{{- if and $existing $existing.data }} -{{- $key = index $existing.data "JWT_SIGN_KEY" | b64dec }} +{{- $existingPriv := lookup "v1" "Secret" $ns "authn-jwt-signing-key" }} +{{- $privPEM := "" }} +{{- if and $existingPriv $existingPriv.data (hasKey $existingPriv.data "private.pem") }} +{{- $privPEM = index $existingPriv.data "private.pem" | b64dec }} {{- else }} -{{- $key = randAlphaNum 12 }} +{{- $privPEM = genPrivateKey "rsa" }} {{- end }} apiVersion: v1 kind: Secret metadata: - name: jwt-sign-key + name: authn-jwt-signing-key namespace: {{ $ns }} # NB: do NOT set app.kubernetes.io/managed-by here. Helm forces this label to # "Helm" on every release resource at apply time; a chart-set value (e.g. @@ -24,5 +32,6 @@ metadata: # helm's live value instead of fighting it. type: Opaque stringData: - JWT_SIGN_KEY: {{ $key | quote }} -{{- end }} \ No newline at end of file + private.pem: | +{{ $privPEM | indent 4 }} +{{- end }} diff --git a/chart/values.schema.json b/chart/values.schema.json index 4625b9c..8e97d8e 100644 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -599,10 +599,33 @@ "type": "string" } }, - "jwtSignKeySecretName": { - "type": "string", - "title": "JWT sign key secret name", - "description": "Name of the Secret holding the JWT signing key (created by the installer umbrella before authn)." + "jwt": { + "type": "object", + "title": "JWT signing key", + "description": "authn signs tokens with an RSA private key (RS256) and publishes the matching public key as a JWKS at GET /.well-known/jwks.json. The private key is mounted from a Secret as a file (never injected as an env var).", + "additionalProperties": false, + "properties": { + "signKeySecretName": { + "type": "string", + "title": "JWT sign key secret name", + "description": "Name of the Secret holding the PEM-encoded RSA private key (created by the installer umbrella before authn)." + }, + "signKeySecretKey": { + "type": "string", + "title": "JWT sign key secret key", + "description": "Key inside that Secret whose value is the PEM private key; it is also the mounted filename." + }, + "mountPath": { + "type": "string", + "title": "JWT signing key mount path", + "description": "Directory the Secret is mounted into. The container reads / (exposed as JWT_SIGN_KEY_FILE)." + }, + "kid": { + "type": "string", + "title": "JWT key ID", + "description": "Key ID advertised in every token header and in the JWKS \"kid\". Must be stable for the lifetime of the key so validators can match it." + } + } }, "global": { "type": "object", @@ -4458,7 +4481,7 @@ } } }, - "description": "Additional envFrom entries appended to the chart-managed ConfigMap and jwt-sign-key Secret.", + "description": "Additional envFrom entries appended to the chart-managed ConfigMap.", "title": "Extra Env From" }, "initContainers": { @@ -4556,10 +4579,33 @@ "description": "Env vars rendered into the chart's snowplow ConfigMap and consumed via envFrom.", "title": "Env" }, - "jwtSignKeySecretName": { - "type": "string", - "description": "Name of the Secret holding the JWT signing key.", - "title": "JWT Sign Key Secret Name" + "jwt": { + "type": "object", + "additionalProperties": false, + "description": "snowplow verifies RS256 JWTs issued by authn using authn's RSA public key, fetched from authn's JWKS endpoint (/.well-known/jwks.json) and cached. No key material is mounted: snowplow holds neither a private/signing key nor a copy of the public key, so rotating authn's keypair needs no snowplow redeploy. The key set is fetched lazily on first validation, not at startup, so snowplow does not depend on authn being up first.", + "title": "JWT verification keys (JWKS)", + "properties": { + "jwksUrl": { + "type": "string", + "description": "Full JWKS URL. Empty derives it from URL_AUTHN as /.well-known/jwks.json; set only to point at a different authn.", + "title": "JWKS URL" + }, + "cacheTTL": { + "type": "string", + "description": "How long a fetched key set is served before it is refreshed (Go duration, e.g. \"5m\").", + "title": "JWKS cache TTL" + }, + "minRefreshInterval": { + "type": "string", + "description": "Minimum gap between two JWKS fetch attempts (Go duration, e.g. \"30s\"). Throttles the refetch an unknown \"kid\" triggers so it cannot become one fetch per request.", + "title": "JWKS minimum refresh interval" + }, + "requestTimeout": { + "type": "string", + "description": "Timeout for a single JWKS fetch (Go duration, e.g. \"5s\"). Also bounds how long a token validation can block.", + "title": "JWKS request timeout" + } + } }, "global": { "type": "object", diff --git a/docs/overview.md b/docs/overview.md index 69e5911..d9f477c 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -89,9 +89,14 @@ Pass B also computes the platform wiring at render time (no post-install patchin exposure `service.type`/`port` flips, browser-reachable peer URLs for the frontend config (`inst.peerurl` / `inst.lbip` / `inst.nodeip`), Vertex/local-model injection, the HITL gate and the autopilot's auto-derived `extraAgents` fleet — the whole surface -is described in [configuration](./configuration.md). `secret.yaml` generates the -`jwt-sign-key` Secret once and reuses it via `lookup` on every later render (reconciles -never rotate it). +is described in [configuration](./configuration.md). `secret.yaml` generates an RSA +private key once and reuses it via `lookup` on every later render (reconciles never +rotate it); it lands in the `authn-jwt-signing-key` Secret (`private.pem`) for authn to +sign RS256 JWTs with. Only the private key is created: verifiers such as snowplow read +the **public** key from authn's JWKS endpoint (`/.well-known/jwks.json`), which authn +derives from this private key at startup. A second Secret holding the public key would +be one more object to keep in sync, and would pin verifiers to a keypair only a +redeploy of all of them could rotate. The **self-reconcile loop**: the `installer` CompositionDefinition points at this chart, so the Installer CR *is* a composition like any other. The cdc re-renders it on diff --git a/docs/usage.md b/docs/usage.md index f3453f9..148c507 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -146,8 +146,9 @@ helm template installer chart/ --namespace krateo-system \ ``` Client-side `helm template` has no live cluster to `lookup`, so composition-mode -renders emit Pass A (and the `jwt-sign-key` Secret) but no Pass B Compositions — every -Pass B gate reads "CRD not served yet". That is the gating working as designed, not a +renders emit Pass A (and the `authn-jwt-signing-key` Secret) +but no Pass B Compositions — every Pass B gate reads "CRD not served yet". That is the +gating working as designed, not a failure. In-repo `Chart.yaml` carries the `CHART_VERSION` placeholder ([release](./release.md)); substitute any semver before templating a working copy. From c166268d467a7dbe58006aeb5f65954c9aa644da Mon Sep 17 00:00:00 2001 From: Diego Braga Date: Thu, 13 Aug 2026 20:41:52 +0200 Subject: [PATCH 2/2] chore: pin authn 0.27.0 + snowplow 1.10.0 (RS256/JWKS auth cutover) --- chart/files/component-pins.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chart/files/component-pins.yaml b/chart/files/component-pins.yaml index 969626d..7406286 100644 --- a/chart/files/component-pins.yaml +++ b/chart/files/component-pins.yaml @@ -2,7 +2,7 @@ components: - name: authn-crd kind: AuthnCrds chart: authn-crds - version: 0.26.2 + version: 0.27.0 tier: platform feature: portal - name: oasgen-provider-crd @@ -20,13 +20,13 @@ components: - name: snowplow-crd kind: SnowplowCrds chart: snowplow-crds - version: 1.9.3 + version: 1.10.0 tier: platform feature: portal - name: authn kind: Authn chart: authn - version: 0.26.2 + version: 0.27.0 deps: - authn-crd tier: platform @@ -40,7 +40,7 @@ components: - name: snowplow kind: Snowplow chart: snowplow - version: 1.9.3 + version: 1.10.0 deps: - snowplow-crd - authn