Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions conformance/tests/gateway-tls-backend-client-certificate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package tests

import (
"testing"

"k8s.io/apimachinery/pkg/types"

gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
h "sigs.k8s.io/gateway-api/conformance/utils/http"
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
"sigs.k8s.io/gateway-api/conformance/utils/suite"
"sigs.k8s.io/gateway-api/pkg/features"
)

func init() {
ConformanceTests = append(ConformanceTests, GatewayTLSBackendClientCertificate)
}

var GatewayTLSBackendClientCertificate = suite.ConformanceTest{
ShortName: "GatewayTLSBackendClientCertificate",
Description: "A Gateway with a client certificate configured should present the certificate when connecting to a backend using TLS.",
Features: []features.FeatureName{
features.SupportGateway,
features.SupportGatewayTLSBackendClientCertificate,
features.SupportHTTPRoute,
features.SupportBackendTLSPolicy,
},
Manifests: []string{"tests/gateway-tls-backend-client-certificate.yaml"},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
ns := "gateway-conformance-infra"

routeNN := types.NamespacedName{Name: "gateway-tls-backend-client-certificate", Namespace: ns}
gwNN := types.NamespacedName{Name: "gateway-tls-backend-client-certificate", Namespace: ns}
policyNN := types.NamespacedName{Name: "gateway-tls-backend-client-certificate-test", Namespace: ns}

kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns})
gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gatewayv1.HTTPRoute{}, false, routeNN)
kubernetes.HTTPRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN)
kubernetes.BackendTLSPolicyMustHaveAcceptedConditionTrue(t, suite.Client, suite.TimeoutConfig, policyNN, gwNN)

t.Run("HTTP request sent to Service using TLS should succeed and the configured client certificate should be presented.", func(t *testing.T) {
expectedClientCert, _, err := GetTLSSecret(suite.Client, types.NamespacedName{Name: "tls-checks-client-certificate", Namespace: ns})
if err != nil {
t.Fatalf("unexpected error finding TLS client certifcate secret: %v", err)
}

h.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr,
h.ExpectedResponse{
Namespace: ns,
Request: h.Request{
Path: "/",
Host: "abc.example.com",
SNI: "abc.example.com",
ClientCert: string(expectedClientCert),
},
Response: h.Response{StatusCodes: []int{200}},
})
})
},
}
133 changes: 133 additions & 0 deletions conformance/tests/gateway-tls-backend-client-certificate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway-tls-backend-client-certificate
namespace: gateway-conformance-infra
spec:
gatewayClassName: "{GATEWAY_CLASS_NAME}"
listeners:
- name: http
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: Same
tls:
backend:
clientCertificateRef:
group: ""
kind: Secret
name: tls-checks-client-certificate
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: gateway-tls-backend-client-certificate
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: gateway-tls-backend-client-certificate
namespace: gateway-conformance-infra
hostnames:
- abc.example.com
rules:
- backendRefs:
- group: ""
kind: Service
name: tls-backend-with-client-cert-validation
port: 443
matches:
- path:
type: Exact
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: BackendTLSPolicy
metadata:
name: gateway-tls-backend-client-certificate-test
namespace: gateway-conformance-infra
spec:
targetRefs:
- group: ""
kind: Service
name: tls-backend-with-client-cert-validation
validation:
caCertificateRefs:
- group: ""
kind: ConfigMap
# This ConfigMap is generated dynamically by the test suite.
name: "tls-checks-ca-certificate"
hostname: "abc.example.com"
---
apiVersion: v1
kind: Service
metadata:
name: tls-backend-with-client-cert-validation
namespace: gateway-conformance-infra
spec:
selector:
app: tls-backend-with-client-cert-validation
ports:
- name: "https"
protocol: TCP
port: 443
targetPort: 8443
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tls-backend-with-client-cert-validation
namespace: gateway-conformance-infra
labels:
app: tls-backend-with-client-cert-validation
spec:
replicas: 1
selector:
matchLabels:
app: tls-backend-with-client-cert-validation
template:
metadata:
labels:
app: tls-backend-with-client-cert-validation
spec:
containers:
- name: tls-backend-with-client-cert-validation
image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
volumeMounts:
- name: secret-volume
mountPath: /etc/secret-volume
- name: configmap-volume
mountPath: /etc/configmap-volume
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: TLS_SERVER_CERT
value: /etc/secret-volume/crt
- name: TLS_SERVER_PRIVKEY
value: /etc/secret-volume/key
- name: TLS_CLIENT_CACERTS
value: /etc/configmap-volume/ca
resources:
requests:
cpu: 10m
volumes:
- name: secret-volume
secret:
secretName: tls-checks-certificate
items:
- key: tls.crt
path: crt
- key: tls.key
path: key
- name: configmap-volume
configMap:
name: tls-checks-ca-certificate
items:
- key: ca.crt
path: ca
7 changes: 7 additions & 0 deletions conformance/utils/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type Request struct {
Protocol string
Body string
SNI string
ClientCert string
}

