Skip to content

Commit 523b97f

Browse files
ziraincnvergencerudrakhpnguyenptkbenjaminch
authored
[release-1.7] cherry pick for v1.7.4 (#9152)
* fix: encode TLS secrets with canonical PEM formatting (#8744) * exclude non-ready https Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> * prevent BoringSSL errors Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> * Add trailing newline to testdata PEMs Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> * add release notes Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> * use shallow copy instead of DeepCopy Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> * use maps.Clone for the data and split the PRs Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> --------- Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> Signed-off-by: zirain <zirain2009@gmail.com> * fix: restore last transition time in merge status conditions (#8962) * fix: restore last transition time in merge status conditions Signed-off-by: Rudrakh Panigrahi <rudrakh97@gmail.com> * add release note Signed-off-by: Rudrakh Panigrahi <rudrakh97@gmail.com> --------- Signed-off-by: Rudrakh Panigrahi <rudrakh97@gmail.com> Signed-off-by: zirain <zirain2009@gmail.com> * fix(xds): hot-reload xDS server cert in GatewayNamespaceMode (#8959) * fix(xds): hot-reload xDS server cert in GatewayNamespaceMode Signed-off-by: nguyenptk <nguyenptk@gmail.com> * extract loadServerTLSConfig helper and add tests Signed-off-by: nguyenptk <nguyenptk@gmail.com> --------- Signed-off-by: nguyenptk <nguyenptk@gmail.com> Signed-off-by: Nguyên (Harry) <nguyenptk@gmail.com> Signed-off-by: zirain <zirain2009@gmail.com> * fix(helm): correct typo in HPA maxReplicas required message (#9032) Signed-off-by: benjaminch <benjamin.chastanier@gmail.com> Signed-off-by: zirain <zirain2009@gmail.com> * fix(gatewayapi): prefer sectioned BackendTLSPolicy over wildcard for same target (#9068) * add passing testcase showing wildcard overriding sectioned backendTLSPolicy Signed-off-by: AlecDiraimondo <50001153+AlecDiraimondo@users.noreply.github.com> * fix(gatewayapi): prefer sectioned BackendTLSPolicy over wildcard for same target When two BackendTLSPolicies target the same backend — one with sectionName set, one without — the wildcard wins whenever it sorts earlier, since policies are iterated in creationTimestamp order with no specificity preference. The sectioned policy is silently dropped: never selected by the translator, no status written. Make getBackendTLSPolicy prefer policies with an explicit sectionName match before falling back to wildcard matches. Matches the specificity ordering SecurityPolicy already implements. Refs #9035 Signed-off-by: AlecDiraimondo <50001153+AlecDiraimondo@users.noreply.github.com> * update release notes Signed-off-by: AlecDiraimondo <50001153+AlecDiraimondo@users.noreply.github.com> * fixes from review: dedup helper, return early if backendTarget has no section specified Signed-off-by: AlecDiraimondo <50001153+AlecDiraimondo@users.noreply.github.com> Co-authored-by: Rudrakh Panigrahi <rudrakh97@gmail.com> --------- Signed-off-by: AlecDiraimondo <50001153+AlecDiraimondo@users.noreply.github.com> Signed-off-by: Alec Diraimondo <drayalec@gmail.com> Co-authored-by: Rudrakh Panigrahi <rudrakh97@gmail.com> Signed-off-by: zirain <zirain2009@gmail.com> * fix(egctl): skip missing CRDs in `x status` bulk modes (#9099) `egctl x status all` (and `xroute`/`xpolicy`) aborted with an error when a Gateway API CRD was not installed in the cluster (e.g. TCPRoute). Missing CRDs are now skipped, and reported on stderr when `-v` is set. Explicit single-resource invocations still error loudly so users get a clear signal when a kind they asked for is unavailable. Signed-off-by: Jeremiah Snapp <jeremiahs@tomonetworks.com> Signed-off-by: zirain <zirain2009@gmail.com> * fix(gatewayapi): reject unsupported BackendRef URLRewrite path rewrites (#9131) gatewayapi: reject BackendRef URLRewrite path modifiers Signed-off-by: Aditya7880900936 <adityasanskarsrivastav788@gmail.com> Signed-off-by: zirain <zirain2009@gmail.com> * fix: force HTTP1 for Service/ServiceImport with websocket appProtocol (#9138) force HTTP1 for Service/ServiceImport with websocket appProtocol Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com> Signed-off-by: zirain <zirain2009@gmail.com> * fix gen Signed-off-by: zirain <zirain2009@gmail.com> --------- Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> Signed-off-by: zirain <zirain2009@gmail.com> Signed-off-by: Rudrakh Panigrahi <rudrakh97@gmail.com> Signed-off-by: nguyenptk <nguyenptk@gmail.com> Signed-off-by: Nguyên (Harry) <nguyenptk@gmail.com> Signed-off-by: benjaminch <benjamin.chastanier@gmail.com> Signed-off-by: AlecDiraimondo <50001153+AlecDiraimondo@users.noreply.github.com> Signed-off-by: Alec Diraimondo <drayalec@gmail.com> Signed-off-by: Jeremiah Snapp <jeremiahs@tomonetworks.com> Signed-off-by: Aditya7880900936 <adityasanskarsrivastav788@gmail.com> Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com> Co-authored-by: Karol Szwaj <karol.szwaj@gmail.com> Co-authored-by: Rudrakh Panigrahi <rudrakh97@gmail.com> Co-authored-by: Nguyên (Harry) <nguyenptk@gmail.com> Co-authored-by: BenjaminCh <benjamin.chastanier@gmail.com> Co-authored-by: Alec Diraimondo <drayalec@gmail.com> Co-authored-by: Jeremiah Snapp <jeremiah.snapp@gmail.com> Co-authored-by: Aditya Sanskar Srivastav <161202916+Aditya7880900936@users.noreply.github.com> Co-authored-by: Huabing (Robin) Zhao <zhaohuabing@gmail.com>
1 parent 20d7d74 commit 523b97f

39 files changed

Lines changed: 4720 additions & 63 deletions

File tree

charts/gateway-helm/templates/envoy-gateway-hpa.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ spec:
1212
{{- if .Values.hpa.minReplicas }}
1313
minReplicas: {{ .Values.hpa.minReplicas }}
1414
{{- end }}
15-
maxReplicas: {{ required ".Values.hps.maxReplicas is required when hpa is enabled" .Values.hpa.maxReplicas }}
15+
maxReplicas: {{ required ".Values.hpa.maxReplicas is required when hpa is enabled" .Values.hpa.maxReplicas }}
1616
{{- if .Values.hpa.behavior }}
1717
behavior:
1818
{{ toYaml .Values.hpa.behavior | indent 4 }}

internal/cmd/egctl/status.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"text/tabwriter"
1616

1717
"github.com/spf13/cobra"
18+
"k8s.io/apimachinery/pkg/api/meta"
1819
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1920
"sigs.k8s.io/controller-runtime/pkg/client"
2021
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -93,20 +94,38 @@ func newStatusCommand() *cobra.Command {
9394
case "all":
9495
for _, rt := range supportedAllTypes {
9596
if err = runStatus(ctx, cmd.OutOrStdout(), k8sClient, rt, namespace, quiet, verbose, allNamespaces, true, true); err != nil {
97+
if meta.IsNoMatchError(err) {
98+
if verbose {
99+
fmt.Fprintf(cmd.ErrOrStderr(), "skipping %s: CRD not installed in cluster\n", rt)
100+
}
101+
continue
102+
}
96103
return err
97104
}
98105
}
99106
return nil
100107
case "xroute":
101108
for _, rt := range supportedXRouteTypes {
102109
if err = runStatus(ctx, cmd.OutOrStdout(), k8sClient, rt, namespace, quiet, verbose, allNamespaces, true, true); err != nil {
110+
if meta.IsNoMatchError(err) {
111+
if verbose {
112+
fmt.Fprintf(cmd.ErrOrStderr(), "skipping %s: CRD not installed in cluster\n", rt)
113+
}
114+
continue
115+
}
103116
return err
104117
}
105118
}
106119
return nil
107120
case "xpolicy":
108121
for _, rt := range supportedXPolicyTypes {
109122
if err = runStatus(ctx, cmd.OutOrStdout(), k8sClient, rt, namespace, quiet, verbose, allNamespaces, true, true); err != nil {
123+
if meta.IsNoMatchError(err) {
124+
if verbose {
125+
fmt.Fprintf(cmd.ErrOrStderr(), "skipping %s: CRD not installed in cluster\n", rt)
126+
}
127+
continue
128+
}
110129
return err
111130
}
112131
}

internal/crypto/cert_load.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,34 @@ func LoadTLSConfig(tlsCrt, tlsKey, caCrt string) (*tls.Config, error) {
5555
},
5656
}, nil
5757
}
58+
59+
// LoadServerTLSConfig returns a server-only TLSConfig from a certificate. Unlike
60+
// LoadTLSConfig it does not require client certificate verification, for callers
61+
// that authenticate peers out-of-band (e.g. JWT).
62+
func LoadServerTLSConfig(tlsCrt, tlsKey string) (*tls.Config, error) {
63+
loadConfig := func() (*tls.Config, error) {
64+
cert, err := tls.LoadX509KeyPair(tlsCrt, tlsKey)
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
return &tls.Config{
70+
Certificates: []tls.Certificate{cert},
71+
NextProtos: []string{"h2"},
72+
MinVersion: tls.VersionTLS13,
73+
}, nil
74+
}
75+
76+
// Attempt to load certificate and key to catch configuration errors early.
77+
if _, err := loadConfig(); err != nil {
78+
return nil, err
79+
}
80+
81+
return &tls.Config{
82+
MinVersion: tls.VersionTLS13,
83+
Rand: rand.Reader,
84+
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
85+
return loadConfig()
86+
},
87+
}, nil
88+
}

internal/crypto/cert_load_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright Envoy Gateway Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
// The full text of the Apache license is available in the LICENSE file at
4+
// the root of the repo.
5+
6+
package crypto
7+
8+
import (
9+
"crypto/tls"
10+
"crypto/x509"
11+
"errors"
12+
"io"
13+
"net"
14+
"path/filepath"
15+
"testing"
16+
"time"
17+
18+
"github.com/stretchr/testify/assert"
19+
"github.com/stretchr/testify/require"
20+
"github.com/tsaarni/certyaml"
21+
"google.golang.org/grpc"
22+
"google.golang.org/grpc/credentials"
23+
)
24+
25+
// TestLoadServerTLSConfig verifies the returned tls.Config re-reads the cert from
26+
// disk on every handshake and does not request a client certificate.
27+
func TestLoadServerTLSConfig(t *testing.T) {
28+
configDir := t.TempDir()
29+
certFile := filepath.Join(configDir, "tls.crt")
30+
keyFile := filepath.Join(configDir, "tls.key")
31+
32+
caCert := certyaml.Certificate{Subject: "cn=test-ca"}
33+
34+
serverCertBefore := certyaml.Certificate{
35+
Subject: "cn=server-before",
36+
SubjectAltNames: []string{"DNS:localhost"},
37+
Issuer: &caCert,
38+
}
39+
serverCertAfter := certyaml.Certificate{
40+
Subject: "cn=server-after",
41+
SubjectAltNames: []string{"DNS:localhost"},
42+
Issuer: &caCert,
43+
}
44+
45+
require.NoError(t, serverCertBefore.WritePEM(certFile, keyFile))
46+
47+
tlsCfg, err := LoadServerTLSConfig(certFile, keyFile)
48+
require.NoError(t, err)
49+
require.NotNil(t, tlsCfg)
50+
51+
require.NotNil(t, tlsCfg.GetConfigForClient)
52+
require.Equal(t, uint16(tls.VersionTLS13), tlsCfg.MinVersion)
53+
require.Equal(t, tls.NoClientCert, tlsCfg.ClientAuth)
54+
require.Empty(t, tlsCfg.Certificates)
55+
56+
// Stand up a TLS server, dial it, rotate the cert on disk, dial again.
57+
caPool := x509.NewCertPool()
58+
ca, err := caCert.X509Certificate()
59+
require.NoError(t, err)
60+
caPool.AddCert(&ca)
61+
62+
srv := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsCfg)))
63+
defer srv.GracefulStop()
64+
65+
l, err := net.Listen("tcp", "localhost:0")
66+
require.NoError(t, err)
67+
68+
go func() { _ = srv.Serve(l) }()
69+
70+
dial := func() *x509.Certificate {
71+
t.Helper()
72+
conn, err := tls.Dial("tcp", l.Addr().String(), &tls.Config{
73+
ServerName: "localhost",
74+
MinVersion: tls.VersionTLS13,
75+
RootCAs: caPool,
76+
NextProtos: []string{"h2"},
77+
})
78+
require.NoError(t, err)
79+
defer conn.Close()
80+
// TLS 1.3 defers server alerts until first read; surface them now.
81+
_ = conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
82+
_, readErr := conn.Read(make([]byte, 1))
83+
if readErr != nil && !errors.Is(readErr, io.EOF) {
84+
var netErr net.Error
85+
require.True(t, errors.As(readErr, &netErr) && netErr.Timeout(),
86+
"unexpected read error after handshake: %v", readErr)
87+
}
88+
require.NotEmpty(t, conn.ConnectionState().PeerCertificates)
89+
return conn.ConnectionState().PeerCertificates[0]
90+
}
91+
92+
gotBefore := dial()
93+
expectedBefore, err := serverCertBefore.X509Certificate()
94+
require.NoError(t, err)
95+
assert.Equal(t, expectedBefore.Subject.CommonName, gotBefore.Subject.CommonName)
96+
97+
require.NoError(t, serverCertAfter.WritePEM(certFile, keyFile))
98+
99+
gotAfter := dial()
100+
expectedAfter, err := serverCertAfter.X509Certificate()
101+
require.NoError(t, err)
102+
assert.Equal(t, expectedAfter.Subject.CommonName, gotAfter.Subject.CommonName)
103+
assert.NotEqual(t, gotBefore.Subject.CommonName, gotAfter.Subject.CommonName)
104+
}
105+
106+
func TestLoadServerTLSConfig_MissingFiles(t *testing.T) {
107+
_, err := LoadServerTLSConfig("/does/not/exist/tls.crt", "/does/not/exist/tls.key")
108+
require.Error(t, err)
109+
}

