Some checks failed
Run Check Script / check (pull_request) Failing after 13s
127 lines
3.8 KiB
Markdown
127 lines
3.8 KiB
Markdown
# Manually mint a Fleet token
|
|
|
|
Use this procedure to test the Zitadel-to-NATS authentication chain. It signs a
|
|
JWT-bearer assertion with a machine key, exchanges it for an OIDC ID token, and
|
|
passes that ID token to the NATS CLI.
|
|
|
|
Do not use `access_token` from the response. Zitadel returns an opaque access
|
|
token by default; production Fleet requires the verifiable `id_token`.
|
|
|
|
## Inputs
|
|
|
|
Read the live values rather than relying on a local Zitadel cache:
|
|
|
|
| Input | Source |
|
|
|---|---|
|
|
| `OIDC_ISSUER_URL` | Callout environment |
|
|
| `PROJECT_ID` | Callout `OIDC_AUDIENCE` environment value |
|
|
| `USER_ID` | Machine key `userId` |
|
|
| `KEY_ID` | Machine key `keyId` |
|
|
| RSA private key | Machine key `key` |
|
|
|
|
For a cached operator key:
|
|
|
|
```bash
|
|
jq -r '.machine_keys["fleet-ops"]' \
|
|
~/.local/share/harmony/zitadel/client-config.json > /tmp/fleet-ops.json
|
|
jq -r '.key' /tmp/fleet-ops.json > /tmp/fleet-ops.pem
|
|
```
|
|
|
|
Protect and remove these temporary files when finished.
|
|
|
|
## Mint with PyJWT
|
|
|
|
Install `PyJWT`, not the unrelated `jwt` package.
|
|
|
|
```python
|
|
import json
|
|
import time
|
|
|
|
import jwt
|
|
import requests
|
|
|
|
OIDC_ISSUER_URL = "https://sso.example.com"
|
|
PROJECT_ID = "..." # OIDC_AUDIENCE from the running callout
|
|
machine_key = json.load(open("/tmp/fleet-ops.json"))
|
|
private_key = open("/tmp/fleet-ops.pem").read()
|
|
now = int(time.time())
|
|
|
|
assertion = jwt.encode(
|
|
{
|
|
"iss": machine_key["userId"],
|
|
"sub": machine_key["userId"],
|
|
"aud": OIDC_ISSUER_URL,
|
|
"iat": now,
|
|
"exp": now + 60,
|
|
},
|
|
private_key,
|
|
algorithm="RS256",
|
|
headers={"kid": machine_key["keyId"]},
|
|
)
|
|
|
|
response = requests.post(
|
|
f"{OIDC_ISSUER_URL.rstrip('/')}/oauth/v2/token",
|
|
data={
|
|
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
"assertion": assertion,
|
|
"scope": (
|
|
"openid "
|
|
"urn:zitadel:iam:org:projects:roles "
|
|
f"urn:zitadel:iam:org:project:id:{PROJECT_ID}:aud"
|
|
),
|
|
},
|
|
timeout=10,
|
|
)
|
|
response.raise_for_status()
|
|
token = response.json()["id_token"]
|
|
|
|
print(jwt.decode(token, options={"verify_signature": False}))
|
|
print(token)
|
|
```
|
|
|
|
The decoded ID token must have the exact issuer configured on the callout, the
|
|
Fleet project in `aud`, a `client_id`, and the Fleet project's role claim. The
|
|
callout accepts `fleet-admin` or `device`; it strips the configured `device-`
|
|
prefix from `client_id` for device identities.
|
|
|
|
Decoding without signature verification is diagnostic only. The callout still
|
|
verifies the signature against Zitadel's JWKS.
|
|
|
|
## Connect to NATS
|
|
|
|
Capture the last output line from the script:
|
|
|
|
```bash
|
|
TOKEN=$(python3 mint.py | tail -1)
|
|
nats --server wss://nats.example.com --token "$TOKEN" kv ls device-info
|
|
```
|
|
|
|
An administrator can inspect a device entry:
|
|
|
|
```bash
|
|
nats --server wss://nats.example.com --token "$TOKEN" \
|
|
kv get device-info info.<device-id>
|
|
```
|
|
|
|
Do not use manual KV writes as the deployment workflow. Apply a namespaced
|
|
Fleet `Deployment` resource so the operator owns target planning, rollout state,
|
|
and status.
|
|
|
|
## Failures
|
|
|
|
| Symptom | Check |
|
|
|---|---|
|
|
| Token response has no `id_token` | Include `openid`; confirm the API application permits an ID token |
|
|
| ID token has no project roles | Include the plural `urn:zitadel:iam:org:projects:roles` scope and verify the role grant |
|
|
| `InvalidAudience` | Use the live callout `OIDC_AUDIENCE` and include the project audience scope |
|
|
| `InvalidIssuer` | Match `OIDC_ISSUER_URL` exactly, including scheme, host, port, and trailing-slash behavior |
|
|
| Missing `client_id` | Confirm the token is an ID token minted for the Fleet API application |
|
|
| Assertion expired | Synchronize the client clock; the assertion window is 60 seconds |
|
|
| `AbstractJWKBase` type error | Remove the `jwt` package and install `PyJWT` |
|
|
|
|
The callout log contains the validation failure:
|
|
|
|
```bash
|
|
kubectl -n <namespace> logs deploy/fleet-callout
|
|
```
|