Skip to content

fix: detect and warn on inconsistent SSL host/certificate configuration - #437

Open
shreemaan-abhishek wants to merge 5 commits into
masterfrom
fix/ssl-conflict-detector-host-and-mtls
Open

fix: detect and warn on inconsistent SSL host/certificate configuration#437
shreemaan-abhishek wants to merge 5 commits into
masterfrom
fix/ssl-conflict-detector-host-and-mtls

Conversation

@shreemaan-abhishek

@shreemaan-abhishek shreemaan-abhishek commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Description

Ports the SSL host/certificate consistency work from apache/apisix-ingress-controller#2810. The SSL conflict detector now warns on overlap instead of denying it at admission, and its detection is made wildcard- and mTLS-aware so the warnings are accurate.

1. Wildcard vs. exact host matching — hosts were compared by exact string equality, so *.example.com and app.example.com (different index keys) were never compared; APISIX resolves exact SNI ahead of a covering wildcard, so they collide. Add sslutil.HostsOverlap / ParentWildcard; exact host also queries its covering wildcard, wildcard host enumerates + filters by overlap; compare with HostsOverlap.

2. mTLS client config in the conflict key — keyed only on server cert hash. Add ClientConfigHash (CA secret ref + depth + skip regex; empty when no mTLS).

3. Overlap reported as a warning, not a denial — the detector previously rejected overlapping objects at admission. That gate misses conflicts from a cert rotating inside its Secret, a GatewayProxy/IngressClass regrouping, a disabled webhook, or pre-existing objects, and blocks edits to already-coexisting configs. Now surfaced as an admission warning on create/update. The reconcile-time status-condition treatment is tracked in #448.

4. Certificate SAN coverage warning (translator)TranslateApisixTls gave no signal when cert SANs don't cover a declared host; now logs a warning via sslutil.HostCoveredBy (APISIX serves the cert regardless).

Notes

Tests

  • unit tests for HostsOverlap / ParentWildcard / HostCoveredBy
  • detector tests for wildcard/exact overlap (both directions) and differing mTLS, with false-positive guards
  • translator tests for SAN coverage

Summary by CodeRabbit

  • Bug Fixes
    • Improved wildcard-aware TLS conflict detection for overlapping exact and wildcard host patterns.
    • Reduced false-positive conflicts for non-overlapping wildcard/exact combinations.
    • TLS conflict checks now include mTLS client-verification settings (not only certificate identity).
    • Webhooks now surface overlapping SSL configuration conflicts as advisory warnings rather than denying admission.
    • TLS translation now adds an informational check for SNI hosts not covered by certificate DNS SANs (no rejection; lenient when no DNS SANs).
  • Tests
    • Added unit tests for hostname overlap/coverage and parent wildcard derivation.
    • Expanded webhook and translator test coverage for wildcard overlap, mTLS differences, and uncovered SNI behavior.

Two correctness gaps in the SSL conflict detector let colliding TLS
configurations be admitted for the same GatewayProxy:

1. Hosts were compared by exact string equality, so a covering wildcard host
   and an exact host were never compared against each other ("*.example.com"
   and "app.example.com" are indexed under different keys). Because APISIX
   resolves an exact SNI ahead of a covering wildcard, two objects whose hosts
   overlap only through a wildcard could both be admitted and then collide at
   the data plane. Add sslutil.HostsOverlap / ParentWildcard (single-label
   wildcard semantics); for an exact host also look up its covering wildcard,
   for a wildcard host enumerate TLS resources and filter by overlap (the
   exact-key index can't answer a suffix query), and compare mappings with
   HostsOverlap instead of string equality.

2. The conflict key used only the server certificate hash, so two objects for
   the same host and server cert but different mTLS client config (spec.client)
   were treated as non-conflicting, leaving client-verification behavior for
   that SNI nondeterministic. Add ClientConfigHash to HostCertMapping (digest
   of the CA secret reference, depth and skip_mtls_uri_regex; empty when no
   mTLS) and treat a differing client config as a conflict too.

Adds unit tests for the overlap helpers and detector-level tests for both
wildcard/exact overlap and differing mTLS config, with false-positive guards.
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

TLS processing now validates declared hosts against certificate DNS SANs, supports single-label wildcard hostname matching, compares mTLS client-verification hashes during conflict detection, and reports SSL conflicts as admission warnings.

Changes

SNI Conflict Detection

Layer / File(s) Summary
Hostname overlap utilities
internal/ssl/util.go, internal/ssl/util_test.go
Adds normalized exact and single-label wildcard overlap checks, directional SAN coverage, and parent wildcard derivation with table-driven tests.
Certificate SAN coverage validation
internal/adc/translator/apisixtls.go, internal/adc/translator/apisixtls_test.go
Checks declared TLS hosts against certificate DNS SANs during translation without rejecting certificates lacking evaluable DNS SANs.
mTLS configuration matching
internal/webhook/v1/ssl/conflict_detector.go, internal/webhook/v1/ssl/conflict_detector_test.go
Adds canonical client configuration hashing to host mappings and detects conflicts when certificate or mTLS hashes differ.
Wildcard conflict lookup
internal/webhook/v1/ssl/conflict_detector.go, internal/webhook/v1/ssl/conflict_detector_test.go
Expands TLS candidate lookup to covering wildcards and all wildcard resources, with overlap and non-overlap tests.
Advisory admission conflicts
internal/webhook/v1/apisixtls_webhook.go, internal/webhook/v1/gateway_webhook.go, internal/webhook/v1/ingress_webhook.go, test/e2e/webhook/ssl_conflict.go
Changes SSL conflict handling from admission errors to accumulated warnings across create and update validation paths, with updated end-to-end assertions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TranslateApisixTls
  participant Certificate
  participant HostCoveredBy
  TranslateApisixTls->>Certificate: extract DNS SANs
  Certificate-->>TranslateApisixTls: return SAN list
  TranslateApisixTls->>HostCoveredBy: check each declared host
  HostCoveredBy-->>TranslateApisixTls: return coverage result
Loading
sequenceDiagram
  participant DetectConflicts
  participant TLSResourceIndex
  participant ExistingTLSResources
  DetectConflicts->>TLSResourceIndex: look up exact and covering wildcard hosts
  TLSResourceIndex->>ExistingTLSResources: enumerate wildcard candidates
  ExistingTLSResources-->>DetectConflicts: return TLS resources and mappings
  DetectConflicts->>DetectConflicts: compare host overlap and configuration hashes
Loading

Possibly related issues

  • #448 — Extends the updated wildcard-aware conflict logic into reconcile-time status conditions.

Suggested reviewers: nic-6443


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Security Check ❌ Error Gateway/Ingress/ApisixTls now admit conflicting SNI/cert collisions as warnings only, weakening TLS uniqueness enforcement. Return an error when DetectConflicts finds true host/cert conflicts; keep warnings only for non-blocking advisory cases behind an explicit opt-in.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: detecting SSL host/certificate inconsistencies and surfacing them as warnings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
E2e Test Quality Review ✅ Passed Real cluster e2e cases cover create/update warning flow across Gateway, Ingress, and ApisixTls with clear, independent specs and no mocks.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ssl-conflict-detector-host-and-mtls

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/webhook/v1/ssl/conflict_detector.go (1)

567-585: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

mappingForHostWithCache should not stop at the first overlap
BuildGatewayMappings and BuildIngressMappings can emit multiple mappings for the same host with different certificate/client-config hashes. Returning the first match lets findExternalConflicts miss other conflicting mappings in the same resource.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/webhook/v1/ssl/conflict_detector.go` around lines 567 - 585, The
mappingForHostWithCache lookup must account for every overlapping mapping rather
than returning only the first one, so findExternalConflicts can detect differing
certificate/client-config hashes within the same resource. Update the
surrounding conflict-detection flow to collect or compare all HostsOverlap
matches while preserving the cache behavior and no-match result.

Source: Coding guidelines

🧹 Nitpick comments (1)
internal/webhook/v1/ssl/conflict_detector.go (1)

354-421: 🚀 Performance & Scalability | 🔵 Trivial

Wildcard candidate enumeration logic looks correct.

Enumerating all TLS resources for wildcard incoming hosts (since the host index can't answer suffix queries) and merging ParentWildcard + no-host candidates for exact hosts is sound. One operational note: listAllTLSResources performs an unfiltered cluster-wide list of Gateways/Ingresses/ApisixTls for every wildcard host in the request, which runs synchronously in the admission webhook path — likely fine against a cache-backed client, but worth keeping in mind for very large clusters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/webhook/v1/ssl/conflict_detector.go` around lines 354 - 421, No code
change is required; the wildcard candidate enumeration in findExternalConflicts
is correct. Retain the existing listAllTLSResources, ParentWildcard, and no-host
candidate behavior, while recognizing that wildcard requests perform a
synchronous cluster-wide resource listing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/webhook/v1/ssl/conflict_detector.go`:
- Around line 274-290: Update clientConfigHash to use an injective encoding for
SkipMTLSUriRegex entries instead of strings.Join with commas, such as
length-prefixed elements or another unambiguous representation. Preserve sorting
and ensure distinct regex lists, including entries containing commas, produce
distinct canonical strings and hashes.
- Around line 100-118: Update the intra-resource conflict logic around the seen
map to compare each valid mapping pair using sslutil.HostsOverlap, so wildcard
and covered exact hosts are evaluated consistently with findExternalConflicts.
Preserve conflicts for differing certificate or client-config hashes, and extend
SSLConflict reporting in both this path and findExternalConflicts to surface
which field diverged when the certificate hashes match.

---

Outside diff comments:
In `@internal/webhook/v1/ssl/conflict_detector.go`:
- Around line 567-585: The mappingForHostWithCache lookup must account for every
overlapping mapping rather than returning only the first one, so
findExternalConflicts can detect differing certificate/client-config hashes
within the same resource. Update the surrounding conflict-detection flow to
collect or compare all HostsOverlap matches while preserving the cache behavior
and no-match result.

---

Nitpick comments:
In `@internal/webhook/v1/ssl/conflict_detector.go`:
- Around line 354-421: No code change is required; the wildcard candidate
enumeration in findExternalConflicts is correct. Retain the existing
listAllTLSResources, ParentWildcard, and no-host candidate behavior, while
recognizing that wildcard requests perform a synchronous cluster-wide resource
listing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09fd8fa6-780c-46d9-911a-6a55b0c2eca1

📥 Commits

Reviewing files that changed from the base of the PR and between 5f84a00 and b5a960e.

📒 Files selected for processing (4)
  • internal/ssl/util.go
  • internal/ssl/util_test.go
  • internal/webhook/v1/ssl/conflict_detector.go
  • internal/webhook/v1/ssl/conflict_detector_test.go

Comment on lines 100 to 118
// First, check for conflicts within the new resource itself.
seen := make(map[string]string, len(newMappings))
seen := make(map[string]HostCertMapping, len(newMappings))
for _, mapping := range newMappings {
if mapping.Host == "" || mapping.CertificateHash == "" {
continue
}
if prev, ok := seen[mapping.Host]; ok {
if prev != mapping.CertificateHash {
if prev.CertificateHash != mapping.CertificateHash ||
prev.ClientConfigHash != mapping.ClientConfigHash {
conflicts = append(conflicts, SSLConflict{
Host: mapping.Host,
ConflictingResource: mapping.ResourceRef,
CertificateHash: prev,
CertificateHash: prev.CertificateHash,
})
}
continue
}
seen[mapping.Host] = mapping.CertificateHash
seen[mapping.Host] = mapping
}

@coderabbitai coderabbitai Bot Jul 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Intra-resource check isn't overlap-aware, unlike the new external check.

seen is keyed by exact mapping.Host string. If a single submitted object produces both a wildcard mapping (e.g. *.example.com) and an exact mapping it covers (e.g. app.example.com) with different CertificateHash/ClientConfigHash (possible for Ingress with multiple spec.tls blocks, or multi-listener Gateways), this loop will never compare them since they hash to different map keys — yet the external path (via sslutil.HostsOverlap) would flag the identical scenario across two different resources. This is an inconsistent enforcement of SNI uniqueness within a single object.

Also note: when a conflict is due only to a ClientConfigHash mismatch (same CertificateHash), the reported SSLConflict.CertificateHash doesn't indicate that the actual divergence was in mTLS config — same limitation applies to the findExternalConflicts conflict construction (Lines 449-453). Worth surfacing the differing field in the conflict result for operator debuggability.

🛠️ Suggested pairwise overlap check
-	seen := make(map[string]HostCertMapping, len(newMappings))
-	for _, mapping := range newMappings {
-		if mapping.Host == "" || mapping.CertificateHash == "" {
-			continue
-		}
-		if prev, ok := seen[mapping.Host]; ok {
-			if prev.CertificateHash != mapping.CertificateHash ||
-				prev.ClientConfigHash != mapping.ClientConfigHash {
-				conflicts = append(conflicts, SSLConflict{
-					Host:                mapping.Host,
-					ConflictingResource: mapping.ResourceRef,
-					CertificateHash:     prev.CertificateHash,
-				})
-			}
-			continue
-		}
-		seen[mapping.Host] = mapping
-	}
+	seen := make(map[string]HostCertMapping, len(newMappings))
+	valid := make([]HostCertMapping, 0, len(newMappings))
+	for _, mapping := range newMappings {
+		if mapping.Host == "" || mapping.CertificateHash == "" {
+			continue
+		}
+		valid = append(valid, mapping)
+		if _, ok := seen[mapping.Host]; !ok {
+			seen[mapping.Host] = mapping
+		}
+	}
+	for i := 0; i < len(valid); i++ {
+		for j := i + 1; j < len(valid); j++ {
+			if !sslutil.HostsOverlap(valid[i].Host, valid[j].Host) {
+				continue
+			}
+			if valid[i].CertificateHash != valid[j].CertificateHash ||
+				valid[i].ClientConfigHash != valid[j].ClientConfigHash {
+				conflicts = append(conflicts, SSLConflict{
+					Host:                valid[j].Host,
+					ConflictingResource: valid[j].ResourceRef,
+					CertificateHash:     valid[i].CertificateHash,
+				})
+			}
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/webhook/v1/ssl/conflict_detector.go` around lines 100 - 118, Update
the intra-resource conflict logic around the seen map to compare each valid
mapping pair using sslutil.HostsOverlap, so wildcard and covered exact hosts are
evaluated consistently with findExternalConflicts. Preserve conflicts for
differing certificate or client-config hashes, and extend SSLConflict reporting
in both this path and findExternalConflicts to surface which field diverged when
the certificate hashes match.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed on the current head. The exact-host map still misses wildcard/exact overlap within one resource, while the external path uses HostsOverlap. Please make the intra-resource check overlap-aware and retain enough conflict detail to distinguish certificate and mTLS differences.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline internal/webhook/v1/ssl/conflict_detector_test.go --items all --type function
rg -n -C 4 '\bSSLConflict\b|\bFormatConflicts\s*\(|\bDetectConflicts\s*\(' \
  internal/webhook/v1/ssl/conflict_detector.go \
  internal/webhook/v1/ssl/conflict_detector_test.go \
  .

Length of output: 40266


@jarvis9443, confirmed. The inspection shows the intra-resource check is still exact-key-only, while the external path is overlap-aware; it also confirms that mTLS-only conflicts are currently rendered as certificate conflicts.

  • Make the intra-resource comparison pairwise and wildcard-aware with sslutil.HostsOverlap.
  • Preserve the representative exact-host mapping used by findExternalConflicts.
  • Add a non-sensitive conflict reason and use it in both intra-resource and external conflict reporting.
  • Add regression tests for same-resource wildcard/exact conflicts and mTLS-only diagnostics.

🧬 Code Graph Analysis Results

Relevant snippets

internal/webhook/v1/ssl/conflict_detector.go

[lines 43-57] Host/cert mapping + conflict output fields

// HostCertMapping represents the relationship between a host and its certificate hash.
type HostCertMapping struct {
	Host            string
	CertificateHash string
	// ClientConfigHash digests the mTLS client-verification config (CA ref, depth,
	// skip regexes). Empty when the resource enforces no mTLS. Two resources for the
	// same host+cert but differing client config are still a conflict.
	ClientConfigHash string
	ResourceRef      string
}

// SSLConflict exposes the conflict details to the admission webhook for reporting.
type SSLConflict struct {
	Host                string
	ConflictingResource string
	CertificateHash     string
}

[lines 78-127] Intra-resource conflict check (currently exact-host keyed; no overlap check)

// 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]HostCertMapping, len(newMappings))
	for _, mapping := range newMappings {
		if mapping.Host == "" || mapping.CertificateHash == "" {
			continue
		}
		if prev, ok := seen[mapping.Host]; ok {
			if prev.CertificateHash != mapping.CertificateHash ||
				prev.ClientConfigHash != mapping.ClientConfigHash {
				conflicts = append(conflicts, SSLConflict{
					Host:                mapping.Host,
					ConflictingResource: mapping.ResourceRef,
					CertificateHash:     prev.CertificateHash,
				})
			}
			continue
		}
		seen[mapping.Host] = mapping
	}

	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
}

