Skip to content

[Bug]: Colliding vhosts accepted at control plane, rejected by Envoy #2183

@mehara-rothila

Description

@mehara-rothila

Description

The gateway controller stores API vhost values verbatim and emits them as Envoy virtual-host domains without normalizing case or surrounding whitespace, and it does not check that vhost domains are unique across APIs. Envoy matches domains case-insensitively, trims surrounding whitespace, and requires every domain in a RouteConfiguration to be unique.

As a result, two APIs whose vhosts differ only by case or whitespace produce duplicate Envoy domains. The control plane reports success (201) and persists the vhost verbatim, but Envoy rejects the entire shared_route_config. Already-programmed routes keep serving from the last accepted snapshot, but every subsequent deploy, update, and delete stops being applied gateway-wide until the offending API is removed.

Expected: the controller normalizes vhost values (lowercase and trim) the way Envoy does before persisting and emitting them, or rejects a vhost that would collide with an existing API's normalized domain with a 400. An un-appliable configuration should never reach the shared RouteConfiguration.

Actual: the colliding API is accepted (201), the data plane rejects the whole RouteConfiguration, and route programming is frozen for the entire gateway until the bad API is deleted.

Steps to Reproduce

Note: the two APIs must have distinct displayName + version; the controller rejects a duplicate displayName/version on a separate check before this path is reached.

  1. Deploy API A on a.example.com:
# api-a.yaml
apiVersion: gateway.api-platform.wso2.com/v1alpha1
kind: RestApi
metadata:
  name: vhost-collision-a
spec:
  displayName: VHost-Collision-A
  version: "1.0"
  context: /vhost-collision-a
  upstream:
    main:
      url: http://sample-backend:9080
  operations:
    - method: GET
      path: /resource
  vhosts:
    main: a.example.com
curl -u admin:admin -H "Content-Type: application/yaml" --data-binary @api-a.yaml \
  http://localhost:9090/api/management/v0.9/rest-apis
# -> 201 Created; routes on a.example.com (HTTP 200)
  1. Deploy API B on A.EXAMPLE.COM (same host, different case):
# api-b.yaml
apiVersion: gateway.api-platform.wso2.com/v1alpha1
kind: RestApi
metadata:
  name: vhost-collision-b
spec:
  displayName: VHost-Collision-B
  version: "1.0"
  context: /vhost-collision-b
  upstream:
    main:
      url: http://sample-backend:9080
  operations:
    - method: GET
      path: /resource
  vhosts:
    main: A.EXAMPLE.COM
curl -u admin:admin -H "Content-Type: application/yaml" --data-binary @api-b.yaml \
  http://localhost:9090/api/management/v0.9/rest-apis
# -> 201 Created  (control plane reports success)
  1. Observe the runtime rejecting the config. The Envoy admin reject counter climbs continuously while the success counter stops advancing:
curl -s "http://localhost:9901/stats?filter=rds" | grep -E "shared_route_config.update_(success|rejected)"
# ...update_rejected: <climbing>
# ...update_success: <frozen>

Gateway runtime log:

RouteConfiguration rejected: Only unique values for domains are permitted.
Duplicate entry of domain a.example.com in route shared_route_config
  1. Confirm the freeze: any further deploy/update/delete returns success at the control plane but is not applied to the data plane. Deleting API B (DELETE /api/management/v0.9/rest-apis/vhost-collision-b) restores normal route programming.

The same collision can be triggered through the gateway-default vhost sentinel: it is matched with an exact string comparison, so case or whitespace variants (for example an uppercase _GATEWAY_DEFAULT_, or the sentinel with a trailing space) bypass resolution, are stored verbatim, and two such variants collide the same way.

Root cause

  • vhost values are not normalized on the write path. TrimSpace is used only to detect blank values (pkg/transform/restapi.go, pkg/policy/builder.go); no lowercasing or trimming is applied to the stored or emitted value.
  • xDS groups routes into one virtual host per exact vhost string (pkg/xds/translator.go, the vhostMap keyed by the raw vhost), so two strings that Envoy folds to the same domain become two virtual hosts with duplicate domains.
  • The only collision guard (pkg/transform/restapi.go, hasSandbox && effectiveMainVHost == effectiveSandboxVHost) compares the main and sandbox vhosts within a single API using an exact comparison. There is no cross-API uniqueness check.
  • The gateway-default vhost sentinel is matched with an exact string comparison (pkg/utils/api_deployment.go, == constants.VHostGatewayDefault), so case or whitespace variants bypass resolution and are stored verbatim.

Suggested fix

Normalize vhost values (lowercase and trim surrounding whitespace) on the write path, matching Envoy's normalization, and reject a vhost that collides with an existing API's normalized domain with a 400. Applying the same trim and case-insensitive comparison to the sentinel resolution closes the related variant.

Environment Details (with versions)

  • Component: gateway-controller and gateway-runtime (Envoy-based)
  • Reproduced on a local integration setup (management API on :9090, Envoy admin on :9901)
  • Branch: main

Metadata

Metadata

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions