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
| Failure | Symptom | Fix |
|---|---|---|
| File world-readable | Startup refused with license: file permissions too permissive | chmod 0600 license.jwt |
| Wrong public key | license: invalid signature | Confirm 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 audience | license: invalid audience (got "mcpgw-staging", want "mcpgw") | Re-mint with aud: "mcpgw" |
| Clock skew | license: 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 = trueresource attribute on every span - Returns 200 from
/readyzso traffic continues
After the grace window, the gateway enters fail-closed mode:
/readyzreturns 503- New connections are still accepted (so you can hit
/healthz) but every/mcprequest returns 503 withlicense_expired /healthzcontinues 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
expwindows. If you need a kill switch faster thanexp, schedule a regular rotation cron.