[lines 129-140] Conflict message formatting (currently describes “different certificate”)

// 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()
}

[lines 269-281] mTLS client config hashing (used to distinguish cert vs mTLS differences)

func clientConfigHash(client *apiv2.ApisixMutualTlsClientConfig) string {
	if client == nil {
		return ""
	}
	regexes := append([]string(nil), client.SkipMTLSUriRegex...)
	sort.Strings(regexes)
	canonical := fmt.Sprintf("ca=%s/%s;depth=%d;skip=%s",
		client.CASecret.Namespace, client.CASecret.Name, client.Depth,
		strings.Join(regexes, ","))
	sum := sha256.Sum256([]byte(canonical))
	return hex.EncodeToString(sum[:])
}

[lines 345-444] External conflict evaluation compares both certificate hash and mTLS client config hash

func (d *ConflictDetector) findExternalConflicts(ctx context.Context, obj client.Object, gatewayProxy *v1alpha1.GatewayProxy, newMappings map[string]HostCertMapping) ([]SSLConflict, error) {
	...
	for _, host := range hostValues {
		...
		for _, candidate := range candidates {
			...
			mapping, ok := d.mappingForHostWithCache(ctx, candidate, host, mappingCache)
			if !ok {
				continue
			}
			// Same server cert AND same mTLS client config: no conflict. A
			// differing client config is still a conflict even when the server
			// cert matches.
			newMapping := newMappings[host]
			if mapping.CertificateHash == newMapping.CertificateHash &&
				mapping.ClientConfigHash == newMapping.ClientConfigHash {
				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,
			}
		}
	}
	...
}

