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: /readyz returns 200 and POST /mcp is proxied normally
  • Reports status=in_grace on the hourly recheck log line (license recheck ok status=in_grace) and on mcpgw 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:

  • /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. mcpgw does not emit a dedicated audit event per rotation today; rotations are observable via the license reloaded on SIGHUP / license recheck ok log 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 exp windows. If you need a kill switch faster than exp, schedule a regular rotation cron.