How to 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 and re-validates it immediately on SIGHUP (v0.5.1+), or on the next hourly recheck. A malformed or expired replacement is rejected and the current license is kept (validate-then-swap), so a fat-fingered rotation never degrades a running gateway.
Recipe
1. Get the new JWT. Request or download the replacement from the Community license page or your commercial contact, then 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)"
On success the gateway logs license reloaded on SIGHUP status=valid. If the new file is bad it logs license reload on SIGHUP rejected; keeping current license and keeps serving on the previous license.
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.
Rotating across multiple replicas (HA)
With several replicas behind a load balancer, place the new JWT on the shared path (or update the mounted Secret) and signal every replica so they all converge within seconds:
# Kubernetes: roll the pods (re-reads the mounted Secret on restart)
kubectl rollout restart deployment/mcpgw
# Or, on a shared volume, SIGHUP every replica in place:
kubectl get pods -l app=mcpgw -o name \
| xargs -I{} kubectl exec {} -- kill -HUP 1
Each replica re-reads the file independently; validate-then-swap means a bad file is rejected per-replica without taking any replica out of rotation. If the replacement is still expired, the “license expired” warning is logged once per replica per distinct token (deduped by content hash), not once per recheck — so a rotation window does not flood alerting.
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:
- Keeps serving:
/readyzreturns 200 andPOST /mcpis proxied normally - Reports
status=in_graceon the hourly recheck log line (license recheck ok status=in_grace) and onmcpgw license/mcpgw doctor - Only refuses service when the license fails for a non-grace reason (bad signature, wrong audience) or is expired beyond
exp + grace_days
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. mcpgw does not emit a dedicated audit event per rotation today; rotations are observable via the
license reloaded on SIGHUP/license recheck oklog lines. - 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.