Skip to content

1.28.0 - Egress Auth Hardening, Configurable Token TTL, and Security Follow-ups

Latest

Choose a tag to compare

@aarora79 aarora79 released this 02 Aug 03:25

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

  1. 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.
  2. (Recommended) Set OAUTH2_ALLOWED_REDIRECT_URIS to your login/logout callback URLs for the hardened OAuth-redirect posture (#1475). Empty keeps the legacy cookie-domain heuristic.
  3. If using caller-supplied asset ids, run scripts/migrate-assets-add-unique-id.py --apply (dry-run by default) before serving traffic, then set ALLOW_CALLER_SUPPLIED_ASSET_ID=true (#1343). Fresh installs get the unique index via init-documentdb-indexes.py.
  4. 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 with Invalid audience (#1533). Realms on the current setup are unaffected.
  5. (BREAKING) docker-compose deployments MUST set METRICS_ADMIN_API_KEY in .env (openssl rand -hex 32, distinct from ingest keys) or the metrics-service /admin/* endpoints return 503. 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 ingest X-API-Key, so any ingest key could change retention or run cleanup. These routes now require a separate METRICS_ADMIN_API_KEY (constant-time compare, denies 503 if unset or weak, never falls back to ingest auth). A second fix validates retention_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_KEY to .env. Until set, /admin/* returns 503; 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.

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.sh

Kubernetes / 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.yaml

Terraform / 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 apply

Major 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.

PR #1531

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.

PR #1541

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.

PR #1549, PR #1511

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.

PR #1587, PR #1588

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.

PR #1536

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.

PR #1343


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 sub across 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 explicit egress_user claim (#1491), and only that claim from self-signed tokens is trusted (#1530).
  • Egress token expiry falls back to the JWT exp claim when the token response omits expires_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 account audience 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 /validate path; 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-known discovery 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_hint so 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 uv for 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_url from env instead of a hardcoded host (#1582), add the basic scope to agent M2M clients so tokens carry aud=mcp-gateway (#1583), stop assigning to the reserved $GROUPS variable in agent-setup verify (#1579), and correct inverted group_mappings in the scope seed files (#1584).
  • Made .env secret generation portable (BSD sed -i was silently no-op on macOS) (#1585).
  • Passed SSRF_ALLOWED_HOSTS/SSRF_ALLOWED_CIDRS to 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_BODY are Count-only on the Keycloak DCR endpoint (block everywhere else), fixing DCR for loopback redirect_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 null tool_list toggle 500 (#1505); made the agent-skills list/search e2e checks deterministic (#1559, #1521).

Bug Fixes

  • Grant server/agent CRUD as * (non-admin), never all, so register_service no 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:


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 account audience 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

Full Changelog: 1.27.1...1.28.0