Repository navigation
feat(gateway): add hashed service-routing hostnames for multi-gateway clusters - #4842
Conversation
|
/ok to test 3f1fd80 |
Cluster Verification ReportVerified end-to-end on a real multi-tenant OpenShift (ROSA) cluster with two gateways sharing one base domain and wildcard certificate — the exact ambiguous scenario from #4840. Environment
Results
Operator-managed ingress was used per the PR's documented model: exact-host passthrough Routes created per emitted hashed hostname, each targeting its own gateway's Service. Also exercised during setup: Helm rendering of |
… clusters Closes NVIDIA#4840 Add an optional service_routing_gateway_name gateway setting (TOML, CLI, env, Helm) that disambiguates exposed service URLs when multiple gateways share a cluster, ingress, and wildcard certificate. When configured, exposed-service hostnames become a deterministic 16-character lowercase base32 SHA-256 label of (gateway name, workspace, sandbox, service) under the same base domain, keeping every hostname exactly one label deep for wildcard-certificate compatibility. Unset gateways preserve the existing readable hostname format byte-for-byte, and configured gateways still accept legacy hosts during migration. Hashed hosts resolve through an in-memory route index hydrated from persisted ServiceEndpoint records at startup, refreshed on miss for multi-replica deployments, maintained across expose, delete, and sandbox cleanup, and enforced for collisions before endpoint persistence. Signed-off-by: Jesse Jaggars <jjaggars@redhat.com>
3f1fd80 to
28551bd
Compare
|
Rebased onto the latest @TaylorMutch the branch was force-pushed for the rebase, so the earlier |
|
/ok to test 28551bd |
|
/ok to test a868504 |
|
/ok to test 17fd4cd |
Dev-cluster verificationVerified the dedicated-controller refactor live on an OpenShift dev cluster (two gateways, Architecture in action
Kind handling
End-to-end lifecycle (gateway-a,
RBAC separation confirmed on-cluster: the gateway ServiceAccount holds only the feed-publisher Role (pinned to the one ConfigMap); the controller ServiceAccount holds the ingress-object Role. No cluster-scoped permissions involved. Verification also surfaced one fix in this branch: the controller subcommand initially ran without a tracing subscriber (722016e). |
The shared-supervisor landing (9a85667) predates the last Branch Checks run on main, so its doc-comment style was never gated. Split the three overlong first doc paragraphs so workspace clippy -D warnings passes; behavior is unchanged. Signed-off-by: Jesse Jaggars <jjaggars@redhat.com>
722016e to
75fe66f
Compare
|
Per review, this PR is now scoped to just the hashed service-routing hostnames ( The follow-up plan is a discovery API on the gateway so ingress provisioning and reconciliation can be built as a controller alongside the gateway rather than inside it — the removed work is preserved on a side branch as a starting point for that design. |
TaylorMutch
left a comment
There was a problem hiding this comment.
Thanks for separating the Kubernetes reconciliation work from this PR. The one-label hashed hostname fits the shared wildcard certificate and exact-host routing use case, and this functionality can land independently of RFC 0017.
I think we should resolve the routing identity model and two request-path issues before merging:
-
OpenShell already has
[openshell.gateway].name, an operator-assigned installation identity shared by its replicas. This introduces a second value described as a gateway name. If URL identity intentionally needs an independent lifecycle, could we name it something likeservice_routing_discriminatorand document that distinction? Otherwise, I would prefer an explicit hashed-hostname mode that derives its discriminator from the existing installation name.gateway_jwt.gateway_idshould remain separate because it has security and Kubernetes ownership semantics. -
A syntactically valid unknown hash currently causes a full all-workspaces service endpoint scan. Because this runs on the public request path before authentication, repeated random Host headers can amplify into repeated database scans.
-
Cross-origin rejections for hashed service hosts no longer emit the existing OCSF security event because the logging path still uses the readable-host parser.
The repository instructions also require updating skills/debug-openshell-cluster/SKILL.md when gateway TOML or Helm rendering changes.
Three changes from PR review: Identity model: replace service_routing_gateway_name with an explicit service_hostname_format mode (readable, the default, or hashed). In hashed mode the discriminator hashed into exposed-service hostnames is the existing gateway installation name ([openshell.gateway].name, operator- or platform-assigned, defaulting to the Helm release name), so no second per-gateway name is introduced. The name must be a DNS label and unique among gateways sharing the service base domain — an operational contract held by the deploying platform, documented in the configuration reference. gateway_jwt.gateway_id remains separate and untouched; renaming the installation now changes every minted URL. Request-path amplification: miss-triggered service route index rebuilds are debounced to one attempt per five seconds. Unknown hash labels reach the rebuild path before authentication, and without the debounce each request carrying a random well-formed label triggered a full all-workspaces endpoint scan. Startup hydration and explicit rebuilds bypass the debounce. OCSF regression: cross-origin rejections for hashed service hosts emitted no security event because the logging path used the readable-host parser only. The emitter now resolves hashed hosts through the in-memory route index (never the miss-triggered rebuild path) and still emits for unknown hash-shaped labels, attributed to the raw label. Also updates the debug-openshell-cluster skill for the new gateway TOML fields, per the repository instructions for gateway config changes. Signed-off-by: Jesse Jaggars <jjaggars@redhat.com>
|
Thanks for the thorough review — all three points are addressed in 2a442b5. Identity model: Took your preferred direction. Request-path amplification: Miss-triggered index rebuilds are now debounced to one attempt per five seconds (the timestamp stamps the attempt, so a failing store can't be retried more than once per window either). A stream of random well-formed labels now costs in-memory lookups plus at most one full-store scan per window. Startup hydration and explicit rebuilds bypass the debounce, so legitimate cold-start misses still recover immediately. OCSF regression: The cross-origin rejection emitter now resolves hashed hosts through the in-memory route index and emits the security event with the resolved sandbox/service identity; unknown hash-shaped labels still emit, attributed to the raw label. It deliberately does not call the miss-triggered rebuild path, so the emitter itself cannot be used for amplification. The On the follow-up: we'd like the discovery API to include cross-gateway hostname-claim validation — with the discriminator being platform-assigned this should be structurally prevented at creation time, but a domain-scoped check in the controller would catch platform bugs and cross-cluster conflicts in a shared zone. |
Dev-cluster verification of 2a442b5Deployed the review-feedback commit to both dev-cluster gateways (multi-arch image built from the branch), migrating values to the new surface: Discriminator derivation — verified exactly: the label minted for a fresh exposure equals the expected base32 SHA-256 of Manual ingress path (external automation owns ingress, per this PR's scope):
Rebuild debounce — fired a burst of concurrent requests carrying random well-formed hash labels: all rejected quickly, zero index-rebuild or error log lines in the gateway, health endpoint responsive throughout, and an in-flight live exposure kept serving 200s. Worst case is one store scan per five-second window as designed. OCSF emitter — the cross-origin rejection path serves the loopback-bound plaintext browser listener, which a Kubernetes gateway (bound non-loopback) correctly refuses plaintext on, so this path is not reachable in an ingress topology; it is covered by the new unit tests exercising the emitter with the OCSF capture harness, including resolved hashed hosts, unknown labels, and non-service hosts. |
|
Non-blocking docs follow-up: the implementation now uses the existing gateway installation name plus
Could those be updated to say that each gateway needs a unique installation name ( |
|
/ok to test 2a442b5 |
TaylorMutch
left a comment
There was a problem hiding this comment.
The substantive review concerns are resolved:
- hashed hostname mode now uses the existing gateway installation name instead of introducing a second gateway identity;
- miss-triggered route-index rebuilds are bounded by a per-replica debounce; and
- hashed-host cross-origin rejections emit the expected OCSF event.
The updated cluster debugging guidance and successful CI/live validation provide good coverage. The remaining stale “routing name” wording is a non-blocking documentation follow-up.
Summary
Adds an optional
service_hostname_format = "hashed"gateway setting that disambiguates exposed-service URLs when multiple gateways share a Kubernetes cluster, ingress, and wildcard certificate. In hashed mode, service hostnames become a deterministic 16-character lowercase base32 SHA-256 label of(installation name, workspace, sandbox, service)under the same base domain — exactly one label deep, so a single shared*.domainwildcard certificate keeps working, and each gateway's URLs are globally unique. The discriminator is the existing[openshell.gateway].nameinstallation identity (operator- or platform-assigned, defaulting to the Helm release name), so no second per-gateway name is introduced; the platform deploying shared-domain gateways is responsible for keeping those names unique, andgateway_jwt.gateway_idstays separate with its security and Kubernetes-ownership semantics. The default (readable) preserves the existing hostname format byte-for-byte.Related Issue
Closes #4840
Changes
crates/openshell-core/src/config.rs:ServiceHostnameFormatenum (readabledefault /hashed),ServiceRoutingConfig.service_hostname_formatfield,with_service_hostname_formatbuilder,Config::service_routing_discriminator()(the installation name, only in hashed mode), DNS-1123-label validation of the installation name when hashed, and config tests.crates/openshell-server/src/config_file.rs:[openshell.gateway] service_hostname_formatTOML parsing/serialization.crates/openshell-server/src/cli.rs:--service-hostname-format/OPENSHELL_SERVICE_HOSTNAME_FORMATwith existing CLI > env > TOML precedence, startup validation of the installation name, and precedence tests.crates/openshell-server/src/service_routing.rs: SHA-256/NUL-delimited tuple hashing with a hand-rolled fixed-size lowercase base32 encoder (no new dependency), hashedendpoint_hostoutput,ServiceRouteIndex(register/reserve/remove/lookup/rebuild), async state-awareresolve_service_hostsupporting both hashed and legacy hosts, and routing tests with fixed vectors.crates/openshell-server/src/lib.rs: route index inServerState, hydrated from persistedServiceEndpointrecords at startup before listeners bind; ambiguous persisted hashes fail startup.crates/openshell-server/src/http.rs: HTTP middleware and loopback plaintext router use the async state-aware service-host classification.crates/openshell-server/src/grpc/service.rs: hash route reserved before endpoint persistence (AlreadyExistson collision, no write), rolled back on persistence failure, removed after successful delete; collision test.crates/openshell-server/src/compute/mod.rs: sandbox cleanup removes route index entries for deleted endpoints.deploy/helm/openshell/:server.serviceRoutingGatewayNamevalue rendered into gateway TOML via_toml.tpl(empty stays unset; explicitgatewayConfigtakes precedence), chart tests, regenerated README.docs/how-it-works/gateways/configuration.mdx(field reference),docs/how-it-works/gateways/overview.mdx(hostname grammar — also fixes the pre-existing omission of the workspace segment),docs/how-it-works/sandboxes/overview.mdx,docs/kubernetes/ingress.mdx(operator-managed exact-host ingress guidance for multi-gateway shared-cert deployments),docs/kubernetes/setup.mdx.Deviations from Plan
None functionally. The route reservation API internally tracks whether the current call inserted the route so persistence-failure rollback cannot remove a pre-existing idempotent reservation from a concurrent request (an implementation detail within the plan's intent).
Testing
mise run pre-commitpasses (format, license headers, Helm lint) — with one pre-existing, unrelated failure:rust:lockfiles:checkrejectsexamples/supervisor-middleware-content-guard/Cargo.lockbecauseclap 4.6.7was yanked from crates.io; that lockfile is untouched by this PR and fails on unmodifiedmainas well.e2e/were modified.Tests added:
openshell-coreconfig: format defaults, discriminator derivation and validation (DNS-label installation name required when hashed),with_server_sanspreserves the format.openshell-serverservice_routing: fixed hash vectors (enh47uqyj7bpyttz,eiud2z5apv3yeucg), hashed URL construction, index idempotency/removal/collision/rebuild-from-store, configured-hash host resolution for Host header and HTTP/2 authority, legacy-host acceptance when configured, unknown-hash fallthrough.openshell-servergrpc::service:expose_service_rejects_route_hash_collision_before_persistence.openshell-serverconfig_file/cli: TOML parse/serialize, file-populated value, CLI/env override precedence, effective-config reach.openshell-cli:service_url_for_gatewaypreserves the hashed label while normalizing the port.openshell-serverservice_routing tests.Verification runs (local):
cargo fmt --check,git diff --check— passcargo clippy --workspace --all-targets -- -D warnings— pass (includes a small fix for main-landedsandbox_runtime.rsdoc lints that predate the last Branch Checks run on main)cargo test -p openshell-core— 607 unit + 1 doctest passcargo test -p openshell-server— 2388 pass (21 Postgres-dependent ignored as expected), including new tests for the miss-triggered rebuild debounce and hashed-host cross-origin OCSF emissioncargo test -p openshell-cli service_url— passmise run helm:test— 361 chart tests passChecklist
Documentation updated:
docs/how-it-works/gateways/configuration.mdx: new TOML field reference (default, validation, precedence, hash URL example, distinction fromgateway_jwt.gateway_id)docs/how-it-works/gateways/overview.mdx: corrected hostname grammar (workspace segment) + legacy vs hashed formatsdocs/how-it-works/sandboxes/overview.mdx: consume printed URLs, not constructed hostnamesdocs/kubernetes/ingress.mdx: exact-host ingress guidance for multi-gateway shared-wildcard-cert deploymentsdocs/kubernetes/setup.mdx: chart-values table entrydeploy/helm/openshell/README.md: regenerated chart values docs