internal/gatewayapi/backendtlspolicy.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,15 +403,15 @@ func (t *Translator) processClientTLSSettings(
403403
return tlsConfig, nil
404404
}
405405

406-
func backendTLSTargetMatched(policy *gwapiv1.BackendTLSPolicy, target gwapiv1.LocalPolicyTargetReferenceWithSectionName, backendNamespace string) bool {
406+
func backendTLSTargetMatched(policy *gwapiv1.BackendTLSPolicy, target gwapiv1.LocalPolicyTargetReferenceWithSectionName, backendNamespace string, shouldSectionNameMatch bool) bool {
407407
for _, currTarget := range policy.Spec.TargetRefs {
408408
if target.Group == currTarget.Group &&
409409
target.Kind == currTarget.Kind &&
410410
backendNamespace == policy.Namespace &&
411411
target.Name == currTarget.Name {
412412
// if section name is not set, then it targets the entire backend
413-
if currTarget.SectionName == nil {
414-
return true
413+
if currTarget.SectionName == nil && target.SectionName != nil {
414+
return !shouldSectionNameMatch
415415
} else if reflect.DeepEqual(currTarget.SectionName, target.SectionName) {
416416
return true
417417
}
@@ -427,8 +427,16 @@ func (t *Translator) getBackendTLSPolicy(
427427
) *gwapiv1.BackendTLSPolicy {
428428
// SectionName is port number for EG Backend object
429429
target := t.getTargetBackendReference(backendRef, backendNamespace)
430+
if target.SectionName != nil {
431+
for _, policy := range policies {
432+
if backendTLSTargetMatched(policy, target, backendNamespace, true) {
433+
// prefer policies that target this specific section over wildcard matches
434+
return policy
435+
}
436+
}
437+
}
430438
for _, policy := range policies {
431-
if backendTLSTargetMatched(policy, target, backendNamespace) {
439+
if backendTLSTargetMatched(policy, target, backendNamespace, false) {
432440
return policy
433441
}
434442
}

internal/gatewayapi/route.go

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1996,6 +1996,9 @@ func (t *Translator) processServiceImportDestinationSetting(
19961996
if servicePort.AppProtocol != nil {
19971997
protocol = serviceAppProtocolToIRAppProtocol(*servicePort.AppProtocol, protocol, false)
19981998
}
1999+
// For WebSocket services, force HTTP/1.1 upstream to ensure Envoy can establish a successful connection,
2000+
// as WebSocket over HTTP/2 is not widely supported by upstreams and can lead to connection failures.
2001+
forceHTTP1Upstream := servicePort.AppProtocol != nil && isWebSocketServiceAppProtocol(*servicePort.AppProtocol)
19992002

20002003
backendIps := serviceImport.Spec.IPs
20012004
isHeadless := len(backendIps) == 0
@@ -2020,11 +2023,12 @@ func (t *Translator) processServiceImportDestinationSetting(
20202023
}
20212024

20222025
return &ir.DestinationSetting{
2023-
Name: name,
2024-
Protocol: protocol,
2025-
Endpoints: endpoints,
2026-
AddressType: addrType,
2027-
Metadata: buildResourceMetadata(serviceImport, ptr.To(gwapiv1.SectionName(strconv.Itoa(int(*backendRef.Port))))),
2026+
Name: name,
2027+
Protocol: protocol,
2028+
ForceHTTP1Upstream: forceHTTP1Upstream,
2029+
Endpoints: endpoints,
2030+
AddressType: addrType,
2031+
Metadata: buildResourceMetadata(serviceImport, ptr.To(gwapiv1.SectionName(strconv.Itoa(int(*backendRef.Port))))),
20282032
}, nil
20292033
}
20302034

@@ -2053,6 +2057,9 @@ func (t *Translator) processServiceDestinationSetting(
20532057
if servicePort.AppProtocol != nil {
20542058
protocol = serviceAppProtocolToIRAppProtocol(*servicePort.AppProtocol, protocol, true)
20552059
}
2060+
// For WebSocket services, force HTTP/1.1 upstream to ensure Envoy can establish a successful connection,
2061+
// as WebSocket over HTTP/2 is not widely supported by upstreams and can lead to connection failures.
2062+
forceHTTP1Upstream := servicePort.AppProtocol != nil && isWebSocketServiceAppProtocol(*servicePort.AppProtocol)
20562063

20572064
isHeadless := isServiceHeadless(service)
20582065

@@ -2074,12 +2081,13 @@ func (t *Translator) processServiceDestinationSetting(
20742081
}
20752082

20762083
return &ir.DestinationSetting{
2077-
Name: name,
2078-
Protocol: protocol,
2079-
Endpoints: endpoints,
2080-
AddressType: addrType,
2081-
PreferLocal: processPreferLocalZone(service),
2082-
Metadata: buildResourceMetadata(service, ptr.To(gwapiv1.SectionName(strconv.Itoa(int(*backendRef.Port))))),
2084+
Name: name,
2085+
Protocol: protocol,
2086+
ForceHTTP1Upstream: forceHTTP1Upstream,
2087+
Endpoints: endpoints,
2088+
AddressType: addrType,
2089+
PreferLocal: processPreferLocalZone(service),
2090+
Metadata: buildResourceMetadata(service, ptr.To(gwapiv1.SectionName(strconv.Itoa(int(*backendRef.Port))))),
20832091
}, nil
20842092
}
20852093

@@ -2537,13 +2545,19 @@ func serviceAppProtocolToIRAppProtocol(ap string, defaultProtocol ir.AppProtocol
25372545
switch {
25382546
case ap == "kubernetes.io/h2c":
25392547
return ir.HTTP2
2548+
case ap == "kubernetes.io/ws" || ap == "kubernetes.io/wss":
2549+
return ir.HTTP
25402550
case ap == "grpc" && grpcCompatibility:
25412551
return ir.GRPC
25422552
default:
25432553
return defaultProtocol
25442554
}
25452555
}
25462556

2557+
func isWebSocketServiceAppProtocol(ap string) bool {
2558+
return ap == "kubernetes.io/ws" || ap == "kubernetes.io/wss"
2559+
}
2560+
25472561
func backendAppProtocolToIRAppProtocol(ap egv1a1.AppProtocolType, defaultProtocol ir.AppProtocol) ir.AppProtocol {
25482562
switch ap {
25492563
case egv1a1.AppProtocolTypeH2C:

internal/gatewayapi/route_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,64 @@ import (
2222
"github.com/envoyproxy/gateway/internal/ir"
2323
)
2424

25+
func TestServiceAppProtocolToIRAppProtocol(t *testing.T) {
26+
tests := []struct {
27+
name string
28+
appProtocol string
29+
defaultProtocol ir.AppProtocol
30+
grpcCompatibility bool
31+
want ir.AppProtocol
32+
wantForceHTTP1 bool
33+
}{
34+
{
35+
name: "h2c",
36+
appProtocol: "kubernetes.io/h2c",
37+
defaultProtocol: ir.HTTP,
38+
want: ir.HTTP2,
39+
},
40+
{
41+
name: "ws",
42+
appProtocol: "kubernetes.io/ws",
43+
defaultProtocol: ir.HTTP2,
44+
want: ir.HTTP,
45+
wantForceHTTP1: true,
46+
},
47+
{
48+
name: "wss",
49+
appProtocol: "kubernetes.io/wss",
50+
defaultProtocol: ir.HTTP2,
51+
want: ir.HTTP,
52+
wantForceHTTP1: true,
53+
},
54+
{
55+
name: "grpc compatible",
56+
appProtocol: "grpc",
57+
defaultProtocol: ir.HTTP,
58+
grpcCompatibility: true,
59+
want: ir.GRPC,
60+
},
61+
{
62+
name: "grpc not compatible",
63+
appProtocol: "grpc",
64+
defaultProtocol: ir.HTTP,
65+
want: ir.HTTP,
66+
},
67+
{
68+
name: "unknown",
69+
appProtocol: "example.com/custom",
70+
defaultProtocol: ir.HTTP,
71+
want: ir.HTTP,
72+
},
73+
}
74+
75+
for _, tt := range tests {
76+
t.Run(tt.name, func(t *testing.T) {
77+
require.Equal(t, tt.want, serviceAppProtocolToIRAppProtocol(tt.appProtocol, tt.defaultProtocol, tt.grpcCompatibility))
78+
require.Equal(t, tt.wantForceHTTP1, isWebSocketServiceAppProtocol(tt.appProtocol))
79+
})
80+
}
81+
}
82+
2583
func TestGetIREndpointsFromEndpointSlices(t *testing.T) {
2684
tests := []struct {
2785
name string

0 commit comments

Comments
 (0)