// ExpectedRequest defines expected properties of a request that reaches a backend.
Expand Down Expand Up @@ -415,6 +416,12 @@ func CompareRoundTrip(t *testing.T, req *roundtripper.Request, cReq *roundtrippe
if expected.ExpectedRequest.SNI != "" && expected.ExpectedRequest.SNI != cReq.TLS.ServerName {
return fmt.Errorf("expected SNI %q to be equal to %q", cReq.TLS.ServerName, expected.ExpectedRequest.SNI)
}

if expected.ExpectedRequest.ClientCert != "" {
if !slices.Contains(cReq.TLS.PeerCertificates, expected.ExpectedRequest.ClientCert) {
return fmt.Errorf("expected client certiifcate was not captured")
}
}
} else if roundtripper.IsRedirect(cRes.StatusCode) {
if expected.RedirectRequest == nil {
return nil
Expand Down
17 changes: 13 additions & 4 deletions conformance/utils/kubernetes/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func MustCreateSelfSignedCertSecret(t *testing.T, namespace, secretName string,

var serverKey, serverCert bytes.Buffer

require.NoError(t, generateRSACert(hosts, &serverKey, &serverCert, nil, nil), "failed to generate RSA certificate")
require.NoError(t, generateRSACert(hosts, &serverKey, &serverCert, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, nil, nil), "failed to generate RSA certificate")

return formatSecret(serverCert, serverKey, namespace, secretName)
}
Expand All @@ -60,7 +60,16 @@ func MustCreateCASignedCertSecret(t *testing.T, namespace, secretName string, ho

var serverCert, serverKey bytes.Buffer

require.NoError(t, generateRSACert(hosts, &serverKey, &serverCert, ca, caPrivKey), "failed to generate CA signed RSA certificate")
require.NoError(t, generateRSACert(hosts, &serverKey, &serverCert, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, ca, caPrivKey), "failed to generate CA signed RSA certificate")

return formatSecret(serverCert, serverKey, namespace, secretName)
}

// MustCreateCASignedClientCertSecret creates a CA-signed SSL certificate and stores it in a secret
func MustCreateCASignedClientCertSecret(t *testing.T, namespace, secretName string, ca *x509.Certificate, caPrivKey *rsa.PrivateKey) *corev1.Secret {
var serverCert, serverKey bytes.Buffer

require.NoError(t, generateRSACert([]string{}, &serverKey, &serverCert, []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, ca, caPrivKey), "failed to generate CA signed RSA client certificate")

return formatSecret(serverCert, serverKey, namespace, secretName)
}
Expand All @@ -87,7 +96,7 @@ func formatSecret(serverCert bytes.Buffer, serverKey bytes.Buffer, namespace str

// generateRSACert generates a basic self-signed certificate if ca and caPrivKey are nil,
// otherwise it creates CA-signed cert with ca and caPrivkey input. Certs are valid for a year.
func generateRSACert(hosts []string, keyOut, certOut io.Writer, ca *x509.Certificate, caPrivKey *rsa.PrivateKey) error {
func generateRSACert(hosts []string, keyOut, certOut io.Writer, extKeyUsage []x509.ExtKeyUsage, ca *x509.Certificate, caPrivKey *rsa.PrivateKey) error {
priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
if err != nil {
return fmt.Errorf("failed to generate key: %w", err)
Expand All @@ -111,7 +120,7 @@ func generateRSACert(hosts []string, keyOut, certOut io.Writer, ca *x509.Certifi
NotAfter: notAfter,

KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
ExtKeyUsage: extKeyUsage,
BasicConstraintsValid: true,
}

Expand Down
8 changes: 8 additions & 0 deletions conformance/utils/kubernetes/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,14 @@ func BackendTLSPolicyMustHaveCondition(t *testing.T, client client.Client, timeo
require.NoErrorf(t, waitErr, "error waiting for BackendTLSPolicy %v status to have a Condition %v", policyNN, condition)
}

func BackendTLSPolicyMustHaveAcceptedConditionTrue(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, policyNN, gwNN types.NamespacedName) {
BackendTLSPolicyMustHaveCondition(t, client, timeoutConfig, policyNN, gwNN, metav1.Condition{
Type: string(gatewayv1.PolicyConditionAccepted),
Status: metav1.ConditionTrue,
Reason: string(gatewayv1.PolicyReasonAccepted),
})
}

// BackendTLSPolicyMustHaveLatestConditions will fail the test if there are
// conditions that were not updated
func BackendTLSPolicyMustHaveLatestConditions(t *testing.T, r *gatewayv1.BackendTLSPolicy) {
Expand Down
9 changes: 5 additions & 4 deletions conformance/utils/roundtripper/roundtripper.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,11 @@ type CapturedRequest struct {
}

type TLS struct {
Version string `json:"version"`
ServerName string `json:"serverName"`
NegotiatedProtocol string `json:"negotiatedProtocol"`
CipherSuite string `json:"cipherSuite"`
Version string `json:"version"`
ServerName string `json:"serverName"`
NegotiatedProtocol string `json:"negotiatedProtocol"`
CipherSuite string `json:"cipherSuite"`
PeerCertificates []string `json:"peerCertificates"`
}

// RedirectRequest contains a follow up request metadata captured from a redirect
Expand Down
2 changes: 2 additions & 0 deletions conformance/utils/suite/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,8 @@ func (suite *ConformanceTestSuite) Setup(t *testing.T, tests []ConformanceTest)
suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{caConfigMap}, suite.Cleanup)
secret = kubernetes.MustCreateCASignedCertSecret(t, "gateway-conformance-infra", "tls-checks-certificate", []string{"abc.example.com", "spiffe://abc.example.com/test-identity", "other.example.com"}, ca, caPrivKey)
suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
secret = kubernetes.MustCreateCASignedClientCertSecret(t, "gateway-conformance-infra", "tls-checks-client-certificate", ca, caPrivKey)
suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)

// The following CA ceritficate is used for BackendTLSPolicy testing to intentionally force TLS validation to fail.
caConfigMap, _, _ = kubernetes.MustCreateCACertConfigMap(t, "gateway-conformance-infra", "mismatch-ca-certificate")
Expand Down
19 changes: 15 additions & 4 deletions pkg/features/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,17 @@ const (
// of HTTP listeners.
SupportGatewayHTTPListenerIsolation FeatureName = "GatewayHTTPListenerIsolation"

// SupportGatewayInfrastructureAnnotations option indicates support for
// SupportGatewayInfrastructurePropagation option indicates support for
// spec.infrastructure.annotations and spec.infrastructure.labels
SupportGatewayInfrastructurePropagation FeatureName = "GatewayInfrastructurePropagation"

// SupportGatewayAddressEmpty option indicates support for an empty
// spec.addresses.value field
// SupportGatewayAddressEmpty option indicates support for an empty
// spec.addresses.value field
SupportGatewayAddressEmpty FeatureName = "GatewayAddressEmpty"

// SupportGatewayTLSBackendClientCertificate option indicates support for
// specifying client certificates, which will be sent to the backend.
SupportGatewayTLSBackendClientCertificate = "GatewayTLSBackendClientCertificate"
)

var (
Expand All @@ -87,11 +91,17 @@ var (
Name: SupportGatewayInfrastructurePropagation,
Channel: FeatureChannelStandard,
}
// GatewayAddressEmptyFeature contains metadata for the SupportGatewayAddressEmpty feature.
// GatewayEmptyAddressFeature contains metadata for the SupportGatewayAddressEmpty feature.
GatewayEmptyAddressFeature = Feature{
Name: SupportGatewayAddressEmpty,
Channel: FeatureChannelStandard,
}

// GatewayTLSBackendClientCertificate contains metadata for the SupportGatewayTLSBackendClientCertificate feature.
GatewayTLSBackendClientCertificate = Feature{
Name: SupportGatewayTLSBackendClientCertificate,
Channel: FeatureChannelExperimental,
}
)

// GatewayExtendedFeatures are extra generic features that implementations may
Expand All @@ -102,4 +112,5 @@ var GatewayExtendedFeatures = sets.New(
GatewayHTTPListenerIsolationFeature,
GatewayInfrastructurePropagationFeature,
GatewayEmptyAddressFeature,
GatewayTLSBackendClientCertificate,
)