Jwtlet is an RFC 8693 OAuth 2.0 token exchange service for participant context operations. Clients exchange a Kubernetes service account token for a signed JWT that encodes participant context claims. The service validates the incoming token against a Kubernetes OIDC issuer, resolves resource mappings and scope-to-claims mappings, and signs the resulting token via HashiCorp Vault.
Jwtlet runs two HTTP servers: a token exchange API (default port 8080) and a management API (default port 8081) for administering resource and scope mappings.
Requires Rust (stable toolchain).
# Build all crates
cargo build
# Build the release binary
cargo build --release -p jwtlet-serverUnit and integration tests:
cargo nextest runor
cargo testEnd-to-end tests run against a local kind Kubernetes cluster with Vault. Requires Docker
and kind installed.
cd e2e
make all # full cycle: cluster setup, build, test, cleanup
# or individual steps:
make setup # create KIND cluster and initialize Vault
make build # build and load the Docker image into the cluster
make test # run tests
make cleanup # tear down the clusterPass a TOML config file as the first argument, or set JWTLET_CONFIG_FILE:
jwtlet-server /path/to/config.toml
# or
JWTLET_CONFIG_FILE=/path/to/config.toml jwtlet-serverIndividual settings can also be overridden with environment variables using the JWTLET__ prefix (double underscore as
the nesting separator), e.g. JWTLET__TOKEN_EXCHANGE_PORT=9090.
# Ports and bind address
token_exchange_port = 8080 # default
management_port = 8081 # default; must differ from token_exchange_port
bind = "0.0.0.0"
# Storage backend: "memory" (default) or "postgres"
[storage_backend]
type = "memory"
# type = "postgres"
# url = "postgresql://user:pass@host:5432/jwtlet"
# Kubernetes OIDC validation
[k8s]
api_server_url = "https://kubernetes.default.svc" # required
cluster_issuer = "https://kubernetes.default.svc.cluster.local" # required
token_file = "/var/run/secrets/kubernetes.io/serviceaccount/token"
# Token issuance
[token]
client_audience = "jwtlet-exchange" # required – expected aud of incoming tokens; use a dedicated value, never the API server audience
audience = "my-service" # required – aud of issued tokens
participant_context_claim = "jwtlet_pc" # default
token_ttl_secs = 3600 # default
# Vault signing backend
[vault]
url = "http://vault:8200" # required
token_file = "/vault/secrets/.vault-token" # use token_file in production
# token = "s.xxxxx" # or a literal token for development
# Management API
[management]
client_audience = "jwtlet-management" # optional – expected aud of management bearer tokens; defaults to token.client_audience (a distinct value is recommended)
# Management API authorization
# Keys are Kubernetes service account identifiers; values are lists of roles:
# jwtlet:management:read – GET /api/v1/mappings, GET /api/v1/scopes
# jwtlet:management:mappings:write – create/update/delete resource mappings
# jwtlet:management:scope:write – create/update/delete scope mappings
[service_accounts]
"system:serviceaccount:my-namespace:my-sa" = ["jwtlet:management:mappings:write", "jwtlet:management:scope:write"]All management API endpoints (/api/v1/mappings, /api/v1/scopes) require a
Authorization: Bearer <token> header. The token must be a Kubernetes service
account token issued for management.client_audience (or token.client_audience when it
is not set). Jwtlet verifies the token via
the Kubernetes TokenReview API (including the returned audience) and checks that the
resolved service account identity holds the role required by the endpoint.
To obtain a suitable token from within a cluster:
kubectl create token my-sa -n my-namespace --audience=jwtlet-managementSet RUST_LOG to control verbosity (trace, debug, info, warn, error):
RUST_LOG=debug jwtlet-server config.tomlMulti-architecture images (linux/amd64, linux/arm64) are published to the GitHub
Container Registry:
docker pull ghcr.io/eclipse-cfm/jwtlet:latest| Tag | Points at |
|---|---|
latest |
the most recent stable release |
0.2.0 |
that exact release |
0.2 |
the latest patch on the 0.2 minor line |
main |
the current main branch |
sha-<short> |
a specific commit |
Pre-releases (for example 0.2.0-rc.1) are published under their exact version only —
they never move latest or the minor-line tag.
Releases are cut by pushing a git tag. The Release workflow verifies the tag, builds and
publishes the image, and then creates the GitHub Release.
-
Bump
versionunder[workspace.package]in the rootCargo.toml. All crates inherit it viaversion.workspace = true, so this is the only version to change. -
Run
cargo checksoCargo.lockpicks up the new workspace-member versions. -
Commit as
chore: release 0.2.0and merge tomain. -
Tag the merge commit and push it:
git tag v0.2.0 git push origin v0.2.0
The tag must match the Cargo.toml version exactly, or the workflow fails before anything
is published. Use a v0.2.0-rc.1 style tag to cut a pre-release.
Apache-2.0