[lines 553-571] External overlap-aware matching uses sslutil.HostsOverlap

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 == "" {
			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) {
			return mapping, true
		}
	}
	return HostCertMapping{}, false
}

internal/ssl/util.go

[lines 165-181] Host normalization (lowercasing + trimming + dedupe)

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
}

[lines 187-208] Wildcard/exact overlap decision used by the external path

func HostsOverlap(a, b string) bool {
	a = strings.ToLower(strings.TrimSpace(a))
	b = strings.ToLower(strings.TrimSpace(b))
	if a == "" || b == "" {
		return false
	}
	if a == b {
		return true
	}
	aWild := strings.HasPrefix(a, "*.")
	bWild := strings.HasPrefix(b, "*.")
	switch {
	case aWild && bWild:
		return false
	case aWild:
		return wildcardCovers(a, b)
	case bWild:
		return wildcardCovers(b, a)
	default:
		return false
	}
}

[lines 243-252] Parent wildcard helper used during external enumeration

func ParentWildcard(host string) string {
	if host == "" || strings.HasPrefix(host, "*.") {
		return ""
	}
	i := strings.IndexByte(host, '.')
	if i < 0 || i == len(host)-1 {
		return ""
	}
	return "*." + host[i+1:]
}

  • 📌 Create a pull request with these changes

Comment thread internal/webhook/v1/ssl/conflict_detector.go
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

conformance test report - apisix mode

apiVersion: gateway.networking.k8s.io/v1
date: "2026-07-29T10:45:11Z"
gatewayAPIChannel: experimental
gatewayAPIVersion: v1.6.0
implementation:
  contact:
  - https://github.com/apache/apisix-ingress-controller/issues
  organization: APISIX
  project: apisix-ingress-controller
  url: https://github.com/apache/apisix-ingress-controller.git
  version: v2.0.0
kind: ConformanceReport
mode: default
profiles:
- core:
    result: partial
    skippedTests:
    - HTTPRouteHTTPSListener
    - HTTPRouteInvalidBackendRefUnknownKind
    - HTTPRouteInvalidCrossNamespaceBackendRef
    - HTTPRouteInvalidNonExistentBackendRef
    - HTTPRouteListenerHostnameMatching
    - HTTPRouteMultipleGateways
    - HTTPRouteNoBackendRefs
    statistics:
      Failed: 0
      Passed: 30
      Skipped: 7
  extended:
    result: partial
    skippedTests:
    - HTTPRouteRedirectPortAndScheme
    statistics:
      Failed: 0
      Passed: 12
      Skipped: 1
    supportedFeatures:
    - GatewayAddressEmpty
    - GatewayPort8080
    - HTTPRouteBackendProtocolWebSocket
    - HTTPRouteDestinationPortMatching
    - HTTPRouteHostRewrite
    - HTTPRouteMethodMatching
    - HTTPRoutePathRewrite
    - HTTPRoutePortRedirect
    - HTTPRouteQueryParamMatching
    - HTTPRouteRequestMirror
    - HTTPRouteResponseHeaderModification
    - HTTPRouteSchemeRedirect
    unsupportedFeatures:
    - BackendTLSPolicy
    - BackendTLSPolicySANValidation
    - GatewayBackendClientCertificate
    - GatewayFrontendClientCertificateValidation
    - GatewayFrontendClientCertificateValidationInsecureFallback
    - GatewayHTTPListenerIsolation
    - GatewayHTTPSListenerDetectMisdirectedRequests
    - GatewayInfrastructurePropagation
    - GatewayStaticAddresses
    - HTTPRoute303RedirectStatusCode
    - HTTPRoute307RedirectStatusCode
    - HTTPRoute308RedirectStatusCode
    - HTTPRouteBackendProtocolH2C
    - HTTPRouteBackendRequestHeaderModification
    - HTTPRouteBackendTimeout
    - HTTPRouteCORS
    - HTTPRouteNamedRouteRule
    - HTTPRouteParentRefPort
    - HTTPRoutePathRedirect
    - HTTPRouteRequestMultipleMirrors
    - HTTPRouteRequestPercentageMirror
    - HTTPRouteRequestTimeout
    - HTTPRouteRetry
    - HTTPRouteRetryBackendTimeout
    - HTTPRouteRetryConnectionError
    - ListenerSet
  name: GATEWAY-HTTP
  summary: Core tests partially succeeded with 7 test skips. Extended tests partially
    succeeded with 1 test skips.
