Skip to content

Commit 54e6250

Browse files
committed
fix: generate unique SSL IDs to prevent certificate conflicts across different hosts (#2592)
Signed-off-by: Ashing Zheng <[email protected]>
1 parent bb9e53d commit 54e6250

File tree

8 files changed

+170
-76
lines changed

8 files changed

+170
-76
lines changed

api/adc/types.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,19 @@ func (n Upstream) MarshalJSON() ([]byte, error) {
472472
return json.Marshal((Alias)(n))
473473
}
474474

475+
func ComposeSSLName(kind, namespace, name string) string {
476+
p := make([]byte, 0, len(kind)+len(namespace)+len(name)+2)
477+
buf := bytes.NewBuffer(p)
478+
479+
buf.WriteString(kind)
480+
buf.WriteByte('_')
481+
buf.WriteString(namespace)
482+
buf.WriteByte('_')
483+
buf.WriteString(name)
484+
485+
return buf.String()
486+
}
487+
475488
// ComposeRouteName uses namespace, name and rule name to compose
476489
// the route name.
477490
func ComposeRouteName(namespace, name string, rule string) string {

internal/adc/translator/apisixtls.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/apache/apisix-ingress-controller/internal/controller/label"
2828
"github.com/apache/apisix-ingress-controller/internal/id"
2929
"github.com/apache/apisix-ingress-controller/internal/provider"
30+
internaltypes "github.com/apache/apisix-ingress-controller/internal/types"
3031
)
3132

3233
func (t *Translator) TranslateApisixTls(tctx *provider.TranslateContext, tls *apiv2.ApisixTls) (*TranslateResult, error) {
@@ -57,7 +58,7 @@ func (t *Translator) TranslateApisixTls(tctx *provider.TranslateContext, tls *ap
5758
// Create SSL object
5859
ssl := &adctypes.SSL{
5960
Metadata: adctypes.Metadata{
60-
ID: id.GenID(tls.Namespace + "_" + tls.Name),
61+
ID: id.GenID(adctypes.ComposeSSLName(internaltypes.KindApisixTls, tls.Namespace, tls.Name)),
6162
Labels: label.GenLabel(tls),
6263
},
6364
Certificates: []adctypes.Certificate{

internal/adc/translator/gateway.go

Lines changed: 2 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"encoding/json"
2323
"encoding/pem"
2424
"fmt"
25-
"slices"
2625

2726
"github.com/pkg/errors"
2827
corev1 "k8s.io/api/core/v1"
@@ -50,7 +49,6 @@ func (t *Translator) TranslateGateway(tctx *provider.TranslateContext, obj *gate
5049
result.SSL = append(result.SSL, ssl...)
5150
}
5251
}
53-
result.SSL = mergeSSLWithSameID(result.SSL)
5452

5553
rk := utils.NamespacedNameKind(obj)
5654
gatewayProxy, ok := tctx.GatewayProxies[rk]
@@ -80,7 +78,7 @@ func (t *Translator) translateSecret(tctx *provider.TranslateContext, listener g
8078
sslObjs := make([]*adctypes.SSL, 0)
8179
switch *listener.TLS.Mode {
8280
case gatewayv1.TLSModeTerminate:
83-
for _, ref := range listener.TLS.CertificateRefs {
81+
for refIndex, ref := range listener.TLS.CertificateRefs {
8482
ns := obj.GetNamespace()
8583
if ref.Namespace != nil {
8684
ns = string(*ref.Namespace)
@@ -122,8 +120,7 @@ func (t *Translator) translateSecret(tctx *provider.TranslateContext, listener g
122120
}
123121
sslObj.Snis = append(sslObj.Snis, hosts...)
124122
}
125-
// Note: use cert as id to avoid duplicate certificate across ssl objects
126-
sslObj.ID = id.GenID(string(cert))
123+
sslObj.ID = id.GenID(fmt.Sprintf("%s_%s_%d", adctypes.ComposeSSLName(internaltypes.KindGateway, obj.Namespace, obj.Name), listener.Name, refIndex))
127124
t.Log.V(1).Info("generated ssl id", "ssl id", sslObj.ID, "secret", secretNN.String())
128125
sslObj.Labels = label.GenLabel(obj)
129126
sslObjs = append(sslObjs, sslObj)
@@ -241,47 +238,3 @@ func (t *Translator) fillPluginMetadataFromGatewayProxy(pluginMetadata adctypes.
241238
pluginMetadata[pluginName] = pluginConfig
242239
}
243240
}
244-
245-
// mergeSSLWithSameID merge ssl with same id
246-
func mergeSSLWithSameID(sslList []*adctypes.SSL) []*adctypes.SSL {
247-
if len(sslList) <= 1 {
248-
return sslList
249-
}
250-
251-
// create a map to store ssl with same id
252-
sslMap := make(map[string]*adctypes.SSL)
253-
for _, ssl := range sslList {
254-
if existing, exists := sslMap[ssl.ID]; exists {
255-
// if ssl with same id exists, merge their snis
256-
// use map to deduplicate
257-
sniMap := make(map[string]struct{})
258-
// add existing snis
259-
for _, sni := range existing.Snis {
260-
sniMap[sni] = struct{}{}
261-
}
262-
// add new snis
263-
for _, sni := range ssl.Snis {
264-
sniMap[sni] = struct{}{}
265-
}
266-
// rebuild deduplicated snis list
267-
newSnis := make([]string, 0, len(sniMap))
268-
for sni := range sniMap {
269-
newSnis = append(newSnis, sni)
270-
}
271-
272-
slices.Sort(newSnis)
273-
// update existing ssl object
274-
existing.Snis = newSnis
275-
} else {
276-
slices.Sort(ssl.Snis)
277-
// if new ssl id, add to map
278-
sslMap[ssl.ID] = ssl
279-
}
280-
}
281-
282-
mergedSSL := make([]*adctypes.SSL, 0, len(sslMap))
283-
for _, ssl := range sslMap {
284-
mergedSSL = append(mergedSSL, ssl)
285-
}
286-
return mergedSSL
287-
}

internal/adc/translator/ingress.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import (
3333
internaltypes "github.com/apache/apisix-ingress-controller/internal/types"
3434
)
3535

36-
func (t *Translator) translateIngressTLS(ingressTLS *networkingv1.IngressTLS, secret *corev1.Secret, labels map[string]string) (*adctypes.SSL, error) {
36+
func (t *Translator) translateIngressTLS(namespace, name string, tlsIndex int, ingressTLS *networkingv1.IngressTLS, secret *corev1.Secret, labels map[string]string) (*adctypes.SSL, error) {
3737
// extract the key pair from the secret
3838
cert, key, err := extractKeyPair(secret, true)
3939
if err != nil {
@@ -64,7 +64,7 @@ func (t *Translator) translateIngressTLS(ingressTLS *networkingv1.IngressTLS, se
6464
},
6565
Snis: hosts,
6666
}
67-
ssl.ID = id.GenID(string(cert))
67+
ssl.ID = id.GenID(fmt.Sprintf("%s_%d", adctypes.ComposeSSLName(internaltypes.KindIngress, namespace, name), tlsIndex))
6868

6969
return ssl, nil
7070
}
@@ -75,7 +75,7 @@ func (t *Translator) TranslateIngress(tctx *provider.TranslateContext, obj *netw
7575
labels := label.GenLabel(obj)
7676

7777
// handle TLS configuration, convert to SSL objects
78-
for _, tls := range obj.Spec.TLS {
78+
for tlsIndex, tls := range obj.Spec.TLS {
7979
if tls.SecretName == "" {
8080
continue
8181
}
@@ -86,7 +86,7 @@ func (t *Translator) TranslateIngress(tctx *provider.TranslateContext, obj *netw
8686
if secret == nil {
8787
continue
8888
}
89-
ssl, err := t.translateIngressTLS(&tls, secret, labels)
89+
ssl, err := t.translateIngressTLS(obj.Namespace, obj.Name, tlsIndex, &tls, secret, labels)
9090
if err != nil {
9191
return nil, err
9292
}

test/e2e/crds/v2/tls.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,5 +310,108 @@ spec:
310310
assert.Equal(GinkgoT(), int64(10), *tls[0].Client.Depth, "client depth should be 10")
311311
assert.Contains(GinkgoT(), tls[0].Client.SkipMtlsURIRegex, skipMtlsUriRegex, "skip_mtls_uri_regex should be set")
312312
})
313+
314+
It("ApisixTls and Ingress with same certificate but different hosts", func() {
315+
By("create shared TLS secret")
316+
err := s.NewKubeTlsSecret("shared-tls-secret", Cert, Key)
317+
Expect(err).NotTo(HaveOccurred(), "creating shared TLS secret")
318+
319+
const apisixTlsSpec = `
320+
apiVersion: apisix.apache.org/v2
321+
kind: ApisixTls
322+
metadata:
323+
name: test-apisixtls-shared
324+
spec:
325+
ingressClassName: %s
326+
hosts:
327+
- api6.com
328+
secret:
329+
name: shared-tls-secret
330+
namespace: %s
331+
`
332+
333+
By("apply ApisixTls with api6.com")
334+
var apisixTls apiv2.ApisixTls
335+
tlsSpec := fmt.Sprintf(apisixTlsSpec, s.Namespace(), s.Namespace())
336+
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-apisixtls-shared"}, &apisixTls, tlsSpec)
337+
338+
const ingressYamlWithTLS = `
339+
apiVersion: networking.k8s.io/v1
340+
kind: Ingress
341+
metadata:
342+
name: test-ingress-tls-shared
343+
spec:
344+
ingressClassName: %s
345+
tls:
346+
- hosts:
347+
- api7.com
348+
secretName: shared-tls-secret
349+
rules:
350+
- host: api7.com
351+
http:
352+
paths:
353+
- path: /
354+
pathType: Prefix
355+
backend:
356+
service:
357+
name: httpbin-service-e2e-test
358+
port:
359+
number: 80
360+
`
361+
362+
By("apply Ingress with api7.com using same certificate")
363+
err = s.CreateResourceFromString(fmt.Sprintf(ingressYamlWithTLS, s.Namespace()))
364+
Expect(err).NotTo(HaveOccurred(), "creating Ingress")
365+
366+
By("verify two SSL objects exist in control plane")
367+
Eventually(func() bool {
368+
tls, err := s.DefaultDataplaneResource().SSL().List(context.Background())
369+
if err != nil {
370+
return false
371+
}
372+
return len(tls) == 2
373+
}).WithTimeout(30 * time.Second).ProbeEvery(1 * time.Second).Should(BeTrue())
374+
375+
tls, err := s.DefaultDataplaneResource().SSL().List(context.Background())
376+
assert.Nil(GinkgoT(), err, "list tls error")
377+
assert.Len(GinkgoT(), tls, 2, "should have exactly 2 SSL objects")
378+
379+
By("verify SSL objects have different IDs and SNIs")
380+
sniFound := make(map[string]bool)
381+
382+
for i := range tls {
383+
// Check certificate content is the same
384+
assert.Len(GinkgoT(), tls[i].Certificates, 1, "each SSL should have 1 certificate")
385+
assert.Equal(GinkgoT(), Cert, tls[i].Certificates[0].Certificate, "certificate should match")
386+
387+
// Track SNIs
388+
for _, sni := range tls[i].Snis {
389+
sniFound[sni] = true
390+
}
391+
}
392+
393+
By("verify both hosts are covered")
394+
assert.True(GinkgoT(), sniFound["api6.com"], "api6.com should be in SNIs")
395+
assert.True(GinkgoT(), sniFound["api7.com"], "api7.com should be in SNIs")
396+
397+
By("test HTTPS request to api6.com")
398+
Eventually(func() int {
399+
return s.NewAPISIXHttpsClient("api6.com").
400+
GET("/get").
401+
WithHost("api6.com").
402+
Expect().
403+
Raw().StatusCode
404+
}).WithTimeout(30 * time.Second).ProbeEvery(1 * time.Second).Should(Equal(http.StatusOK))
405+
406+
By("test HTTPS request to api7.com")
407+
Eventually(func() int {
408+
return s.NewAPISIXHttpsClient("api7.com").
409+
GET("/get").
410+
WithHost("api7.com").
411+
Expect().
412+
Raw().StatusCode
413+
}).WithTimeout(30 * time.Second).ProbeEvery(1 * time.Second).Should(Equal(http.StatusOK))
414+
})
415+
313416
})
314417
})

test/e2e/gatewayapi/gateway.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,8 @@ spec:
284284
Eventually(func() error {
285285
tls, err := s.DefaultDataplaneResource().SSL().List(context.Background())
286286
Expect(err).NotTo(HaveOccurred(), "list ssl")
287-
if len(tls) != 1 {
288-
return fmt.Errorf("expect 1 ssl, got %d", len(tls))
287+
if len(tls) != 2 {
288+
return fmt.Errorf("expect 2 ssl, got %d", len(tls))
289289
}
290290
if len(tls[0].Certificates) != 1 {
291291
return fmt.Errorf("expect 1 certificate, got %d", len(tls[0].Certificates))
@@ -305,7 +305,7 @@ spec:
305305
Eventually(func() string {
306306
tls, err := s.DefaultDataplaneResource().SSL().List(context.Background())
307307
Expect(err).NotTo(HaveOccurred(), "list ssl")
308-
if len(tls) < 1 {
308+
if len(tls) != 2 {
309309
return ""
310310
}
311311
if len(tls[0].Certificates) < 1 {

test/e2e/gatewayapi/tcproute.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,21 +72,22 @@ spec:
7272

7373
// Create GatewayClass
7474
Expect(s.CreateResourceFromString(s.GetGatewayClassYaml())).NotTo(HaveOccurred(), "creating GatewayClass")
75+
Expect(s.CreateResourceFromString(s.GetGatewayClassYaml())).
76+
NotTo(HaveOccurred(), "creating GatewayClass")
7577

7678
// Create Gateway with TCP listener
7779
Expect(s.CreateResourceFromString(fmt.Sprintf(tcpGateway, s.Namespace(), s.Namespace()))).
7880
NotTo(HaveOccurred(), "creating Gateway")
7981
})
8082

8183
It("should route TCP traffic to backend service", func() {
82-
gatewayName := s.Namespace()
8384
By("creating TCPRoute")
84-
Expect(s.CreateResourceFromString(fmt.Sprintf(tcpRoute, gatewayName))).
85+
Expect(s.CreateResourceFromString(fmt.Sprintf(tcpRoute, s.Namespace()))).
8586
NotTo(HaveOccurred(), "creating TCPRoute")
8687
time.Sleep(2 * time.Second)
8788

8889
By("verifying TCPRoute is functional")
89-
s.HTTPOverTCPConnectAssert(true, time.Minute*5) // should be able to connect
90+
s.HTTPOverTCPConnectAssert(true, time.Minute*3) // should be able to connect
9091
By("sending TCP traffic to verify routing")
9192
s.RequestAssert(&scaffold.RequestAssert{
9293
Client: s.NewAPISIXClientOnTCPPort(),
@@ -101,7 +102,7 @@ spec:
101102
Expect(s.DeleteResource("TCPRoute", "tcp-app-1")).
102103
NotTo(HaveOccurred(), "deleting TCPRoute")
103104

104-
s.HTTPOverTCPConnectAssert(false, time.Minute*5)
105+
s.HTTPOverTCPConnectAssert(false, time.Minute*3)
105106
})
106107
})
107108
})

test/e2e/scaffold/assertion.go

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -200,35 +200,58 @@ func (s *Scaffold) HTTPOverTCPConnectAssert(shouldRespond bool, timeout time.Dur
200200
defer func() {
201201
_ = conn.Close()
202202
}()
203-
_, _ = fmt.Fprintf(conn, "GET /get HTTP/1.1\r\nHost: localhost\r\n\r\n")
203+
_, _ = fmt.Fprintf(conn, "GET /get HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
204+
205+
// Read response - loop until EOF or error
206+
// Set deadline for total read time (not per-read)
207+
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
208+
var responseBuilder strings.Builder
209+
buf := make([]byte, 4096)
210+
211+
for {
212+
n, err := conn.Read(buf)
213+
if n > 0 {
214+
responseBuilder.Write(buf[:n])
215+
}
204216

205-
// Read response
206-
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
207-
buf := make([]byte, 1024)
208-
n, err := conn.Read(buf)
217+
// EOF is expected with Connection: close
218+
if err == io.EOF {
219+
break
220+
}
221+
222+
// Other errors (including timeout)
223+
if err != nil {
224+
// If we haven't received any data yet, this is an error
225+
if responseBuilder.Len() == 0 {
226+
return fmt.Errorf("read error before receiving data: %v", err)
227+
}
228+
// If we have partial data, treat timeout as potential issue
229+
if strings.Contains(err.Error(), "timeout") {
230+
break // Use whatever we've received so far
231+
}
232+
return fmt.Errorf("read error: %v", err)
233+
}
234+
}
235+
236+
response := responseBuilder.String()
209237

210238
if shouldRespond {
211239
// Should get a response (HTTP 200 from httpbin)
212-
if err != nil || n == 0 {
213-
return fmt.Errorf("expected response but got error: %v or empty response", err)
240+
if len(response) == 0 {
241+
return fmt.Errorf("expected response but got empty response")
214242
}
215243
// Check if we got a valid HTTP response
216-
response := string(buf[:n])
217244
if !strings.Contains(response, "HTTP/1.1") {
218245
return fmt.Errorf("expected HTTP response but got: %s", response)
219246
}
220247
} else {
221248
// Should get no response or connection reset
222-
if err == nil && n > 0 {
223-
return fmt.Errorf("expected no response but got: %s", string(buf[:n]))
224-
}
225-
// EOF or timeout is expected when no route is configured
226-
if err != io.EOF && !strings.Contains(err.Error(), "timeout") {
227-
return fmt.Errorf("expected EOF or timeout but got: %v", err)
249+
if len(response) > 0 {
250+
return fmt.Errorf("expected no response but got: %s", response)
228251
}
229252
}
230253
return nil
231-
}).WithTimeout(timeout).WithPolling(2 * time.Second).Should(Succeed())
254+
}).WithTimeout(timeout).WithPolling(10 * time.Second).Should(Succeed())
232255
}
233256

234257
func (s *Scaffold) RequestAssert(r *RequestAssert) bool {

0 commit comments

Comments
 (0)