Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/5-internal/WPB-27377-ListenerSet
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for ListenerSet in wire-ingress / EnvoyGateway
97 changes: 78 additions & 19 deletions charts/wire-ingress/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
A Helm chart for Wire server ingress using the **Kubernetes Gateway API**.

The chart targets **Envoy Gateway** as the Gateway API controller.
Each release renders one `ListenerSet`; chart-managed Routes attach to that
ListenerSet rather than directly to the Gateway.

---

Expand Down Expand Up @@ -56,7 +58,8 @@ possible. Most existing values files should work with minimal changes.
Add a `gateway` block to your values and review at least the following keys:

- `gateway.className` — set to the `GatewayClass` name created during installation (see above).
- `gateway.create` — if `false`, you must create a `Gateway` object yourself and set `gateway.name` to its name.
- `gateway.create` — if `false`, you must create a `Gateway` object yourself and set `gateway.name`
and, if needed, `gateway.namespace` to its name and namespace.
- `gateway.listeners.https.hostname` — set to `*.<your-domain>`. This assumes all domains under
`config.dns.*` are subdomains of `<your-domain>`. If that is not the case, create your own
`Gateway` and set `gateway.create: false`.
Expand All @@ -78,18 +81,19 @@ name overrides, etc.) can be found in `values.yaml`.

| Key | Default | Description |
|---|---|---|
| `gateway.create` | `true` | If `false`, no `Gateway` resource is createdset `gateway.name` to reference an existing one. Useful when sharing a Gateway across multiple releases. |
| `gateway.create` | `true` | If `false`, no `Gateway` resource is created. The chart still creates a ListenerSet and you must set `gateway.name` and, if needed, `gateway.namespace` to reference the parent Gateway. Useful when sharing a Gateway across multiple releases. |
| `gateway.className` | `""` | **Required.** Name of the `GatewayClass` installed by the Envoy Gateway controller (e.g. `envoy`). Must match the `GatewayClass` object whose `spec.controllerName` is `gateway.envoyproxy.io/gatewayclass-controller`. |
| `gateway.alpn.enabled` | `true` | Enables ALPN configuration via `ClientTrafficPolicy` to support HTTP/2 despite overlapping certificate SANs across multiple service listeners. When disabled, ALPN defaults to HTTP/1.1 only. |
| `gateway.namespace` | `""` | Namespace of the parent Gateway. Defaults to the release namespace when empty. Only needed when `gateway.create: false` points at a Gateway in another namespace. |
| `gateway.alpn.enabled` | `true` | Enables ALPN configuration via a `ClientTrafficPolicy` on the parent Gateway (which Envoy Gateway applies to the ListenerSet's listeners) to support HTTP/2 despite overlapping certificate SANs across multiple service listeners. When disabled, ALPN defaults to HTTP/1.1 only. |
| `gateway.alpn.protocols` | `[h2, http/1.1]` | List of ALPN protocols to advertise to clients. Defaults to HTTP/2 with HTTP/1.1 fallback. |
| `gateway.listeners.http.enabled` | `false` | Enables the HTTP listener on port 80. Required for HTTP01 ACME challenges via cert-manager's `gatewayHTTPRoute` solver — see [HTTP01 certificate challenges](#http01-certificate-challenges). |
| `gateway.listeners.http.enabled` | `false` | Enables the HTTP listener on port 80 on the parent Gateway. Required for HTTP01 ACME challenges via cert-manager's `gatewayHTTPRoute` solver. When enabled, the chart annotates the generated Certificate so cert-manager can fall back to the parent Gateway for the ACME HTTPRoute. The parent Gateway must allow HTTPRoutes from the release namespace. See [HTTP01 certificate challenges](#http01-certificate-challenges). |
| `gateway.envoyProxy.create` | `true` | If `false`, no `EnvoyProxy` resource is created. Set `gateway.envoyProxy.name` to reference an existing one, or leave it empty to inherit the GatewayClass-level `EnvoyProxy`. |
| `gateway.envoyProxy.name` | _(derived)_ | When `create: true` — name of the created resource. When `create: false` — name of an existing `EnvoyProxy` to reference via `infrastructure.parametersRef`. |
| `gateway.envoyProxy.spec` | `{}` | Free-form [EnvoyProxySpec](https://gateway.envoyproxy.io/docs/api/extension_types/#envoyproxyspec) merged verbatim. Use to set `mergeGateways`, custom service annotations, etc. |
| `gateway.manageServiceType` | `true` | Shorthand that sets `envoyService.type` to `gateway.serviceType`. Disable when managing the service type via `gateway.envoyProxy.spec` directly. |
| `gateway.serviceType` | `LoadBalancer` | Service type for the Envoy proxy service. Only used when `gateway.manageServiceType: true`. |
| `gateway.infrastructure.annotations` | `{}` | Annotations forwarded to the LoadBalancer Service provisioned by Envoy Gateway — see [Gateway API docs](https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.GatewayInfrastructure). Use for cloud-specific LB settings (e.g. AWS NLB). |
| `gateway.proxyProtocol.enabled` | `false` | Creates a `ClientTrafficPolicy` enabling PROXY protocol on all listeners. Required when the upstream load balancer is configured to send PROXY protocol headers. |
| `gateway.proxyProtocol.enabled` | `false` | Creates a `ClientTrafficPolicy` enabling PROXY protocol on the parent Gateway (which Envoy Gateway applies to the ListenerSet's listeners). Required when the upstream load balancer is configured to send PROXY protocol headers. |
| `gateway.patchPolicies.enabled` | `true` | Controls whether `EnvoyPatchPolicy` resources are created — see [EnvoyPatchPolicy](#envoypatchpolicy). |
| `gateway.patchPolicies.targetGatewayClass` | `false` | When `true`, `EnvoyPatchPolicy` targets the `GatewayClass` instead of the `Gateway`. **Required when `gateway.envoyProxy.spec.mergeGateways: true`**: with merged Gateways, policies targeting a `Gateway` are not applied — they must target the `GatewayClass`. Leave `false` for single-Gateway deployments (e.g. integration tests). |
| `gateway.controllerNamespace` | `envoy-gateway-system` | Can be ignored, relevant only for integration tests. Namespace where Envoy Gateway runs its proxy pods. Change only if Envoy Gateway was installed into a non-default namespace. |
Expand Down Expand Up @@ -170,13 +174,55 @@ used where the standard Gateway API has gaps. These resources are clearly marked

### Gateway creation is optional

The chart can optionally create a `Gateway` resource (controlled by `gateway.create: true`).
When `gateway.create: false`, all `HTTPRoute` and policy resources still reference the gateway by
name (`gateway.name`). This allows operators to share a Gateway across multiple charts or manage it
separately.
The chart always creates one `ListenerSet` per release. Chart-managed `HTTPRoute`s attach to that
ListenerSet.

The default values create the Gateway. The default `gateway.name` is derived from the release name,
so that self-referencing is consistent by default.
Chart-managed `HTTPRoute`s attach to the ListenerSet. Envoy Gateway `ClientTrafficPolicy`
resources instead target the parent `Gateway`, because Envoy Gateway v1.8.x does not allow a
`ClientTrafficPolicy` to target a `ListenerSet`. Envoy Gateway merges the ListenerSet's listeners
into the parent Gateway before policy translation, so a Gateway-scoped policy still applies to
them; where a policy must affect only one listener (federator mTLS) it is narrowed with
`sectionName`.

When `gateway.create: true`, the chart also creates the parent `Gateway` in the release namespace
and configures it to accept ListenerSets from the same namespace. The Gateway keeps a placeholder
listener so current Gateway API validation is satisfied, while the real traffic listeners live in
the ListenerSet.

When `gateway.create: false`, set `gateway.name` and, if needed, `gateway.namespace` to attach the
ListenerSet to an existing Gateway. That Gateway must already allow ListenerSet attachment.

#### Shared/demo

Many releases can share a platform-managed Gateway:

```yaml
gateway:
create: false
name: shared-wire-gateway
namespace: ingress-system
listeners:
https:
hostname: "*.demo.example.com"
```

#### Dedicated/production

The chart can also create its own dedicated Gateway and ListenerSet:

```yaml
gateway:
create: true
className: envoy
listeners:
https:
hostname: "*.example.com"
http:
enabled: true
```

In this model the Gateway, ListenerSet, Envoy proxy fleet, and LoadBalancer all stay dedicated to
the release.

### EnvoyProxy resource

Expand Down Expand Up @@ -214,9 +260,12 @@ header. Envoy's virtual-host matching is exact, so the trailing dot causes a `ro
error. Adding the FQDN as an additional domain in the route configuration allows Envoy to match
both the bare hostname and the FQDN.

The policy patches the `RouteConfiguration` named `<namespace>/<gateway>/federator`. Route
configuration names are per-namespace even when multiple Gateways share a single Envoy proxy, so
the name is predictable from chart values.
The policy patches the `RouteConfiguration` named
`<parent-gateway-namespace>/<gateway>/federator`. Route configuration names are per-namespace even
when multiple Gateways share a single Envoy proxy, so the name is predictable from chart values.
With the current Envoy Gateway schema, `gateway.patchPolicies.targetGatewayClass: false` only works
when the parent Gateway lives in the release namespace. If the parent Gateway is in another
namespace, set `gateway.patchPolicies.targetGatewayClass: true` or expect the chart to fail fast.

**`gateway.patchPolicies.targetGatewayClass`** controls what the policy targets:

Expand Down Expand Up @@ -262,7 +311,10 @@ disabled — setting both fails template rendering with a clear error.

cert-manager can complete ACME HTTP01 challenges through the Gateway using the `gatewayHTTPRoute`
solver (cert-manager >= 1.14). The **default solver** in this chart uses `gatewayHTTPRoute` — it
requires the HTTP listener to be enabled:
requires the HTTP listener to be enabled. When `gateway.listeners.http.enabled: true`, the chart
adds `acme.cert-manager.io/http01-parentreffallback: "true"` to the generated Certificate so
cert-manager attaches the challenge HTTPRoute to the parent Gateway instead of the ListenerSet.
That means a shared Gateway must allow HTTPRoutes from the release namespace:

```yaml
gateway:
Expand All @@ -271,6 +323,9 @@ gateway:
enabled: true # required for HTTP01 challenges
```

If you are using a shared Gateway in another namespace, make sure its HTTP listener allows
HTTPRoutes from the release namespace. If that is not acceptable, use a DNS01 solver instead.

If you cannot or do not want to open port 80, use a DNS01 solver instead by setting

```yaml
Expand Down Expand Up @@ -310,7 +365,10 @@ federator:

### HTTP/2 support with ALPN ClientTrafficPolicy

The chart creates a `ClientTrafficPolicy` resource that explicitly configures ALPN protocols when `gateway.alpn.enabled: true`. This is necessary because when a single certificate with multiple SANs is used across multiple listeners on the same port, Envoy would otherwise disable HTTP/2 as a safety measure to prevent connection coalescing attacks.
The chart creates a `ClientTrafficPolicy` resource that explicitly configures ALPN protocols when
`gateway.alpn.enabled: true`. This is necessary because when a single certificate with multiple SANs
is used across multiple listeners on the same port, Envoy would otherwise disable HTTP/2 as a safety
measure to prevent connection coalescing attacks.

The policy sets `spec.tls.alpnProtocols` to:
```yaml
Expand All @@ -324,9 +382,10 @@ This allows HTTP/2 to be negotiated while maintaining support for older clients

Federator mTLS is implemented using:

- `ClientTrafficPolicy` to configure TLS settings on the federator `Gateway` listener (client
certificate validation, verify depth)
- A separate `Gateway` listener for the federator so that mTLS settings apply only to that listener
- `ClientTrafficPolicy` targeting the parent Gateway, scoped via `sectionName` to the federator
listener, to configure TLS settings (client certificate validation, verify depth)
- A separate ListenerSet listener for the federator so that mTLS settings apply only to that
listener
- `X-SSL-Certificate` header forwarding is handled via an `EnvoyExtensionPolicy` with an inline
Lua filter that reads the URL-encoded PEM client certificate from the connection and injects it
as a request header, matching nginx's `$ssl_client_escaped_cert` behaviour
33 changes: 32 additions & 1 deletion charts/wire-ingress/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ Name of the Gateway resource. Uses gateway.name if set, otherwise derives one fr
{{- define "wire-ingress.gatewayName" -}}
{{- if .Values.gateway.name -}}
{{ .Values.gateway.name }}
{{- else -}}
{{- else if .Values.gateway.create -}}
{{ include "wire-ingress.fullname" . }}-gateway
{{- else -}}
{{- fail "gateway.name must be set when gateway.create is false" -}}
{{- end -}}
{{- end -}}

Expand Down Expand Up @@ -206,3 +208,32 @@ Call with a dict: {https, ssl, base, websockets (bool)}.
{{- $csp = printf "%s upgrade-insecure-requests" $csp -}}
{{- $csp -}}
{{- end -}}

{{/*
Name of the ListenerSet resource. One ListenerSet is created per release.
*/}}
{{- define "wire-ingress.listenerSetName" -}}
{{ include "wire-ingress.fullname" . }}-listeners
{{- end -}}

{{/*
Namespace of the parent Gateway. When the chart creates the Gateway, the
parent Gateway is always in the release namespace. When attaching to an
existing Gateway, gateway.namespace can override that default.
*/}}
{{- define "wire-ingress.parentGatewayNamespace" -}}
{{- if .Values.gateway.create -}}
{{ .Release.Namespace }}
{{- else -}}
{{ default .Release.Namespace .Values.gateway.namespace }}
{{- end -}}
{{- end -}}

{{/*
Hostname for the placeholder Gateway listener required by the current Gateway
API validation. It is intentionally non-routable and must not overlap with the
real ListenerSet hostnames.
*/}}
{{- define "wire-ingress.gatewayDummyHostname" -}}
{{ printf "%s.invalid" (include "wire-ingress.fullname" .) }}
{{- end -}}
4 changes: 4 additions & 0 deletions charts/wire-ingress/templates/certificate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ metadata:
chart: "{{ $root.Chart.Name }}-{{ $root.Chart.Version }}"
release: "{{ $root.Release.Name }}"
heritage: "{{ $root.Release.Service }}"
{{- if $root.Values.gateway.listeners.http.enabled }}
annotations:
acme.cert-manager.io/http01-parentreffallback: "true"
{{- end }}
spec:
issuerRef:
name: {{ $domain.issuerName | quote }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: {{ include "wire-ingress.gatewayName" . }}-alpn
name: {{ include "wire-ingress.listenerSetName" . }}-alpn
namespace: {{ .Release.Namespace }}
labels:
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
spec:
{{/* Envoy Gateway v1.8.x rejects ListenerSet as a ClientTrafficPolicy
targetRef (only Gateway is allowed). Targeting the parent Gateway with
no sectionName applies ALPN to every listener on the Gateway, including
the ones contributed by the ListenerSet. */}}
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
{{- if .Values.federator.enabled }}
{{/* Envoy Gateway-specific (gateway.envoyproxy.io/v1alpha1).
Enforces mTLS client certificate validation on the federator listener only. */}}
Enforces mTLS client certificate validation on the federator ListenerSet listener only. */}}
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: {{ include "wire-ingress.gatewayName" . }}-federator-mtls
name: {{ include "wire-ingress.listenerSetName" . }}-federator-mtls
namespace: {{ .Release.Namespace }}
labels:
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
spec:
{{/* Envoy Gateway v1.8.x rejects ListenerSet as a ClientTrafficPolicy
targetRef (only Gateway is allowed). The federator listener lives on the
ListenerSet but is merged into the parent Gateway's listener set, so we
target the Gateway and scope to that listener via sectionName. */}}
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: {{ include "wire-ingress.gatewayName" . }}-proxy-protocol
name: {{ include "wire-ingress.listenerSetName" . }}-proxy-protocol
namespace: {{ .Release.Namespace }}
labels:
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
spec:
{{/* Envoy Gateway v1.8.x rejects ListenerSet as a ClientTrafficPolicy
targetRef (only Gateway is allowed). Targeting the parent Gateway with
no sectionName applies PROXY protocol to every listener on the Gateway,
including the ones contributed by the ListenerSet. */}}
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{{- if and .Values.federator.enabled .Values.gateway.patchPolicies.enabled }}
{{- if and (not .Values.gateway.patchPolicies.targetGatewayClass) (ne (include "wire-ingress.parentGatewayNamespace" .) .Release.Namespace) }}
{{- fail "gateway.patchPolicies.targetGatewayClass must be true when the parent Gateway is in a different namespace" }}
{{- end }}
{{/* Adds the FQDN variant (trailing dot) of the federator hostname to the
virtual host's domain list so Envoy matches requests whose :authority
header carries a trailing dot.
Expand Down Expand Up @@ -27,13 +30,12 @@ spec:
{{- else }}
kind: Gateway
name: {{ include "wire-ingress.gatewayName" . | quote }}
namespace: {{ .Release.Namespace | quote }}
{{- end }}
type: JSONPatch
jsonPatches:
- type: "type.googleapis.com/envoy.config.route.v3.RouteConfiguration"
# Route config name: <namespace>/<gateway-name>/federator
name: {{ printf "%s/%s/federator" .Release.Namespace (include "wire-ingress.gatewayName" .) | quote }}
name: {{ printf "%s/%s/federator" (include "wire-ingress.parentGatewayNamespace" .) (include "wire-ingress.gatewayName" .) | quote }}
operation:
op: add
path: "/virtual_hosts/0/domains/-"
Expand Down
Loading