Rotate the license JWT

Problem: your license is approaching expiry, you have changed plans, or you are responding to a compromised key. You need to swap the JWT with no downtime.

Solution: drop the new JWT in the same path with the same permissions, then send SIGHUP. The gateway re-reads it on the hourly recheck or immediately on SIGHUP (v1.1+).

Recipe

1. Get the new JWT. Sign in to mcpgw.dev, generate or download the replacement, and verify its claims locally:

echo "<new-jwt>" | tr '.' '\n' | sed -n '2p' | base64 -d 2>/dev/null | jq .

You should see at minimum sub, iss, exp, aud: "mcpgw". The exp should be far enough in the future to be useful.

2. Place the file with the right permissions:

echo "<new-jwt>" > /etc/mcpgw/license.jwt.new
chmod 0600 /etc/mcpgw/license.jwt.new
chown mcpgw:mcpgw /etc/mcpgw/license.jwt.new
mv /etc/mcpgw/license.jwt.new /etc/mcpgw/license.jwt

The mv is atomic on the same filesystem. mcpgw will not see a half-written file.

3. Force re-read. mcpgw re-validates the license on its hourly cadence. To force an immediate recheck, send SIGHUP:

docker kill --signal=HUP mcpgw
# or
systemctl kill --signal=HUP mcpgw.service
# or
kill -HUP "$(pgrep -f mcpgw)"

The gateway logs license: ok (sub=acme-corp, exp=2025-06-01).

4. Verify the readyz endpoint reports healthy:

curl -sf http://localhost:7332/readyz && echo "ready"

A 503 from /readyz means the license is invalid or expired beyond the grace window.

What can go wrong

FailureSymptomFix
File world-readableStartup refused with license: file permissions too permissivechmod 0600 license.jwt
Wrong public keylicense: invalid signatureConfirm the gateway was built with the production LICENSE_PUBKEY_HEX ldflag matching the issuing key
Expired beyond grace/readyz returns 503; logs show license: expired beyond grace (exp=..., grace=30d)Mint a new JWT immediately
Wrong audiencelicense: invalid audience (got "mcpgw-staging", want "mcpgw")Re-mint with aud: "mcpgw"
Clock skewlicense: not yet valid (nbf in future)Sync host clock; mcpgw allows up to 60s skew

Grace window behavior

mcpgw fails open during the grace window. If exp is in the past but within exp + grace_days, the gateway:

  • Logs a warning every minute: license: in grace window (expired N days ago, K days remaining)
  • Sets a mcp.license.grace = true resource attribute on every span
  • Returns 200 from /readyz so traffic continues

After the grace window, the gateway enters fail-closed mode:

  • /readyz returns 503
  • New connections are still accepted (so you can hit /healthz) but every /mcp request returns 503 with license_expired
  • /healthz continues to return 200 so your orchestrator does not aggressively restart

Default grace is 30 days. Override per-license via the grace_days claim on issuance.

Pitfalls

  • License rotation is not rate-limited. You can rotate as often as you want — but the audit log will show one event: "license_rotated" line per rotation. SIEM alerts that depend on a steady license should be aware.
  • The license JWT is bearer-equivalent for auditing purposes. Anyone with the file can run a gateway that authenticates as your tenant. Treat it like an API key — no shared mailboxes, no chat paste.
  • The gateway never phones home. mcpgw verifies the JWT entirely offline using the baked-in Ed25519 public key. There is no live revocation API; revocation is achieved by short exp windows. If you need a kill switch faster than exp, schedule a regular rotation cron.