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 351d20a5 feat: add certificate conflict detection to admission 
webhooks (#2603)
351d20a5 is described below

commit 351d20a51af496012b822bae225839cac6832750
Author: Ashing Zheng <[email protected]>
AuthorDate: Thu Oct 16 21:58:11 2025 +0800

    feat: add certificate conflict detection to admission webhooks (#2603)
    
    Signed-off-by: Ashing Zheng <[email protected]>
---
 internal/adc/translator/apisixtls.go              |    5 +-
 internal/adc/translator/apisixupstream.go         |    3 +-
 internal/adc/translator/gateway.go                |   70 +-
 internal/adc/translator/ingress.go                |    5 +-
 internal/controller/indexer/indexer.go            |   29 +
 internal/controller/indexer/ssl_host.go           |  143 +++
 internal/ssl/util.go                              |  142 +++
 internal/webhook/v1/apisixtls_webhook.go          |   13 +
 internal/webhook/v1/gateway_webhook.go            |   13 +
 internal/webhook/v1/ingress_webhook.go            |   14 +-
 internal/webhook/v1/ssl/conflict_detector.go      |  513 +++++++++
 internal/webhook/v1/ssl/conflict_detector_test.go |  418 +++++++
 test/e2e/webhook/ssl_conflict.go                  | 1215 +++++++++++++++++++++
 13 files changed, 2510 insertions(+), 73 deletions(-)

diff --git a/internal/adc/translator/apisixtls.go 
b/internal/adc/translator/apisixtls.go
index bd67893e..ea428711 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"
+       sslutils "github.com/apache/apisix-ingress-controller/internal/ssl"
        internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
 )
 
@@ -44,7 +45,7 @@ func (t *Translator) TranslateApisixTls(tctx 
*provider.TranslateContext, tls *ap
        }
 
        // Extract cert and key from secret
-       cert, key, err := extractKeyPair(secret, true)
+       cert, key, err := sslutils.ExtractKeyPair(secret, true)
        if err != nil {
                return nil, err
        }
@@ -81,7 +82,7 @@ func (t *Translator) TranslateApisixTls(tctx 
*provider.TranslateContext, tls *ap
                        return nil, fmt.Errorf("client CA secret %s not found", 
caSecretKey.String())
                }
 
-               ca, _, err := extractKeyPair(caSecret, false)
+               ca, _, err := sslutils.ExtractKeyPair(caSecret, false)
                if err != nil {
                        return nil, err
                }
diff --git a/internal/adc/translator/apisixupstream.go 
b/internal/adc/translator/apisixupstream.go
index 86a39e62..33e626fe 100644
--- a/internal/adc/translator/apisixupstream.go
+++ b/internal/adc/translator/apisixupstream.go
@@ -29,6 +29,7 @@ import (
        "github.com/apache/apisix-ingress-controller/api/adc"
        apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/internal/provider"
+       sslutils "github.com/apache/apisix-ingress-controller/internal/ssl"
        "github.com/apache/apisix-ingress-controller/internal/utils"
 )
 
@@ -187,7 +188,7 @@ func translateApisixUpstreamClientTLS(tctx 
*provider.TranslateContext, config *a
                return errors.Errorf("sercret %s not found", secretNN)
        }
 
-       cert, key, err := extractKeyPair(secret, true)
+       cert, key, err := sslutils.ExtractKeyPair(secret, true)
        if err != nil {
                return err
        }
diff --git a/internal/adc/translator/gateway.go 
b/internal/adc/translator/gateway.go
index 2ee2454c..53c67144 100644
--- a/internal/adc/translator/gateway.go
+++ b/internal/adc/translator/gateway.go
@@ -18,13 +18,10 @@
 package translator
 
 import (
-       "crypto/x509"
        "encoding/json"
-       "encoding/pem"
        "fmt"
 
        "github.com/pkg/errors"
-       corev1 "k8s.io/api/core/v1"
        "k8s.io/apimachinery/pkg/types"
        gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 
@@ -33,6 +30,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"
+       sslutils "github.com/apache/apisix-ingress-controller/internal/ssl"
        internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
        "github.com/apache/apisix-ingress-controller/internal/utils"
 )
@@ -97,7 +95,7 @@ func (t *Translator) translateSecret(tctx 
*provider.TranslateContext, listener g
                                        t.Log.Error(errors.New("secret data is 
nil"), "failed to get secret data", "secret", secretNN)
                                        return nil, fmt.Errorf("no secret data 
found for %s/%s", ns, name)
                                }
-                               cert, key, err := extractKeyPair(secret, true)
+                               cert, key, err := 
sslutils.ExtractKeyPair(secret, true)
                                if err != nil {
                                        t.Log.Error(err, "extract key pair", 
"secret", secretNN)
                                        return nil, err
@@ -110,7 +108,7 @@ func (t *Translator) translateSecret(tctx 
*provider.TranslateContext, listener g
                                if listener.Hostname != nil && 
*listener.Hostname != "" {
                                        sslObj.Snis = append(sslObj.Snis, 
string(*listener.Hostname))
                                } else {
-                                       hosts, err := extractHost(cert)
+                                       hosts, err := 
sslutils.ExtractHostsFromCertificate(cert)
                                        if err != nil {
                                                return nil, err
                                        }
@@ -137,68 +135,6 @@ func (t *Translator) translateSecret(tctx 
*provider.TranslateContext, listener g
        return sslObjs, nil
 }
 
-func extractHost(cert []byte) ([]string, error) {
-       block, _ := pem.Decode(cert)
-       if block == nil {
-               return nil, errors.New("parse certificate: not in PEM format")
-       }
-       der, err := x509.ParseCertificate(block.Bytes)
-       if err != nil {
-               return nil, errors.Wrap(err, "parse certificate")
-       }
-       hosts := make([]string, 0, len(der.DNSNames))
-       for _, dnsName := range der.DNSNames {
-               if dnsName != "*" {
-                       hosts = append(hosts, dnsName)
-               }
-       }
-       return hosts, nil
-}
-
-func extractKeyPair(s *corev1.Secret, hasPrivateKey bool) ([]byte, []byte, 
error) {
-       if _, ok := s.Data["cert"]; ok {
-               return extractApisixSecretKeyPair(s, hasPrivateKey)
-       } else if _, ok := s.Data[corev1.TLSCertKey]; ok {
-               return extractKubeSecretKeyPair(s, hasPrivateKey)
-       } else if ca, ok := s.Data[corev1.ServiceAccountRootCAKey]; ok && 
!hasPrivateKey {
-               return ca, nil, nil
-       } else {
-               return nil, nil, errors.New("unknown secret format")
-       }
-}
-
-func extractApisixSecretKeyPair(s *corev1.Secret, hasPrivateKey bool) (cert 
[]byte, key []byte, err error) {
-       var ok bool
-       cert, ok = s.Data["cert"]
-       if !ok {
-               return nil, nil, errors.New("missing cert field")
-       }
-
-       if hasPrivateKey {
-               key, ok = s.Data["key"]
-               if !ok {
-                       return nil, nil, errors.New("missing key field")
-               }
-       }
-       return
-}
-
-func extractKubeSecretKeyPair(s *corev1.Secret, hasPrivateKey bool) (cert 
[]byte, key []byte, err error) {
-       var ok bool
-       cert, ok = s.Data[corev1.TLSCertKey]
-       if !ok {
-               return nil, nil, errors.New("missing cert field")
-       }
-
-       if hasPrivateKey {
-               key, ok = s.Data[corev1.TLSPrivateKeyKey]
-               if !ok {
-                       return nil, nil, errors.New("missing key field")
-               }
-       }
-       return
-}
-
 // fillPluginsFromGatewayProxy fill plugins from GatewayProxy to given plugins
 func (t *Translator) fillPluginsFromGatewayProxy(plugins adctypes.GlobalRule, 
gatewayProxy *v1alpha1.GatewayProxy) {
        if gatewayProxy == nil {
diff --git a/internal/adc/translator/ingress.go 
b/internal/adc/translator/ingress.go
index 1d2a4186..894224b2 100644
--- a/internal/adc/translator/ingress.go
+++ b/internal/adc/translator/ingress.go
@@ -32,19 +32,20 @@ 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"
+       sslutils "github.com/apache/apisix-ingress-controller/internal/ssl"
        internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
 )
 
 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)
+       cert, key, err := sslutils.ExtractKeyPair(secret, true)
        if err != nil {
                return nil, err
        }
 
        hosts := ingressTLS.Hosts
        if len(hosts) == 0 {
-               certHosts, err := extractHost(cert)
+               certHosts, err := sslutils.ExtractHostsFromCertificate(cert)
                if err != nil {
                        return nil, err
                }
diff --git a/internal/controller/indexer/indexer.go 
b/internal/controller/indexer/indexer.go
index 8982630e..b234a1c2 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -46,6 +46,7 @@ const (
        IngressClassParametersRef = "ingressClassParametersRef"
        ConsumerGatewayRef        = "consumerGatewayRef"
        PolicyTargetRefs          = "targetRefs"
+       TLSHostIndexRef           = "tlsHostRefs"
        GatewayClassIndexRef      = "gatewayClassRef"
        ApisixUpstreamRef         = "apisixUpstreamRef"
        PluginConfigIndexRef      = "pluginConfigRefs"
@@ -99,6 +100,16 @@ func setupGatewayIndexer(mgr ctrl.Manager) error {
        ); err != nil {
                return err
        }
+
+       if err := mgr.GetFieldIndexer().IndexField(
+               context.Background(),
+               &gatewayv1.Gateway{},
+               TLSHostIndexRef,
+               GatewayTLSHostIndexFunc,
+       ); err != nil {
+               return err
+       }
+
        return nil
 }
 
@@ -404,6 +415,15 @@ func setupIngressIndexer(mgr ctrl.Manager) error {
                return err
        }
 
+       if err := mgr.GetFieldIndexer().IndexField(
+               context.Background(),
+               &networkingv1.Ingress{},
+               TLSHostIndexRef,
+               IngressTLSHostIndexFunc,
+       ); err != nil {
+               return err
+       }
+
        return nil
 }
 
@@ -849,6 +869,15 @@ func setupApisixTlsIndexer(mgr ctrl.Manager) error {
                return err
        }
 
+       if err := mgr.GetFieldIndexer().IndexField(
+               context.Background(),
+               &apiv2.ApisixTls{},
+               TLSHostIndexRef,
+               ApisixTlsHostIndexFunc,
+       ); err != nil {
+               return err
+       }
+
        return nil
 }
 
diff --git a/internal/controller/indexer/ssl_host.go 
b/internal/controller/indexer/ssl_host.go
new file mode 100644
index 00000000..9838d43f
--- /dev/null
+++ b/internal/controller/indexer/ssl_host.go
@@ -0,0 +1,143 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package indexer
+
+import (
+       "sort"
+
+       networkingv1 "k8s.io/api/networking/v1"
+       ctrl "sigs.k8s.io/controller-runtime"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       sslutil "github.com/apache/apisix-ingress-controller/internal/ssl"
+)
+
+var (
+       tlsHostIndexLogger = ctrl.Log.WithName("tls-host-indexer")
+       // Empty host is used to match the resource which does not specify any 
explicit host.
+       emptyHost = ""
+)
+
+// GatewayTLSHostIndexFunc indexes Gateways by their TLS SNI hosts.
+func GatewayTLSHostIndexFunc(rawObj client.Object) []string {
+       gateway, ok := rawObj.(*gatewayv1.Gateway)
+       if !ok {
+               return nil
+       }
+       if len(gateway.Spec.Listeners) == 0 {
+               return nil
+       }
+
+       hosts := make(map[string]struct{})
+
+       for _, listener := range gateway.Spec.Listeners {
+               if listener.TLS == nil || len(listener.TLS.CertificateRefs) == 
0 {
+                       continue
+               }
+
+               hasExplicitHost := false
+               if listener.Hostname != nil {
+                       candidates := 
sslutil.NormalizeHosts([]string{string(*listener.Hostname)})
+                       for _, host := range candidates {
+                               if host == "" {
+                                       continue
+                               }
+                               hasExplicitHost = true
+                               hosts[host] = struct{}{}
+                       }
+               }
+
+               if !hasExplicitHost {
+                       hosts[emptyHost] = struct{}{}
+               }
+       }
+
+       tlsHostIndexLogger.Info("GatewayTLSHostIndexFunc", "hosts", 
hostSetToSlice(hosts), "len", len(hostSetToSlice(hosts)))
+
+       return hostSetToSlice(hosts)
+}
+
+// IngressTLSHostIndexFunc indexes Ingresses by their TLS SNI hosts.
+func IngressTLSHostIndexFunc(rawObj client.Object) []string {
+       ingress, ok := rawObj.(*networkingv1.Ingress)
+       if !ok {
+               return nil
+       }
+       if len(ingress.Spec.TLS) == 0 {
+               return nil
+       }
+
+       hosts := make(map[string]struct{})
+       for _, tls := range ingress.Spec.TLS {
+               if tls.SecretName == "" {
+                       continue
+               }
+
+               hasExplicitHost := false
+               candidates := sslutil.NormalizeHosts(tls.Hosts)
+               for _, host := range candidates {
+                       if host == "" {
+                               continue
+                       }
+                       hasExplicitHost = true
+                       hosts[host] = struct{}{}
+               }
+
+               if !hasExplicitHost {
+                       hosts[emptyHost] = struct{}{}
+               }
+       }
+
+       return hostSetToSlice(hosts)
+}
+
+// ApisixTlsHostIndexFunc indexes ApisixTls resources by their declared TLS 
hosts.
+func ApisixTlsHostIndexFunc(rawObj client.Object) []string {
+       tls, ok := rawObj.(*apiv2.ApisixTls)
+       if !ok {
+               return nil
+       }
+       if len(tls.Spec.Hosts) == 0 {
+               return nil
+       }
+
+       hostSet := make(map[string]struct{}, len(tls.Spec.Hosts))
+       for _, host := range tls.Spec.Hosts {
+               for _, normalized := range 
sslutil.NormalizeHosts([]string{string(host)}) {
+                       if normalized == "" {
+                               continue
+                       }
+                       hostSet[normalized] = struct{}{}
+               }
+       }
+       return hostSetToSlice(hostSet)
+}
+
+func hostSetToSlice(set map[string]struct{}) []string {
+       if len(set) == 0 {
+               return nil
+       }
+       result := make([]string, 0, len(set))
+       for host := range set {
+               result = append(result, host)
+       }
+       sort.Strings(result)
+       return result
+}
diff --git a/internal/ssl/util.go b/internal/ssl/util.go
new file mode 100644
index 00000000..f5fc6b19
--- /dev/null
+++ b/internal/ssl/util.go
@@ -0,0 +1,142 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ssl
+
+import (
+       "crypto/sha256"
+       "crypto/x509"
+       "encoding/hex"
+       "encoding/pem"
+       "errors"
+       "strings"
+
+       corev1 "k8s.io/api/core/v1"
+)
+
+var (
+       // ErrUnknownSecretFormat indicates the secret does not contain 
supported TLS data keys.
+       ErrUnknownSecretFormat = errors.New("unknown secret format")
+       // ErrMissingCert indicates the secret is missing the certificate part.
+       ErrMissingCert = errors.New("missing cert field")
+       // ErrMissingKey indicates the secret is missing the private key part 
when it is required.
+       ErrMissingKey = errors.New("missing key field")
+       // ErrInvalidPEM is returned when the provided certificate is not valid 
PEM encoded data.
+       ErrInvalidPEM = errors.New("certificate is not valid PEM data")
+)
+
+// ExtractKeyPair extracts the certificate and, optionally, the private key 
from a Secret.
+//
+// Supported formats:
+//  1. APISIX style: data keys `cert` and `key`
+//  2. Kubernetes TLS secret: data keys `tls.crt` and `tls.key`
+//  3. Kubernetes CA secret: data key `ca.crt` (without private key)
+func ExtractKeyPair(secret *corev1.Secret, includePrivateKey bool) ([]byte, 
[]byte, error) {
+       if secret == nil {
+               return nil, nil, ErrMissingCert
+       }
+
+       if cert, ok := secret.Data["cert"]; ok {
+               if includePrivateKey {
+                       key, ok := secret.Data["key"]
+                       if !ok {
+                               return nil, nil, ErrMissingKey
+                       }
+                       return cert, key, nil
+               }
+               return cert, nil, nil
+       }
+
+       if cert, ok := secret.Data[corev1.TLSCertKey]; ok {
+               if includePrivateKey {
+                       key, ok := secret.Data[corev1.TLSPrivateKeyKey]
+                       if !ok {
+                               return nil, nil, ErrMissingKey
+                       }
+                       return cert, key, nil
+               }
+               return cert, nil, nil
+       }
+
+       if cert, ok := secret.Data[corev1.ServiceAccountRootCAKey]; ok && 
!includePrivateKey {
+               return cert, nil, nil
+       }
+
+       return nil, nil, ErrUnknownSecretFormat
+}
+
+// ExtractCertificate extracts only the certificate data from a Secret.
+func ExtractCertificate(secret *corev1.Secret) ([]byte, error) {
+       cert, _, err := ExtractKeyPair(secret, false)
+       return cert, err
+}
+
+// ExtractHostsFromCertificate parses the certificate PEM block and returns 
the DNS names.
+func ExtractHostsFromCertificate(certPEM []byte) ([]string, error) {
+       block, _ := pem.Decode(certPEM)
+       if block == nil {
+               return nil, ErrInvalidPEM
+       }
+
+       cert, err := x509.ParseCertificate(block.Bytes)
+       if err != nil {
+               return nil, err
+       }
+
+       hosts := make([]string, 0, len(cert.DNSNames))
+       for _, dnsName := range cert.DNSNames {
+               if dnsName != "*" {
+                       hosts = append(hosts, dnsName)
+               }
+       }
+       return hosts, nil
+}
+
+// NormalizeHosts removes duplicate entries
+func NormalizeHosts(hosts []string) []string {
+       if len(hosts) == 0 {
+               return nil
+       }
+
+       normalized := make([]string, 0, len(hosts))
+       seen := make(map[string]struct{}, len(hosts))
+       for _, host := range hosts {
+               candidate := strings.ToLower(strings.TrimSpace(host))
+               if _, ok := seen[candidate]; ok {
+                       continue
+               }
+               seen[candidate] = struct{}{}
+               normalized = append(normalized, candidate)
+       }
+       return normalized
+}
+
+// CertificateHash returns the SHA-256 hash of the leaf certificate contained 
in the PEM data.
+// The hash is calculated from the DER-encoded bytes so that formatting 
differences (whitespace,
+// line endings, certificate ordering) do not affect the result.
+func CertificateHash(certPEM []byte) (string, error) {
+       block, _ := pem.Decode(certPEM)
+       if block == nil {
+               return "", ErrInvalidPEM
+       }
+
+       cert, err := x509.ParseCertificate(block.Bytes)
+       if err != nil {
+               return "", err
+       }
+
+       sum := sha256.Sum256(cert.Raw)
+       return hex.EncodeToString(sum[:]), nil
+}
diff --git a/internal/webhook/v1/apisixtls_webhook.go 
b/internal/webhook/v1/apisixtls_webhook.go
index 16bcf88f..a033fc48 100644
--- a/internal/webhook/v1/apisixtls_webhook.go
+++ b/internal/webhook/v1/apisixtls_webhook.go
@@ -30,6 +30,7 @@ import (
        apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/internal/controller"
        
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
+       sslvalidator 
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/ssl"
 )
 
 var apisixTlsLog = logf.Log.WithName("apisixtls-resource")
@@ -67,6 +68,12 @@ func (v *ApisixTlsCustomValidator) ValidateCreate(ctx 
context.Context, obj runti
                return nil, nil
        }
 
+       detector := sslvalidator.NewConflictDetector(v.Client)
+       conflicts := detector.DetectConflicts(ctx, tls)
+       if len(conflicts) > 0 {
+               return nil, fmt.Errorf("%s", 
sslvalidator.FormatConflicts(conflicts))
+       }
+
        return v.collectWarnings(ctx, tls), nil
 }
 
@@ -80,6 +87,12 @@ func (v *ApisixTlsCustomValidator) ValidateUpdate(ctx 
context.Context, oldObj, n
                return nil, nil
        }
 
+       detector := sslvalidator.NewConflictDetector(v.Client)
+       conflicts := detector.DetectConflicts(ctx, tls)
+       if len(conflicts) > 0 {
+               return nil, fmt.Errorf("%s", 
sslvalidator.FormatConflicts(conflicts))
+       }
+
        return v.collectWarnings(ctx, tls), nil
 }
 
diff --git a/internal/webhook/v1/gateway_webhook.go 
b/internal/webhook/v1/gateway_webhook.go
index bb21b236..baf67784 100644
--- a/internal/webhook/v1/gateway_webhook.go
+++ b/internal/webhook/v1/gateway_webhook.go
@@ -33,6 +33,7 @@ import (
        v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
        internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
        
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
+       sslvalidator 
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/ssl"
 )
 
 // nolint:unused
@@ -86,6 +87,12 @@ func (v *GatewayCustomValidator) ValidateCreate(ctx 
context.Context, obj runtime
                return nil, nil
        }
 
+       detector := sslvalidator.NewConflictDetector(v.Client)
+       conflicts := detector.DetectConflicts(ctx, gateway)
+       if len(conflicts) > 0 {
+               return nil, fmt.Errorf("%s", 
sslvalidator.FormatConflicts(conflicts))
+       }
+
        warnings := v.warnIfMissingGatewayProxyForGateway(ctx, gateway)
        warnings = append(warnings, v.collectReferenceWarnings(ctx, gateway)...)
 
@@ -109,6 +116,12 @@ func (v *GatewayCustomValidator) ValidateUpdate(ctx 
context.Context, oldObj, new
                return nil, nil
        }
 
+       detector := sslvalidator.NewConflictDetector(v.Client)
+       conflicts := detector.DetectConflicts(ctx, gateway)
+       if len(conflicts) > 0 {
+               return nil, fmt.Errorf("%s", 
sslvalidator.FormatConflicts(conflicts))
+       }
+
        warnings := v.warnIfMissingGatewayProxyForGateway(ctx, gateway)
        warnings = append(warnings, v.collectReferenceWarnings(ctx, gateway)...)
 
diff --git a/internal/webhook/v1/ingress_webhook.go 
b/internal/webhook/v1/ingress_webhook.go
index 10e18ab4..03ee8f5b 100644
--- a/internal/webhook/v1/ingress_webhook.go
+++ b/internal/webhook/v1/ingress_webhook.go
@@ -31,6 +31,7 @@ import (
 
        "github.com/apache/apisix-ingress-controller/internal/controller"
        
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
+       sslvalidator 
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/ssl"
 )
 
 var ingresslog = logf.Log.WithName("ingress-resource")
@@ -142,6 +143,12 @@ func (v *IngressCustomValidator) ValidateCreate(ctx 
context.Context, obj runtime
                return nil, nil
        }
 
+       detector := sslvalidator.NewConflictDetector(v.Client)
+       conflicts := detector.DetectConflicts(ctx, ingress)
+       if len(conflicts) > 0 {
+               return nil, fmt.Errorf("%s", 
sslvalidator.FormatConflicts(conflicts))
+       }
+
        // Check for unsupported annotations and generate warnings
        warnings := checkUnsupportedAnnotations(ingress)
        warnings = append(warnings, v.collectReferenceWarnings(ctx, ingress)...)
@@ -160,10 +167,15 @@ func (v *IngressCustomValidator) ValidateUpdate(ctx 
context.Context, oldObj, new
                return nil, nil
        }
 
+       detector := sslvalidator.NewConflictDetector(v.Client)
+       conflicts := detector.DetectConflicts(ctx, ingress)
+       if len(conflicts) > 0 {
+               return nil, fmt.Errorf("%s", 
sslvalidator.FormatConflicts(conflicts))
+       }
+
        // Check for unsupported annotations and generate warnings
        warnings := checkUnsupportedAnnotations(ingress)
        warnings = append(warnings, v.collectReferenceWarnings(ctx, ingress)...)
-
        return warnings, nil
 }
 
diff --git a/internal/webhook/v1/ssl/conflict_detector.go 
b/internal/webhook/v1/ssl/conflict_detector.go
new file mode 100644
index 00000000..c39cd274
--- /dev/null
+++ b/internal/webhook/v1/ssl/conflict_detector.go
@@ -0,0 +1,513 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ssl
+
+import (
+       "context"
+       "fmt"
+       "sort"
+       "strings"
+
+       corev1 "k8s.io/api/core/v1"
+       networkingv1 "k8s.io/api/networking/v1"
+       "k8s.io/apimachinery/pkg/types"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       "sigs.k8s.io/controller-runtime/pkg/log"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/controller"
+       
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
+       sslutil "github.com/apache/apisix-ingress-controller/internal/ssl"
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+var logger = log.Log.WithName("ssl-conflict-detector")
+
+// HostCertMapping represents the relationship between a host and its 
certificate hash.
+type HostCertMapping struct {
+       Host            string
+       CertificateHash string
+       ResourceRef     string
+}
+
+// SSLConflict exposes the conflict details to the admission webhook for 
reporting.
+type SSLConflict struct {
+       Host                string
+       ConflictingResource string
+       CertificateHash     string
+}
+
+// ConflictDetector detects SSL conflicts among Gateway, Ingress, and 
ApisixTls resources.
+type ConflictDetector struct {
+       client      client.Client
+       secretCache map[types.NamespacedName]*secretInfo
+}
+
+type secretInfo struct {
+       hash  string
+       hosts []string
+}
+
+// NewConflictDetector creates a detector backed by the provided client.
+func NewConflictDetector(c client.Client) *ConflictDetector {
+       return &ConflictDetector{
+               client:      c,
+               secretCache: make(map[types.NamespacedName]*secretInfo),
+       }
+}
+
+// DetectConflicts returns the list of conflicts between the new resource and
+// existing resources that are associated with the same GatewayProxy. 
Best-effort:
+// failures while enumerating existing resources or reading Secrets will be 
logged
+// and result in no conflicts instead of blocking the admission.
+func (d *ConflictDetector) DetectConflicts(ctx context.Context, obj 
client.Object) []SSLConflict {
+       newMappings := d.buildMappingsForObject(ctx, obj)
+       if len(newMappings) == 0 {
+               return nil
+       }
+       gatewayProxy, err := d.resolveGatewayProxy(ctx, obj)
+       if err != nil {
+               logger.Error(err, "failed to resolve GatewayProxy", "object", 
objectKey(obj))
+               return nil
+       }
+       if gatewayProxy == nil {
+               return nil
+       }
+
+       conflicts := make([]SSLConflict, 0)
+
+       // First, check for conflicts within the new resource itself.
+       seen := make(map[string]string, len(newMappings))
+       for _, mapping := range newMappings {
+               if mapping.Host == "" || mapping.CertificateHash == "" {
+                       continue
+               }
+               if prev, ok := seen[mapping.Host]; ok {
+                       if prev != mapping.CertificateHash {
+                               conflicts = append(conflicts, SSLConflict{
+                                       Host:                mapping.Host,
+                                       ConflictingResource: 
mapping.ResourceRef,
+                                       CertificateHash:     prev,
+                               })
+                       }
+                       continue
+               }
+               seen[mapping.Host] = mapping.CertificateHash
+       }
+
+       if len(conflicts) > 0 {
+               return conflicts
+       }
+
+       externalConflicts, err := d.findExternalConflicts(ctx, obj, 
gatewayProxy, seen)
+       if err != nil {
+               logger.Error(err, "failed to evaluate existing TLS host 
mappings", "gatewayProxy", objectKey(gatewayProxy))
+               return conflicts
+       }
+
+       conflicts = append(conflicts, externalConflicts...)
+       return conflicts
+}
+
+// FormatConflicts renders a human-readable error message for multiple 
conflicts.
+func FormatConflicts(conflicts []SSLConflict) string {
+       if len(conflicts) == 0 {
+               return ""
+       }
+       var sb strings.Builder
+       sb.WriteString("SSL configuration conflicts detected:")
+       for _, conflict := range conflicts {
+               sb.WriteString(fmt.Sprintf("\n- Host '%s' is already configured 
with a different certificate in %s", conflict.Host, 
conflict.ConflictingResource))
+       }
+       return sb.String()
+}
+
+// BuildGatewayMappings calculates host-to-certificate mappings for a Gateway.
+func (d *ConflictDetector) BuildGatewayMappings(ctx context.Context, gateway 
*gatewayv1.Gateway) []HostCertMapping {
+       mappings := make([]HostCertMapping, 0)
+
+       if gateway == nil {
+               return mappings
+       }
+
+       for _, listener := range gateway.Spec.Listeners {
+               if listener.TLS == nil || listener.TLS.CertificateRefs == nil {
+                       continue
+               }
+               for _, ref := range listener.TLS.CertificateRefs {
+                       if ref.Kind != nil && *ref.Kind != 
internaltypes.KindSecret {
+                               continue
+                       }
+                       if ref.Group != nil && string(*ref.Group) != 
corev1.GroupName {
+                               continue
+                       }
+                       secretNN := types.NamespacedName{
+                               Namespace: gateway.Namespace,
+                               Name:      string(ref.Name),
+                       }
+                       if ref.Namespace != nil && *ref.Namespace != "" {
+                               secretNN.Namespace = string(*ref.Namespace)
+                       }
+
+                       info, err := d.getSecretInfo(ctx, secretNN)
+                       if err != nil {
+                               logger.Error(err, "failed to read secret for 
Gateway", "gateway", objectKey(gateway), "secret", secretNN)
+                               continue
+                       }
+
+                       hosts := make([]string, 0, 1)
+                       if listener.Hostname != nil && *listener.Hostname != "" 
{
+                               hosts = append(hosts, 
string(*listener.Hostname))
+                       }
+                       hosts = sslutil.NormalizeHosts(hosts)
+                       if len(hosts) == 0 {
+                               hosts = info.hosts
+                       }
+                       for _, host := range hosts {
+                               mappings = append(mappings, HostCertMapping{
+                                       Host:            host,
+                                       CertificateHash: info.hash,
+                                       ResourceRef:     
fmt.Sprintf("%s/%s/%s", internaltypes.KindGateway, gateway.Namespace, 
gateway.Name),
+                               })
+                       }
+               }
+       }
+
+       return mappings
+}
+
+// BuildIngressMappings calculates host-to-certificate mappings for an Ingress.
+func (d *ConflictDetector) BuildIngressMappings(ctx context.Context, ingress 
*networkingv1.Ingress) []HostCertMapping {
+       mappings := make([]HostCertMapping, 0)
+       if ingress == nil {
+               return mappings
+       }
+
+       for _, tls := range ingress.Spec.TLS {
+               if tls.SecretName == "" {
+                       continue
+               }
+               secretNN := types.NamespacedName{Namespace: ingress.Namespace, 
Name: tls.SecretName}
+               info, err := d.getSecretInfo(ctx, secretNN)
+               if err != nil {
+                       logger.Error(err, "failed to read secret for Ingress", 
"ingress", objectKey(ingress), "secret", secretNN)
+                       continue
+               }
+
+               hosts := sslutil.NormalizeHosts(tls.Hosts)
+               if len(hosts) == 0 {
+                       hosts = info.hosts
+               }
+               for _, host := range hosts {
+                       mappings = append(mappings, HostCertMapping{
+                               Host:            host,
+                               CertificateHash: info.hash,
+                               ResourceRef:     fmt.Sprintf("%s/%s/%s", 
internaltypes.KindIngress, ingress.Namespace, ingress.Name),
+                       })
+               }
+       }
+
+       return mappings
+}
+
+// BuildApisixTlsMappings calculates host-to-certificate mappings for an 
ApisixTls resource.
+func (d *ConflictDetector) BuildApisixTlsMappings(ctx context.Context, tls 
*apiv2.ApisixTls) []HostCertMapping {
+       mappings := make([]HostCertMapping, 0)
+       if tls == nil {
+               return mappings
+       }
+
+       secretNN := types.NamespacedName{
+               Namespace: tls.Spec.Secret.Namespace,
+               Name:      tls.Spec.Secret.Name,
+       }
+       info, err := d.getSecretInfo(ctx, secretNN)
+       if err != nil {
+               logger.Error(err, "failed to read secret for ApisixTls", 
"apisixtls", objectKey(tls), "secret", secretNN)
+               return mappings
+       }
+
+       hosts := make([]string, 0, len(tls.Spec.Hosts))
+       for _, host := range tls.Spec.Hosts {
+               hosts = append(hosts, string(host))
+       }
+       hosts = sslutil.NormalizeHosts(hosts)
+       // NOTICE: hosts is required by the CRD, so this should never happen
+       // if len(hosts) == 0 {
+       //      hosts = info.hosts
+       // }
+       for _, host := range hosts {
+               mappings = append(mappings, HostCertMapping{
+                       Host:            host,
+                       CertificateHash: info.hash,
+                       ResourceRef:     fmt.Sprintf("%s/%s/%s", 
internaltypes.KindApisixTls, tls.Namespace, tls.Name),
+               })
+       }
+
+       return mappings
+}
+
+func (d *ConflictDetector) getSecretInfo(ctx context.Context, nn 
types.NamespacedName) (*secretInfo, error) {
+       if nn.Name == "" || nn.Namespace == "" {
+               return nil, fmt.Errorf("secret namespaced name is incomplete: 
%s", nn)
+       }
+       if info, ok := d.secretCache[nn]; ok {
+               return info, nil
+       }
+
+       var secret corev1.Secret
+       if err := d.client.Get(ctx, nn, &secret); err != nil {
+               return nil, err
+       }
+
+       cert, err := sslutil.ExtractCertificate(&secret)
+       if err != nil {
+               return nil, err
+       }
+
+       hash, err := sslutil.CertificateHash(cert)
+       if err != nil {
+               return nil, err
+       }
+       hosts, err := sslutil.ExtractHostsFromCertificate(cert)
+       if err != nil {
+               logger.Error(err, "failed to extract hosts from certificate", 
"secret", nn)
+               hosts = nil
+       }
+       info := &secretInfo{
+               hash:  hash,
+               hosts: sslutil.NormalizeHosts(hosts),
+       }
+       d.secretCache[nn] = info
+       return info, nil
+}
+
+func (d *ConflictDetector) resolveGatewayProxy(ctx context.Context, obj 
client.Object) (*v1alpha1.GatewayProxy, error) {
+       switch resource := obj.(type) {
+       case *gatewayv1.Gateway:
+               return controller.GetGatewayProxyByGateway(ctx, d.client, 
resource)
+       case *networkingv1.Ingress:
+               ingressClass, err := controller.FindMatchingIngressClass(ctx, 
d.client, logger, resource)
+               if err != nil {
+                       return nil, err
+               }
+               if ingressClass == nil {
+                       return nil, nil
+               }
+               return controller.GetGatewayProxyByIngressClass(ctx, d.client, 
ingressClass)
+       case *apiv2.ApisixTls:
+               ingressClass, err := controller.FindMatchingIngressClass(ctx, 
d.client, logger, resource)
+               if err != nil {
+                       return nil, err
+               }
+               if ingressClass == nil {
+                       return nil, nil
+               }
+               return controller.GetGatewayProxyByIngressClass(ctx, d.client, 
ingressClass)
+       default:
+               return nil, fmt.Errorf("unsupported object type %T", obj)
+       }
+}
+
+func (d *ConflictDetector) findExternalConflicts(ctx context.Context, obj 
client.Object, gatewayProxy *v1alpha1.GatewayProxy, hosts map[string]string) 
([]SSLConflict, error) {
+       excludeUID := obj.GetUID()
+       hostValues := make([]string, 0, len(hosts))
+       for host := range hosts {
+               hostValues = append(hostValues, host)
+       }
+       sort.Strings(hostValues)
+
+       conflictSet := make(map[string]SSLConflict)
+       proxyCache := make(map[types.UID]*v1alpha1.GatewayProxy)
+       mappingCache := make(map[types.UID][]HostCertMapping)
+
+       var noHostCandidates []client.Object
+       noHostFetched := false
+
+       for _, host := range hostValues {
+               candidates, err := d.listResourcesByHost(ctx, host)
+               if err != nil {
+                       logger.Error(err, "failed to list resources by host", 
"host", host)
+                       return nil, err
+               }
+               if host != "" {
+                       if !noHostFetched {
+                               // List resources with empty host.
+                               noHostCandidates, err = 
d.listResourcesByHost(ctx, "")
+                               if err != nil {
+                                       logger.Error(err, "failed to list 
resources by host", "host", "", "object", objectKey(obj))
+                                       return nil, err
+                               }
+                               noHostFetched = true
+                       }
+                       candidates = mergeCandidateObjects(candidates, 
noHostCandidates)
+               }
+               for _, candidate := range candidates {
+                       if candidate.GetUID() == excludeUID {
+                               continue
+                       }
+
+                       resolvedProxy, err := 
d.resolveGatewayProxyWithCache(ctx, candidate, proxyCache)
+                       if err != nil {
+                               logger.Error(err, "failed to resolve 
GatewayProxy for indexed resource", "resource", objectKey(candidate), "host", 
host)
+                               continue
+                       }
+                       // we only check if the resolved proxy is the same as 
the gateway proxy,
+                       if resolvedProxy == nil || 
!gatewayProxiesEqual(resolvedProxy, gatewayProxy) {
+                               continue
+                       }
+
+                       mapping, ok := d.mappingForHostWithCache(ctx, 
candidate, host, mappingCache)
+                       if !ok {
+                               continue
+                       }
+                       // same cert hash, no conflict
+                       if mapping.CertificateHash == hosts[host] {
+                               continue
+                       }
+
+                       key := fmt.Sprintf("%s|%s|%s", host, 
mapping.ResourceRef, mapping.CertificateHash)
+                       if _, exists := conflictSet[key]; exists {
+                               continue
+                       }
+                       conflictSet[key] = SSLConflict{
+                               Host:                host,
+                               ConflictingResource: mapping.ResourceRef,
+                               CertificateHash:     mapping.CertificateHash,
+                       }
+               }
+       }
+
+       if len(conflictSet) == 0 {
+               return nil, nil
+       }
+
+       keys := make([]string, 0, len(conflictSet))
+       for key := range conflictSet {
+               keys = append(keys, key)
+       }
+       sort.Strings(keys)
+
+       results := make([]SSLConflict, 0, len(keys))
+       for _, key := range keys {
+               results = append(results, conflictSet[key])
+       }
+       return results, nil
+}
+
+func (d *ConflictDetector) listResourcesByHost(ctx context.Context, host 
string) ([]client.Object, error) {
+       results := make([]client.Object, 0)
+
+       var gatewayList gatewayv1.GatewayList
+       if err := d.client.List(ctx, &gatewayList, 
client.MatchingFields{indexer.TLSHostIndexRef: host}); err != nil {
+               return nil, err
+       }
+       for i := range gatewayList.Items {
+               results = append(results, gatewayList.Items[i].DeepCopy())
+       }
+
+       var ingressList networkingv1.IngressList
+       if err := d.client.List(ctx, &ingressList, 
client.MatchingFields{indexer.TLSHostIndexRef: host}); err != nil {
+               return nil, err
+       }
+       for i := range ingressList.Items {
+               results = append(results, ingressList.Items[i].DeepCopy())
+       }
+
+       var tlsList apiv2.ApisixTlsList
+       if err := d.client.List(ctx, &tlsList, 
client.MatchingFields{indexer.TLSHostIndexRef: host}); err != nil {
+               return nil, err
+       }
+       for i := range tlsList.Items {
+               results = append(results, tlsList.Items[i].DeepCopy())
+       }
+
+       return results, nil
+}
+
+func mergeCandidateObjects(primary, additional []client.Object) 
[]client.Object {
+       if len(additional) == 0 {
+               return primary
+       }
+       seen := make(map[types.UID]struct{}, len(primary))
+       for _, obj := range primary {
+               seen[obj.GetUID()] = struct{}{}
+       }
+       for _, obj := range additional {
+               if _, exists := seen[obj.GetUID()]; exists {
+                       continue
+               }
+               primary = append(primary, obj)
+               seen[obj.GetUID()] = struct{}{}
+       }
+       return primary
+}
+
+func (d *ConflictDetector) resolveGatewayProxyWithCache(ctx context.Context, 
obj client.Object, cache map[types.UID]*v1alpha1.GatewayProxy) 
(*v1alpha1.GatewayProxy, error) {
+       if proxy, ok := cache[obj.GetUID()]; ok {
+               return proxy, nil
+       }
+       proxy, err := d.resolveGatewayProxy(ctx, obj)
+       if err != nil {
+               return nil, err
+       }
+       cache[obj.GetUID()] = proxy
+       return proxy, nil
+}
+
+func (d *ConflictDetector) mappingForHostWithCache(ctx context.Context, obj 
client.Object, host string, cache map[types.UID][]HostCertMapping) 
(HostCertMapping, bool) {
+       mappings, ok := cache[obj.GetUID()]
+       if !ok {
+               mappings = d.buildMappingsForObject(ctx, obj)
+               cache[obj.GetUID()] = mappings
+       }
+
+       for _, mapping := range mappings {
+               if mapping.Host == host {
+                       return mapping, true
+               }
+       }
+       return HostCertMapping{}, false
+}
+
+func (d *ConflictDetector) buildMappingsForObject(ctx context.Context, obj 
client.Object) []HostCertMapping {
+       switch resource := obj.(type) {
+       case *gatewayv1.Gateway:
+               return d.BuildGatewayMappings(ctx, resource)
+       case *networkingv1.Ingress:
+               return d.BuildIngressMappings(ctx, resource)
+       case *apiv2.ApisixTls:
+               return d.BuildApisixTlsMappings(ctx, resource)
+       default:
+               return nil
+       }
+}
+
+func gatewayProxiesEqual(a, b *v1alpha1.GatewayProxy) bool {
+       if a == nil || b == nil {
+               return false
+       }
+       return a.Namespace == b.Namespace && a.Name == b.Name
+}
+
+func objectKey(obj client.Object) types.NamespacedName {
+       if obj == nil {
+               return types.NamespacedName{}
+       }
+       return types.NamespacedName{Namespace: obj.GetNamespace(), Name: 
obj.GetName()}
+}
diff --git a/internal/webhook/v1/ssl/conflict_detector_test.go 
b/internal/webhook/v1/ssl/conflict_detector_test.go
new file mode 100644
index 00000000..d9d3063f
--- /dev/null
+++ b/internal/webhook/v1/ssl/conflict_detector_test.go
@@ -0,0 +1,418 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ssl
+
+import (
+       "context"
+       "crypto/rand"
+       "crypto/rsa"
+       "crypto/x509"
+       "crypto/x509/pkix"
+       "encoding/pem"
+       "fmt"
+       "math/big"
+       "testing"
+       "time"
+
+       corev1 "k8s.io/api/core/v1"
+       networkingv1 "k8s.io/api/networking/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/runtime"
+       "k8s.io/utils/ptr"
+       "sigs.k8s.io/controller-runtime/pkg/client/fake"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       "github.com/apache/apisix-ingress-controller/internal/controller/config"
+       
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
+       internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+const (
+       testNamespace    = "default"
+       testIngressClass = "example-class"
+)
+
+func TestConflictDetectorDetectsGatewayConflict(t *testing.T) {
+       scheme := buildScheme(t)
+       secretA := newTLSSecret(t, "cert-a", []string{"example.com"})
+       secretB := newTLSSecret(t, "cert-b", []string{"example.com"})
+
+       gatewayProxy := &v1alpha1.GatewayProxy{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo-gp",
+                       Namespace: testNamespace,
+                       UID:       "gatewayproxy-uid",
+               },
+       }
+
+       modeTerminate := gatewayv1.TLSModeTerminate
+       hostname := gatewayv1.Hostname("example.com")
+       gateway := &gatewayv1.Gateway{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo-gateway",
+                       Namespace: testNamespace,
+                       UID:       "gateway-uid",
+               },
+               Spec: gatewayv1.GatewaySpec{
+                       GatewayClassName: gatewayv1.ObjectName("demo-gc"),
+                       Listeners: []gatewayv1.Listener{
+                               {
+                                       Name:     "tls",
+                                       Protocol: gatewayv1.HTTPSProtocolType,
+                                       Port:     443,
+                                       Hostname: &hostname,
+                                       TLS: &gatewayv1.GatewayTLSConfig{
+                                               Mode: &modeTerminate,
+                                               CertificateRefs: 
[]gatewayv1.SecretObjectReference{
+                                                       {Name: 
gatewayv1.ObjectName(secretA.Name)},
+                                               },
+                                       },
+                               },
+                       },
+               },
+       }
+       gateway.Spec.Infrastructure = &gatewayv1.GatewayInfrastructure{
+               ParametersRef: &gatewayv1.LocalParametersReference{
+                       Group: gatewayv1.Group(v1alpha1.GroupVersion.Group),
+                       Kind:  gatewayv1.Kind(internaltypes.KindGatewayProxy),
+                       Name:  gatewayProxy.Name,
+               },
+       }
+
+       ingressClass := &networkingv1.IngressClass{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name: testIngressClass,
+               },
+               Spec: networkingv1.IngressClassSpec{
+                       Controller: config.ControllerConfig.ControllerName,
+                       Parameters: 
&networkingv1.IngressClassParametersReference{
+                               APIGroup:  ptr.To(v1alpha1.GroupVersion.Group),
+                               Kind:      internaltypes.KindGatewayProxy,
+                               Name:      gatewayProxy.Name,
+                               Namespace: ptr.To(testNamespace),
+                       },
+               },
+       }
+
+       fakeClient := fake.NewClientBuilder().
+               WithScheme(scheme).
+               WithIndex(&gatewayv1.Gateway{}, indexer.ParametersRef, 
indexer.GatewayParametersRefIndexFunc).
+               WithIndex(&gatewayv1.Gateway{}, indexer.TLSHostIndexRef, 
indexer.GatewayTLSHostIndexFunc).
+               WithIndex(&networkingv1.IngressClass{}, 
indexer.IngressClassParametersRef, indexer.IngressClassParametersRefIndexFunc).
+               WithIndex(&networkingv1.Ingress{}, indexer.IngressClassRef, 
indexer.IngressClassRefIndexFunc).
+               WithIndex(&networkingv1.Ingress{}, indexer.TLSHostIndexRef, 
indexer.IngressTLSHostIndexFunc).
+               WithIndex(&apiv2.ApisixTls{}, indexer.IngressClassRef, 
indexer.ApisixTlsIngressClassIndexFunc).
+               WithIndex(&apiv2.ApisixTls{}, indexer.TLSHostIndexRef, 
indexer.ApisixTlsHostIndexFunc).
+               WithObjects(secretA, secretB, gatewayProxy, gateway, 
ingressClass).
+               Build()
+
+       detector := NewConflictDetector(fakeClient)
+       ctx := context.Background()
+
+       newTls := &apiv2.ApisixTls{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "incoming",
+                       Namespace: testNamespace,
+                       UID:       "apisixtls-uid",
+               },
+               Spec: apiv2.ApisixTlsSpec{
+                       IngressClassName: testIngressClass,
+                       Hosts:            []apiv2.HostType{"example.com"},
+                       Secret: apiv2.ApisixSecret{
+                               Name:      secretB.Name,
+                               Namespace: secretB.Namespace,
+                       },
+               },
+       }
+
+       conflicts := detector.DetectConflicts(ctx, newTls)
+       if len(conflicts) != 1 {
+               t.Fatalf("expected 1 conflict, got %d", len(conflicts))
+       }
+       conflict := conflicts[0]
+       if conflict.Host != "example.com" {
+               t.Fatalf("unexpected host: %s", conflict.Host)
+       }
+       expectedRef := fmt.Sprintf("Gateway/%s/%s", gateway.Namespace, 
gateway.Name)
+       if conflict.ConflictingResource != expectedRef {
+               t.Fatalf("unexpected conflicting resource: %s", 
conflict.ConflictingResource)
+       }
+}
+
+func TestConflictDetectorAllowedWhenCertificateMatches(t *testing.T) {
+       scheme := buildScheme(t)
+       secret := newTLSSecret(t, "shared-cert", []string{"shared.example.com"})
+
+       gatewayProxy := &v1alpha1.GatewayProxy{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "gp",
+                       Namespace: testNamespace,
+                       UID:       "gatewayproxy-uid-2",
+               },
+       }
+       modeTerminate := gatewayv1.TLSModeTerminate
+       listenerHostname := gatewayv1.Hostname("shared.example.com")
+       gateway := &gatewayv1.Gateway{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "gw",
+                       Namespace: testNamespace,
+                       UID:       "gateway-uid-2",
+               },
+               Spec: gatewayv1.GatewaySpec{
+                       GatewayClassName: gatewayv1.ObjectName("gc"),
+                       Listeners: []gatewayv1.Listener{
+                               {
+                                       Name:     "tls",
+                                       Protocol: gatewayv1.HTTPSProtocolType,
+                                       Port:     443,
+                                       Hostname: &listenerHostname,
+                                       TLS: &gatewayv1.GatewayTLSConfig{
+                                               Mode:            &modeTerminate,
+                                               CertificateRefs: 
[]gatewayv1.SecretObjectReference{{Name: gatewayv1.ObjectName(secret.Name)}},
+                                       },
+                               },
+                       },
+               },
+       }
+       gateway.Spec.Infrastructure = &gatewayv1.GatewayInfrastructure{
+               ParametersRef: &gatewayv1.LocalParametersReference{
+                       Group: gatewayv1.Group(v1alpha1.GroupVersion.Group),
+                       Kind:  gatewayv1.Kind(internaltypes.KindGatewayProxy),
+                       Name:  gatewayProxy.Name,
+               },
+       }
+
+       ingressClass := &networkingv1.IngressClass{
+               ObjectMeta: metav1.ObjectMeta{Name: testIngressClass},
+               Spec: networkingv1.IngressClassSpec{
+                       Controller: config.ControllerConfig.ControllerName,
+                       Parameters: 
&networkingv1.IngressClassParametersReference{
+                               APIGroup:  ptr.To(v1alpha1.GroupVersion.Group),
+                               Kind:      internaltypes.KindGatewayProxy,
+                               Name:      gatewayProxy.Name,
+                               Namespace: ptr.To(testNamespace),
+                       },
+               },
+       }
+
+       client := fake.NewClientBuilder().
+               WithScheme(scheme).
+               WithIndex(&gatewayv1.Gateway{}, indexer.ParametersRef, 
indexer.GatewayParametersRefIndexFunc).
+               WithIndex(&gatewayv1.Gateway{}, indexer.TLSHostIndexRef, 
indexer.GatewayTLSHostIndexFunc).
+               WithIndex(&networkingv1.IngressClass{}, 
indexer.IngressClassParametersRef, indexer.IngressClassParametersRefIndexFunc).
+               WithIndex(&networkingv1.Ingress{}, indexer.IngressClassRef, 
indexer.IngressClassRefIndexFunc).
+               WithIndex(&networkingv1.Ingress{}, indexer.TLSHostIndexRef, 
indexer.IngressTLSHostIndexFunc).
+               WithIndex(&apiv2.ApisixTls{}, indexer.IngressClassRef, 
indexer.ApisixTlsIngressClassIndexFunc).
+               WithIndex(&apiv2.ApisixTls{}, indexer.TLSHostIndexRef, 
indexer.ApisixTlsHostIndexFunc).
+               WithObjects(secret, gatewayProxy, gateway, ingressClass).
+               Build()
+
+       detector := NewConflictDetector(client)
+       ctx := context.Background()
+
+       newTls := &apiv2.ApisixTls{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "allowed",
+                       Namespace: testNamespace,
+                       UID:       "apisixtls-uid-2",
+               },
+               Spec: apiv2.ApisixTlsSpec{
+                       IngressClassName: testIngressClass,
+                       Hosts:            
[]apiv2.HostType{"shared.example.com"},
+                       Secret:           apiv2.ApisixSecret{Name: secret.Name, 
Namespace: secret.Namespace},
+               },
+       }
+
+       conflicts := detector.DetectConflicts(ctx, newTls)
+       if len(conflicts) != 0 {
+               t.Fatalf("expected no conflicts, got %v", conflicts)
+       }
+}
+
+func buildScheme(t *testing.T) *runtime.Scheme {
+       scheme := runtime.NewScheme()
+       for _, add := range []func(*runtime.Scheme) error{
+               corev1.AddToScheme,
+               networkingv1.AddToScheme,
+               gatewayv1.Install,
+               apiv2.AddToScheme,
+               v1alpha1.AddToScheme,
+       } {
+               if err := add(scheme); err != nil {
+                       t.Fatalf("failed to add to scheme: %v", err)
+               }
+       }
+       return scheme
+}
+
+func newTLSSecret(t *testing.T, name string, hosts []string) *corev1.Secret {
+       cert, key := generateCertificate(t, hosts)
+       return &corev1.Secret{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      name,
+                       Namespace: testNamespace,
+               },
+               Type: corev1.SecretTypeTLS,
+               Data: map[string][]byte{
+                       corev1.TLSCertKey:       cert,
+                       corev1.TLSPrivateKeyKey: key,
+               },
+       }
+}
+
+func TestConflictDetectorDetectsSelfConflict(t *testing.T) {
+       scheme := buildScheme(t)
+       secretA := newTLSSecret(t, "cert-a", []string{"example.com"})
+       secretB := newTLSSecret(t, "cert-b", []string{"example.com"})
+
+       gatewayProxy := &v1alpha1.GatewayProxy{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo-gp",
+                       Namespace: testNamespace,
+                       UID:       "gatewayproxy-uid-3",
+               },
+       }
+
+       modeTerminate := gatewayv1.TLSModeTerminate
+       hostname := gatewayv1.Hostname("example.com")
+       // Create a Gateway with TWO listeners using DIFFERENT certificates for 
the SAME host
+       gateway := &gatewayv1.Gateway{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo-gateway",
+                       Namespace: testNamespace,
+                       UID:       "gateway-uid-3",
+               },
+               Spec: gatewayv1.GatewaySpec{
+                       GatewayClassName: gatewayv1.ObjectName("demo-gc"),
+                       Listeners: []gatewayv1.Listener{
+                               {
+                                       Name:     "tls-1",
+                                       Protocol: gatewayv1.HTTPSProtocolType,
+                                       Port:     443,
+                                       Hostname: &hostname,
+                                       TLS: &gatewayv1.GatewayTLSConfig{
+                                               Mode: &modeTerminate,
+                                               CertificateRefs: 
[]gatewayv1.SecretObjectReference{
+                                                       {Name: 
gatewayv1.ObjectName(secretA.Name)},
+                                               },
+                                       },
+                               },
+                               {
+                                       Name:     "tls-2",
+                                       Protocol: gatewayv1.HTTPSProtocolType,
+                                       Port:     8443,
+                                       Hostname: &hostname,
+                                       TLS: &gatewayv1.GatewayTLSConfig{
+                                               Mode: &modeTerminate,
+                                               CertificateRefs: 
[]gatewayv1.SecretObjectReference{
+                                                       {Name: 
gatewayv1.ObjectName(secretB.Name)},
+                                               },
+                                       },
+                               },
+                       },
+               },
+       }
+       gateway.Spec.Infrastructure = &gatewayv1.GatewayInfrastructure{
+               ParametersRef: &gatewayv1.LocalParametersReference{
+                       Group: gatewayv1.Group(v1alpha1.GroupVersion.Group),
+                       Kind:  gatewayv1.Kind(internaltypes.KindGatewayProxy),
+                       Name:  gatewayProxy.Name,
+               },
+       }
+
+       fakeClient := fake.NewClientBuilder().
+               WithScheme(scheme).
+               WithIndex(&gatewayv1.Gateway{}, indexer.ParametersRef, 
indexer.GatewayParametersRefIndexFunc).
+               WithIndex(&gatewayv1.Gateway{}, indexer.TLSHostIndexRef, 
indexer.GatewayTLSHostIndexFunc).
+               WithIndex(&networkingv1.IngressClass{}, 
indexer.IngressClassParametersRef, indexer.IngressClassParametersRefIndexFunc).
+               WithIndex(&networkingv1.Ingress{}, indexer.IngressClassRef, 
indexer.IngressClassRefIndexFunc).
+               WithIndex(&networkingv1.Ingress{}, indexer.TLSHostIndexRef, 
indexer.IngressTLSHostIndexFunc).
+               WithIndex(&apiv2.ApisixTls{}, indexer.IngressClassRef, 
indexer.ApisixTlsIngressClassIndexFunc).
+               WithIndex(&apiv2.ApisixTls{}, indexer.TLSHostIndexRef, 
indexer.ApisixTlsHostIndexFunc).
+               WithObjects(secretA, secretB, gatewayProxy, gateway).
+               Build()
+
+       detector := NewConflictDetector(fakeClient)
+       ctx := context.Background()
+
+       // Build mappings for this Gateway - should have 2 mappings for same 
host with different certs
+       mappings := detector.BuildGatewayMappings(ctx, gateway)
+       if len(mappings) != 2 {
+               t.Fatalf("expected 2 mappings, got %d", len(mappings))
+       }
+
+       // Both mappings should be for the same host
+       if mappings[0].Host != mappings[1].Host {
+               t.Fatalf("expected same host, got %s and %s", mappings[0].Host, 
mappings[1].Host)
+       }
+
+       // But with different certificate hashes
+       if mappings[0].CertificateHash == mappings[1].CertificateHash {
+               t.Fatalf("expected different certificate hashes, but they are 
the same: %s", mappings[0].CertificateHash)
+       }
+
+       // DetectConflicts should detect this self-conflict
+       conflicts := detector.DetectConflicts(ctx, gateway)
+
+       // Should detect 1 conflict (the resource conflicts with itself)
+       if len(conflicts) != 1 {
+               t.Fatalf("expected 1 self-conflict, got %d", len(conflicts))
+       }
+
+       conflict := conflicts[0]
+       if conflict.Host != "example.com" {
+               t.Fatalf("unexpected host: %s", conflict.Host)
+       }
+
+       // The conflicting resource should point to itself
+       expectedRef := fmt.Sprintf("Gateway/%s/%s", gateway.Namespace, 
gateway.Name)
+       if conflict.ConflictingResource != expectedRef {
+               t.Fatalf("unexpected conflicting resource: %s, expected %s", 
conflict.ConflictingResource, expectedRef)
+       }
+}
+
+func generateCertificate(t *testing.T, hosts []string) ([]byte, []byte) {
+       t.Helper()
+       priv, err := rsa.GenerateKey(rand.Reader, 2048)
+       if err != nil {
+               t.Fatalf("failed to generate private key: %v", err)
+       }
+       serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
+       if err != nil {
+               t.Fatalf("failed to generate serial: %v", err)
+       }
+       template := &x509.Certificate{
+               SerialNumber: serial,
+               Subject: pkix.Name{
+                       CommonName: hosts[0],
+               },
+               DNSNames:              hosts,
+               NotBefore:             time.Now().Add(-time.Hour),
+               NotAfter:              time.Now().Add(24 * time.Hour),
+               KeyUsage:              x509.KeyUsageKeyEncipherment | 
x509.KeyUsageDigitalSignature,
+               ExtKeyUsage:           
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+               BasicConstraintsValid: true,
+       }
+       derBytes, err := x509.CreateCertificate(rand.Reader, template, 
template, &priv.PublicKey, priv)
+       if err != nil {
+               t.Fatalf("failed to create certificate: %v", err)
+       }
+       certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: 
derBytes})
+       keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: 
x509.MarshalPKCS1PrivateKey(priv)})
+       return certPEM, keyPEM
+}
diff --git a/test/e2e/webhook/ssl_conflict.go b/test/e2e/webhook/ssl_conflict.go
new file mode 100644
index 00000000..8c0ff4bb
--- /dev/null
+++ b/test/e2e/webhook/ssl_conflict.go
@@ -0,0 +1,1215 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package webhook
+
+import (
+       "fmt"
+       "time"
+
+       . "github.com/onsi/ginkgo/v2"
+       . "github.com/onsi/gomega"
+
+       "github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test SSL/TLS Conflict Detection", Label("webhook"), func() {
+       s := scaffold.NewScaffold(scaffold.Options{
+               Name:          "ssl-conflict-test",
+               EnableWebhook: true,
+       })
+
+       BeforeEach(func() {
+               By("creating GatewayProxy")
+               err := s.CreateResourceFromString(s.GetGatewayProxySpec())
+               Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+               time.Sleep(5 * time.Second)
+
+               By("creating GatewayClass")
+               err = s.CreateResourceFromString(s.GetGatewayClassYaml())
+               Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
+               time.Sleep(2 * time.Second)
+
+               By("creating IngressClass")
+               err = 
s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "")
+               Expect(err).NotTo(HaveOccurred(), "creating IngressClass")
+               time.Sleep(2 * time.Second)
+       })
+
+       Context("ApisixTls conflict detection", func() {
+               It("should reject ApisixTls with conflicting certificate for 
same host", func() {
+                       host := "conflict.example.com"
+                       secretA := "tls-cert-a"
+                       secretB := "tls-cert-b"
+
+                       By("creating two different TLS secrets")
+                       createApisixTLSSecret(s, secretA, host, "creating 
secret A")
+                       createApisixTLSSecret(s, secretB, host, "creating 
secret B")
+                       time.Sleep(2 * time.Second)
+
+                       By("creating first ApisixTls with certificate A")
+                       tlsAYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: tls-a
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), s.Namespace(), host, secretA, s.Namespace())
+                       err := s.CreateResourceFromString(tlsAYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating ApisixTls 
A")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to create second ApisixTls with 
certificate B for same host")
+                       tlsBYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: tls-b
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), s.Namespace(), host, secretB, s.Namespace())
+                       err = s.CreateResourceFromString(tlsBYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting conflict 
when creating ApisixTls B")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+                       Expect(err.Error()).To(ContainSubstring(host))
+                       Expect(err.Error()).To(ContainSubstring("ApisixTls"))
+               })
+
+               It("should allow ApisixTls with same certificate for same 
host", func() {
+                       host := "shared.example.com"
+                       sharedSecret := "tls-shared-cert"
+
+                       By("creating a shared TLS secret")
+                       createKubeTLSSecret(s, sharedSecret, host, "creating 
shared secret")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating first ApisixTls with shared certificate")
+                       tls1YAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: tls-shared-1
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), s.Namespace(), host, sharedSecret, s.Namespace())
+                       err := s.CreateResourceFromString(tls1YAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating first 
ApisixTls")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating second ApisixTls with same certificate for 
same host")
+                       tls2YAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: tls-shared-2
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), s.Namespace(), host, sharedSecret, s.Namespace())
+                       err = s.CreateResourceFromString(tls2YAML)
+                       Expect(err).NotTo(HaveOccurred(), "second ApisixTls 
should be allowed with same certificate")
+               })
+       })
+
+       Context("Gateway and ApisixTls conflict detection", func() {
+               It("should reject Gateway with conflicting certificate against 
existing ApisixTls", func() {
+                       host := "gateway-vs-tls.example.com"
+                       secretA := "gateway-cert-a"
+                       secretB := "gateway-cert-b"
+
+                       By("creating two different TLS secrets")
+                       createKubeTLSSecret(s, secretA, host, "creating secret 
A")
+                       createKubeTLSSecret(s, secretB, host, "creating secret 
B")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating ApisixTls with certificate A")
+                       tlsYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: apisixtls-first
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), s.Namespace(), host, secretA, s.Namespace())
+                       err := s.CreateResourceFromString(tlsYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating ApisixTls")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to create Gateway with certificate B for 
same host")
+                       hostname := host
+                       gatewayYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-conflict
+  namespace: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: https
+    protocol: HTTPS
+    port: 443
+    hostname: %s
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - name: %s
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`, s.Namespace(), s.Namespace(), hostname, secretB)
+                       err = s.CreateResourceFromString(gatewayYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting conflict 
when creating Gateway")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+                       Expect(err.Error()).To(ContainSubstring(host))
+               })
+
+               It("should allow Gateway with same certificate as existing 
ApisixTls", func() {
+                       host := "gateway-tls-allowed.example.com"
+                       sharedSecret := "gateway-shared-cert"
+
+                       By("creating a shared TLS secret")
+                       createKubeTLSSecret(s, sharedSecret, host, "creating 
shared secret")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating ApisixTls with shared certificate")
+                       tlsYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: apisixtls-allowed
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), s.Namespace(), host, sharedSecret, s.Namespace())
+                       err := s.CreateResourceFromString(tlsYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating ApisixTls")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating Gateway with same certificate")
+                       hostname := host
+                       gatewayYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-allowed
+  namespace: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: https
+    protocol: HTTPS
+    port: 443
+    hostname: %s
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - name: %s
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`, s.Namespace(), s.Namespace(), hostname, sharedSecret)
+                       err = s.CreateResourceFromString(gatewayYAML)
+                       Expect(err).NotTo(HaveOccurred(), "Gateway should be 
allowed with same certificate")
+               })
+
+               It("should reject ApisixTls when Gateway without hostname uses 
different certificate", func() {
+                       host := "gateway-no-host-conflict.example.com"
+                       secretA := "gateway-no-host-cert-a"
+                       secretB := "gateway-no-host-cert-b"
+
+                       By("creating two different TLS secrets")
+                       createKubeTLSSecret(s, secretA, host, "creating secret 
A")
+                       createKubeTLSSecret(s, secretB, host, "creating secret 
B")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating Gateway without explicit hostname using 
certificate A")
+                       gatewayYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-no-host
+  namespace: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: https
+    protocol: HTTPS
+    port: 443
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - name: %s
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`, s.Namespace(), s.Namespace(), secretA)
+                       err := s.CreateResourceFromString(gatewayYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating Gateway 
without hostname")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to create ApisixTls with certificate B 
for same host")
+                       tlsYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: apisixtls-no-host-conflict
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), s.Namespace(), host, secretB, s.Namespace())
+                       err = s.CreateResourceFromString(tlsYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting conflict 
when creating ApisixTls without hostname on existing Gateway")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+                       Expect(err.Error()).To(ContainSubstring(host))
+               })
+       })
+
+       Context("Gateway self-conflict detection", func() {
+               It("should reject Gateway with multiple listeners using 
different certificates for same host", func() {
+                       host := "self-conflict.example.com"
+                       secretA := "gateway-self-cert-a"
+                       secretB := "gateway-self-cert-b"
+
+                       By("creating two different TLS secrets")
+                       createKubeTLSSecret(s, secretA, host, "creating secret 
A")
+                       createKubeTLSSecret(s, secretB, host, "creating secret 
B")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to create Gateway with two listeners 
using different certificates for same host")
+                       hostname := host
+                       gatewayYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-self-conflict
+  namespace: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: https-1
+    protocol: HTTPS
+    port: 443
+    hostname: %s
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - name: %s
+  - name: https-2
+    protocol: HTTPS
+    port: 8443
+    hostname: %s
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - name: %s
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`, s.Namespace(), s.Namespace(), hostname, secretA, hostname, secretB)
+                       err := s.CreateResourceFromString(gatewayYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting 
self-conflict in Gateway")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+                       Expect(err.Error()).To(ContainSubstring(host))
+               })
+       })
+
+       Context("Ingress conflict detection", func() {
+               It("should reject Ingress with conflicting certificate in its 
own TLS config", func() {
+                       host := "ingress-self-conflict.example.com"
+                       secretA := "ingress-self-cert-a"
+                       secretB := "ingress-self-cert-b"
+
+                       By("creating two different TLS secrets")
+                       createKubeTLSSecret(s, secretA, host, "creating secret 
A")
+                       createKubeTLSSecret(s, secretB, host, "creating secret 
B")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating a backend service for Ingress")
+                       serviceYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service-self
+  namespace: %s
+spec:
+  selector:
+    app: test
+  ports:
+  - port: 80
+    targetPort: 80
+`, s.Namespace())
+                       err := s.CreateResourceFromString(serviceYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating service")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to create Ingress with two TLS configs 
using different certificates for same host")
+                       ingressYAML := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: ingress-self-conflict
+  namespace: %s
+spec:
+  ingressClassName: %s
+  tls:
+  - hosts:
+    - %s
+    secretName: %s
+  - hosts:
+    - %s
+    secretName: %s
+  rules:
+  - host: %s
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: test-service-self
+            port:
+              number: 80
+`, s.Namespace(), s.Namespace(), host, secretA, host, secretB, host)
+                       err = s.CreateResourceFromString(ingressYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting 
self-conflict in Ingress")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+                       Expect(err.Error()).To(ContainSubstring(host))
+               })
+
+               It("should reject Ingress with conflicting certificate against 
existing ApisixTls", func() {
+                       host := "ingress-vs-tls.example.com"
+                       secretA := "ingress-cert-a"
+                       secretB := "ingress-cert-b"
+
+                       By("creating two different TLS secrets")
+                       createKubeTLSSecret(s, secretA, host, "creating secret 
A")
+                       createKubeTLSSecret(s, secretB, host, "creating secret 
B")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating ApisixTls with certificate A")
+                       tlsYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: apisixtls-ingress-test
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), s.Namespace(), host, secretA, s.Namespace())
+                       err := s.CreateResourceFromString(tlsYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating ApisixTls")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating a backend service for Ingress")
+                       serviceYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service
+  namespace: %s
+spec:
+  selector:
+    app: test
+  ports:
+  - port: 80
+    targetPort: 80
+`, s.Namespace())
+                       err = s.CreateResourceFromString(serviceYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating service")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to create Ingress with certificate B for 
same host")
+                       ingressYAML := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: ingress-conflict
+  namespace: %s
+spec:
+  ingressClassName: %s
+  tls:
+  - hosts:
+    - %s
+    secretName: %s
+  rules:
+  - host: %s
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: test-service
+            port:
+              number: 80
+`, s.Namespace(), s.Namespace(), host, secretB, host)
+                       err = s.CreateResourceFromString(ingressYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting conflict 
when creating Ingress")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+                       Expect(err.Error()).To(ContainSubstring(host))
+               })
+
+               It("should allow Ingress with same certificate as existing 
Gateway", func() {
+                       host := "ingress-gateway-allowed.example.com"
+                       sharedSecret := "ingress-gateway-shared-cert"
+
+                       By("creating a shared TLS secret")
+                       createKubeTLSSecret(s, sharedSecret, host, "creating 
shared secret")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating Gateway with shared certificate")
+                       hostname := host
+                       gatewayYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-for-ingress
+  namespace: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: https
+    protocol: HTTPS
+    port: 443
+    hostname: %s
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - name: %s
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`, s.Namespace(), s.Namespace(), hostname, sharedSecret)
+                       err := s.CreateResourceFromString(gatewayYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating Gateway")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating a backend service for Ingress")
+                       serviceYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service-2
+  namespace: %s
+spec:
+  selector:
+    app: test
+  ports:
+  - port: 80
+    targetPort: 80
+`, s.Namespace())
+                       err = s.CreateResourceFromString(serviceYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating service")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating Ingress with same certificate")
+                       ingressYAML := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: ingress-allowed
+  namespace: %s
+spec:
+  ingressClassName: %s
+  tls:
+  - hosts:
+    - %s
+    secretName: %s
+  rules:
+  - host: %s
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: test-service-2
+            port:
+              number: 80
+`, s.Namespace(), s.Namespace(), host, sharedSecret, host)
+                       err = s.CreateResourceFromString(ingressYAML)
+                       Expect(err).NotTo(HaveOccurred(), "Ingress should be 
allowed with same certificate")
+               })
+
+               It("should reject Ingress when Gateway without hostname uses 
different certificate", func() {
+                       host := "gateway-ingress-no-host-conflict.example.com"
+                       secretA := "gateway-ingress-no-host-cert-a"
+                       secretB := "gateway-ingress-no-host-cert-b"
+
+                       By("creating two different TLS secrets")
+                       createKubeTLSSecret(s, secretA, host, "creating secret 
A")
+                       createKubeTLSSecret(s, secretB, host, "creating secret 
B")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating Gateway without explicit hostname using 
certificate A")
+                       gatewayYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-ingress-no-host
+  namespace: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: https
+    protocol: HTTPS
+    port: 443
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - name: %s
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`, s.Namespace(), s.Namespace(), secretA)
+                       err := s.CreateResourceFromString(gatewayYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating Gateway 
without hostname")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating a backend service for Ingress")
+                       serviceYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service-ingress-no-host
+  namespace: %s
+spec:
+  selector:
+    app: test
+  ports:
+  - port: 80
+    targetPort: 80
+`, s.Namespace())
+                       err = s.CreateResourceFromString(serviceYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating service")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to create Ingress without explicit host 
using certificate B")
+                       ingressYAML := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: ingress-no-host-conflict
+  namespace: %s
+spec:
+  ingressClassName: %s
+  tls:
+  - secretName: %s
+  rules:
+  - http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: test-service-ingress-no-host
+            port:
+              number: 80
+`, s.Namespace(), s.Namespace(), secretB)
+                       err = s.CreateResourceFromString(ingressYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting conflict 
when creating Ingress without hostname")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+                       Expect(err.Error()).To(ContainSubstring(host))
+               })
+       })
+
+       Context("Default IngressClass conflict detection", func() {
+               It("should reject Ingress without explicit class when default 
class uses a different certificate", func() {
+                       host := "default-ingress-conflict.example.com"
+                       secretA := "default-ingress-cert-a"
+                       secretB := "default-ingress-cert-b"
+                       defaultClassName := fmt.Sprintf("%s-default", 
s.Namespace())
+
+                       By("creating TLS secrets for default ingress test")
+                       createKubeTLSSecret(s, secretA, host, "creating secret 
A")
+                       createKubeTLSSecret(s, secretB, host, "creating secret 
B")
+
+                       By("creating default IngressClass with APISIX 
controller")
+                       defaultIngressClassYAML := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: IngressClass
+metadata:
+  name: %s
+  annotations:
+    ingressclass.kubernetes.io/is-default-class: "true"
+spec:
+  controller: %s
+  parameters:
+    apiGroup: "apisix.apache.org"
+    kind: "GatewayProxy"
+    name: "apisix-proxy-config"
+    namespace: %s
+    scope: Namespace
+`, defaultClassName, s.GetControllerName(), s.Namespace())
+                       err := 
s.CreateResourceFromStringWithNamespace(defaultIngressClassYAML, "")
+                       Expect(err).NotTo(HaveOccurred(), "creating default 
IngressClass")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating backend service for default ingress test")
+                       serviceYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service-default
+  namespace: %s
+spec:
+  selector:
+    app: test
+  ports:
+  - port: 80
+    targetPort: 80
+`, s.Namespace())
+                       err = s.CreateResourceFromString(serviceYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating service")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating baseline Ingress with certificate A")
+                       ingressAYAML := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: ingress-default-a
+  namespace: %s
+spec:
+  tls:
+  - hosts:
+    - %s
+    secretName: %s
+  rules:
+  - host: %s
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: test-service-default
+            port:
+              number: 80
+`, s.Namespace(), host, secretA, host)
+                       err = s.CreateResourceFromString(ingressAYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating baseline 
Ingress")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to create second Ingress with 
conflicting certificate via default class")
+                       ingressBYAML := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: ingress-default-b
+  namespace: %s
+spec:
+  tls:
+  - hosts:
+    - %s
+    secretName: %s
+  rules:
+  - host: %s
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: test-service-default
+            port:
+              number: 80
+`, s.Namespace(), host, secretB, host)
+                       err = s.CreateResourceFromString(ingressBYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting conflict 
when creating second Ingress")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+                       Expect(err.Error()).To(ContainSubstring(host))
+               })
+
+               It("should reject ApisixTls without explicit class when default 
class uses a different certificate", func() {
+                       host := "default-tls-conflict.example.com"
+                       secretA := "default-tls-cert-a"
+                       secretB := "default-tls-cert-b"
+                       defaultClassName := fmt.Sprintf("%s-default-tls", 
s.Namespace())
+
+                       By("creating TLS secrets for default ApisixTls test")
+                       createKubeTLSSecret(s, secretA, host, "creating secret 
A")
+                       createKubeTLSSecret(s, secretB, host, "creating secret 
B")
+
+                       By("creating default IngressClass required for 
ApisixTls admission")
+                       defaultIngressClassYAML := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: IngressClass
+metadata:
+  name: %s
+  annotations:
+    ingressclass.kubernetes.io/is-default-class: "true"
+spec:
+  controller: %s
+  parameters:
+    apiGroup: "apisix.apache.org"
+    kind: "GatewayProxy"
+    name: "apisix-proxy-config"
+    namespace: %s
+    scope: Namespace
+`, defaultClassName, s.GetControllerName(), s.Namespace())
+                       err := 
s.CreateResourceFromStringWithNamespace(defaultIngressClassYAML, "")
+                       Expect(err).NotTo(HaveOccurred(), "creating default 
IngressClass")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating baseline ApisixTls without explicit 
ingress class")
+                       tlsAYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: tls-default-a
+  namespace: %s
+spec:
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), host, secretA, s.Namespace())
+                       err = s.CreateResourceFromString(tlsAYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating baseline 
ApisixTls")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to create ApisixTls with conflicting 
certificate without class override")
+                       tlsBYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: tls-default-b
+  namespace: %s
+spec:
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), host, secretB, s.Namespace())
+                       err = s.CreateResourceFromString(tlsBYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting conflict 
when creating second ApisixTls")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+                       Expect(err.Error()).To(ContainSubstring(host))
+               })
+       })
+
+       Context("Update scenario conflict detection", func() {
+               It("should reject Ingress update that switches to a conflicting 
certificate", func() {
+                       host := "ingress-update-conflict.example.com"
+                       secretA := "ingress-update-cert-a"
+                       secretB := "ingress-update-cert-b"
+
+                       By("creating TLS secrets for ingress update test")
+                       createKubeTLSSecret(s, secretA, host, "creating secret 
A")
+                       createKubeTLSSecret(s, secretB, host, "creating secret 
B")
+
+                       By("creating ApisixTls with certificate A to establish 
existing mapping")
+                       tlsYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: tls-update-baseline
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), s.Namespace(), host, secretA, s.Namespace())
+                       err := s.CreateResourceFromString(tlsYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating baseline 
ApisixTls for ingress update")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating backend service for ingress update test")
+                       serviceYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service-update
+  namespace: %s
+spec:
+  selector:
+    app: test
+  ports:
+  - port: 80
+    targetPort: 80
+`, s.Namespace())
+                       err = s.CreateResourceFromString(serviceYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating service")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating initial Ingress with matching certificate")
+                       ingressBaseYAML := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: ingress-update
+  namespace: %s
+spec:
+  ingressClassName: %s
+  tls:
+  - hosts:
+    - %s
+    secretName: %s
+  rules:
+  - host: %s
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: test-service-update
+            port:
+              number: 80
+`, s.Namespace(), s.Namespace(), host, secretA, host)
+                       err = s.CreateResourceFromString(ingressBaseYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating initial 
Ingress")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to update Ingress to use conflicting 
certificate B")
+                       ingressUpdatedYAML := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: ingress-update
+  namespace: %s
+spec:
+  ingressClassName: %s
+  tls:
+  - hosts:
+    - %s
+    secretName: %s
+  rules:
+  - host: %s
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: test-service-update
+            port:
+              number: 80
+`, s.Namespace(), s.Namespace(), host, secretB, host)
+                       err = s.CreateResourceFromString(ingressUpdatedYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting conflict 
when updating Ingress certificate")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+                       Expect(err.Error()).To(ContainSubstring(host))
+               })
+
+               It("should reject Gateway update that switches to a conflicting 
certificate", func() {
+                       host := "gateway-update-conflict.example.com"
+                       secretA := "gateway-update-cert-a"
+                       secretB := "gateway-update-cert-b"
+
+                       By("creating TLS secrets for gateway update test")
+                       createKubeTLSSecret(s, secretA, host, "creating secret 
A")
+                       createKubeTLSSecret(s, secretB, host, "creating secret 
B")
+
+                       By("creating ApisixTls with certificate A to establish 
host ownership")
+                       tlsYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: tls-gateway-update
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), s.Namespace(), host, secretA, s.Namespace())
+                       err := s.CreateResourceFromString(tlsYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating baseline 
ApisixTls for gateway update")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating initial Gateway using certificate A")
+                       gatewayBaseYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-update
+  namespace: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: https
+    protocol: HTTPS
+    port: 443
+    hostname: %s
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - name: %s
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`, s.Namespace(), s.Namespace(), host, secretA)
+                       err = s.CreateResourceFromString(gatewayBaseYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating initial 
Gateway")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to update Gateway to use conflicting 
certificate B")
+                       gatewayUpdatedYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-update
+  namespace: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: https
+    protocol: HTTPS
+    port: 443
+    hostname: %s
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - name: %s
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`, s.Namespace(), s.Namespace(), host, secretB)
+                       err = s.CreateResourceFromString(gatewayUpdatedYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting conflict 
when updating Gateway certificate")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+                       Expect(err.Error()).To(ContainSubstring(host))
+               })
+       })
+
+       Context("Mixed resource conflict detection", func() {
+               It("should handle conflicts among Gateway, Ingress, and 
ApisixTls", func() {
+                       host := "mixed.example.com"
+                       secretA := "mixed-cert-a"
+                       secretB := "mixed-cert-b"
+                       secretC := "mixed-cert-c"
+
+                       By("creating three different TLS secrets")
+                       createKubeTLSSecret(s, secretA, host, "creating secret 
A")
+                       createKubeTLSSecret(s, secretB, host, "creating secret 
B")
+                       createKubeTLSSecret(s, secretC, host, "creating secret 
C")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("creating Gateway with certificate A")
+                       hostname := host
+                       gatewayYAML := fmt.Sprintf(`
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-mixed
+  namespace: %s
+spec:
+  gatewayClassName: %s
+  listeners:
+  - name: https
+    protocol: HTTPS
+    port: 443
+    hostname: %s
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - name: %s
+  infrastructure:
+    parametersRef:
+      group: apisix.apache.org
+      kind: GatewayProxy
+      name: apisix-proxy-config
+`, s.Namespace(), s.Namespace(), hostname, secretA)
+                       err := s.CreateResourceFromString(gatewayYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating Gateway 
with cert A")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to create ApisixTls with certificate B")
+                       tlsYAML := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixTls
+metadata:
+  name: apisixtls-mixed
+  namespace: %s
+spec:
+  ingressClassName: %s
+  hosts:
+  - %s
+  secret:
+    name: %s
+    namespace: %s
+`, s.Namespace(), s.Namespace(), host, secretB, s.Namespace())
+                       err = s.CreateResourceFromString(tlsYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting conflict 
when creating ApisixTls with different cert")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+
+                       By("creating a backend service")
+                       serviceYAML := fmt.Sprintf(`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service-3
+  namespace: %s
+spec:
+  selector:
+    app: test
+  ports:
+  - port: 80
+    targetPort: 80
+`, s.Namespace())
+                       err = s.CreateResourceFromString(serviceYAML)
+                       Expect(err).NotTo(HaveOccurred(), "creating service")
+
+                       time.Sleep(2 * time.Second)
+
+                       By("attempting to create Ingress with certificate C")
+                       ingressYAML := fmt.Sprintf(`
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: ingress-mixed
+  namespace: %s
+spec:
+  ingressClassName: %s
+  tls:
+  - hosts:
+    - %s
+    secretName: %s
+  rules:
+  - host: %s
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: test-service-3
+            port:
+              number: 80
+`, s.Namespace(), s.Namespace(), host, secretC, host)
+                       err = s.CreateResourceFromString(ingressYAML)
+                       Expect(err).Should(HaveOccurred(), "expecting conflict 
when creating Ingress with different cert")
+                       Expect(err.Error()).To(ContainSubstring("SSL 
configuration conflicts detected"))
+               })
+       })
+})
+
+func createApisixTLSSecret(s *scaffold.Scaffold, secretName, host, 
failureMessage string) {
+       cert, key := s.GenerateCert(GinkgoT(), []string{host})
+       err := s.NewSecret(secretName, cert.String(), key.String())
+       Expect(err).NotTo(HaveOccurred(), failureMessage)
+}
+
+func createKubeTLSSecret(s *scaffold.Scaffold, secretName, host, 
failureMessage string) {
+       cert, key := s.GenerateCert(GinkgoT(), []string{host})
+       err := s.NewKubeTlsSecret(secretName, cert.String(), key.String())
+       Expect(err).NotTo(HaveOccurred(), failureMessage)
+}


Reply via email to