Skip to content
Closed
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
163 changes: 163 additions & 0 deletions docs/jwt-jwks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# authn JWT signing & JWKS

authn issues JSON Web Tokens for the Krateo platform. As of the asymmetric-signing
migration it signs with **RS256** using an RSA private key and publishes the matching
public key as a **JWKS** so that any validator — Snowplow, agentgateway, or a third
party — can verify tokens without sharing a secret.

## What a token looks like

- **Algorithm:** `RS256` (asymmetric). Symmetric `HS256` is no longer used.
- **Header:** carries a `kid` identifying the signing key (required by agentgateway).
- **Claims:** `username`, `groups`, `iss: "krateo.io"`, `sub`, `exp`, `iat`, `nbf`.
No `aud`.

The signing itself lives in the shared `plumbing/jwtutil` library
(`CreateToken` / `Validate`); authn only supplies the key material and `kid`.

## Configuration

authn reads two settings at startup:

| Flag | Env | Meaning |
| --- | --- | --- |
| `--jwt-sign-key-file` | `JWT_SIGN_KEY_FILE` | Path to the PEM-encoded RSA **private** key. |
| `--jwt-kid` | `JWT_KID` | Key ID stamped into every token header and the JWKS. |

Both are required; authn exits at boot if the key file is missing/unparseable or
the `kid` is empty. The private key is read from a **file** (mounted from a Secret),
never passed as a raw env value.

## Creating the signing-key Secret

Generate an RSA keypair and store the private key in a Secret. authn derives the
public key (and the JWKS) from it at runtime — you only ever store the private half.

```sh
# 1. Generate a 2048-bit RSA private key.
openssl genrsa -out private.pem 2048

# 2. Create the Secret in authn's namespace.
kubectl create secret generic jwt-sign-key \
--namespace krateo-system \
--from-file=private.pem=./private.pem
```

The Helm chart mounts this Secret at `/etc/authn/jwt/private.pem` and sets
`JWT_SIGN_KEY_FILE` accordingly. Relevant `values.yaml`:

```yaml
jwt:
signKeySecretName: jwt-sign-key # Secret name
signKeySecretKey: private.pem # key within the Secret + mounted filename
mountPath: /etc/authn/jwt # mount directory
kid: krateo-authn-key-1 # stable key ID (kid)
```

> Keep `kid` stable for the life of the key. Changing the key without changing the
> `kid` (or vice versa) makes previously issued tokens unverifiable.

## The JWKS endpoint

authn serves the public key set at:

```
GET /.well-known/jwks.json
```

on its normal service port (default `8082`) — no separate service or route change
is required. The response is a standard JWKS:

```json
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "krateo-authn-key-1",
"n": "<base64url-modulus>",
"e": "AQAB"
}
]
}
```

Quick check once deployed:

```sh
kubectl -n krateo-system port-forward svc/authn 8082:8082
curl -s http://localhost:8082/.well-known/jwks.json | jq
```

## Consuming the JWKS

### agentgateway (remote JWKS)

Point agentgateway at the endpoint via `jwks.remote` so it refetches on a cadence
(supports key rotation). The `issuer` matches authn's `iss`; omit `audiences`
because authn emits no `aud`.

```yaml
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayPolicy
metadata:
name: authn-jwt
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: mcp
traffic:
jwtAuthentication:
mode: Strict
providers:
- issuer: "krateo.io"
jwks:
remote:
backendRef:
name: authn # authn's Service
kind: Service
namespace: krateo-system
port: 8082
jwksPath: /.well-known/jwks.json
cacheDuration: 5m
```

`groups`-based RBAC is enforced by a separate authorization policy over the decoded
claims.

### Snowplow

Snowplow validates the same tokens through the shared `plumbing/jwtutil` (via the
`server/use.UserConfig` middleware), which now verifies **RS256 with a public key**
instead of the old shared secret. Snowplow therefore takes authn's **public** key
(PEM), mounted as a file just like authn's private key:

| Flag | Env | Meaning |
| --- | --- | --- |
| `--jwt-public-key-file` | `JWT_PUBLIC_KEY_FILE` | Path to the PEM-encoded RSA **public** key (authn's public half). |

Derive the public key from the same private key authn signs with and distribute it
to Snowplow (e.g. a Secret/ConfigMap mounted as a file):

```sh
openssl rsa -in private.pem -pubout -out public.pem
kubectl create secret generic authn-jwt-public-key \
--namespace krateo-system \
--from-file=public.pem=./public.pem
```

The key is parsed once at middleware setup, not per request. (A future option is
to have Snowplow consume authn's JWKS endpoint directly for rotation; the current
`UserConfig` uses a static public key.)

## Migration notes (HS256 → RS256)

- The old shared secret (`JWT_SIGN_KEY` / `AUTHN_JWT_SECRET`) is gone. Replace the
`jwt-sign-key` Secret's contents with the PEM private key described above.
- **Version alignment:** authn, Snowplow, and any other validator must all build
against the `plumbing` version that carries the asymmetric `jwtutil`/`UserConfig`
API. A validator still on the HS256 build will reject the new RS256 tokens.
- Snowplow's `--jwt-sign-key` / `JWT_SIGN_KEY` must be repointed to authn's PEM
**public** key once it is rebuilt against the new `plumbing`.
90 changes: 46 additions & 44 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/krateoplatformops/authn

go 1.24.2
go 1.25.6

require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
Expand All @@ -10,81 +10,83 @@ require (
github.com/google/uuid v1.6.0
github.com/krateoplatformops/plumbing v0.3.3
github.com/rs/zerolog v1.32.0
github.com/stretchr/testify v1.10.0
golang.org/x/oauth2 v0.27.0
k8s.io/api v0.33.0
k8s.io/apimachinery v0.33.0
k8s.io/client-go v0.33.0
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
sigs.k8s.io/controller-runtime v0.20.4
github.com/stretchr/testify v1.11.1
golang.org/x/oauth2 v0.34.0
k8s.io/api v0.35.3
k8s.io/apimachinery v0.35.3
k8s.io/client-go v0.35.3
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
sigs.k8s.io/controller-runtime v0.22.3
sigs.k8s.io/controller-tools v0.17.3
sigs.k8s.io/e2e-framework v0.6.0
)

require github.com/krateoplatformops/snowplow v0.0.0-20250508092448-4cff77fa45a5

replace github.com/krateoplatformops/plumbing => ../plumbing

require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/gobuffalo/flect v1.0.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/spdystream v0.5.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/vladimirvivien/gexe v0.4.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.30.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.33.0 // indirect
k8s.io/component-base v0.33.0 // indirect
k8s.io/apiextensions-apiserver v0.35.1 // indirect
k8s.io/component-base v0.35.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
Loading
Loading