Skip to content

Route to services by host or path prefix - #169

Merged
woksin merged 8 commits into
mainfrom
feature/150-host-and-path-routing
Oct 1, 2026
Merged

woksin merged 8 commits into
mainfrom
feature/150-host-and-path-routing

Conversation

@woksin

@woksin woksin commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Added

  • Services can declare Hosts and/or PathPrefix alongside x-cratis-microservice (or legacy Service-ID) and ?service= selection. A path prefix takes precedence over explicit service selection, which takes precedence over a host-only route. Prefix API routes target the backend and other prefix routes target the frontend, falling back to the backend when no frontend is configured. StripPathPrefix removes the prefix and announces it in X-Forwarded-Prefix, including on anonymous paths. Set ClientCredentials.RoutePrefix to the external API prefix, such as /reporting/api, for bearer requests under /reporting. Host declarations accept Unicode and punycode names; ambiguous host and prefix declarations are refused at startup. See Routing by host or path prefix. (Route multiple services by host or path prefix, not only by Service-ID header or ?service= query #150)

Security

A service can declare Hosts and a PathPrefix (optionally stripped and
announced in X-Forwarded-Prefix) so several applications can sit behind one
AuthProxy without a Service-ID header or ?service= query parameter. The
precedence is stated once in ServiceRoutes and shared by the route table, the
per-service authorization gate and client-credentials token resolution, and
ambiguous declarations fail at startup.
@woksin woksin self-assigned this Oct 1, 2026
@woksin woksin added the minor label Oct 1, 2026
@woksin

woksin commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Notes for reviewers (not part of the release note):

Design decisions (all conservative; flag any you want changed)

  • Precedence: anonymous paths → host+prefix → prefix on every host → Service-ID header → ?service= → host only → single-service catch-all. A path prefix claims its part of the URL, so it beats an explicit header. A host is only a default, so an Arc frontend on a host can still name another service's backend with the header. This is the main product decision; the alternative ("host beats header") would break that Arc pattern.
  • One implementation of the precedence (ServiceRoutes.Resolve) is used by the route table orders, AccessPolicy (per-service authorization) and ClientCredentialsServiceResolver. Before this, AccessPolicy resolved only by header/query. With host routing, that would have let a host-routed request skip the target service's requirements. The client-credentials handler now also refuses a token whose named service is not where the route table sends the request. Resolve mirrors the table exactly: a header naming a backend-only service for a non-/api path falls through, as the routes do. AccessPolicy keeps its old stricter fallback for requests that match no route at all.
  • Prefix is kept by default (StripPathPrefix: false). The backend sees the path unchanged and uses UsePathBase. When stripped, X-Forwarded-Prefix = request PathBase + prefix. YARP's default X-Forwarded-* set is re-added explicitly for those routes, because YARP appends its defaults last and would overwrite the prefix.
  • Ambiguity fails at startup: same host for two host-only services (a port-less entry overlaps every port), equal or nested prefixes on the same hosts, wildcards and URLs in Hosts, prefixes under /api or AuthProxy-reserved paths (reusing AnonymousPathPolicy), routing declared without an endpoint, StripPathPrefix without a prefix. Wildcard hosts are deliberately unsupported for now.
  • A single service that declares Hosts or PathPrefix gets no plain catch-all. It asked to be reached only through them.
  • Route orders were renumbered (header/query moved from 1/2/10/11 to 10/11/20/21) to make room. No public API depends on them.
  • Overlap with Align tenant and service headers with Arc and strip inbound tenant headers #165 (header names): this branch uses Headers.ServiceId. If Align tenant and service headers with Arc and strip inbound tenant headers #165 merges first, ServiceRoutes.Resolve needs ServiceSelection / both header names. That is a small conflict in AccessPolicy.cs, ClientCredentialsServiceResolver.cs and MicroserviceReverseProxyConfigProvider.cs comments.

Local gate (mirrors CI): dotnet build -c Debug ✅ and -c Release ✅ (0 warnings), dotnet test -c Debug --no-build ✅ (Aspire 74, Security 259, AuthProxy 2214), security specs -c Release ✅, dotnet publish -c Release ✅. I did not build the Docker image locally.

Coverage: end-to-end security specs on a running proxy with three origins cover prefix strip + X-Forwarded-Prefix + query, assets under the prefix, prefix beating the header, host-routed service requirements enforced (403, origin untouched), header beating host, unrouted 404, and AuthProxy-owned /.cratis/* on a routed host. WebSocket/SSE are not exercised end to end. They use the same matched routes and YARP's standard upgrade handling.

This is security-sensitive and needs a cross-provider review before merge.

@woksin

woksin commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all confirmed review findings, combining the duplicate authorization/header-parsing and anonymous-prefix reports. Authorization and bearer-service resolution now use the selected YARP route. Added regression coverage for quoted/comma-separated service headers, host-selected client-credentials services sharing /api, prefixed anonymous APIs with distinct frontend/backend origins, invalid host syntax, and real WebSocket/SSE traffic.

Local checks passed: solution Debug and Release builds (zero warnings), full Debug specs (74 Aspire, 2223 unit, 270 security), Release security specs (270), frontend high/critical dependency audit, NuGet vulnerability audit, Release publish, and the Docker image build. Checks ran through pi-phase. Linux/x64 execution remains for GitHub CI; local execution used macOS/arm64 and the Docker build used Linux/arm64.

The additional dotnet format whitespace --verify-no-changes --no-restore check fails on existing repository formatting, including inline [Fact] declarations retained by the repository's spec conventions; CI has no separate formatter gate. No unrelated formatting was changed.

The rejected host-scoping finding was intentionally ignored. No product decisions are outstanding. No merge or label changes were made.

@woksin

woksin commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Merged origin/main in 2264e8e. The only conflicted file was Source/AuthProxy/ReverseProxy/MicroserviceReverseProxyConfigProvider.cs: retained #166’s ClusterFor endpoint → service → root → default activity-timeout precedence and #169’s ServiceMetadata with optional strip-prefix metadata. #165’s dual tenant/service headers and inbound stripping are preserved. No product decision was needed.

Requested local checks: solution Release build, AuthProxy.Specs (Debug), and AuthProxy.Security.Specs (Debug) each skipped after pi-phase’s 120-second queue limit; none started (exit 75). PR CI remains the full gate; no CI watch or PR merge performed. Rechecked the release-note body against git diff --stat origin/main...HEAD; both bullets still match, so no body edit was needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant