diff --git a/conformance/base/manifests.yaml b/conformance/base/manifests.yaml index e415e16e21..22e34634dd 100644 --- a/conformance/base/manifests.yaml +++ b/conformance/base/manifests.yaml @@ -730,3 +730,66 @@ data: foo.bar.com:53 { whoami } +--- +apiVersion: v1 +kind: Service +metadata: + name: backendtlspolicy-test + namespace: gateway-conformance-infra +spec: + selector: + app: backendtlspolicy-test + ports: + - protocol: TCP + port: 443 + targetPort: 8443 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backendtlspolicy-test + namespace: gateway-conformance-infra + labels: + app: backendtlspolicy-test +spec: + replicas: 1 + selector: + matchLabels: + app: backendtlspolicy-test + template: + metadata: + labels: + app: backendtlspolicy-test + spec: + containers: + - name: backendtlspolicy-test + image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd + volumeMounts: + - name: secret-volume + mountPath: /etc/secret-volume + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CA_CERT + value: /etc/secret-volume/crt + - name: CA_CERT_KEY + value: /etc/secret-volume/key + resources: + requests: + cpu: 10m + volumes: + - name: secret-volume + secret: + secretName: backend-tls-checks-certificate + items: + - key: tls.crt + path: crt + - key: tls.key + path: key +--- \ No newline at end of file diff --git a/conformance/echo-basic/echo-basic.go b/conformance/echo-basic/echo-basic.go index 98f8de5a7a..b8c34e2aa0 100644 --- a/conformance/echo-basic/echo-basic.go +++ b/conformance/echo-basic/echo-basic.go @@ -23,6 +23,7 @@ import ( "encoding/pem" "fmt" "io" + "net" "net/http" "os" "regexp" @@ -30,6 +31,8 @@ import ( "strings" "time" + "github.com/paultag/sniff/parser" + "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "golang.org/x/net/websocket" @@ -48,15 +51,17 @@ type RequestAssertions struct { Context `json:",inline"` TLS *TLSAssertions `json:"tls,omitempty"` + SNI string `json:"sni"` } // TLSAssertions contains information about the TLS connection. type TLSAssertions struct { - Version string `json:"version"` - PeerCertificates []string `json:"peerCertificates,omitempty"` - ServerName string `json:"serverName"` - NegotiatedProtocol string `json:"negotiatedProtocol,omitempty"` - CipherSuite string `json:"cipherSuite"` + Version string `json:"version"` + PeerCertificates []string `json:"peerCertificates,omitempty"` + // ServerName is the SNI. + ServerName string `json:"serverName"` + NegotiatedProtocol string `json:"negotiatedProtocol,omitempty"` + CipherSuite string `json:"cipherSuite"` } type preserveSlashes struct { @@ -109,6 +114,7 @@ func main() { httpMux.HandleFunc("/health", healthHandler) httpMux.HandleFunc("/status/", statusHandler) httpMux.HandleFunc("/", echoHandler) + httpMux.HandleFunc("/backendTLS", echoHandler) httpMux.Handle("/ws", websocket.Handler(wsHandler)) httpHandler := &preserveSlashes{httpMux} @@ -124,11 +130,13 @@ func main() { go runH2CServer(h2cPort, errchan) - // Enable HTTPS if certificate and private key are given. - if os.Getenv("TLS_SERVER_CERT") != "" && os.Getenv("TLS_SERVER_PRIVKEY") != "" { + // Enable HTTPS if server certificate and private key are given. (TLS_SERVER_CERT, TLS_SERVER_PRIVKEY) + // Enable secure backend if CA certificate and key are given. (CA_CERT, CA_CERT_KEY) + if os.Getenv("TLS_SERVER_CERT") != "" && os.Getenv("TLS_SERVER_PRIVKEY") != "" || + os.Getenv("CA_CERT") != "" && os.Getenv("CA_CERT_KEY") != "" { go func() { fmt.Printf("Starting server, listening on port %s (https)\n", httpsPort) - err := listenAndServeTLS(fmt.Sprintf(":%s", httpsPort), os.Getenv("TLS_SERVER_CERT"), os.Getenv("TLS_SERVER_PRIVKEY"), os.Getenv("TLS_CLIENT_CACERTS"), httpHandler) + err := listenAndServeTLS(fmt.Sprintf(":%s", httpsPort), os.Getenv("TLS_SERVER_CERT"), os.Getenv("TLS_SERVER_PRIVKEY"), os.Getenv("CA_CERT"), httpHandler) if err != nil { errchan <- err } @@ -201,15 +209,27 @@ func runH2CServer(h2cPort string, errchan chan<- error) { } func echoHandler(w http.ResponseWriter, r *http.Request) { + var sni string + fmt.Printf("Echoing back request made to %s to client (%s)\n", r.RequestURI, r.RemoteAddr) // If the request has form ?delay=[:duration] wait for duration // For example, ?delay=10s will cause the response to wait 10s before responding - if err := delayResponse(r); err != nil { + err := delayResponse(r) + if err != nil { processError(w, err, http.StatusInternalServerError) return } + // If the request was made to URI backendTLS, then get the server name indication and + // add it to the RequestAssertions. It will be echoed back later. + if strings.Contains(r.RequestURI, "backendTLS") { + sni, err = sniffForSNI(r.RemoteAddr) + if err != nil { + // Todo: research if for some test cases there won't be one + } + } + requestAssertions := RequestAssertions{ r.RequestURI, r.Host, @@ -220,6 +240,7 @@ func echoHandler(w http.ResponseWriter, r *http.Request) { context, tlsStateToAssertions(r.TLS), + sni, } js, err := json.MarshalIndent(requestAssertions, "", " ") @@ -296,6 +317,40 @@ func listenAndServeTLS(addr string, serverCert string, serverPrivKey string, cli return srv.ListenAndServeTLS(serverCert, serverPrivKey) } +// sniffForSNI uses the request address to listen for the incoming TLS connection, +// and tries to find the server name indication from that connection. +func sniffForSNI(addr string) (string, error) { + var sni string + + // Listen to get the SNI, and store in config. + listener, err := net.Listen("tcp", addr) + if err != nil { + return "", err + } + defer listener.Close() + + for { + conn, err := listener.Accept() + if err != nil { + return "", err + } + data := make([]byte, 4096) + _, err = conn.Read(data) + if err != nil { + return "", fmt.Errorf("could not read socket: %v", err) + } + // Take an incoming TLS Client Hello and return the SNI name. + sni, err = parser.GetHostname(data[:]) + if err != nil { + return "", fmt.Errorf("error getting SNI: %v", err) + } + if sni == "" { + return "", fmt.Errorf("no server name indication found") + } + return sni, nil + } +} + func tlsStateToAssertions(connectionState *tls.ConnectionState) *TLSAssertions { if connectionState != nil { var state TLSAssertions diff --git a/conformance/tests/backendtlspolicy-normative.go b/conformance/tests/backendtlspolicy-normative.go new file mode 100644 index 0000000000..cf577c1a06 --- /dev/null +++ b/conformance/tests/backendtlspolicy-normative.go @@ -0,0 +1,64 @@ +/* +Copyright 2024 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" + "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, BackendTLSPolicyNormative) +} + +var BackendTLSPolicyNormative = suite.ConformanceTest{ + ShortName: "BackendTLSPolicyNormative", + Description: "A single service that is targeted by a BackendTLSPolicy must successfully complete TLS termination", + Features: []features.SupportedFeature{ + features.SupportGateway, + features.SupportBackendTLSPolicy, + }, + Manifests: []string{"tests/backendtlspolicy-normative.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "gateway-conformance-infra-test", Namespace: ns} + gwNN := types.NamespacedName{Name: "gateway-backendtlspolicy", Namespace: ns} + + kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns}) + + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + t.Run("Simple request targeting BackendTLSPolicy should reach infra-backend", func(t *testing.T) { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, + http.ExpectedResponse{ + Request: http.Request{ + Path: "/backendTLS", + }, + Response: http.Response{StatusCode: 200}, + Backend: "infra-backend-v1", + Namespace: "gateway-conformance-infra", + SNI: "abc.example.com", + // TODO - in addition to the SNI, we also need to check if the cert that was seen is the correct one. + }) + }) + }, +} diff --git a/conformance/tests/backendtlspolicy-normative.yaml b/conformance/tests/backendtlspolicy-normative.yaml new file mode 100644 index 0000000000..a622bc0eba --- /dev/null +++ b/conformance/tests/backendtlspolicy-normative.yaml @@ -0,0 +1,34 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: gateway-backendtlspolicy + namespace: gateway-conformance-infra +spec: + gatewayClassName: "{GATEWAY_CLASS_NAME}" + listeners: + - name: https + port: 443 + protocol: HTTPS + hostname: "*.example.com" + allowedRoutes: + namespaces: + from: Same + kinds: + - kind: HTTPRoute +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: BackendTLSPolicy +metadata: + name: normative-test-backendtlspolicy + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: "" + kind: Service + name: "backendtlspolicy-test" + validation: + caCertificateRefs: + group: "" + kind: Secret + name: "backend-tls-checks-certificate" + hostname: "abc.example.com" \ No newline at end of file diff --git a/conformance/utils/http/http.go b/conformance/utils/http/http.go index 3e5fd211e0..2a54c30f55 100644 --- a/conformance/utils/http/http.go +++ b/conformance/utils/http/http.go @@ -58,6 +58,9 @@ type ExpectedResponse struct { // User Given TestCase name TestCaseName string + + // SNI is the server name indication seen by the backend. + SNI string } // Request can be used as both the request to make and a means to verify diff --git a/conformance/utils/kubernetes/certificate.go b/conformance/utils/kubernetes/certificate.go index f7b3b5b924..069c307931 100644 --- a/conformance/utils/kubernetes/certificate.go +++ b/conformance/utils/kubernetes/certificate.go @@ -117,3 +117,79 @@ func generateRSACert(hosts []string, keyOut, certOut io.Writer) error { return nil } + +// MustCreateCASignedCertSecret will create a secret using a CA Certificate, and public and private key for that certificate. +func MustCreateCASignedCertSecret(t *testing.T, namespace, secretName string, hosts []string) *corev1.Secret { + require.NotEmpty(t, hosts, "require a non-empty hosts for Subject Alternate Name values") + + var serverKey, serverCert bytes.Buffer + + require.NoError(t, generateCACert(hosts, &serverKey, &serverCert), "failed to generate CA certificate") + + data := map[string][]byte{ + corev1.TLSCertKey: serverCert.Bytes(), + corev1.TLSPrivateKeyKey: serverKey.Bytes(), + } + + newSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: secretName, + }, + Type: corev1.SecretTypeTLS, + Data: data, + } + + return newSecret +} + +// generateCACert generates a CA Certificate signed certificate valid for a year. +func generateCACert(hosts []string, keyOut, certOut io.Writer) error { + // Create the CA certificate. + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2024), + Subject: pkix.Name{ + Organization: []string{"Kubernetes Gateway API"}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"Boston"}, + StreetAddress: []string{"Melnea Cass Blvd"}, + PostalCode: []string{"02120"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + IsCA: true, // Indicates this is a CA Certificate. + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + ca.IPAddresses = append(ca.IPAddresses, ip) + } else { + ca.DNSNames = append(ca.DNSNames, h) + } + } + + // Generate the private key. + caPrivKey, err := rsa.GenerateKey(rand.Reader, rsaBits) + if err != nil { + return err + } + + // Generate the certificate using the CA certificate. + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + return err + } + + if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: caBytes}); err != nil { + return fmt.Errorf("failed creating cert: %w", err) + } + + if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey)}); err != nil { + return fmt.Errorf("failed creating key: %w", err) + } + return nil +} diff --git a/conformance/utils/suite/suite.go b/conformance/utils/suite/suite.go index bd1e1fe08e..34ae3d161f 100644 --- a/conformance/utils/suite/suite.go +++ b/conformance/utils/suite/suite.go @@ -353,6 +353,8 @@ func (suite *ConformanceTestSuite) Setup(t *testing.T, tests []ConformanceTest) suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup) secret = kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-app-backend", "tls-passthrough-checks-certificate", []string{"abc.example.com"}) suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup) + caSecret := kubernetes.MustCreateCASignedCertSecret(t, "gateway-conformance-infra", "backend-tls-checks-certificate", []string{"abc.example.com"}) + suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{caSecret}, suite.Cleanup) tlog.Logf(t, "Test Setup: Ensuring Gateways and Pods from base manifests are ready") namespaces := []string{ diff --git a/go.mod b/go.mod index 478443cf9a..a0997e649b 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.0 require ( github.com/ahmetb/gen-crd-api-reference-docs v0.3.0 github.com/miekg/dns v1.1.58 + github.com/paultag/sniff v0.0.0-20200207005214-cf7e4d167732 github.com/stretchr/testify v1.9.0 golang.org/x/net v0.24.0 golang.org/x/sync v0.7.0 diff --git a/go.sum b/go.sum index 64e0859808..2ee2f9d982 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8 github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE= github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= +github.com/paultag/sniff v0.0.0-20200207005214-cf7e4d167732 h1:nkseUkzjazCNyGhkRwnJ1OiHSwMXazsJQx+Ci+oVLEM= +github.com/paultag/sniff v0.0.0-20200207005214-cf7e4d167732/go.mod h1:J3XXNGJINXLa4yIivdUT0Ad/srv2q0pSOWbbm6El2EY= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/pkg/features/features.go b/pkg/features/features.go index c9e3c1c52d..8dd434f530 100644 --- a/pkg/features/features.go +++ b/pkg/features/features.go @@ -261,6 +261,20 @@ var GRPCRouteCoreFeatures = sets.New( SupportGRPCRoute, ) +// ----------------------------------------------------------------------------- +// Features - Policy Conformance (Experimental) +// ----------------------------------------------------------------------------- + +const ( + // This option indicates support for BackendTLSPolicy. + SupportBackendTLSPolicy SupportedFeature = "BackendTLSPolicy" +) + +// ExperimentalPolicyFeatures includes all the supported features for experimental policies. +var ExperimentalPolicyFeatures = sets.New( + SupportBackendTLSPolicy, +) + // ----------------------------------------------------------------------------- // Features - Compilations // ----------------------------------------------------------------------------- @@ -279,4 +293,5 @@ var AllFeatures = sets.New[SupportedFeature](). Insert(TLSRouteCoreFeatures.UnsortedList()...). Insert(MeshCoreFeatures.UnsortedList()...). Insert(MeshExtendedFeatures.UnsortedList()...). - Insert(GRPCRouteCoreFeatures.UnsortedList()...) + Insert(GRPCRouteCoreFeatures.UnsortedList()...). + Insert(ExperimentalPolicyFeatures.UnsortedList()...)