feat: support a CA bundle for the control plane connection - #447
feat: support a CA bundle for the control plane connection#447shreemaan-abhishek wants to merge 3 commits into
Conversation
tlsVerify offered only two states: verify against the system trust store, or do not verify at all. A control plane using a self-signed or private-CA certificate has no way to satisfy the first, so the only escape from the connection error is tlsVerify: false -- which turns the insecure opt-out into copy-paste boilerplate. Add the missing third state: an optional PEM-encoded caBundle on GatewayProxy.spec.provider.controlPlane, carried through the translated config to the ADC server, which verifies the control plane against it in place of the system trust store. Unusable CA material is rejected up front -- by a CEL rule at admission and by a PEM parse in the translator -- rather than surfacing later as an opaque TLS failure.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds an optional PEM CA bundle to GatewayProxy control-plane configuration, validates and translates it into ADC configuration, sends it in ADC requests, and redacts its contents from logs. ChangesControl-plane CA bundle support
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant GatewayProxy
participant TranslateGatewayProxyToConfig
participant HTTPADCExecutor
participant ADCServer
GatewayProxy->>TranslateGatewayProxyToConfig: provide caBundle and tlsVerify
TranslateGatewayProxyToConfig->>TranslateGatewayProxyToConfig: validate PEM certificates
TranslateGatewayProxyToConfig->>HTTPADCExecutor: set Config.CaBundle
HTTPADCExecutor->>ADCServer: send caCert in request options
Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@api/v1alpha1/gatewayproxy_types.go`:
- Line 123: The caBundle validation currently accepts certificate-looking
content without enforcing a strictly valid PEM certificate bundle. Update the
validation logic around the GatewayProxy caBundle handling in
internal/adc/translator/gatewayproxy.go:60-64 to parse every PEM block, reject
trailing garbage and non-certificate blocks, and accept only valid certificates;
then update the caBundle XValidation annotation in
api/v1alpha1/gatewayproxy_types.go:123 and regenerate the corresponding CRD
validation in config/crd/bases/apisix.apache.org_gatewayproxies.yaml:168-170.
🪄 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: a05e8c5e-fca5-4cea-88a4-9b2035151478
📒 Files selected for processing (9)
api/adc/types.goapi/v1alpha1/gatewayproxy_types.goconfig/crd-nocel/apisix.apache.org_v2.yamlconfig/crd/bases/apisix.apache.org_gatewayproxies.yamldocs/en/latest/reference/api-reference.mdinternal/adc/client/executor.gointernal/adc/client/executor_test.gointernal/adc/translator/gatewayproxy.gointernal/adc/translator/gatewayproxy_test.go
| // ControlPlaneProvider defines configuration for control plane provider. | ||
| // +kubebuilder:validation:XValidation:rule="has(self.endpoints) != has(self.service)" | ||
| // +kubebuilder:validation:XValidation:rule="oldSelf == null || (!has(self.mode) && !has(oldSelf.mode)) || self.mode == oldSelf.mode",message="mode is immutable" | ||
| // +kubebuilder:validation:XValidation:rule="!has(self.caBundle) || self.caBundle.contains('-----BEGIN CERTIFICATE-----')",message="caBundle must be a PEM-encoded certificate" |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== api/v1alpha1/gatewayproxy_types.go ==\n'
sed -n '1,220p' api/v1alpha1/gatewayproxy_types.go
printf '\n== config/crd/bases/apisix.apache.org_gatewayproxies.yaml ==\n'
sed -n '150,190p' config/crd/bases/apisix.apache.org_gatewayproxies.yaml
printf '\n== internal/adc/translator/gatewayproxy.go ==\n'
sed -n '1,120p' internal/adc/translator/gatewayproxy.goRepository: api7/api7-ingress-controller
Length of output: 13909
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat > /tmp/pemtst.go <<'GO'
package main
import (
"crypto/x509"
"encoding/pem"
"fmt"
)
func main() {
// One minimal self-signed-ish CERTIFICATE block is not required for parser behavior;
// any valid certificate PEM data would do. Use a real-looking cert block from docs? No,
// just inspect parser behavior with canned inputs and report append result.
tests := map[string]string{
"valid_cert_only": `-----BEGIN CERTIFICATE-----
MIIBszCCAVmgAwIBAgIUH9hWQJ4zJr9fQfFQj3c1e1nXqk0wCgYIKoZIzj0EAwIw
EjEQMA4GA1UEAwwHZHVtbXkwHhcNMjQwMTAxMDAwMDAwWhcNMzQwMTAxMDAwMDAw
WjASMRAwDgYDVQQDDAdkdW1teTAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQw
8H2Xq3dKx8VnNf8nJf6m8j2W2VQ0Wz1k2k1wP+T6R3T+8bGQG2bP1F0Q4p9j5h7W
qV6p3o+QvG9j8P2jUzBRMB0GA1UdDgQWBBQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
MB8GA1UdIwQYMBaAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwDwYDVR0TAQH/BAUw
AwEB/zAKBggqhkjOPQQDAgNJADBGAiEA0Q==
-----END CERTIFICATE-----
`,
"valid_cert_plus_garbage": `-----BEGIN CERTIFICATE-----
MIIBszCCAVmgAwIBAgIUH9hWQJ4zJr9fQfFQj3c1e1nXqk0wCgYIKoZIzj0EAwIw
EjEQMA4GA1UEAwwHZHVtbXkwHhcNMjQwMTAxMDAwMDAwWhcNMzQwMTAxMDAwMDAw
WjASMRAwDgYDVQQDDAdkdW1teTAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQw
8H2Xq3dKx8VnNf8nJf6m8j2W2VQ0Wz1k2k1wP+T6R3T+8bGQG2bP1F0Q4p9j5h7W
qV6p3o+QvG9j8P2jUzBRMB0GA1UdDgQWBBQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
MB8GA1UdIwQYMBaAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwDwYDVR0TAQH/BAUw
AwEB/zAKBggqhkjOPQQDAgNJADBGAiEA0Q==
-----END CERTIFICATE-----
garbage`,
"garbage_then_valid_cert": `garbage
-----BEGIN CERTIFICATE-----
MIIBszCCAVmgAwIBAgIUH9hWQJ4zJr9fQfFQj3c1e1nXqk0wCgYIKoZIzj0EAwIw
EjEQMA4GA1UEAwwHZHVtbXkwHhcNMjQwMTAxMDAwMDAwWhcNMzQwMTAxMDAwMDAw
WjASMRAwDgYDVQQDDAdkdW1teTAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQw
8H2Xq3dKx8VnNf8nJf6m8j2W2VQ0Wz1k2k1wP+T6R3T+8bGQG2bP1F0Q4p9j5h7W
qV6p3o+QvG9j8P2jUzBRMB0GA1UdDgQWBBQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
MB8GA1UdIwQYMBaAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwDwYDVR0TAQH/BAUw
AwEB/zAKBggqhkjOPQQDAgNJADBGAiEA0Q==
-----END CERTIFICATE-----
`,
"header_only": `-----BEGIN CERTIFICATE-----`,
"non_cert_pem": `-----BEGIN FOO-----
abcd
-----END FOO-----`,
}
for name, s := range tests {
pool := x509.NewCertPool()
ok := pool.AppendCertsFromPEM([]byte(s))
fmt.Printf("%s: ok=%v subjects=%d firstBlock=%q\n", name, ok, len(pool.Subjects()), firstBlockType(s))
}
}
func firstBlockType(s string) string {
b, _ := pem.Decode([]byte(s))
if b == nil {
return "nil"
}
return b.Type
}
GO
go run /tmp/pemtst.goRepository: api7/api7-ingress-controller
Length of output: 469
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat /tmp/coderabbit-shell-logs/shell-output-*.log | tail -n +1 >/dev/nullRepository: api7/api7-ingress-controller
Length of output: 246
🏁 Script executed:
#!/bin/bash
set -euo pipefail
bundle=""
for f in /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem /etc/pki/tls/certs/ca-bundle.crt; do
if [ -f "$f" ]; then bundle="$f"; break; fi
done
if [ -z "$bundle" ]; then
echo "no system CA bundle found" >&2
exit 1
fi
cert=$(awk '
BEGIN{p=0}
/BEGIN CERTIFICATE/ {p=1}
p {print}
/END CERTIFICATE/ {exit}
' "$bundle")
cat > /tmp/pemtst.go <<'GO'
package main
import (
"crypto/x509"
"fmt"
)
func check(name, s string) {
pool := x509.NewCertPool()
ok := pool.AppendCertsFromPEM([]byte(s))
fmt.Printf("%s: ok=%v subjects=%d\n", name, ok, len(pool.Subjects()))
}
func main() {
valid := CERTDATA
check("valid_only", valid)
check("valid_plus_garbage", valid+"\nGARBAGE\n")
check("valid_plus_noncert_pem", valid+"\n-----BEGIN FOO-----\na\n-----END FOO-----\n")
check("garbage_plus_valid", "GARBAGE\n"+valid)
check("two_valid", valid+"\n"+valid)
}
GO
python3 - <<'PY'
from pathlib import Path
p = Path('/tmp/pemtst.go')
s = p.read_text()
cert = Path('/tmp/cert.pem').write_text("""__CERT__""")
PYRepository: api7/api7-ingress-controller
Length of output: 166
🏁 Script executed:
#!/bin/bash
set -euo pipefail
bundle=""
for f in /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem /etc/pki/tls/certs/ca-bundle.crt; do
if [ -f "$f" ]; then bundle="$f"; break; fi
done
if [ -z "$bundle" ]; then
echo "no system CA bundle found" >&2
exit 1
fi
cert_file=/tmp/one_cert.pem
awk '
BEGIN{p=0}
/BEGIN CERTIFICATE/ {p=1}
p {print}
/END CERTIFICATE/ {exit}
' "$bundle" > "$cert_file"
python3 - <<'PY'
from pathlib import Path
cert = Path('/tmp/one_cert.pem').read_text()
go = f'''package main
import (
"crypto/x509"
"fmt"
)
func check(name, s string) {{
pool := x509.NewCertPool()
ok := pool.AppendCertsFromPEM([]byte(s))
fmt.Printf("%s: ok=%v subjects=%d\\n", name, ok, len(pool.Subjects()))
}}
func main() {{
valid := {cert!r}
check("valid_only", valid)
check("valid_plus_garbage", valid+"\\nGARBAGE\\n")
check("valid_plus_noncert_pem", valid+"\\n-----BEGIN FOO-----\\na\\n-----END FOO-----\\n")
check("garbage_plus_valid", "GARBAGE\\n"+valid)
check("two_valid", valid+"\\n"+valid)
}}
'''
Path('/tmp/pemtst.go').write_text(go)
PY
go run /tmp/pemtst.goRepository: api7/api7-ingress-controller
Length of output: 253
🏁 Script executed:
#!/bin/bash
set -euo pipefail
bundle=""
for f in /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem /etc/pki/tls/certs/ca-bundle.crt; do
if [ -f "$f" ]; then bundle="$f"; break; fi
done
if [ -z "$bundle" ]; then
echo "no system CA bundle found" >&2
exit 1
fi
awk '
BEGIN{p=0}
/BEGIN CERTIFICATE/ {p=1}
p {print}
/END CERTIFICATE/ {exit}
' "$bundle" > /tmp/one_cert.pem
python3 - <<'PY'
from pathlib import Path
cert = Path('/tmp/one_cert.pem').read_text()
go = f'''package main
import (
"crypto/x509"
"fmt"
)
func check(name, s string) {{
pool := x509.NewCertPool()
ok := pool.AppendCertsFromPEM([]byte(s))
fmt.Printf("%s: ok=%v subjects=%d\\n", name, ok, len(pool.Subjects()))
}}
func main() {{
valid := {cert!r}
check("valid_only", valid)
check("valid_plus_garbage", valid+"\\nGARBAGE\\n")
check("valid_plus_noncert_pem", valid+"\\n-----BEGIN FOO-----\\na\\n-----END FOO-----\\n")
check("garbage_plus_valid", "GARBAGE\\n"+valid)
check("two_valid", valid+"\\n"+valid)
}}
'''
Path('/tmp/pemtst.go').write_text(go)
PY
go run /tmp/pemtst.goRepository: api7/api7-ingress-controller
Length of output: 253
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== caBundle references ==\n'
rg -n "caBundle|CaBundle" api internal config test . -g '!**/vendor/**'
printf '\n== relevant tests near gatewayproxy ==\n'
fd -a 'gatewayproxy' api internal test config | sed -n '1,120p'Repository: api7/api7-ingress-controller
Length of output: 8992
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find tests or docs around gateway proxy control-plane validation.
rg -n "caBundle|AppendCertsFromPEM|ControlPlaneProvider|GatewayProxyProvider" \
api internal test config docs . \
-g '!**/vendor/**' -g '!**/dist/**' -g '!**/build/**'Repository: api7/api7-ingress-controller
Length of output: 12292
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Probe AppendCertsFromPEM with a known real certificate and trailing garbage.
bundle=""
for f in /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem /etc/pki/tls/certs/ca-bundle.crt; do
if [ -f "$f" ]; then bundle="$f"; break; fi
done
if [ -z "$bundle" ]; then
echo "no system CA bundle found" >&2
exit 1
fi
cert_file=/tmp/one_cert.pem
awk '
BEGIN{p=0}
/BEGIN CERTIFICATE/ {p=1}
p {print}
/END CERTIFICATE/ {exit}
' "$bundle" > "$cert_file"
python3 - <<'PY'
from pathlib import Path
cert = Path('/tmp/one_cert.pem').read_text()
go = f'''package main
import (
"crypto/x509"
"fmt"
)
func check(name, s string) {{
pool := x509.NewCertPool()
ok := pool.AppendCertsFromPEM([]byte(s))
fmt.Printf("%s: ok=%v subjects=%d\\n", name, ok, len(pool.Subjects()))
}}
func main() {{
valid := {cert!r}
check("valid_only", valid)
check("valid_plus_garbage", valid+"\\nGARBAGE\\n")
check("garbage_plus_valid", "GARBAGE\\n"+valid)
check("valid_plus_noncert_pem", valid+"\\n-----BEGIN FOO-----\\na\\n-----END FOO-----\\n")
}}
'''
Path('/tmp/pemtst.go').write_text(go)
PY
go run /tmp/pemtst.goRepository: api7/api7-ingress-controller
Length of output: 253
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== internal/adc/translator/gatewayproxy_test.go ==\n'
sed -n '1,180p' internal/adc/translator/gatewayproxy_test.go
printf '\n== internal/webhook/v1/gatewayproxy_webhook_test.go ==\n'
sed -n '1,180p' internal/webhook/v1/gatewayproxy_webhook_test.goRepository: api7/api7-ingress-controller
Length of output: 9191
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Better probe using a real PEM block and JSON-escaped Go string.
bundle=""
for f in /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem /etc/pki/tls/certs/ca-bundle.crt; do
if [ -f "$f" ]; then bundle="$f"; break; fi
done
if [ -z "$bundle" ]; then
echo "no system CA bundle found" >&2
exit 1
fi
awk '
BEGIN{p=0}
/BEGIN CERTIFICATE/ {p=1}
p {print}
/END CERTIFICATE/ {exit}
' "$bundle" > /tmp/one_cert.pem
python3 - <<'PY'
from pathlib import Path
import json
cert = Path('/tmp/one_cert.pem').read_text()
go = f'''package main
import (
"crypto/x509"
"fmt"
)
func check(name, s string) {{
pool := x509.NewCertPool()
ok := pool.AppendCertsFromPEM([]byte(s))
fmt.Printf("%s: ok=%v subjects=%d\\n", name, ok, len(pool.Subjects()))
}}
func main() {{
valid := {json.dumps(cert)}
check("valid_only", valid)
check("valid_plus_garbage", valid+"\\nGARBAGE\\n")
check("garbage_plus_valid", "GARBAGE\\n"+valid)
check("valid_plus_noncert_pem", valid+"\\n-----BEGIN FOO-----\\na\\n-----END FOO-----\\n")
}}
'''
Path('/tmp/pemtst.go').write_text(go)
PY
go run /tmp/pemtst.goRepository: api7/api7-ingress-controller
Length of output: 318
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Focus the docs/tests search on gatewayproxy validation behavior.
rg -n "invalid caBundle|caBundle.*PEM|AppendCertsFromPEM|tlsVerify is disabled|GatewayProxyToConfigCaBundle" \
internal/adc/translator/gatewayproxy_test.go \
internal/webhook/v1/gatewayproxy_webhook_test.go \
internal/webhook/v1/adc_validation_test.go \
docs/en/latest/reference/api-reference.mdRepository: api7/api7-ingress-controller
Length of output: 710
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== internal/webhook/v1/gatewayproxy_webhook.go ==\n'
sed -n '1,220p' internal/webhook/v1/gatewayproxy_webhook.go
printf '\n== internal/webhook/v1/adc_validation_test.go ==\n'
sed -n '1,180p' internal/webhook/v1/adc_validation_test.go
printf '\n== internal/webhook/v1/gatewayproxy_webhook_test.go ==\n'
sed -n '1,180p' internal/webhook/v1/gatewayproxy_webhook_test.goRepository: api7/api7-ingress-controller
Length of output: 16366
🌐 Web query:
Go crypto/x509 AppendCertsFromPEM trailing garbage behavior
💡 Result:
In the Go programming language, the crypto/x509.CertPool.AppendCertsFromPEM method handles PEM-encoded data by attempting to parse a series of certificates [1][2][3]. Its behavior regarding "garbage" (data that is not a valid PEM block or a valid certificate) is permissive: it silently skips any data that does not conform to the expected format [2][3]. Specifically, the function operates in a loop that uses pem.Decode to extract PEM blocks [2][4][3]: 1. If pem.Decode returns nil (indicating no more PEM blocks are found), the function breaks the loop [2][4][3]. Any remaining data at that point is treated as "trailing garbage" and is ignored without returning an error [2][3]. 2. If a PEM block is found but it does not have the type "CERTIFICATE" or contains headers, the function ignores that block and continues to the next one [2][3]. 3. If the block is a valid PEM "CERTIFICATE" but cannot be parsed by x509.ParseCertificate, the error is ignored, and the function continues processing the remaining data [2][3]. Because of this silent skipping, AppendCertsFromPEM will return true as long as at least one valid certificate is found and added to the pool [2][3]. It does not report whether invalid data, non-certificate blocks, or malformed certificate blocks were present [2][3]. Developers requiring strict validation of a PEM bundle (e.g., to ensure no invalid or unexpected data exists) should implement manual parsing using encoding/pem and x509.ParseCertificate to inspect every block individually [5].
Citations:
- 1: https://pkg.go.dev/crypto/x509
- 2: https://go.googlesource.com/go/+/go1.13.3/src/crypto/x509/cert_pool.go
- 3: https://github.com/golang/go/blob/master/src/crypto/x509/cert_pool.go
- 4: https://github.com/gcc-mirror/gcc/blob/833ed6d8012a/libgo/go/crypto/x509/cert_pool.go
- 5: appleboy/go-httpclient@2397c52
Tighten caBundle validation. The admission rule only checks for a CERTIFICATE header, and AppendCertsFromPEM accepts bundles with at least one valid cert even when trailing garbage or non-certificate PEM blocks remain, so malformed bundle data can still be stored and forwarded unchanged. If strict PEM is intended, validate every block and regenerate the CRD.
📍 Affects 3 files
api/v1alpha1/gatewayproxy_types.go#L123-L123(this comment)config/crd/bases/apisix.apache.org_gatewayproxies.yaml#L168-L170internal/adc/translator/gatewayproxy.go#L60-L64
🤖 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 `@api/v1alpha1/gatewayproxy_types.go` at line 123, The caBundle validation
currently accepts certificate-looking content without enforcing a strictly valid
PEM certificate bundle. Update the validation logic around the GatewayProxy
caBundle handling in internal/adc/translator/gatewayproxy.go:60-64 to parse
every PEM block, reject trailing garbage and non-certificate blocks, and accept
only valid certificates; then update the caBundle XValidation annotation in
api/v1alpha1/gatewayproxy_types.go:123 and regenerate the corresponding CRD
validation in config/crd/bases/apisix.apache.org_gatewayproxies.yaml:168-170.
conformance test report - apisix-standalone modeapiVersion: gateway.networking.k8s.io/v1
date: "2026-07-29T04:21:05Z"
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 modeapiVersion: gateway.networking.k8s.io/v1
date: "2026-07-29T04:20:45Z"
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-29T04:39:41Z"
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
- 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.
- 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.
succeededProvisionalTests:
- GatewayOptionalAddressValue |
x509.CertPool skips PEM blocks it cannot decode, so a bundle whose second certificate is broken passed validation here and failed later at the ADC server, which parses the whole bundle. Reject it up front instead.
|
Pushed a follow-up from the review of the ADC-side PR (api7/adc#537): the CA bundle is now validated by parsing every certificate in it.
Test cases added for a header with no certificate, an unparseable body, a private key in place of a certificate, and one good plus one broken certificate; a multi-certificate bundle is still accepted. |
Both call sites pass http.MethodPut, and the new test made unparam report it. Set the method inside instead of threading it through.
Closes #446
What this PR does
GatewayProxy.spec.provider.controlPlane.tlsVerifyoffers only two states today:tlsVerify: false— no verification.tlsVerify: true— verify against the system trust store only, which works only if the control plane's certificate chains to a publicly trusted CA.There is no way to supply a custom CA, so for the common case of a self-signed or private-CA control plane a user who wants verification on has no path to make it succeed. The only escape from the resulting connection error is
tlsVerify: false— which risks turning the insecure opt-out into copy-paste boilerplate, and undercuts the secure default being introduced in #438.This adds the missing third state —
tlsVerify: true+ a CA bundle:Design notes
Inline PEM, not a Secret/ConfigMap ref (one of the open questions in #446). A CA certificate is public material, so a Secret buys no confidentiality here, and an inline field is what Kubernetes itself uses for the same job (
WebhookClientConfig.CABundle). It also keeps the change to the data path: no new watch, index, or RBAC rule, and rotating the bundle is an edit of the GatewayProxy the controller already reconciles on. AcaBundleRefcan be layered on later without breaking this field.Invalid CA material fails fast, in two places. A CEL rule rejects a non-PEM
caBundleat admission, and the translator parses it withx509.CertPool.AppendCertsFromPEMand returns an error before any config is pushed — so a typo surfaces as a clear message instead of an opaque TLS failure at connect time.Interaction with
tlsVerify. The bundle replaces the system trust store when verification is on, and is ignored when it is off — the controller logs that case rather than silently doing nothing. It is still sent, so flippingtlsVerifyback on needs no other change.Wire compatibility. The bundle reaches the ADC server as
caCertin the task options,omitemptyso that a GatewayProxy without a CA bundle produces byte-for-byte the request an older ADC server already accepts. Logging carries onlyhasCaCert/hasCaBundlebooleans, never the material itself.Dependency
The ADC-side blocker flagged in #446 is real: the ADC server picks between two static agents (
rejectUnauthorizedtrue/false) and has no way to trust a custom CA. The companion change is api7/adc#537. Until an ADC build carrying it ships, the field is accepted, validated, and transmitted, but not yet acted on by the ADC server.Sync
Paired open source PR, same change: apache/apisix-ingress-controller#2826. The Go hunks are identical between the two repos; this side additionally updates
config/crd-nocel, which has no upstream counterpart.Changes
api/v1alpha1/gatewayproxy_types.go:caBundleonControlPlaneProvider, plus the CEL validation rule.api/adc/types.go:Config.CaBundle;MarshalJSONreportshasCaBundlerather than the PEM.internal/adc/translator/gatewayproxy.go: validate the PEM, warn whentlsVerifyis off, set it on the config.internal/adc/client/executor.go: carry it to the ADC server ascaCert.crd-nocelbundle (no CEL rule there, by design), API reference updated by hand to match.Tests
internal/adc/translator/gatewayproxy_test.go(new): the bundle reachesConfig, stays empty when unset, is rejected when not PEM, and survivestlsVerify: false.internal/adc/client/executor_test.go(new):caCertis absent from the request body without a bundle and present with one, withtlsSkipVerifystill false.Both CRD variants were also exercised against a real API server via envtest:
config/crd/basesadmits a PEM bundle and rejectsnot-a-certificatewithcaBundle must be a PEM-encoded certificate, and theconfig/crd-nocelbundle installs cleanly and round-tripscaBundle. Those checks are not committed, since this package has no envtest specs today and adding the first one would makego test ./internal/controllerrequire the kubebuilder assets.Summary by CodeRabbit
New Features
caBundle) to verify the control-plane TLS certificate.Bug Fixes
Documentation / Tests
caBundlefield.