License JWT claims
mcpgw licenses are Ed25519-signed JWTs. The gateway verifies them entirely offline using a public key baked into the binary at build time. There is no live revocation API; revocation is achieved by short exp windows.
For the operational guide see How-to: rotate license. For the design rationale see Explanation: licensing.
Header
{ "alg": "EdDSA", "typ": "JWT" }
alg is always EdDSA. mcpgw uses golang-jwt/jwt/v5’s WithValidMethods(["EdDSA"]) to refuse any other algorithm — none, HS256, and similar are rejected at parse time.
Claims
{
"iss": "mcpgw-prod",
"sub": "acme-corp",
"aud": "mcpgw",
"exp": 1735689600,
"nbf": 1733097600,
"iat": 1733097600,
"plan": "team",
"servers": ["*"],
"grace_days": 30
}
| Claim | Type | Required | Notes |
|---|---|---|---|
iss | string | yes | Issuer. Must equal mcpgw-prod (or the test issuer for non-production builds). |
sub | string | yes | Subject — the customer/tenant identifier. Appears in audit log as license.subject on rotation events. |
aud | string | yes | Audience. Must be exactly "mcpgw". mcpgw rejects any other value. |
exp | int (Unix seconds) | yes | Expiry. Combined with grace_days to produce the fail-closed cutoff. |
nbf | int (Unix seconds) | no | Not-before. mcpgw allows up to 60s clock skew. |
iat | int (Unix seconds) | no | Issued-at. Informational only. |
plan | string | no | Plan tier — community, team, enterprise. Informational; mcpgw does not gate features by plan in v1. |
servers | list of strings | no | Allowed upstream names. ["*"] means any. Today the gateway logs but does not enforce; enforcement is on the roadmap. |
grace_days | int | no | Override for the default 30-day grace window. |
Verification flow
On startup and on every hour-tick:
- Read the file at
license.path. - Refuse to proceed if the file mode is not
0600or stricter. - Parse the JWT, refusing any non-EdDSA algorithm.
- Verify the Ed25519 signature against the baked-in public key.
- Validate the
audclaim equals"mcpgw". - Validate
nbf(with 60s clock skew tolerance). - Compare
expagainst current time:now < exp: license is valid, gateway operates normally.exp <= now < exp + grace_days: license is in grace, gateway logs a warning every minute and setsmcp.license.grace = trueon every span.now >= exp + grace_days: license is expired beyond grace,/readyzreturns 503, every/mcprequest returns 503 withlicense_invalid.
What revocation looks like
mcpgw never phones home. Therefore there is no online revocation. To revoke a license:
- Issue all production licenses with short
expwindows (e.g. 90 days). - Establish a regular rotation cadence (e.g. every 60 days).
- To revoke, simply do not re-issue. The gateway will fail-closed at
exp + grace_days.
For faster kill-switch behavior, reduce grace_days to a small value (e.g. 1 day) and shorten exp to ~7 days.
This is a deliberate design choice — see Explanation: licensing for the trade-off analysis.
Build-time public key wiring
The gateway binary embeds the public key via -ldflags. The release workflow injects it from secrets.LICENSE_PUBKEY_HEX:
go build -trimpath \
-ldflags "-s -w -X main.licensePubKeyHex=${LICENSE_PUBKEY_HEX}" \
-o mcpgw ./cmd/mcpgw
LICENSE_PUBKEY_HEX is a 64-character hex string (32 raw bytes of Ed25519 public key). Validation rules:
- Empty →
licensePubKeyremainsnil;license.Verifyfails closed. - Wrong length or bad hex → init() panics with a clear message; the binary refuses to run.
- Correct → public key is decoded once at process start and used for every signature check.
This means a production binary is bound to a single public key. Re-keying requires re-releasing the binary. This is intentional — see Explanation: licensing for the security argument.
Failure modes
| Failure | Cause | Operator action |
|---|---|---|
license: file permissions too permissive | Mode is more open than 0600 | chmod 0600 license.jwt |
license: invalid signature | Wrong public key for this binary, or tampered JWT | Confirm the binary was built with the matching LICENSE_PUBKEY_HEX; re-mint if tampered |
license: invalid audience | aud claim is not "mcpgw" | Re-mint with correct audience |
license: not yet valid | nbf is in the future beyond skew tolerance | Sync host clock |
license: expired beyond grace | now >= exp + grace_days | Mint a new JWT |
license: invalid algorithm | JWT header alg is not EdDSA | Re-mint with the correct signer |
Related
- How-to: rotate license
- Health endpoints —
/readyzsemantics - Explanation: licensing