This is an automated email from the ASF dual-hosted git repository.
ronething pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new 5f0d1af1 fix: generate unique SSL IDs to prevent certificate conflicts
across different hosts (#2592)
5f0d1af1 is described below
commit 5f0d1af1dea737f8c509ee6f52b34f55bccd5b2d
Author: Ashing Zheng <[email protected]>
AuthorDate: Mon Oct 13 17:07:52 2025 +0800
fix: generate unique SSL IDs to prevent certificate conflicts across
different hosts (#2592)
Signed-off-by: Ashing Zheng <[email protected]>
---
api/adc/types.go | 13 +++++
internal/adc/translator/apisixtls.go | 3 +-
internal/adc/translator/gateway.go | 51 +-----------------
internal/adc/translator/ingress.go | 8 +--
internal/provider/init/init.go | 3 +-
internal/provider/register.go | 3 +-
test/e2e/crds/v2/tls.go | 102 +++++++++++++++++++++++++++++++++++
test/e2e/gatewayapi/gateway.go | 6 +--
test/e2e/gatewayapi/tcproute.go | 16 ++----
test/e2e/scaffold/assertion.go | 53 ++++++++++++------
10 files changed, 172 insertions(+), 86 deletions(-)
diff --git a/api/adc/types.go b/api/adc/types.go
index 48abd10f..8c2a9506 100644
--- a/api/adc/types.go
+++ b/api/adc/types.go
@@ -473,6 +473,19 @@ func (n Upstream) MarshalJSON() ([]byte, error) {
return json.Marshal((Alias)(n))
}
+func ComposeSSLName(kind, namespace, name string) string {
+ p := make([]byte, 0, len(kind)+len(namespace)+len(name)+2)
+ buf := bytes.NewBuffer(p)
+
+ buf.WriteString(kind)
+ buf.WriteByte('_')
+ buf.WriteString(namespace)
+ buf.WriteByte('_')
+ buf.WriteString(name)
+
+ return buf.String()
+}
+
// ComposeRouteName uses namespace, name and rule name to compose
// the route name.
func ComposeRouteName(namespace, name string, rule string) string {
diff --git a/internal/adc/translator/apisixtls.go
b/internal/adc/translator/apisixtls.go
index 2f05facf..bd67893e 100644
--- a/internal/adc/translator/apisixtls.go
+++ b/internal/adc/translator/apisixtls.go
@@ -27,6 +27,7 @@ import (
"github.com/apache/apisix-ingress-controller/internal/controller/label"
"github.com/apache/apisix-ingress-controller/internal/id"
"github.com/apache/apisix-ingress-controller/internal/provider"
+ internaltypes
"github.com/apache/apisix-ingress-controller/internal/types"
)
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
// Create SSL object
ssl := &adctypes.SSL{
Metadata: adctypes.Metadata{
- ID: id.GenID(tls.Namespace + "_" + tls.Name),
+ ID:
id.GenID(adctypes.ComposeSSLName(internaltypes.KindApisixTls, tls.Namespace,
tls.Name)),
Labels: label.GenLabel(tls),
},
Certificates: []adctypes.Certificate{
diff --git a/internal/adc/translator/gateway.go
b/internal/adc/translator/gateway.go
index db284845..2ee2454c 100644
--- a/internal/adc/translator/gateway.go
+++ b/internal/adc/translator/gateway.go
@@ -22,7 +22,6 @@ import (
"encoding/json"
"encoding/pem"
"fmt"
- "slices"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
@@ -50,7 +49,6 @@ func (t *Translator) TranslateGateway(tctx
*provider.TranslateContext, obj *gate
result.SSL = append(result.SSL, ssl...)
}
}
- result.SSL = mergeSSLWithSameID(result.SSL)
rk := utils.NamespacedNameKind(obj)
gatewayProxy, ok := tctx.GatewayProxies[rk]
@@ -80,7 +78,7 @@ func (t *Translator) translateSecret(tctx
*provider.TranslateContext, listener g
sslObjs := make([]*adctypes.SSL, 0)
switch *listener.TLS.Mode {
case gatewayv1.TLSModeTerminate:
- for _, ref := range listener.TLS.CertificateRefs {
+ for refIndex, ref := range listener.TLS.CertificateRefs {
ns := obj.GetNamespace()
if ref.Namespace != nil {
ns = string(*ref.Namespace)
@@ -122,8 +120,7 @@ func (t *Translator) translateSecret(tctx
*provider.TranslateContext, listener g
}
sslObj.Snis = append(sslObj.Snis,
hosts...)
}
- // Note: use cert as id to avoid duplicate
certificate across ssl objects
- sslObj.ID = id.GenID(string(cert))
+ sslObj.ID = id.GenID(fmt.Sprintf("%s_%s_%d",
adctypes.ComposeSSLName(internaltypes.KindGateway, obj.Namespace, obj.Name),
listener.Name, refIndex))
t.Log.V(1).Info("generated ssl id", "ssl id",
sslObj.ID, "secret", secretNN.String())
sslObj.Labels = label.GenLabel(obj)
sslObjs = append(sslObjs, sslObj)
@@ -241,47 +238,3 @@ func (t *Translator)
fillPluginMetadataFromGatewayProxy(pluginMetadata adctypes.
pluginMetadata[pluginName] = pluginConfig
}
}
-
-// mergeSSLWithSameID merge ssl with same id
-func mergeSSLWithSameID(sslList []*adctypes.SSL) []*adctypes.SSL {
- if len(sslList) <= 1 {
- return sslList
- }
-
- // create a map to store ssl with same id
- sslMap := make(map[string]*adctypes.SSL)
- for _, ssl := range sslList {
- if existing, exists := sslMap[ssl.ID]; exists {
- // if ssl with same id exists, merge their snis
- // use map to deduplicate
- sniMap := make(map[string]struct{})
- // add existing snis
- for _, sni := range existing.Snis {
- sniMap[sni] = struct{}{}
- }
- // add new snis
- for _, sni := range ssl.Snis {
- sniMap[sni] = struct{}{}
- }
- // rebuild deduplicated snis list
- newSnis := make([]string, 0, len(sniMap))
- for sni := range sniMap {
- newSnis = append(newSnis, sni)
- }
-
- slices.Sort(newSnis)
- // update existing ssl object
- existing.Snis = newSnis
- } else {
- slices.Sort(ssl.Snis)
- // if new ssl id, add to map
- sslMap[ssl.ID] = ssl
- }
- }
-
- mergedSSL := make([]*adctypes.SSL, 0, len(sslMap))
- for _, ssl := range sslMap {
- mergedSSL = append(mergedSSL, ssl)
- }
- return mergedSSL
-}
diff --git a/internal/adc/translator/ingress.go
b/internal/adc/translator/ingress.go
index f17b159f..98cb4c3e 100644
--- a/internal/adc/translator/ingress.go
+++ b/internal/adc/translator/ingress.go
@@ -33,7 +33,7 @@ import (
internaltypes
"github.com/apache/apisix-ingress-controller/internal/types"
)
-func (t *Translator) translateIngressTLS(ingressTLS *networkingv1.IngressTLS,
secret *corev1.Secret, labels map[string]string) (*adctypes.SSL, error) {
+func (t *Translator) translateIngressTLS(namespace, name string, tlsIndex int,
ingressTLS *networkingv1.IngressTLS, secret *corev1.Secret, labels
map[string]string) (*adctypes.SSL, error) {
// extract the key pair from the secret
cert, key, err := extractKeyPair(secret, true)
if err != nil {
@@ -64,7 +64,7 @@ func (t *Translator) translateIngressTLS(ingressTLS
*networkingv1.IngressTLS, se
},
Snis: hosts,
}
- ssl.ID = id.GenID(string(cert))
+ ssl.ID = id.GenID(fmt.Sprintf("%s_%d",
adctypes.ComposeSSLName(internaltypes.KindIngress, namespace, name), tlsIndex))
return ssl, nil
}
@@ -75,7 +75,7 @@ func (t *Translator) TranslateIngress(tctx
*provider.TranslateContext, obj *netw
labels := label.GenLabel(obj)
// handle TLS configuration, convert to SSL objects
- for _, tls := range obj.Spec.TLS {
+ for tlsIndex, tls := range obj.Spec.TLS {
if tls.SecretName == "" {
continue
}
@@ -86,7 +86,7 @@ func (t *Translator) TranslateIngress(tctx
*provider.TranslateContext, obj *netw
if secret == nil {
continue
}
- ssl, err := t.translateIngressTLS(&tls, secret, labels)
+ ssl, err := t.translateIngressTLS(obj.Namespace, obj.Name,
tlsIndex, &tls, secret, labels)
if err != nil {
return nil, err
}
diff --git a/internal/provider/init/init.go b/internal/provider/init/init.go
index b6ed9e99..be21c07d 100644
--- a/internal/provider/init/init.go
+++ b/internal/provider/init/init.go
@@ -18,11 +18,12 @@
package init
import (
+ "github.com/go-logr/logr"
+
"github.com/apache/apisix-ingress-controller/internal/controller/status"
"github.com/apache/apisix-ingress-controller/internal/manager/readiness"
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/provider/apisix"
- "github.com/go-logr/logr"
)
func init() {
diff --git a/internal/provider/register.go b/internal/provider/register.go
index fddb1af5..a9feb032 100644
--- a/internal/provider/register.go
+++ b/internal/provider/register.go
@@ -21,9 +21,10 @@ import (
"fmt"
"net/http"
+ "github.com/go-logr/logr"
+
"github.com/apache/apisix-ingress-controller/internal/controller/status"
"github.com/apache/apisix-ingress-controller/internal/manager/readiness"
- "github.com/go-logr/logr"
)
type RegisterHandler interface {
diff --git a/test/e2e/crds/v2/tls.go b/test/e2e/crds/v2/tls.go
index 1564fb15..a7edff3d 100644
--- a/test/e2e/crds/v2/tls.go
+++ b/test/e2e/crds/v2/tls.go
@@ -336,5 +336,107 @@ spec:
assert.Contains(GinkgoT(),
tls[0].Client.SkipMtlsURIRegex, skipMtlsUriRegex, "skip_mtls_uri_regex should
be set")
})
+ It("ApisixTls and Ingress with same certificate but different
hosts", func() {
+ By("create shared TLS secret")
+ err := s.NewKubeTlsSecret("shared-tls-secret", Cert,
Key)
+ Expect(err).NotTo(HaveOccurred(), "creating shared TLS
secret")
+
+ const apisixTlsSpec = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+ name: test-apisixtls-shared
+spec:
+ ingressClassName: %s
+ hosts:
+ - api6.com
+ secret:
+ name: shared-tls-secret
+ namespace: %s
+`
+
+ By("apply ApisixTls with api6.com")
+ var apisixTls apiv2.ApisixTls
+ tlsSpec := fmt.Sprintf(apisixTlsSpec, s.Namespace(),
s.Namespace())
+ applier.MustApplyAPIv2(types.NamespacedName{Namespace:
s.Namespace(), Name: "test-apisixtls-shared"}, &apisixTls, tlsSpec)
+
+ const ingressYamlWithTLS = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: test-ingress-tls-shared
+spec:
+ ingressClassName: %s
+ tls:
+ - hosts:
+ - api7.com
+ secretName: shared-tls-secret
+ rules:
+ - host: api7.com
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: httpbin-service-e2e-test
+ port:
+ number: 80
+`
+
+ By("apply Ingress with api7.com using same certificate")
+ err =
s.CreateResourceFromString(fmt.Sprintf(ingressYamlWithTLS, s.Namespace()))
+ Expect(err).NotTo(HaveOccurred(), "creating Ingress")
+
+ By("verify two SSL objects exist in control plane")
+ Eventually(func() bool {
+ tls, err :=
s.DefaultDataplaneResource().SSL().List(context.Background())
+ if err != nil {
+ return false
+ }
+ return len(tls) == 2
+ }).WithTimeout(30 * time.Second).ProbeEvery(1 *
time.Second).Should(BeTrue())
+
+ tls, err :=
s.DefaultDataplaneResource().SSL().List(context.Background())
+ assert.Nil(GinkgoT(), err, "list tls error")
+ assert.Len(GinkgoT(), tls, 2, "should have exactly 2
SSL objects")
+
+ By("verify SSL objects have different IDs and SNIs")
+ sniFound := make(map[string]bool)
+
+ for i := range tls {
+ // Check certificate content is the same
+ assert.Len(GinkgoT(), tls[i].Certificates, 1,
"each SSL should have 1 certificate")
+ assert.Equal(GinkgoT(), Cert,
tls[i].Certificates[0].Certificate, "certificate should match")
+
+ // Track SNIs
+ for _, sni := range tls[i].Snis {
+ sniFound[sni] = true
+ }
+ }
+
+ By("verify both hosts are covered")
+ assert.True(GinkgoT(), sniFound["api6.com"], "api6.com
should be in SNIs")
+ assert.True(GinkgoT(), sniFound["api7.com"], "api7.com
should be in SNIs")
+
+ By("test HTTPS request to api6.com")
+ Eventually(func() int {
+ return s.NewAPISIXHttpsClient("api6.com").
+ GET("/get").
+ WithHost("api6.com").
+ Expect().
+ Raw().StatusCode
+ }).WithTimeout(30 * time.Second).ProbeEvery(1 *
time.Second).Should(Equal(http.StatusOK))
+
+ By("test HTTPS request to api7.com")
+ Eventually(func() int {
+ return s.NewAPISIXHttpsClient("api7.com").
+ GET("/get").
+ WithHost("api7.com").
+ Expect().
+ Raw().StatusCode
+ }).WithTimeout(30 * time.Second).ProbeEvery(1 *
time.Second).Should(Equal(http.StatusOK))
+ })
+
})
})
diff --git a/test/e2e/gatewayapi/gateway.go b/test/e2e/gatewayapi/gateway.go
index c161b499..f1e8ca3a 100644
--- a/test/e2e/gatewayapi/gateway.go
+++ b/test/e2e/gatewayapi/gateway.go
@@ -284,8 +284,8 @@ spec:
Eventually(func() error {
tls, err :=
s.DefaultDataplaneResource().SSL().List(context.Background())
Expect(err).NotTo(HaveOccurred(), "list ssl")
- if len(tls) != 1 {
- return fmt.Errorf("expect 1 ssl, got
%d", len(tls))
+ if len(tls) != 2 {
+ return fmt.Errorf("expect 2 ssl, got
%d", len(tls))
}
if len(tls[0].Certificates) != 1 {
return fmt.Errorf("expect 1
certificate, got %d", len(tls[0].Certificates))
@@ -305,7 +305,7 @@ spec:
Eventually(func() string {
tls, err :=
s.DefaultDataplaneResource().SSL().List(context.Background())
Expect(err).NotTo(HaveOccurred(), "list ssl")
- if len(tls) < 1 {
+ if len(tls) != 2 {
return ""
}
if len(tls[0].Certificates) < 1 {
diff --git a/test/e2e/gatewayapi/tcproute.go b/test/e2e/gatewayapi/tcproute.go
index d9c30375..b6de0a42 100644
--- a/test/e2e/gatewayapi/tcproute.go
+++ b/test/e2e/gatewayapi/tcproute.go
@@ -72,25 +72,17 @@ spec:
NotTo(HaveOccurred(), "creating GatewayProxy")
// Create GatewayClass
- gatewayClassName := s.Namespace()
Expect(s.CreateResourceFromString(s.GetGatewayClassYaml())).
NotTo(HaveOccurred(), "creating GatewayClass")
- gcyaml, _ := s.GetResourceYaml("GatewayClass",
gatewayClassName)
- s.ResourceApplied("GatewayClass", gatewayClassName,
gcyaml, 1)
// Create Gateway with TCP listener
- gatewayName := s.Namespace()
-
Expect(s.CreateResourceFromString(fmt.Sprintf(tcpGateway, gatewayName,
gatewayClassName))).
+
Expect(s.CreateResourceFromString(fmt.Sprintf(tcpGateway, s.Namespace(),
s.Namespace()))).
NotTo(HaveOccurred(), "creating Gateway")
-
- gwyaml, _ := s.GetResourceYaml("Gateway", gatewayName)
- s.ResourceApplied("Gateway", gatewayName, gwyaml, 1)
})
It("should route TCP traffic to backend service", func() {
- gatewayName := s.Namespace()
By("creating TCPRoute")
- Expect(s.CreateResourceFromString(fmt.Sprintf(tcpRoute,
gatewayName))).
+ Expect(s.CreateResourceFromString(fmt.Sprintf(tcpRoute,
s.Namespace()))).
NotTo(HaveOccurred(), "creating TCPRoute")
// Verify TCPRoute status becomes programmed
@@ -98,7 +90,7 @@ spec:
s.ResourceApplied("TCPRoute", "tcp-app-1", routeYaml, 1)
By("verifying TCPRoute is functional")
- s.HTTPOverTCPConnectAssert(true, time.Minute*5) //
should be able to connect
+ s.HTTPOverTCPConnectAssert(true, time.Minute*3) //
should be able to connect
By("sending TCP traffic to verify routing")
s.RequestAssert(&scaffold.RequestAssert{
Client: s.NewAPISIXClientOnTCPPort(),
@@ -113,7 +105,7 @@ spec:
Expect(s.DeleteResource("TCPRoute", "tcp-app-1")).
NotTo(HaveOccurred(), "deleting TCPRoute")
- s.HTTPOverTCPConnectAssert(false, time.Minute*5)
+ s.HTTPOverTCPConnectAssert(false, time.Minute*3)
})
})
})
diff --git a/test/e2e/scaffold/assertion.go b/test/e2e/scaffold/assertion.go
index 612e2c81..a7a18246 100644
--- a/test/e2e/scaffold/assertion.go
+++ b/test/e2e/scaffold/assertion.go
@@ -200,35 +200,58 @@ func (s *Scaffold) HTTPOverTCPConnectAssert(shouldRespond
bool, timeout time.Dur
defer func() {
_ = conn.Close()
}()
- _, _ = fmt.Fprintf(conn, "GET /get HTTP/1.1\r\nHost:
localhost\r\n\r\n")
+ _, _ = fmt.Fprintf(conn, "GET /get HTTP/1.1\r\nHost:
localhost\r\nConnection: close\r\n\r\n")
+
+ // Read response - loop until EOF or error
+ // Set deadline for total read time (not per-read)
+ _ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
+ var responseBuilder strings.Builder
+ buf := make([]byte, 4096)
+
+ for {
+ n, err := conn.Read(buf)
+ if n > 0 {
+ responseBuilder.Write(buf[:n])
+ }
- // Read response
- _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
- buf := make([]byte, 1024)
- n, err := conn.Read(buf)
+ // EOF is expected with Connection: close
+ if err == io.EOF {
+ break
+ }
+
+ // Other errors (including timeout)
+ if err != nil {
+ // If we haven't received any data yet, this is
an error
+ if responseBuilder.Len() == 0 {
+ return fmt.Errorf("read error before
receiving data: %v", err)
+ }
+ // If we have partial data, treat timeout as
potential issue
+ if strings.Contains(err.Error(), "timeout") {
+ break // Use whatever we've received so
far
+ }
+ return fmt.Errorf("read error: %v", err)
+ }
+ }
+
+ response := responseBuilder.String()
if shouldRespond {
// Should get a response (HTTP 200 from httpbin)
- if err != nil || n == 0 {
- return fmt.Errorf("expected response but got
error: %v or empty response", err)
+ if len(response) == 0 {
+ return fmt.Errorf("expected response but got
empty response")
}
// Check if we got a valid HTTP response
- response := string(buf[:n])
if !strings.Contains(response, "HTTP/1.1") {
return fmt.Errorf("expected HTTP response but
got: %s", response)
}
} else {
// Should get no response or connection reset
- if err == nil && n > 0 {
- return fmt.Errorf("expected no response but
got: %s", string(buf[:n]))
- }
- // EOF or timeout is expected when no route is
configured
- if err != io.EOF && !strings.Contains(err.Error(),
"timeout") {
- return fmt.Errorf("expected EOF or timeout but
got: %v", err)
+ if len(response) > 0 {
+ return fmt.Errorf("expected no response but
got: %s", response)
}
}
return nil
- }).WithTimeout(timeout).WithPolling(2 * time.Second).Should(Succeed())
+ }).WithTimeout(timeout).WithPolling(10 * time.Second).Should(Succeed())
}
func (s *Scaffold) RequestAssert(r *RequestAssert) bool {