License JWT reference

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
}
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:

  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

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