Skip to content

Commit eb81653

Browse files
committed
TEST/MEDIUM: ssl-passthrough: show allow-list dropped from https frontend for non-passthrough ingresses
Demonstrates a fail-open bug in addRules() (pkg/ingress/ingress.go): once any single ingress in the cluster uses ssl-passthrough, the controller-wide haproxy.SSLPassthrough flag is true, and REQ_DENY/ REQ_CAPTURE rules for every ingress get placed on {http, ssl} instead of {http, https} - dropping the https frontend even for ingresses that never opted into passthrough. Their TLS traffic still terminates on the https frontend (relayed there via the ssl frontend's default backend, or arriving directly on the QUIC binds), so their allow-list/deny-list goes unenforced on HTTPS while plain HTTP is still correctly filtered. The test deploys an unrelated ssl-passthrough sidecar ingress, verifies passthrough is genuinely active (SNI observed at the echo backend), then asserts a plain allow-list ingress still returns 403 over HTTPS for a non-allow-listed client, plus a positive control (widened allow-list must give 200). This test FAILS on master: the 403 never comes because the https frontend carries no deny rule; requests reach the backend with 200.
1 parent fc12067 commit eb81653

3 files changed

Lines changed: 149 additions & 0 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2026 HAProxy Technologies LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:build e2e_https
16+
17+
package https
18+
19+
import (
20+
"encoding/json"
21+
"io"
22+
"net/http"
23+
"testing"
24+
25+
"github.com/haproxytech/kubernetes-ingress/deploy/tests/e2e"
26+
"github.com/stretchr/testify/suite"
27+
)
28+
29+
// Adding AllowListWithClusterPassthroughSuite, just to be able to debug directly here and not from CRDSuite
30+
type AllowListWithClusterPassthroughSuite struct {
31+
HTTPSSuite
32+
}
33+
34+
func TestAllowListWithClusterPassthroughSuite(t *testing.T) {
35+
suite.Run(t, new(AllowListWithClusterPassthroughSuite))
36+
}
37+
38+
func (suite *AllowListWithClusterPassthroughSuite) Test_AllowList_With_ClusterPassthrough() {
39+
// A second, unrelated ingress using ssl-passthrough. Its mere presence in
40+
// the cluster flips haproxy.SSLPassthrough, which must not affect rule
41+
// placement for other, non-passthrough ingresses.
42+
defer func() {
43+
suite.Require().NoError(suite.test.Delete("config/passthrough-sidecar-delete.yaml"))
44+
}()
45+
sidecarHost := "passthrough-sidecar." + suite.test.GetNS() + ".test"
46+
passthroughData := tmplData{
47+
Host: sidecarHost,
48+
Port: "https",
49+
IngAnnotations: []struct{ Key, Value string }{
50+
{"ssl-passthrough", "'true'"},
51+
},
52+
}
53+
suite.Require().NoError(suite.test.Apply("config/passthrough-sidecar.yaml.tmpl", suite.test.GetNS(), passthroughData))
54+
55+
// Confirm the sidecar's ssl-passthrough is genuinely active before relying
56+
// on it as this test's precondition - mirrors passthrough_test.go's own
57+
// Reach_Backend check. The echo backend only reports a TLS SNI when
58+
// HAProxy relayed a raw passthrough connection instead of terminating
59+
// TLS itself, so this can't pass unless SSLPassthrough actually flipped.
60+
sidecarClient, err := e2e.NewHTTPSClient(sidecarHost)
61+
suite.Require().NoError(err)
62+
suite.Eventually(func() bool {
63+
res, cls, err := sidecarClient.Do()
64+
if res == nil {
65+
suite.T().Log(err)
66+
return false
67+
}
68+
defer cls()
69+
body, err := io.ReadAll(res.Body)
70+
if err != nil {
71+
return false
72+
}
73+
type echoServerResponse struct {
74+
TLS struct {
75+
SNI string `json:"sni"`
76+
} `json:"tls"`
77+
}
78+
response := &echoServerResponse{}
79+
if err := json.Unmarshal(body, response); err != nil {
80+
return false
81+
}
82+
return response.TLS.SNI == sidecarHost
83+
}, e2e.WaitDuration, e2e.TickDuration, "expected the sidecar ssl-passthrough ingress to be active before testing the main ingress's allow-list")
84+
85+
// The ingress under test: allow-list only, no ssl-passthrough of its own.
86+
// suite.client (created in BeforeTest) targets suite.tmplData.Host over HTTPS.
87+
suite.tmplData.IngAnnotations = []struct{ Key, Value string }{
88+
{"allow-list", "6.6.6.6"},
89+
}
90+
suite.Require().NoError(suite.test.Apply("config/ingress.yaml.tmpl", suite.test.GetNS(), suite.tmplData))
91+
92+
suite.Eventually(func() bool {
93+
res, cls, err := suite.client.Do()
94+
if res == nil {
95+
suite.T().Log(err)
96+
return false
97+
}
98+
defer cls()
99+
// The test client is never 6.6.6.6, so its HTTPS request must be
100+
// denied - even though a coexisting ingress uses ssl-passthrough.
101+
return res.StatusCode == http.StatusForbidden
102+
}, e2e.WaitDuration, e2e.TickDuration, "expected HTTPS request to the non-passthrough ingress to be blocked by allow-list")
103+
104+
// Positive control on the same ingress/host/frontend: widen the allow-list
105+
// to everyone and confirm the same client now gets through. FrontHTTPS is
106+
// shared by every TLS-terminated ingress in the cluster, so proving the
107+
// prior 403 alone doesn't rule out a regression that makes the deny
108+
// unconditional (denies everyone) rather than correctly scoped.
109+
suite.tmplData.IngAnnotations = []struct{ Key, Value string }{
110+
{"allow-list", "0.0.0.0/0"},
111+
}
112+
suite.Require().NoError(suite.test.Apply("config/ingress.yaml.tmpl", suite.test.GetNS(), suite.tmplData))
113+
114+
suite.Eventually(func() bool {
115+
res, cls, err := suite.client.Do()
116+
if res == nil {
117+
suite.T().Log(err)
118+
return false
119+
}
120+
defer cls()
121+
return res.StatusCode == http.StatusOK
122+
}, e2e.WaitDuration, e2e.TickDuration, "expected HTTPS request to succeed once the allow-list is widened to everyone")
123+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: Ingress
2+
apiVersion: networking.k8s.io/v1
3+
metadata:
4+
name: http-echo-passthrough-sidecar
5+
namespace: e2e-tests-https
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
kind: Ingress
2+
apiVersion: networking.k8s.io/v1
3+
metadata:
4+
name: http-echo-passthrough-sidecar
5+
annotations:
6+
{{range .IngAnnotations}}
7+
{{ .Key }}: {{ .Value}}
8+
{{end}}
9+
spec:
10+
ingressClassName: haproxy
11+
rules:
12+
- host: {{ .Host }}
13+
http:
14+
paths:
15+
- path: /
16+
pathType: Prefix
17+
backend:
18+
service:
19+
name: http-echo
20+
port:
21+
name: {{ .Port }}

0 commit comments

Comments
 (0)