- core:
    result: partial
    skippedTests:
    - GRPCRouteListenerHostnameMatching
    statistics:
      Failed: 0
      Passed: 14
      Skipped: 1
  extended:
    result: success
    statistics:
      Failed: 0
      Passed: 1
      Skipped: 0
    supportedFeatures:
    - GatewayAddressEmpty
    - GatewayPort8080
    unsupportedFeatures:
    - GatewayBackendClientCertificate
    - GatewayFrontendClientCertificateValidation
    - GatewayFrontendClientCertificateValidationInsecureFallback
    - GatewayHTTPListenerIsolation
    - GatewayHTTPSListenerDetectMisdirectedRequests
    - GatewayInfrastructurePropagation
    - GatewayStaticAddresses
    - ListenerSet
  name: GATEWAY-GRPC
  summary: Core tests partially succeeded with 1 test skips. Extended tests succeeded.
- core:
    result: partial
    skippedTests:
    - TLSRouteHostnameIntersection
    - TLSRouteInvalidBackendRefNonexistent
    - TLSRouteInvalidBackendRefUnknownKind
    - TLSRouteSimpleSameNamespace
    statistics:
      Failed: 0
      Passed: 16
      Skipped: 4
  extended:
    result: partial
    skippedTests:
    - TLSRouteTerminateSimpleSameNamespace
    statistics:
      Failed: 0
      Passed: 3
      Skipped: 1
    supportedFeatures:
    - GatewayAddressEmpty
    - GatewayPort8080
    - TLSRouteModeTerminate
    unsupportedFeatures:
    - GatewayBackendClientCertificate
    - GatewayFrontendClientCertificateValidation
    - GatewayFrontendClientCertificateValidationInsecureFallback
    - GatewayHTTPListenerIsolation
    - GatewayHTTPSListenerDetectMisdirectedRequests
    - GatewayInfrastructurePropagation
    - GatewayStaticAddresses
    - ListenerSet
    - TLSRouteModeMixed
  name: GATEWAY-TLS
  summary: Core tests partially succeeded with 4 test skips. Extended tests partially
    succeeded with 1 test skips.
succeededProvisionalTests:
- GatewayOptionalAddressValue

@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

conformance test report - apisix-standalone mode

apiVersion: gateway.networking.k8s.io/v1
date: "2026-07-29T10:45:31Z"
gatewayAPIChannel: experimental
gatewayAPIVersion: v1.6.0
implementation:
  contact:
  - https://github.com/apache/apisix-ingress-controller/issues
  organization: APISIX
  project: apisix-ingress-controller
  url: https://github.com/apache/apisix-ingress-controller.git
  version: v2.0.0
kind: ConformanceReport
mode: default
profiles:
- core:
    result: partial
    skippedTests:
    - HTTPRouteHTTPSListener
    - HTTPRouteInvalidBackendRefUnknownKind
    - HTTPRouteInvalidCrossNamespaceBackendRef
    - HTTPRouteInvalidNonExistentBackendRef
    - HTTPRouteListenerHostnameMatching
    - HTTPRouteMultipleGateways
    - HTTPRouteNoBackendRefs
    statistics:
      Failed: 0
      Passed: 30
      Skipped: 7
  extended:
    result: partial
    skippedTests:
    - HTTPRouteRedirectPortAndScheme
    statistics:
      Failed: 0
      Passed: 12
      Skipped: 1
    supportedFeatures:
    - GatewayAddressEmpty
    - GatewayPort8080
    - HTTPRouteBackendProtocolWebSocket
    - HTTPRouteDestinationPortMatching
    - HTTPRouteHostRewrite
    - HTTPRouteMethodMatching
    - HTTPRoutePathRewrite
    - HTTPRoutePortRedirect
    - HTTPRouteQueryParamMatching
    - HTTPRouteRequestMirror
    - HTTPRouteResponseHeaderModification
    - HTTPRouteSchemeRedirect
    unsupportedFeatures:
    - BackendTLSPolicy
    - BackendTLSPolicySANValidation
    - GatewayBackendClientCertificate
    - GatewayFrontendClientCertificateValidation
    - GatewayFrontendClientCertificateValidationInsecureFallback
    - GatewayHTTPListenerIsolation
    - GatewayHTTPSListenerDetectMisdirectedRequests
    - GatewayInfrastructurePropagation
    - GatewayStaticAddresses
    - HTTPRoute303RedirectStatusCode
    - HTTPRoute307RedirectStatusCode
    - HTTPRoute308RedirectStatusCode
    - HTTPRouteBackendProtocolH2C
    - HTTPRouteBackendRequestHeaderModification
    - HTTPRouteBackendTimeout
    - HTTPRouteCORS
    - HTTPRouteNamedRouteRule
    - HTTPRouteParentRefPort
    - HTTPRoutePathRedirect
    - HTTPRouteRequestMultipleMirrors
    - HTTPRouteRequestPercentageMirror
    - HTTPRouteRequestTimeout
    - HTTPRouteRetry
    - HTTPRouteRetryBackendTimeout
    - HTTPRouteRetryConnectionError
    - ListenerSet
  name: GATEWAY-HTTP
  summary: Core tests partially succeeded with 7 test skips. Extended tests partially
    succeeded with 1 test skips.
- core:
    result: partial
    skippedTests:
    - GRPCRouteListenerHostnameMatching
    statistics:
      Failed: 0
      Passed: 14
      Skipped: 1
  extended:
    result: success
    statistics:
      Failed: 0
      Passed: 1
      Skipped: 0
    supportedFeatures:
    - GatewayAddressEmpty
    - GatewayPort8080
    unsupportedFeatures:
    - GatewayBackendClientCertificate
    - GatewayFrontendClientCertificateValidation
    - GatewayFrontendClientCertificateValidationInsecureFallback
    - GatewayHTTPListenerIsolation
    - GatewayHTTPSListenerDetectMisdirectedRequests
    - GatewayInfrastructurePropagation
    - GatewayStaticAddresses
    - ListenerSet
  name: GATEWAY-GRPC
  summary: Core tests partially succeeded with 1 test skips. Extended tests succeeded.
- core:
    result: partial
    skippedTests:
    - TLSRouteHostnameIntersection
    - TLSRouteInvalidBackendRefNonexistent
    - TLSRouteInvalidBackendRefUnknownKind
    - TLSRouteSimpleSameNamespace
    statistics:
      Failed: 0
      Passed: 16
      Skipped: 4
  extended:
    result: partial
    skippedTests:
    - TLSRouteTerminateSimpleSameNamespace
    statistics:
      Failed: 0
      Passed: 3
      Skipped: 1
    supportedFeatures:
    - GatewayAddressEmpty
    - GatewayPort8080
    - TLSRouteModeTerminate
    unsupportedFeatures:
    - GatewayBackendClientCertificate
    - GatewayFrontendClientCertificateValidation
    - GatewayFrontendClientCertificateValidationInsecureFallback
    - GatewayHTTPListenerIsolation
    - GatewayHTTPSListenerDetectMisdirectedRequests
    - GatewayInfrastructurePropagation
    - GatewayStaticAddresses
    - ListenerSet
    - TLSRouteModeMixed
  name: GATEWAY-TLS
  summary: Core tests partially succeeded with 4 test skips. Extended tests partially
    succeeded with 1 test skips.
succeededProvisionalTests:
- GatewayOptionalAddressValue

@shreemaan-abhishek shreemaan-abhishek changed the title fix: match overlapping hosts and mTLS config in SSL conflict detector fix: harden SSL host/certificate consistency (conflict detector + SAN coverage) Jul 19, 2026
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

conformance test report

apiVersion: gateway.networking.k8s.io/v1
date: "2026-07-29T11:03:53Z"
gatewayAPIChannel: experimental
gatewayAPIVersion: v1.6.0
implementation:
  contact:
  - https://github.com/apache/apisix-ingress-controller/issues
  organization: APISIX
  project: apisix-ingress-controller
  url: https://github.com/apache/apisix-ingress-controller.git
  version: v2.0.0
kind: ConformanceReport
mode: default
profiles:
- core:
    failedTests:
    - GatewayModifyListeners
    - HTTPRouteMultipleGateways
    - HTTPRouteNoBackendRefs
    result: failure
    skippedTests:
    - HTTPRouteHTTPSListener
    statistics:
      Failed: 3
      Passed: 33
      Skipped: 1
  extended:
    result: partial
    skippedTests:
    - HTTPRouteRedirectPortAndScheme
    statistics:
      Failed: 0
      Passed: 12
      Skipped: 1
    supportedFeatures:
    - GatewayAddressEmpty
    - GatewayPort8080
    - HTTPRouteBackendProtocolWebSocket
    - HTTPRouteDestinationPortMatching
    - HTTPRouteHostRewrite
    - HTTPRouteMethodMatching
    - HTTPRoutePathRewrite
    - HTTPRoutePortRedirect
    - HTTPRouteQueryParamMatching
    - HTTPRouteRequestMirror
    - HTTPRouteResponseHeaderModification
    - HTTPRouteSchemeRedirect
    unsupportedFeatures:
    - BackendTLSPolicy
    - BackendTLSPolicySANValidation
    - GatewayBackendClientCertificate
    - GatewayFrontendClientCertificateValidation
    - GatewayFrontendClientCertificateValidationInsecureFallback
    - GatewayHTTPListenerIsolation
    - GatewayHTTPSListenerDetectMisdirectedRequests
    - GatewayInfrastructurePropagation
    - GatewayStaticAddresses
    - HTTPRoute303RedirectStatusCode
    - HTTPRoute307RedirectStatusCode
    - HTTPRoute308RedirectStatusCode
    - HTTPRouteBackendProtocolH2C
    - HTTPRouteBackendRequestHeaderModification
    - HTTPRouteBackendTimeout
    - HTTPRouteCORS
    - HTTPRouteNamedRouteRule
    - HTTPRouteParentRefPort
    - HTTPRoutePathRedirect
    - HTTPRouteRequestMultipleMirrors
    - HTTPRouteRequestPercentageMirror
    - HTTPRouteRequestTimeout
    - HTTPRouteRetry
    - HTTPRouteRetryBackendTimeout
    - HTTPRouteRetryConnectionError
    - ListenerSet
  name: GATEWAY-HTTP
  summary: Core tests failed with 3 test failures. Extended tests partially succeeded
    with 1 test skips.
