AlinsRan commented on code in PR #2810:
URL: 
https://github.com/apache/apisix-ingress-controller/pull/2810#discussion_r3662348827


##########
internal/ssl/util.go:
##########
@@ -180,6 +180,77 @@ func NormalizeHosts(hosts []string) []string {
        return normalized
 }
 
+// HostsOverlap reports whether two SNI host patterns can both match a common
+// concrete hostname. It understands single-label wildcards ("*.example.com"
+// matches "app.example.com" but not "a.b.example.com"). Two distinct wildcards
+// never share a concrete host.
+func HostsOverlap(a, b string) bool {

Review Comment:
   An exact host and a covering wildcard are not a conflict — please drop this 
part.
   
   APISIX reverses each SNI and uses it as a radixtree path. An exact SNI is a 
fully static path, so it goes into `hash_path`, and `match_route` checks 
`hash_path` before walking the tree that holds the wildcard prefixes. 
`app.example.com` deterministically gets its own SSL object and 
`other.example.com` gets the wildcard one. "Wildcard cert plus a dedicated cert 
for one subdomain" is a standard setup, and this change rejects it at admission.
   
   The nondeterminism this detector exists to catch is two SSL objects carrying 
the *same* SNI: they land in the same `hash_path[path]` array and `sort_route` 
ties on both priority and path length, so the winner is insertion order. 
Exact-key matching already covers that.



##########
internal/webhook/v1/ssl/conflict_detector.go:
##########
@@ -478,7 +572,12 @@ func (d *ConflictDetector) mappingForHostWithCache(ctx 
context.Context, obj clie
        }
 
        for _, mapping := range mappings {
-               if mapping.Host == host {
+               if mapping.Host == "" {
+                       continue
+               }
+               // Wildcard-aware: an exact host and a covering wildcard 
overlap even
+               // though their index keys differ ("app.example.com" vs 
"*.example.com").
+               if sslutil.HostsOverlap(mapping.Host, host) {

Review Comment:
   First overlap wins here, so the outcome depends on mapping order: an exact 
host using the very same certificate gets reported as a conflict.
   
   Repro — existing Ingress with `tls[0]={hosts:[*.example.com], 
secret:cert-a}`, `tls[1]={hosts:[app.example.com], secret:cert-b}`, then an 
ApisixTls for `app.example.com` with `cert-b`. The wildcard entry is hit first, 
`cert-a != cert-b`, conflict.
   
   If overlap matching stays, match an exact `mapping.Host == host` first and 
only fall back to overlap.



##########
internal/webhook/v1/ssl/conflict_detector.go:
##########
@@ -410,6 +471,39 @@ func (d *ConflictDetector) findExternalConflicts(ctx 
context.Context, obj client
        return results, nil
 }
 
+// listAllTLSResources enumerates every TLS-bearing Gateway, Ingress and
+// ApisixTls. Used when the incoming host is a wildcard, whose covered exact
+// hosts can't be resolved through the exact-key host index.
+func (d *ConflictDetector) listAllTLSResources(ctx context.Context) 
([]client.Object, error) {

Review Comment:
   This lists every Gateway, Ingress and ApisixTls in the cluster and then runs 
`resolveGatewayProxy` (→ `FindMatchingIngressClass`) per candidate, on every 
admission whose host starts with `*.`. Wildcard hosts are the common case for 
TLS, so this is the common path, and it runs inside the admission timeout.
   
   If overlap detection is kept, index for it instead: have `ssl_host.go` emit 
`ParentWildcard(host)` as an extra index key for exact hosts, and a 
`*.example.com` lookup resolves through the index. This function and the extra 
exact-side List then both go away.



##########
internal/webhook/v1/ssl/conflict_detector.go:
##########
@@ -251,17 +258,37 @@ func (d *ConflictDetector) BuildApisixTlsMappings(ctx 
context.Context, tls *apiv
        // if len(hosts) == 0 {
        //      hosts = info.hosts
        // }
+       clientHash := clientConfigHash(tls.Spec.Client)
        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),
+                       Host:             host,
+                       CertificateHash:  info.hash,
+                       ClientConfigHash: clientHash,
+                       ResourceRef:      fmt.Sprintf("%s/%s/%s", 
internaltypes.KindApisixTls, tls.Namespace, tls.Name),
                })
        }
 
        return mappings
 }
 
+// clientConfigHash digests an ApisixTls mTLS client-verification config into a
+// stable key. Returns "" when no mTLS is configured, so a resource that 
enforces
+// mTLS and one that doesn't produce different keys for the same host+cert. It
+// keys on the CA secret reference (namespace/name), which uniquely identifies
+// the trust anchor, plus depth and the skip regexes.
+func clientConfigHash(client *apiv2.ApisixMutualTlsClientConfig) string {

Review Comment:
   Gateway listeners are missing here. `translateFrontendValidation` writes the 
same `ssl.client` field from `listener.tls.frontendValidation`, but 
`BuildGatewayMappings` leaves `ClientConfigHash` empty. Two Gateways on the 
same host and cert with different client CAs still pass, and any Gateway 
compared against an ApisixTls looks like "no mTLS".
   
   Related: `FormatConflicts` always reports "is already configured with a 
different certificate", which is misleading when the certificates are identical 
and only the mTLS config differs.



##########
internal/webhook/v1/ssl/conflict_detector.go:
##########
@@ -92,22 +98,23 @@ func (d *ConflictDetector) DetectConflicts(ctx 
context.Context, obj client.Objec
        conflicts := make([]SSLConflict, 0)
 
        // First, check for conflicts within the new resource itself.
-       seen := make(map[string]string, len(newMappings))
+       seen := make(map[string]HostCertMapping, len(newMappings))

Review Comment:
   The self-conflict check still compares exact `mapping.Host`, so it misses 
what the external path now catches. A single Ingress with 
`tls[0]={hosts:[*.example.com], secret:a}` and 
`tls[1]={hosts:[app.example.com], secret:b}` passes. Whichever matching rule 
you settle on, both loops should use it.



##########
internal/adc/translator/apisixtls.go:
##########
@@ -50,6 +50,14 @@ func (t *Translator) TranslateApisixTls(tctx 
*provider.TranslateContext, tls *ap
                return nil, err
        }
 
+       // APISIX serves the cert regardless of SAN, so this is advisory: warn 
when a
+       // declared host isn't covered by the cert SANs (clients may reject it).
+       if uncovered, sans := uncoveredSNIHosts(cert, tls.Spec.Hosts); 
len(uncovered) > 0 {

Review Comment:
   Move this to the ApisixTls webhook as an `admission.Warnings`. APISIX never 
looks at cert SANs when selecting an SSL object, so this is purely advice for 
the user, and a log line in the translator is not something they will see — 
`ValidateCreate`/`ValidateUpdate` already return warnings that show up on 
`kubectl apply`. It also avoids re-parsing the certificate on every reconcile.
   
   The PR description says translation is rejected when a host is not covered; 
the code only logs. Worth updating.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to