fix: detect and warn on inconsistent SSL host/certificate configuration - #437
fix: detect and warn on inconsistent SSL host/certificate configuration#437shreemaan-abhishek wants to merge 5 commits into
Conversation
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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughTLS 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. ChangesSNI Conflict Detection
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
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
Possibly related issues
Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
mappingForHostWithCacheshould not stop at the first overlap
BuildGatewayMappingsandBuildIngressMappingscan emit multiple mappings for the same host with different certificate/client-config hashes. Returning the first match letsfindExternalConflictsmiss 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 | 🔵 TrivialWildcard 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:listAllTLSResourcesperforms 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
📒 Files selected for processing (4)
internal/ssl/util.gointernal/ssl/util_test.gointernal/webhook/v1/ssl/conflict_detector.gointernal/webhook/v1/ssl/conflict_detector_test.go
| // 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 | ||
| } |
There was a problem hiding this comment.
🎯 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
🧩 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
conformance test report - apisix modeapiVersion: 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 |
conformance test report - apisix-standalone modeapiVersion: 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 |
conformance test reportapiVersion: 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 |
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.
01591af to
338395a
Compare
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.
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.
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.comandapp.example.com(different index keys) were never compared; APISIX resolves exact SNI ahead of a covering wildcard, so they collide. Addsslutil.HostsOverlap/ParentWildcard; exact host also queries its covering wildcard, wildcard host enumerates + filters by overlap; compare withHostsOverlap.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/IngressClassregrouping, 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) —
TranslateApisixTlsgave no signal when cert SANs don't cover a declared host; now logs a warning viasslutil.HostCoveredBy(APISIX serves the cert regardless).Notes
Tests
HostsOverlap/ParentWildcard/HostCoveredBySummary by CodeRabbit