- core:
    failedTests:
    - GatewayModifyListeners
    result: failure
    statistics:
      Failed: 1
      Passed: 14
      Skipped: 0
  extended:
    result: success
    statistics:
      Failed: 0
      Passed: 1
      Skipped: 0
    supportedFeatures:
    - GatewayAddressEmpty
    - GatewayPort8080
    unsupportedFeatures:
    - GatewayBackendClientCertificate
    - GatewayFrontendClientCertificateValidation
    - GatewayFrontendClientCertificateValidationInsecureFallback
    - GatewayHTTPListenerIsolation
    - GatewayHTTPSListenerDetectMisdirectedRequests
    - GatewayInfrastructurePropagation
    - GatewayStaticAddresses
    - ListenerSet
  name: GATEWAY-GRPC
  summary: Core tests failed with 1 test failures. Extended tests succeeded.
- core:
    failedTests:
    - GatewayModifyListeners
    - TLSRouteHostnameIntersection
    - TLSRouteInvalidBackendRefNonexistent
    - TLSRouteInvalidBackendRefUnknownKind
    - TLSRouteSimpleSameNamespace
    result: failure
    statistics:
      Failed: 5
      Passed: 15
      Skipped: 0
  extended:
    failedTests:
    - TLSRouteTerminateSimpleSameNamespace
    result: failure
    statistics:
      Failed: 1
      Passed: 3
      Skipped: 0
    supportedFeatures:
    - GatewayAddressEmpty
    - GatewayPort8080
    - TLSRouteModeTerminate
    unsupportedFeatures:
    - GatewayBackendClientCertificate
    - GatewayFrontendClientCertificateValidation
    - GatewayFrontendClientCertificateValidationInsecureFallback
    - GatewayHTTPListenerIsolation
    - GatewayHTTPSListenerDetectMisdirectedRequests
    - GatewayInfrastructurePropagation
    - GatewayStaticAddresses
    - ListenerSet
    - TLSRouteModeMixed
  name: GATEWAY-TLS
  summary: Core tests failed with 5 test failures. Extended tests failed with 1 test
    failures.
succeededProvisionalTests:
- GatewayOptionalAddressValue

@shreemaan-abhishek shreemaan-abhishek self-assigned this Jul 20, 2026
TranslateApisixTls copied spec.hosts verbatim into the SSL object's SNIs and
only checked that the referenced Secret existed and yielded a keypair. A
certificate whose SANs don't cover a declared host (e.g. an internal cert bound
to a public hostname) was programmed with no signal, so external clients would
receive a certificate invalid for the requested host.

APISIX serves whatever certificate is configured for an SNI regardless of its
SANs (the client validates), so this is advisory rather than fatal: log a
warning listing the uncovered hosts and the certificate SANs, using the
wildcard-aware sslutil.HostCoveredBy (a wildcard SAN covers single-label
subdomains, an exact SAN covers only itself). No warning when the cert declares
no DNS SANs or can't be parsed.
The SSL conflict detector rejected overlapping SSL objects at admission. That
gate only sees object create/update, so it misses conflicts introduced by a
certificate rotating inside its Secret, a GatewayProxy or IngressClass
regrouping, a disabled webhook, or objects that predate the webhook. It also
blocks edits to configurations that already coexist. APISIX admits overlapping
SSL objects and resolves the served certificate by SNI specificity, so a
conflict is now surfaced as an admission warning on create and update.
@shreemaan-abhishek shreemaan-abhishek changed the title fix: harden SSL host/certificate consistency (conflict detector + SAN coverage) fix: detect and warn on inconsistent SSL host/certificate configuration Jul 29, 2026
The ssl_conflict webhook suite asserted overlapping SSL config was rejected at
admission. Now that the detector reports overlap as an admission warning, each
case expects the resource to be admitted and the kubectl output to contain the
conflict warning instead.
Admission warnings are delivered as HTTP Warning headers, and Kubernetes drops
a warning that contains newlines. FormatConflicts built a multi-line message, so
the conflict warning never reached the client. Join the per-conflict details on
one line instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants