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.


{ "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
}
ClaimTypeRequiredNotes
issstringyesIssuer. Must equal mcpgw-prod (or the test issuer for non-production builds).
substringyesSubject — the customer/tenant identifier. Appears in audit log as license.subject on rotation events.
audstringyesAudience. Must be exactly "mcpgw". mcpgw rejects any other value.
expint (Unix seconds)yesExpiry. Combined with grace_days to produce the fail-closed cutoff.
nbfint (Unix seconds)noNot-before. mcpgw allows up to 60s clock skew.
iatint (Unix seconds)noIssued-at. Informational only.
planstringnoPlan tier — community, team, enterprise. Informational; mcpgw does not gate features by plan in v1.
serverslist of stringsnoAllowed upstream names. ["*"] means any. Today the gateway logs but does not enforce; enforcement is on the roadmap.
grace_daysintnoOverride for the default 30-day grace window.

Verification flow

On startup and on every hour-tick:

  1. Read the file at license.path.
  2. Refuse to proceed if the file mode is not 0600 or stricter.
  3. Parse the JWT, refusing any non-EdDSA algorithm.
  4. Verify the Ed25519 signature against the baked-in public key.
  5. Validate the aud claim equals "mcpgw".
  6. Validate nbf (with 60s clock skew tolerance).
  7. Compare exp against 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 sets mcp.license.grace = true on every span.
    • now >= exp + grace_days: license is expired beyond grace, /readyz returns 503, every /mcp request returns 503 with license_invalid.

What revocation looks like

mcpgw never phones home. Therefore there is no online revocation. To revoke a license:

  1. Issue all production licenses with short exp windows (e.g. 90 days).
  2. Establish a regular rotation cadence (e.g. every 60 days).
  3. 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 → licensePubKey remains nil; license.Verify fails 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

FailureCauseOperator action
license: file permissions too permissiveMode is more open than 0600chmod 0600 license.jwt
license: invalid signatureWrong public key for this binary, or tampered JWTConfirm the binary was built with the matching LICENSE_PUBKEY_HEX; re-mint if tampered
license: invalid audienceaud claim is not "mcpgw"Re-mint with correct audience
license: not yet validnbf is in the future beyond skew toleranceSync host clock
license: expired beyond gracenow >= exp + grace_daysMint a new JWT
license: invalid algorithmJWT header alg is not EdDSARe-mint with the correct signer