Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions chart/files/component-pins.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -40,7 +40,7 @@ components:
- name: snowplow
kind: Snowplow
chart: snowplow
version: 1.9.3
version: 1.10.0
deps:
- snowplow-crd
- authn
Expand Down
31 changes: 20 additions & 11 deletions chart/templates/secret.yaml
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -24,5 +32,6 @@ metadata:
# helm's live value instead of fighting it.
type: Opaque
stringData:
JWT_SIGN_KEY: {{ $key | quote }}
{{- end }}
private.pem: |
{{ $privPEM | indent 4 }}
{{- end }}
64 changes: 55 additions & 9 deletions chart/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <mountPath>/<signKeySecretKey> (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",
Expand Down Expand Up @@ -4485,7 +4508,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": {
Expand Down Expand Up @@ -4583,10 +4606,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 <URL_AUTHN>/.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",
Expand Down
11 changes: 8 additions & 3 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading