Release 1.28.0 - Egress Auth Hardening, Configurable Token TTL, and Security Follow-ups
August 2026
Upgrading from 1.27.1
This section covers everything you need to know to upgrade from 1.27.1 to 1.28.0. Read the operator checklist below first: this release has one breaking change (metrics-service admin auth) and several one-time migration actions.
Operator Upgrade Checklist
- Re-connect existing egress (third-party) connections once. PR #1491 changed the vault key so the browser-consent path and the "Generate JWT Token" path share one bucket. Connections created before this release are keyed on the old id and must be re-connected once after upgrade. Re-consent is quick and self-service.
- (Recommended) Set
OAUTH2_ALLOWED_REDIRECT_URISto your login/logout callback URLs for the hardened OAuth-redirect posture (#1475). Empty keeps the legacy cookie-domain heuristic. - If using caller-supplied asset ids, run
scripts/migrate-assets-add-unique-id.py --apply(dry-run by default) before serving traffic, then setALLOW_CALLER_SUPPLIED_ASSET_ID=true(#1343). Fresh installs get the unique index viainit-documentdb-indexes.py. - If your Keycloak realm predates the audience mapper (never ran the current
init-keycloak.sh/upgrade-realm-for-dcr.sh), re-run setup and have users re-authenticate before upgrading.account-only tokens are now rejected withInvalid audience(#1533). Realms on the current setup are unaffected. - (BREAKING) docker-compose deployments MUST set
METRICS_ADMIN_API_KEYin.env(openssl rand -hex 32, distinct from ingest keys) or the metrics-service/admin/*endpoints return503. Ingest and the in-process daily cleanup keep working. Admin runbooks calling/admin/*must switch from the ingest key to this admin key. Terraform/CDK provision it automatically (#1539).
Breaking Changes
- Metrics-service admin surface now requires a dedicated key (PR #1539). Every metrics-service
/admin/*route (retention preview/cleanup, policy read/update, database stats/size) previously shared the ingestX-API-Key, so any ingest key could change retention or run cleanup. These routes now require a separateMETRICS_ADMIN_API_KEY(constant-time compare, denies503if unset or weak, never falls back to ingest auth). A second fix validatesretention_days: a zero/negative value previously pushed the cleanup cutoff to now/future and could wipe the whole table; it is now validated at the route, constructor, and update path, clamped to a safe past floor, and re-validated on DB load.- docker-compose upgraders MUST add
METRICS_ADMIN_API_KEYto.env. Until set,/admin/*returns503; ingest (POST /metrics,/flush) and the daily cleanup are unaffected, and a loud startup WARNING announces that the admin surface is disabled. - Terraform / CDK deployments provision the key automatically (distinct from the ingest key) and need no operator action.
- docker-compose upgraders MUST add
New Environment Variables
| Variable | Default | Description |
|---|---|---|
MCP_TOKEN_DEFAULT_TTL_HOURS |
8 |
Default lifetime for minted MCP access tokens (UI and API mint flows) (#1477). |
MCP_TOKEN_MAX_TTL_HOURS |
24 |
Ceiling for a per-registration expires_in_hours; requests above it are clamped (#1477). |
OAUTH2_ALLOWED_REDIRECT_URIS |
empty | Comma-separated absolute login/logout redirect URIs. Empty falls back to the legacy cookie-domain heuristic; set it for the hardened OAuth-redirect posture (#1475). |
ALLOW_CALLER_SUPPLIED_ASSET_ID |
false |
Opt-in: allow callers to supply their own asset id at registration. Fail-closed; when off, a supplied id is rejected with 422 (#1343). |
RATE_LIMIT_QUARANTINE_FAIL_CLOSED |
false |
When true, a quarantine-lookup failure denies the request instead of allowing it (#1504). |
METRICS_ADMIN_API_KEY (secret) |
none | BREAKING. Gates metrics-service /admin/*. Must be >=32 chars, not a known placeholder, and distinct from any ingest key. Unset = /admin/* returns 503 (ingest unaffected). Terraform/CDK auto-generate; compose must set it (#1539). Not in the Helm parameter reference, consistent with other metrics-service-only keys. |
Upgrade Instructions
Docker Compose
cd mcp-gateway-registry
git pull origin main
git checkout 1.28.0
# Review new env vars in .env.example and update your .env.
# REQUIRED for docker-compose: set METRICS_ADMIN_API_KEY (openssl rand -hex 32),
# distinct from your ingest key, or the metrics-service /admin/* routes return 503.
# Then rebuild and restart:
./build_and_run.shKubernetes / Helm (EKS)
cd mcp-gateway-registry
git pull origin main
git checkout 1.28.0
# REQUIRED: subchart templates/values changed this release, so rebuild the
# packaged dependencies before upgrading (a plain helm upgrade would otherwise
# use stale .tgz subcharts).
cd charts/mcp-gateway-registry-stack
helm dependency build
helm dependency update
# Update values.yaml if needed, then upgrade:
helm upgrade mcp-gateway . -f your-values.yamlTerraform / ECS
cd mcp-gateway-registry
git pull origin main
git checkout 1.28.0
# Update your .tfvars with any new variables. METRICS_ADMIN_API_KEY is
# generated automatically (distinct from the ingest key) - no action needed.
cd terraform/aws-ecs
terraform plan
terraform applyMajor Features
Egress pat mode - per-user static PAT/API-key injection
A new pat egress auth mode lets the gateway inject a per-user static PAT or API key (with a bounded TTL) toward a backend, with a configurable inject header. Admins can seed a PAT on behalf of another user (audit-logged, admin-scoped). CLI and frontend support are included, and the credential is store-only / inject-only: the gateway can never read it back. This completes the credentials broker so it now covers 3LO, OBO, and static-token modes.
Rate limiting: per-caller-per-target axis, server groups, and quarantine
Rate limiting gains a caller_target axis (per-caller-per-target limits), a server_group axis for per-member-uniform limits, and a quarantine kill switch to block a specific caller or target. Quarantine add/remove is admin-only from the Users and M2M UI (and refuses to quarantine an admin caller), with a RATE_LIMIT_QUARANTINE_FAIL_CLOSED toggle. Rate Limits moved to their own top-level "Traffic Management" menu.
UI-authoritative scope creation
Scopes can be created and managed from the UI as the authoritative source (with a reload and validated config), instead of requiring direct MongoDB inserts. The entity tabs also stay visible with an access hint when the caller's discovery scope is missing, rather than disappearing.
Configurable MCP access-token TTL
MCP access-token lifetimes are no longer hardcoded to 8 hours. Operators set a default via MCP_TOKEN_DEFAULT_TTL_HOURS and a hard ceiling via MCP_TOKEN_MAX_TTL_HOURS; a per-registration expires_in_hours is honored and clamped to the ceiling. The UI mint flows and the API both use the configured default, and the new knobs are wired across Docker (including podman and prebuilt-image compose variants) with FAQ documentation.
Per-user egress "Connect" affordance
Server cards now surface a per-user "Connect" action and a connect modal, so a user can initiate the egress OAuth consent for a specific server directly from the UI (and from discover/search result rows). The affordance is bound to the canonical egress_user identity.
Caller-supplied asset id at registration
Servers, agents, and skills may optionally supply their own id (a UUID, ARN, or URN) at registration instead of auto-generating one, enabling 1:1 mapping to external identity schemes and idempotent CI/GitOps registration. Gated by ALLOW_CALLER_SUPPLIED_ASSET_ID (default false, fail-closed): when off, a supplied id is rejected with 422 and ids auto-generate as before. A safe-charset allowlist validates the id, duplicates return 409, and a unique partial index enforces uniqueness per asset type.
What's New
Egress Authentication
- Transient Vault/OpenBao retry on egress token save, with bounded exponential backoff (0.5/1/2/4s) instead of failing immediately; a hard save failure on the OAuth consent callback returns a clear, retryable
503 "Connection not saved"page instead of a silent 500 (#1490). - The token-vend read path now applies the same transient-failure hardening (#1529).
- Egress vault keyed on the OIDC
subacross both consent and vend, fixing a "0 tools despite a valid token" bug where the browser-consent path and the manually-generated JWT keyed on different ids. The self-signed user token now carries an explicitegress_userclaim (#1491), and only that claim from self-signed tokens is trusted (#1530). - Egress token expiry falls back to the JWT
expclaim when the token response omitsexpires_in(#1542).
Security Fixes
- Uniform CSRF coverage applied to ~75 session-authenticated mutating endpoints that lacked it, plus an opt-in exact-match OAuth redirect allowlist via
OAUTH2_ALLOWED_REDIRECT_URIS(#1475), with a redact-and-count observability follow-up (redirect_rejected_total{flow,reason}) (#1525). - Command-injection fix: removed an
exec()path in the token-refresh utility (#1524), with a dead-import cleanup follow-up (#1589). - Rejected Keycloak's default
accountaudience during access-token validation, closing a same-realm cross-client confused-deputy; a token must now carry an audience naming this gateway (#1533). - Redacted backend URLs and authorization-policy dumps on the hot
/validatepath; verbose traces moved to DEBUG (#1486), with residual sensitive-logging cleanup (#1486 follow-ups). - Hardened the metrics-service admin surface (admin/ingest privilege separation + retention-range validation) (#1539); bounded the cardinality of attacker-influenced Prometheus metric labels (#1534); prevented silent suppression of audit records (#1537).
- Verified IdP upstream TLS to close a MITM auth-bypass on the nginx proxy hop (#1540).
- Added per-type authorization scopes for custom entities (#1492) and skill-discovery access-check parity (#1493).
- Hardened the AgentCore sync adapter's egress and secret handling (#1527) and the ARD catalog /
.well-knowndiscovery endpoints (#1528). - Secrets Manager overflow sharding and concurrency hardening, including retrying overflow reads that race a generation cleanup (#1520).
Authentication
- Internalized the OAuth2 logout
id_token_hintso logout works behind CloudFront and on the service-to-service hop (#1509, #1545). - Guarded the Entra logout URL length to avoid
AADSTS90015(#1508). - Fixed the FastAPI lifespan handler (replaced a dead
@app.on_event(startup)legacy-scope-audit handler) (#1523).
Deployment & Setup
- macOS setup fixes: gate the Python check on >=3.14 and ask
uvfor the 3.14 interpreter (#1580), generate secrets that later phases require in Phase 4 (#1581), enable the registered server and document the SSRF allowlist (#1586). - Keycloak/agent setup fixes: derive the agent-token
keycloak_urlfrom env instead of a hardcoded host (#1582), add thebasicscope to agent M2M clients so tokens carryaud=mcp-gateway(#1583), stop assigning to the reserved$GROUPSvariable in agent-setup verify (#1579), and correct invertedgroup_mappingsin the scope seed files (#1584). - Made
.envsecret generation portable (BSDsed -iwas silently no-op on macOS) (#1585). - Passed
SSRF_ALLOWED_HOSTS/SSRF_ALLOWED_CIDRSto the registry in the prebuilt and podman compose variants (#1515). - Normalized nginx location paths to a trailing slash (#1507).
Infrastructure & CDK
- Enabled the WAF path in CDK and path-scoped the CommonRuleSet so
EC2MetaDataSSRF_BODF/GenericRFI_BODYare Count-only on the Keycloak DCR endpoint (block everywhere else), fixing DCR for loopbackredirect_uris while preserving SSRF defense; Terraform brought to parity (#1478).
Frontend Improvements
- SearchableSelect dropdown now renders in a portal so table overflow no longer clips it (#87b64aa9).
- Entity tabs (Skills/Agents) stay visible with an access hint when discovery scope is missing (#1511).
Documentation
- Recorded ADR 0001: the registry will not act as a DCR authorization server, establishing the
docs/adr/convention (#1590). - Documented required and security-critical config params in the setup guides and added IdP CA-bundle params to the unified parameter reference (#1561).
- Added a FAQ for admin-seeding per-user egress PATs (#1535).
Testing
- Added the pre-release end-to-end smoke test (
tests/e2e_release_test.py) and fixed a nulltool_listtoggle 500 (#1505); made the agent-skills list/search e2e checks deterministic (#1559, #1521).
Bug Fixes
- Grant server/agent CRUD as
*(non-admin), neverall, soregister_serviceno longer auto-promotes the caller to admin (#266afbcf). - Fixed the dead
@app.on_event(startup)legacy-scope-audit handler (#1523). - Surfaced the ARD catalog cap in the config UI and corrected a stale nginx match comment (#1555).
Closed Issues
| Issue | Title | Closed By |
|---|---|---|
| #1578 | macOS setup: registered server not enabled / SSRF allowlist undocumented | PR #1586 |
| #1577 | build: .env secret generation not portable (BSD sed) | PR #1585 |
| #1574 | Inverted group_mappings in scope seed files | PR #1584 |
| #1573 | Agent M2M clients missing the basic scope (aud=mcp-gateway) |
PR #1583 |
| #1572 | Agent token uses hardcoded Keycloak host | PR #1582 |
| #1570 | Agent setup verify assigns to reserved $GROUPS |
PR #1579 |
| #1569 | macOS setup: Phase 4 missing required secrets | PR #1581 |
| #1568 | macOS setup: Python version check / interpreter selection | PR #1580 |
| #1560 | Config parameter documentation gaps | PR #1561 |
| #1513 | SSRF allow-list env vars not passed in prebuilt/podman compose | PR #1515 |
| #1506 | Egress: pat mode - per-user static PAT/API-key injection | PR #1531 |
| #1504 | Rate limiting: per-target axis + quarantine | PR #1541 |
| #1503 | OAuth2 logout id_token_hint / logout behind CloudFront | PR #1509, PR #1545 |
| #1502 | Entra logout failure (AADSTS90015) | PR #1508 |
| #1501 | nginx trailing-slash location normalization | PR #1507 |
| #1494 | Scope-level access creation in the UI | PR #1549 (manual) |
| #1477 | Configurable MCP access-token TTL | PR #1587 |
| #1276 | Optional caller-supplied asset id at registration | PR #1343 (manual) |
| #1265 | Umbrella: Egress Token Credentials Broker | manual (all sub-issues merged) |
| #995 | Decision: registry will not implement RFC 7591 DCR server-side | PR #1590 |
Pull Requests Included
| PR | Title |
|---|---|
| #1590 | docs(adr): record decision that registry will not be a DCR authorization server |
| #1589 | chore(cli): drop now-unused exec import from tokenRefresh |
| #1588 | fix(token-ttl): complete #1477 - UI mint flows, compose wiring, FAQ accuracy |
| #1587 | feat(auth): make MCP access-token TTL configurable |
| #1586 | fix(macos-setup): enable the registered server, and document the SSRF allowlist |
| #1585 | fix(build): make .env secret generation portable; BSD sed -i silently no-ops |
| #1584 | fix(scopes): correct the inverted group_mappings in the scope seed files |
| #1583 | fix(keycloak): add the 'basic' scope to agent M2M clients so tokens carry aud=mcp-gateway |
| #1582 | fix(keycloak): derive agent token keycloak_url from env, not a hardcoded host |
| #1581 | fix(macos-setup): generate the secrets later phases require in Phase 4 |
| #1580 | fix(macos-setup): ask uv for the 3.14 interpreter, not PATH's python3 |
| #1579 | fix(keycloak): don't assign to reserved $GROUPS in agent setup verify |
| #1561 | docs: document required/security-critical config params in setup guides |
| #1559 | test(e2e): make agent-skills list/search checks deterministic |
| #1556 | chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates |
| #1555 | fix(ard): surface catalog cap in config UI and correct nginx match comment |
| #1554 | chore(deps): weekly lockfile update (2026-07-27) |
| #1553 | chore(deps): bump the actions group in /.github/workflows with 3 updates |
| #1549 | feat(#1494): make UI scope creation authoritative (reload + validated config) |
| #1545 | fix(logout): make internalized OAuth2 logout work behind CloudFront / on the S2S hop |
| #1542 | fix(egress): fall back to JWT exp when token response omits expires_in |
| #1541 | feat(rate-limit): caller_target axis, server groups, and quarantine kill switch |
| #1540 | Verify IdP upstream TLS |
| #1539 | Harden the metrics-service admin surface |
| #1537 | Prevent silent suppression of audit records |
| #1536 | feat(egress): surface per-user connect on server card + connect modal |
| #1535 | docs: FAQ for admin-seeding per-user egress PATs |
| #1534 | Bound cardinality of Prometheus metric labels |
| #1533 | Reject Keycloak's default account audience during access-token validation |
| #1531 | feat(egress): pat mode - per-user static PAT/API-key injection (bounded TTL) |
| #1530 | fix(egress): only trust the egress_user claim from self-signed tokens |
| #1529 | fix(egress): apply transient-failure hardening to the token vend read path |
| #1528 | Harden ARD catalog / .well-known discovery endpoints |
| #1527 | Harden the AgentCore sync adapter's egress and secret handling |
| #1525 | observability: redact + count rejected OAuth redirects |
| #1524 | Security: Command Injection via exec() in token refresh utility |
| #1523 | Fix dead @app.on_event(startup) legacy-scope-audit handler |
| #1521 | mock agent health in test and update jose scanning |
| #1520 | Secrets Manager overflow sharding and concurrency hardening |
| #1517 | chore(deps): bump the actions group in /.github/workflows with 5 updates |
| #1515 | fix(compose): pass SSRF_ALLOWED_HOSTS/CIDRS to registry in prebuilt and podman |
| #1511 | Keep Skills/Agents tabs visible with an access hint when discovery scope is missing |
| #1510 | chore(deps): bump torch from 2.12.1 to 2.13.0 in the uv group |
| #1509 | fix: internalize OAuth2 logout id_token_hint |
| #1508 | fix: guard Entra logout URL length to avoid AADSTS90015 |
| #1507 | fix: normalise nginx location paths to trailing slash |
| #1505 | feat: pre-release end-to-end smoke test + null tool_list toggle 500 fix |
| #1493 | Skill discovery parity |
| #1492 | Add per-type authorization scopes for custom entities |
| #1491 | fix(egress): key per-user egress vault on OIDC sub across consent + vend |
| #1490 | fix(egress): retry transient Vault/OpenBao failures on token save |
| #1486 | Redact URLs |
| #1478 | fix(cdk): enable WAF path and restore built-in MCP server flow |
| #1475 | Harden CSRF coverage and OAuth redirect validation |
| #1343 | honor optional caller-supplied asset id at registration |
Security Dependency Updates
| Package | Previous | Updated | Scope |
|---|---|---|---|
| torch | 2.12.1 | 2.13.0 | Python (uv) |
| npm_and_yarn group | - | - | Frontend (2 updates) |
| GitHub Actions group | - | - | CI (multiple updates across #1517, #1553) |
Contributors
Thank you to all contributors for this release:
- Amit Arora (@aarora79)
- omrishiv (@omrishiv)
- Vrinda Bhandari (@vrindabhandari)
- Gonçalo Faustino (@go-faustino)
- Bill Tarr (@billtarr-aws)
- Harshit Gupta (@harshitkgupta)
- ssafari (@ssafari)
- soheilkhan (@soheilkhan)
- tomaioo (@tomaioo)
- rahul188 (@rahul188)
Support
Full Changelog: 1.27.1...1.28.0
What's Changed
- chore: update image tags to 1.27.1 by @github-actions[bot] in #1500
- feat: pre-release end-to-end smoke test + null tool_list toggle 500 fix (1.28.0) by @aarora79 in #1505
- Add per-type authorization scopes for custom entities by @omrishiv in #1492
- chore(deps): bump torch from 2.12.1 to 2.13.0 in the uv group across 1 directory by @dependabot[bot] in #1510
- Skill discovery parity by @omrishiv in #1493
- Keep Skills/Agents tabs visible with an access hint when discovery scope is missing by @aarora79 in #1511
- fix(compose): pass SSRF_ALLOWED_HOSTS/CIDRS to registry in prebuilt and podman (#1513) by @rahul188 in #1515
- #1276: honor optional caller-supplied asset id at registration by @vrindabhandari in #1343
- chore(deps): bump the actions group in /.github/workflows with 5 updates by @dependabot[bot] in #1517
- Harden CSRF coverage and OAuth redirect validation by @omrishiv in #1475
- Fix dead @app.on_event(startup) legacy-scope-audit handler by @soheilkhan in #1523
- observability: redact + count rejected OAuth redirects (#1475 follow-up) by @aarora79 in #1525
- fix(cdk): enable WAF path and restore built-in MCP server flow by @harshitkgupta in #1478
- Redact URLs by @omrishiv in #1486
- fix(egress): retry transient Vault/OpenBao failures on token save; surface hard save failures to the user by @go-faustino in #1490
- fix(egress): key per-user egress vault on OIDC sub across consent + vend by @go-faustino in #1491
- fix(egress): only trust the egress_user claim from self-signed tokens (follow-up to #1491) by @go-faustino in #1530
- feat(egress): pat mode — per-user static PAT/API-key injection (bounded TTL) (#1506) by @aarora79 in #1531
- docs: FAQ for admin-seeding per-user egress PATs by @aarora79 in #1535
- feat(egress): surface per-user connect on server card + connect modal (#1495) by @aarora79 in #1536
- feat(rate-limit): caller_target axis, server groups, and quarantine kill switch (#1504) by @aarora79 in #1541
- fix: normalise nginx location paths to trailing slash (#1501) by @omrishiv in #1507
- fix: guard Entra logout URL length to avoid AADSTS90015 (#1502) by @omrishiv in #1508
- fix: internalize OAuth2 logout id_token_hint (#1503) by @omrishiv in #1509
- fix(logout): make internalized OAuth2 logout work behind CloudFront / on the S2S hop (#1503) by @aarora79 in #1545
- Secrets Manager overflow sharding and concurrency hardening by @omrishiv in #1520
- mock agent health in test and update jose scanning by @omrishiv in #1521
- Harden the AgentCore sync adapter's egress and secret handling by @omrishiv in #1527
- Harden ARD catalog / .well-known discovery endpoints by @omrishiv in #1528
- chore(deps): bump the actions group in /.github/workflows with 3 updates by @dependabot[bot] in #1553
- chore(deps): weekly lockfile update (2026-07-27) by @github-actions[bot] in #1554
- fix(ard): surface catalog cap in config UI and correct nginx match comment by @aarora79 in #1555
- test(e2e): make agent-skills list/search checks deterministic by @aarora79 in #1559
- Reject Keycloak's default
accountaudience during access-token validation by @omrishiv in #1533 - Bound cardinality of Prometheus metric labels by @omrishiv in #1534
- Prevent silent suppression of audit records by @omrishiv in #1537
- Harden the metrics-service admin surface by @omrishiv in #1539
- docs: document required/security-critical config params in setup guides by @aarora79 in #1561
- Verify IdP upstream TLS by @omrishiv in #1540
- chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates by @dependabot[bot] in #1556
- feat(auth): make MCP access-token TTL configurable (#1477) by @aarora79 in #1587
- fix(token-ttl): complete #1477 — UI mint flows, compose wiring, FAQ accuracy by @aarora79 in #1588
- Security: Command Injection via exec() in token refresh utility by @tomaioo in #1524
- chore(cli): drop now-unused exec import from tokenRefresh by @aarora79 in #1589
- fix(egress): apply transient-failure hardening to the token vend read path (follow-up to #1490) by @go-faustino in #1529
- fix(egress): fall back to JWT exp when token response omits expires_in by @go-faustino in #1542
- feat(#1494): make UI scope creation authoritative (reload + validated… by @vrindabhandari in #1549
- fix(keycloak): don't assign to reserved $GROUPS in agent setup verify by @billtarr-aws in #1579
- fix(macos-setup): ask uv for the 3.14 interpreter, not PATH's python3 by @billtarr-aws in #1580
- fix(macos-setup): generate the secrets later phases require in Phase 4 by @billtarr-aws in #1581
- fix(keycloak): derive agent token keycloak_url from env, not a hardcoded host by @billtarr-aws in #1582
- fix(keycloak): add the 'basic' scope to agent M2M clients so tokens carry aud=mcp-gateway by @billtarr-aws in #1583
- fix(scopes): correct the inverted group_mappings in the scope seed files by @billtarr-aws in #1584
- fix(build): make .env secret generation portable; BSD sed -i silently no-ops by @billtarr-aws in #1585
- fix(macos-setup): enable the registered server, and document the SSRF allowlist by @billtarr-aws in #1586
- docs(adr): record decision that registry will not be a DCR authorization server (#995) by @aarora79 in #1590
New Contributors
- @rahul188 made their first contribution in #1515
- @tomaioo made their first contribution in #1524
- @billtarr-aws made their first contribution in #1579
Full Changelog: 1.27.1...1.28.0