diff --git a/tests/framework/framework.go b/tests/framework/framework.go index 7c2f471b..1770c10e 100644 --- a/tests/framework/framework.go +++ b/tests/framework/framework.go @@ -3,6 +3,7 @@ package framework import ( "context" "crypto/tls" + "crypto/x509" "fmt" "io" "math/rand" @@ -78,14 +79,12 @@ func NewFramework(ctx context.Context, t *testing.T, o ...options.Framework) *fr utilruntime.Must(gatewayv1alpha2.AddToScheme(scheme)) utilruntime.Must(gatewayv1beta1.AddToScheme(scheme)) utilruntime.Must(gatewayv1.AddToScheme(scheme)) - codec := serializer.NewCodecFactory(scheme) cli, err := client.NewWithWatch(config, client.Options{Scheme: scheme}) require.NoError(t, err) return &framework{ scheme: scheme, - codec: codec, config: config, cli: cli, } @@ -93,7 +92,6 @@ func NewFramework(ctx context.Context, t *testing.T, o ...options.Framework) *fr type framework struct { scheme *runtime.Scheme - codec serializer.CodecFactory config *rest.Config cli client.WithWatch } @@ -216,8 +214,7 @@ func (f *framework) StartController(ctx context.Context, t *testing.T) { type Response struct { HTTPResponse *http.Response Body string - EchoResponse bool - ReqHeaders map[string]string + EchoResponse EchoResponse } func (f *framework) Request(ctx context.Context, t *testing.T, method, host, path string, o ...options.Request) Response { @@ -232,18 +229,40 @@ func (f *framework) Request(ctx context.Context, t *testing.T, method, host, pat require.NoError(t, err) req.Host = host req.URL.Path = path + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: opt.TLSSkipVerify, + ServerName: opt.SNI, + }, + } + if opt.ClientCA != nil { + pool := x509.NewCertPool() + ok := pool.AppendCertsFromPEM(opt.ClientCA) + require.True(t, ok) + transport.TLSClientConfig.RootCAs = pool + } + if opt.ClientCrtPEM != nil && opt.ClientKeyPEM != nil { + cert, err := tls.X509KeyPair(opt.ClientCrtPEM, opt.ClientKeyPEM) + require.NoError(t, err) + + // transport.TLSClientConfig.Certificates is also an option, but when using it, + // http client filters out client side certificates whose issuer's DN does not + // match the DN from the CAs provided by the server. If any certificate matches, + // no certificate is provided in the TLS handshake. We don't want this behavior, + // our tests expect that the certificate is always sent when provided. + transport.TLSClientConfig.GetClientCertificate = func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { + return &cert, nil + } + } cli := http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: opt.TLSSkipVerify, - }, - }, + Transport: transport, } var res *http.Response - if opt.ExpectResponseCode > 0 { + switch { + case opt.ExpectResponseCode > 0: require.EventuallyWithT(t, func(collect *assert.CollectT) { res, err = cli.Do(req) if !assert.NoError(collect, err) { @@ -251,33 +270,28 @@ func (f *framework) Request(ctx context.Context, t *testing.T, method, host, pat } assert.Equal(collect, opt.ExpectResponseCode, res.StatusCode) }, 5*time.Second, time.Second) - } else { + case opt.ExpectX509Error != "": + require.EventuallyWithT(t, func(collect *assert.CollectT) { + _, err := cli.Do(req) + // better if matching some x509.<...>Error{} instead, + // but error.Is() does not render to true due to the server's + // x509 certificate attached to the error instance. + assert.ErrorContains(collect, err, opt.ExpectX509Error) + }, 5*time.Second, time.Second) + return Response{EchoResponse: buildEchoResponse(t, "")} + default: res, err = cli.Do(req) require.NoError(t, err) } - require.NotNil(t, res, "request closure reassigned the response") + require.NotNil(t, res, "request closure should reassign the response") body, err := io.ReadAll(res.Body) require.NoError(t, err) - reqHeaders := make(map[string]string) t.Logf("response body:\n%s\n", body) strbody := string(body) - echoResponse := strings.HasPrefix(strbody, "echoserver:\n") - if echoResponse { - for _, l := range strings.Split(strbody, "\n")[1:] { - if l == "" { - continue - } - eq := strings.Index(l, "=") - k := strings.ToLower(l[:eq]) - v := l[eq+1:] - reqHeaders[k] = v - } - } return Response{ HTTPResponse: res, Body: strbody, - EchoResponse: echoResponse, - ReqHeaders: reqHeaders, + EchoResponse: buildEchoResponse(t, strbody), } } @@ -301,11 +315,49 @@ func (f *framework) Client() client.WithWatch { return f.cli } +func (f *framework) CreateSecret(ctx context.Context, t *testing.T, secretData map[string][]byte, o ...options.Object) *corev1.Secret { + opt := options.ParseObjectOptions(o...) + data := ` +apiVersion: v1 +kind: Secret +metadata: + name: "" + namespace: default +` + name := randomName("secret") + + secret := f.CreateObject(t, data).(*corev1.Secret) + secret.Name = name + secret.Data = secretData + opt.Apply(secret) + + t.Logf("creating Secret %s/%s\n", secret.Namespace, secret.Name) + + err := f.cli.Create(ctx, secret) + require.NoError(t, err) + + t.Cleanup(func() { + secret := corev1.Secret{} + secret.Namespace = "default" + secret.Name = name + err := f.cli.Delete(ctx, &secret) + assert.NoError(t, client.IgnoreNotFound(err)) + }) + return secret +} + +func (f *framework) CreateSecretTLS(ctx context.Context, t *testing.T, crt, key []byte, o ...options.Object) *corev1.Secret { + return f.CreateSecret(ctx, t, map[string][]byte{ + corev1.TLSCertKey: crt, + corev1.TLSPrivateKeyKey: key, + }) +} + func (f *framework) CreateService(ctx context.Context, t *testing.T, serverPort int32, o ...options.Object) *corev1.Service { opt := options.ParseObjectOptions(o...) data := ` apiVersion: v1 -Kind: Service +kind: Service metadata: name: "" namespace: default @@ -340,7 +392,7 @@ spec: func (f *framework) CreateEndpoints(ctx context.Context, t *testing.T, serverPort int32) *corev1.Endpoints { data := ` apiVersion: v1 -Kind: Endpoints +kind: Endpoints metadata: annotations: haproxy-ingress.github.io/ip-override: 127.0.0.1 @@ -397,14 +449,22 @@ spec: number: 8080 ` name := randomName("ing") - hostname := name + ".local" + hostname := opt.IngressOpt.CustomHostName + if hostname == "" { + hostname = name + ".local" + } ing := f.CreateObject(t, data).(*networking.Ingress) ing.Name = name ing.Spec.Rules[0].Host = hostname ing.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Name = svc.Name opt.Apply(ing) - if opt.IngressOpt.DefaultTLS { + if opt.IngressOpt.CustomTLSSecret != "" { + ing.Spec.TLS = []networking.IngressTLS{{ + Hosts: []string{hostname}, + SecretName: opt.IngressOpt.CustomTLSSecret, + }} + } else if opt.IngressOpt.DefaultTLS { ing.Spec.TLS = []networking.IngressTLS{{Hosts: []string{hostname}}} } @@ -647,15 +707,57 @@ spec: } func (f *framework) CreateObject(t *testing.T, data string) client.Object { - obj, _, err := f.codec.UniversalDeserializer().Decode([]byte(data), nil, nil) + obj, _, err := serializer.NewCodecFactory(f.scheme).UniversalDeserializer().Decode([]byte(data), nil, nil) require.NoError(t, err) return obj.(client.Object) } -func (f *framework) CreateHTTPServer(ctx context.Context, t *testing.T) int32 { +type EchoResponse struct { + Parsed bool + ServerName string + Port int + Path string + ReqHeaders map[string]string +} + +func buildEchoResponse(t *testing.T, body string) EchoResponse { + if !strings.HasPrefix(body, "echoserver: ") { + // instantiate all pointers, so we can use assert on tests + // without leading to nil pointer deref. + return EchoResponse{ReqHeaders: make(map[string]string)} + } + lines := strings.Split(body, "\n") + header := echoHeaderRegex.FindStringSubmatch(lines[0]) + port, err := strconv.Atoi(header[2]) + require.NoError(t, err) + res := EchoResponse{ + Parsed: true, + ServerName: header[1], + Port: port, + Path: header[3], + ReqHeaders: make(map[string]string), + } + for _, l := range lines[1:] { + if l == "" { + continue + } + eq := strings.Index(l, "=") + k := strings.ToLower(l[:eq]) + v := l[eq+1:] + res.ReqHeaders[k] = v + } + return res +} + +// Example: echoserver: service-name 8080 /app +var echoHeaderRegex = regexp.MustCompile(`^echoserver: ([a-z0-9-]+) ([0-9]+) ([a-z0-9/]+)$`) + +func (f *framework) CreateHTTPServer(ctx context.Context, t *testing.T, serverName string) int32 { + serverPort := int32(32768 + rand.Intn(32767)) + mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - content := "echoserver:\n" + content := fmt.Sprintf("echoserver: %s %d %s\n", serverName, serverPort, r.URL.Path) for name, values := range r.Header { for _, value := range values { content += fmt.Sprintf("%s=%s\n", name, value) @@ -665,7 +767,6 @@ func (f *framework) CreateHTTPServer(ctx context.Context, t *testing.T) int32 { assert.NoError(t, err) }) - serverPort := int32(32768 + rand.Intn(32767)) server := http.Server{ Addr: fmt.Sprintf(":%d", serverPort), Handler: mux, @@ -674,7 +775,8 @@ func (f *framework) CreateHTTPServer(ctx context.Context, t *testing.T) int32 { done := make(chan bool) go func() { - _ = server.ListenAndServe() + err := server.ListenAndServe() + assert.ErrorIs(t, err, http.ErrServerClosed) done <- true }() diff --git a/tests/framework/options/certificates.go b/tests/framework/options/certificates.go new file mode 100644 index 00000000..214118a0 --- /dev/null +++ b/tests/framework/options/certificates.go @@ -0,0 +1,27 @@ +package options + +type Certificate func(o *certificateOpt) + +func DNS(dns ...string) Certificate { + return func(o *certificateOpt) { + o.DNS = dns + } +} + +func InvalidDates() Certificate { + return func(o *certificateOpt) { + o.InvalidDates = true + } +} + +type certificateOpt struct { + DNS []string + InvalidDates bool +} + +func ParseCertificateOptions(opts ...Certificate) (opt certificateOpt) { + for _, o := range opts { + o(&opt) + } + return opt +} diff --git a/tests/framework/options/objects.go b/tests/framework/options/objects.go index dc120309..d3dff0d7 100644 --- a/tests/framework/options/objects.go +++ b/tests/framework/options/objects.go @@ -18,12 +18,23 @@ func AddConfigKeyAnnotation(key, value string) Object { } } -func DefaultHostTLS() Object { +func DefaultTLS() Object { return func(o *objectOpt) { o.IngressOpt.DefaultTLS = true } } +func CustomTLS(secret string) Object { + return func(o *objectOpt) { + o.IngressOpt.CustomTLSSecret = secret + } +} + +func CustomHostName(hostname string) Object { + return func(o *objectOpt) { + o.IngressOpt.CustomHostName = hostname + } +} func Listener(name, proto string, port int32) Object { return func(o *objectOpt) { o.GatewayOpt.Listeners = append(o.GatewayOpt.Listeners, ListenerOpt{ @@ -45,7 +56,9 @@ type objectOpt struct { } type IngressOpt struct { - DefaultTLS bool + DefaultTLS bool + CustomTLSSecret string + CustomHostName string } type GatewayOpt struct { diff --git a/tests/framework/options/request.go b/tests/framework/options/request.go index 357e4e88..672f6469 100644 --- a/tests/framework/options/request.go +++ b/tests/framework/options/request.go @@ -8,17 +8,52 @@ func ExpectResponseCode(code int) Request { } } -func HTTPSRequest(skipVerify bool) Request { +func ExpectX509Error(msg string) Request { + return func(o *requestOpt) { + o.ExpectX509Error = msg + } +} + +func HTTPSRequest() Request { return func(o *requestOpt) { o.HTTPS = true - o.TLSSkipVerify = skipVerify + } +} + +func TLSSkipVerify() Request { + return func(o *requestOpt) { + o.TLSSkipVerify = true + } +} + +func ClientCA(ca []byte) Request { + return func(o *requestOpt) { + o.ClientCA = ca + } +} + +func SNI(servername string) Request { + return func(o *requestOpt) { + o.SNI = servername + } +} + +func ClientCertificateKeyPEM(crt, key []byte) Request { + return func(o *requestOpt) { + o.ClientCrtPEM = crt + o.ClientKeyPEM = key } } type requestOpt struct { ExpectResponseCode int + ExpectX509Error string HTTPS bool TLSSkipVerify bool + ClientCA []byte + SNI string + ClientCrtPEM []byte + ClientKeyPEM []byte } func ParseRequestOptions(opts ...Request) (opt requestOpt) { diff --git a/tests/framework/ssl.go b/tests/framework/ssl.go new file mode 100644 index 00000000..7ea6666f --- /dev/null +++ b/tests/framework/ssl.go @@ -0,0 +1,88 @@ +package framework + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/jcmoraisjr/haproxy-ingress/tests/framework/options" +) + +const ( + CertificateIssuerCN = "HAProxy Ingress issuer" + CertificateClientCN = "HAProxy Ingress client" +) + +func CreateCA(t *testing.T, cn string) (ca, key []byte) { + serial, err := rand.Int(rand.Reader, big.NewInt(2^63)) + require.NoError(t, err) + notBefore := time.Now().Add(-time.Hour) + notAfter := notBefore.Add(24 * time.Hour) + template := x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: cn, + }, + NotBefore: notBefore, + NotAfter: notAfter, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 1, + } + priv, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + cader, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + require.NoError(t, err) + keyder := x509.MarshalPKCS1PrivateKey(priv) + ca = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cader}) + key = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyder}) + return ca, key +} + +func CreateCertificate(t *testing.T, ca, cakey []byte, cn string, o ...options.Certificate) (crt, key []byte) { + opt := options.ParseCertificateOptions(o...) + + cakeyder, _ := pem.Decode(cakey) + cakeyrsa, err := x509.ParsePKCS1PrivateKey(cakeyder.Bytes) + require.NoError(t, err) + + cader, _ := pem.Decode(ca) + cax509, err := x509.ParseCertificate(cader.Bytes) + require.NoError(t, err) + + serial, err := rand.Int(rand.Reader, big.NewInt(2^63)) + require.NoError(t, err) + + var notBefore, notAfter time.Time + if opt.InvalidDates { + notBefore = time.Now().Add(-24 * time.Hour) + notAfter = notBefore.Add(12 * time.Hour) + } else { + notBefore = time.Now().Add(-time.Hour) + notAfter = notBefore.Add(24 * time.Hour) + } + template := x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{ + CommonName: cn, + }, + NotBefore: notBefore, + NotAfter: notAfter, + DNSNames: opt.DNS, + } + priv, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + crtder, err := x509.CreateCertificate(rand.Reader, &template, cax509, &priv.PublicKey, cakeyrsa) + require.NoError(t, err) + keyder := x509.MarshalPKCS1PrivateKey(priv) + crt = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: crtder}) + key = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyder}) + return crt, key +} diff --git a/tests/framework/utils.go b/tests/framework/utils.go new file mode 100644 index 00000000..3c1372ab --- /dev/null +++ b/tests/framework/utils.go @@ -0,0 +1,21 @@ +package framework + +import ( + "fmt" + "math/rand" +) + +func AppendStringMap(src, add map[string]string) map[string]string { + out := make(map[string]string) + for k, v := range src { + out[k] = v + } + for k, v := range add { + out[k] = v + } + return out +} + +func RandomHostName() string { + return fmt.Sprintf("host-%08d.local", rand.Intn(1e8)) +} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index fdbcb8d0..36ef206c 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -2,8 +2,12 @@ package integration_test import ( "context" + "crypto/sha1" + "encoding/hex" + "encoding/pem" "fmt" "net/http" + "strings" "testing" "time" @@ -23,7 +27,7 @@ func TestIntegrationIngress(t *testing.T) { ctx := context.Background() f := framework.NewFramework(ctx, t) - httpServerPort := f.CreateHTTPServer(ctx, t) + httpServerPort := f.CreateHTTPServer(ctx, t, "default") lbingpre1 := "127.0.0.1" require.NotEqual(t, framework.PublishAddress, lbingpre1) @@ -34,6 +38,21 @@ func TestIntegrationIngress(t *testing.T) { err := f.Client().Status().Update(ctx, ingpre1) require.NoError(t, err) + caValid, cakeyValid := framework.CreateCA(t, framework.CertificateIssuerCN) + crtValid, keyValid := framework.CreateCertificate(t, caValid, cakeyValid, framework.CertificateClientCN) + secretCA := f.CreateSecret(ctx, t, map[string][]byte{"ca.crt": caValid}) + + caFake, cakeyFake := framework.CreateCA(t, framework.CertificateIssuerCN) + crtFake, keyFake := framework.CreateCertificate(t, caFake, cakeyFake, framework.CertificateClientCN) + + commonReqHeaders := map[string]string{ + "accept-encoding": "gzip", + "user-agent": "Go-http-client/1.1", + "x-forwarded-for": "127.0.0.1", + "x-forwarded-proto": "https", + "x-real-ip": "127.0.0.1", + } + f.StartController(ctx, t) t.Run("hello world", func(t *testing.T) { @@ -41,30 +60,30 @@ func TestIntegrationIngress(t *testing.T) { svc := f.CreateService(ctx, t, httpServerPort) _, hostname := f.CreateIngress(ctx, t, svc) res := f.Request(ctx, t, http.MethodGet, hostname, "/", options.ExpectResponseCode(http.StatusOK)) - assert.True(t, res.EchoResponse) - assert.Equal(t, "http", res.ReqHeaders["x-forwarded-proto"]) + assert.True(t, res.EchoResponse.Parsed) + assert.Equal(t, "http", res.EchoResponse.ReqHeaders["x-forwarded-proto"]) }) t.Run("should not redirect to https", func(t *testing.T) { t.Parallel() svc := f.CreateService(ctx, t, httpServerPort) _, hostname := f.CreateIngress(ctx, t, svc, - options.DefaultHostTLS(), + options.DefaultTLS(), options.AddConfigKeyAnnotation(ingtypes.BackSSLRedirect, "false"), ) res := f.Request(ctx, t, http.MethodGet, hostname, "/", options.ExpectResponseCode(http.StatusOK)) - assert.True(t, res.EchoResponse) + assert.True(t, res.EchoResponse.Parsed) }) t.Run("should redirect to https", func(t *testing.T) { t.Parallel() svc := f.CreateService(ctx, t, httpServerPort) _, hostname := f.CreateIngress(ctx, t, svc, - options.DefaultHostTLS(), + options.DefaultTLS(), options.AddConfigKeyAnnotation(ingtypes.BackSSLRedirect, "true"), ) res := f.Request(ctx, t, http.MethodGet, hostname, "/", options.ExpectResponseCode(http.StatusFound)) - assert.False(t, res.EchoResponse) + assert.False(t, res.EchoResponse.Parsed) assert.Equal(t, fmt.Sprintf("https://%s/", hostname), res.HTTPResponse.Header.Get("location")) }) @@ -73,54 +92,235 @@ func TestIntegrationIngress(t *testing.T) { svc := f.CreateService(ctx, t, httpServerPort) _, hostname := f.CreateIngress(ctx, t, svc) res := f.Request(ctx, t, http.MethodGet, hostname, "/", options.ExpectResponseCode(http.StatusOK)) - assert.True(t, res.EchoResponse) - reqHeaders := map[string]string{ - "accept-encoding": "gzip", - "user-agent": "Go-http-client/1.1", - "x-forwarded-for": "127.0.0.1", + assert.True(t, res.EchoResponse.Parsed) + reqHeaders := framework.AppendStringMap(commonReqHeaders, map[string]string{ "x-forwarded-proto": "http", - "x-real-ip": "127.0.0.1", - } - assert.Equal(t, reqHeaders, res.ReqHeaders) + }) + assert.Equal(t, reqHeaders, res.EchoResponse.ReqHeaders) }) t.Run("should send default http headers on https request", func(t *testing.T) { t.Parallel() svc := f.CreateService(ctx, t, httpServerPort) - _, hostname := f.CreateIngress(ctx, t, svc, options.DefaultHostTLS()) + _, hostname := f.CreateIngress(ctx, t, svc, options.DefaultTLS()) res := f.Request(ctx, t, http.MethodGet, hostname, "/", options.ExpectResponseCode(http.StatusOK), - options.HTTPSRequest(true), + options.HTTPSRequest(), + options.TLSSkipVerify(), ) - assert.True(t, res.EchoResponse) - reqHeaders := map[string]string{ - "accept-encoding": "gzip", - "user-agent": "Go-http-client/1.1", - "x-forwarded-for": "127.0.0.1", - "x-forwarded-proto": "https", - "x-real-ip": "127.0.0.1", - } - assert.Equal(t, reqHeaders, res.ReqHeaders) + assert.True(t, res.EchoResponse.Parsed) + assert.Equal(t, commonReqHeaders, res.EchoResponse.ReqHeaders) }) t.Run("should redirect to https before app-root", func(t *testing.T) { t.Parallel() svc := f.CreateService(ctx, t, httpServerPort) _, hostname := f.CreateIngress(ctx, t, svc, - options.DefaultHostTLS(), + options.DefaultTLS(), options.AddConfigKeyAnnotation(ingtypes.BackSSLRedirect, "true"), options.AddConfigKeyAnnotation(ingtypes.HostAppRoot, "/app"), ) - res := f.Request(ctx, t, http.MethodGet, hostname, "/", options.ExpectResponseCode(http.StatusFound)) - assert.False(t, res.EchoResponse) + res := f.Request(ctx, t, http.MethodGet, hostname, "/", + options.ExpectResponseCode(http.StatusFound), + ) + assert.False(t, res.EchoResponse.Parsed) assert.Equal(t, fmt.Sprintf("https://%s/", hostname), res.HTTPResponse.Header.Get("location")) - res = f.Request(ctx, t, http.MethodGet, hostname, "/", options.HTTPSRequest(true)) - assert.False(t, res.EchoResponse) + res = f.Request(ctx, t, http.MethodGet, hostname, "/", + options.HTTPSRequest(), + options.TLSSkipVerify(), + ) + assert.False(t, res.EchoResponse.Parsed) assert.Equal(t, "/app", res.HTTPResponse.Header.Get("location")) }) + t.Run("should fail TLS connection on default fake server crt and valid local ca", func(t *testing.T) { + t.Parallel() + svc := f.CreateService(ctx, t, httpServerPort) + _, hostname := f.CreateIngress(ctx, t, svc, + options.DefaultTLS(), + ) + + res := f.Request(ctx, t, http.MethodGet, hostname, "/", + options.HTTPSRequest(), + options.SNI("localhost"), // fake certificate has `localhost` in certificates's SAN + options.ExpectX509Error("x509: certificate signed by unknown authority"), + ) + assert.False(t, res.EchoResponse.Parsed) + }) + + t.Run("should fail TLS connection on custom server crt with invalid dates", func(t *testing.T) { + t.Parallel() + hostname := framework.RandomHostName() + ca, cakey := framework.CreateCA(t, "custom CA") + crt, key := framework.CreateCertificate(t, ca, cakey, hostname, + options.DNS(hostname), + options.InvalidDates(), + ) + secret := f.CreateSecretTLS(ctx, t, crt, key) + + svc := f.CreateService(ctx, t, httpServerPort) + _, _ = f.CreateIngress(ctx, t, svc, + options.CustomHostName(hostname), + options.CustomTLS(secret.Name), + ) + + res := f.Request(ctx, t, http.MethodGet, hostname, "/", + options.HTTPSRequest(), + options.ClientCA(ca), + options.SNI(hostname), + options.ExpectX509Error("x509: certificate has expired or is not yet valid"), + ) + assert.False(t, res.EchoResponse.Parsed) + }) + + t.Run("should succeed TLS connection on custom server crt and valid local ca", func(t *testing.T) { + t.Parallel() + hostname := framework.RandomHostName() + ca, cakey := framework.CreateCA(t, "custom CA") + crt, key := framework.CreateCertificate(t, ca, cakey, hostname, + options.DNS(hostname), + ) + secret := f.CreateSecretTLS(ctx, t, crt, key) + + svc := f.CreateService(ctx, t, httpServerPort) + _, _ = f.CreateIngress(ctx, t, svc, + options.CustomHostName(hostname), + options.CustomTLS(secret.Name), + ) + + res := f.Request(ctx, t, http.MethodGet, hostname, "/", + options.HTTPSRequest(), + options.ClientCA(ca), + options.SNI(hostname), + options.ExpectResponseCode(200), + ) + assert.True(t, res.EchoResponse.Parsed) + assert.Equal(t, commonReqHeaders, res.EchoResponse.ReqHeaders) + }) + + t.Run("should deny 496 mTLS with no client crt", func(t *testing.T) { + t.Parallel() + svc := f.CreateService(ctx, t, httpServerPort) + _, hostname := f.CreateIngress(ctx, t, svc, + options.DefaultTLS(), + options.AddConfigKeyAnnotation(ingtypes.HostAuthTLSSecret, secretCA.Name), + ) + + res := f.Request(ctx, t, http.MethodGet, hostname, "/", + options.HTTPSRequest(), + options.TLSSkipVerify(), + options.SNI(hostname), + options.ExpectResponseCode(496), + ) + assert.False(t, res.EchoResponse.Parsed) + }) + + t.Run("should deny 495 mTLS with invalid client crt", func(t *testing.T) { + t.Parallel() + svc := f.CreateService(ctx, t, httpServerPort) + _, hostname := f.CreateIngress(ctx, t, svc, + options.DefaultTLS(), + options.AddConfigKeyAnnotation(ingtypes.HostAuthTLSSecret, secretCA.Name), + ) + + res := f.Request(ctx, t, http.MethodGet, hostname, "/", + options.HTTPSRequest(), + options.TLSSkipVerify(), + options.SNI(hostname), + options.ClientCertificateKeyPEM(crtFake, keyFake), + options.ExpectResponseCode(495), + ) + assert.False(t, res.EchoResponse.Parsed) + }) + + t.Run("should deny 495 mTLS with misconfigured ingress", func(t *testing.T) { + t.Parallel() + svc := f.CreateService(ctx, t, httpServerPort) + _, hostname := f.CreateIngress(ctx, t, svc, + options.DefaultTLS(), + options.AddConfigKeyAnnotation(ingtypes.HostAuthTLSSecret, "do not exist"), + ) + + res := f.Request(ctx, t, http.MethodGet, hostname, "/", + options.HTTPSRequest(), + options.TLSSkipVerify(), + options.SNI(hostname), + options.ClientCertificateKeyPEM(crtValid, keyValid), + options.ExpectResponseCode(495), + ) + assert.False(t, res.EchoResponse.Parsed) + }) + + t.Run("should deny 421 mTLS with distinct host", func(t *testing.T) { + t.Parallel() + svc := f.CreateService(ctx, t, httpServerPort) + _, hostname := f.CreateIngress(ctx, t, svc, + options.DefaultTLS(), + options.AddConfigKeyAnnotation(ingtypes.HostAuthTLSSecret, secretCA.Name), + ) + + res := f.Request(ctx, t, http.MethodGet, "trying-bypass.local", "/", + options.HTTPSRequest(), + options.TLSSkipVerify(), + options.SNI(hostname), + options.ClientCertificateKeyPEM(crtValid, keyValid), + options.ExpectResponseCode(421), + ) + assert.False(t, res.EchoResponse.Parsed) + }) + + t.Run("should allow mTLS without an optional crt", func(t *testing.T) { + t.Parallel() + svc := f.CreateService(ctx, t, httpServerPort) + _, hostname := f.CreateIngress(ctx, t, svc, + options.DefaultTLS(), + options.AddConfigKeyAnnotation(ingtypes.HostAuthTLSSecret, secretCA.Name), + options.AddConfigKeyAnnotation(ingtypes.HostAuthTLSVerifyClient, "optional"), + ) + + res := f.Request(ctx, t, http.MethodGet, hostname, "/", + options.HTTPSRequest(), + options.TLSSkipVerify(), + options.SNI(hostname), + options.ExpectResponseCode(200), + ) + assert.True(t, res.EchoResponse.Parsed) + reqHeaders := framework.AppendStringMap(commonReqHeaders, map[string]string{ + "x-ssl-client-cn": "", + "x-ssl-client-dn": "", + "x-ssl-client-sha1": "", + }) + assert.Equal(t, reqHeaders, res.EchoResponse.ReqHeaders) + }) + + t.Run("should allow mTLS with valid crt", func(t *testing.T) { + t.Parallel() + svc := f.CreateService(ctx, t, httpServerPort) + _, hostname := f.CreateIngress(ctx, t, svc, + options.DefaultTLS(), + options.AddConfigKeyAnnotation(ingtypes.HostAuthTLSSecret, secretCA.Name), + ) + + res := f.Request(ctx, t, http.MethodGet, hostname, "/", + options.HTTPSRequest(), + options.TLSSkipVerify(), + options.SNI(hostname), + options.ClientCertificateKeyPEM(crtValid, keyValid), + options.ExpectResponseCode(200), + ) + assert.True(t, res.EchoResponse.Parsed) + crtder, _ := pem.Decode(crtValid) + sha1sum := sha1.Sum(crtder.Bytes) + reqHeaders := framework.AppendStringMap(commonReqHeaders, map[string]string{ + "x-ssl-client-cn": framework.CertificateClientCN, + "x-ssl-client-dn": "/CN=" + framework.CertificateClientCN, + "x-ssl-client-sha1": strings.ToUpper(hex.EncodeToString(sha1sum[:])), + }) + assert.Equal(t, reqHeaders, res.EchoResponse.ReqHeaders) + }) + t.Run("should take leader", func(t *testing.T) { t.Parallel() assert.EventuallyWithT(t, func(collect *assert.CollectT) { @@ -254,7 +454,7 @@ func TestIntegrationGateway(t *testing.T) { t.Run("v1alpha2", func(t *testing.T) { f := framework.NewFramework(ctx, t, options.CRDs("gateway-api-v040-v1alpha2")) f.StartController(ctx, t) - httpServerPort := f.CreateHTTPServer(ctx, t) + httpServerPort := f.CreateHTTPServer(ctx, t, "gw-v1alpha2") gc := f.CreateGatewayClassA2(ctx, t) t.Run("hello world", func(t *testing.T) { @@ -263,15 +463,15 @@ func TestIntegrationGateway(t *testing.T) { svc := f.CreateService(ctx, t, httpServerPort) _, hostname := f.CreateHTTPRouteA2(ctx, t, gw, svc) res := f.Request(ctx, t, http.MethodGet, hostname, "/", options.ExpectResponseCode(http.StatusOK)) - assert.True(t, res.EchoResponse) - assert.Equal(t, "http", res.ReqHeaders["x-forwarded-proto"]) + assert.True(t, res.EchoResponse.Parsed) + assert.Equal(t, "http", res.EchoResponse.ReqHeaders["x-forwarded-proto"]) }) }) t.Run("v1beta1", func(t *testing.T) { f := framework.NewFramework(ctx, t, options.CRDs("gateway-api-v050-v1beta1-experimental")) f.StartController(ctx, t) - httpServerPort := f.CreateHTTPServer(ctx, t) + httpServerPort := f.CreateHTTPServer(ctx, t, "gw-v1beta1") gc := f.CreateGatewayClassB1(ctx, t) t.Run("hello world", func(t *testing.T) { @@ -280,15 +480,15 @@ func TestIntegrationGateway(t *testing.T) { svc := f.CreateService(ctx, t, httpServerPort) _, hostname := f.CreateHTTPRouteB1(ctx, t, gw, svc) res := f.Request(ctx, t, http.MethodGet, hostname, "/", options.ExpectResponseCode(http.StatusOK)) - assert.True(t, res.EchoResponse) - assert.Equal(t, "http", res.ReqHeaders["x-forwarded-proto"]) + assert.True(t, res.EchoResponse.Parsed) + assert.Equal(t, "http", res.EchoResponse.ReqHeaders["x-forwarded-proto"]) }) }) t.Run("v1", func(t *testing.T) { f := framework.NewFramework(ctx, t, options.CRDs("gateway-api-v100-v1-experimental")) f.StartController(ctx, t) - httpServerPort := f.CreateHTTPServer(ctx, t) + httpServerPort := f.CreateHTTPServer(ctx, t, "gw-v1") tcpServerPort := f.CreateTCPServer(ctx, t) gc := f.CreateGatewayClassV1(ctx, t) @@ -298,8 +498,8 @@ func TestIntegrationGateway(t *testing.T) { svc := f.CreateService(ctx, t, httpServerPort) _, hostname := f.CreateHTTPRouteV1(ctx, t, gw, svc) res := f.Request(ctx, t, http.MethodGet, hostname, "/", options.ExpectResponseCode(http.StatusOK)) - assert.True(t, res.EchoResponse) - assert.Equal(t, "http", res.ReqHeaders["x-forwarded-proto"]) + assert.True(t, res.EchoResponse.Parsed) + assert.Equal(t, "http", res.EchoResponse.ReqHeaders["x-forwarded-proto"]) }) t.Run("expose TCPRoute", func(t *testing.T) {