diff --git a/contrib/completions/bash/openshift b/contrib/completions/bash/openshift index 1b22e35e0743..6d74f54fbdcc 100644 --- a/contrib/completions/bash/openshift +++ b/contrib/completions/bash/openshift @@ -15651,6 +15651,7 @@ _openshift_infra_router() flags+=("--context=") flags+=("--default-certificate=") flags+=("--default-certificate-path=") + flags+=("--extended-validation") flags+=("--fields=") flags+=("--hostname-template=") flags+=("--include-udp-endpoints") diff --git a/pkg/cmd/infra/router/template.go b/pkg/cmd/infra/router/template.go index b27dbd4a9b79..4a7399684225 100644 --- a/pkg/cmd/infra/router/template.go +++ b/pkg/cmd/infra/router/template.go @@ -15,6 +15,7 @@ import ( "github.com/openshift/origin/pkg/cmd/util" "github.com/openshift/origin/pkg/cmd/util/clientcmd" + "github.com/openshift/origin/pkg/router" "github.com/openshift/origin/pkg/router/controller" templateplugin "github.com/openshift/origin/pkg/router/template" "github.com/openshift/origin/pkg/util/proc" @@ -54,6 +55,7 @@ type TemplateRouter struct { ReloadInterval time.Duration DefaultCertificate string DefaultCertificatePath string + ExtendedValidation bool RouterService *ktypes.NamespacedName } @@ -77,6 +79,7 @@ func (o *TemplateRouter) Bind(flag *pflag.FlagSet) { flag.StringVar(&o.TemplateFile, "template", util.Env("TEMPLATE_FILE", ""), "The path to the template file to use") flag.StringVar(&o.ReloadScript, "reload", util.Env("RELOAD_SCRIPT", ""), "The path to the reload script to use") flag.DurationVar(&o.ReloadInterval, "interval", reloadInterval(), "Controls how often router reloads are invoked. Mutiple router reload requests are coalesced for the duration of this interval since the last reload time.") + flag.BoolVar(&o.ExtendedValidation, "extended-validation", util.Env("EXTENDED_VALIDATION", "") == "true", "If set, then an additional extended validation step is performed on all routes admitted in by this router.") } type RouterStats struct { @@ -192,7 +195,11 @@ func (o *TemplateRouterOptions) Run() error { } statusPlugin := controller.NewStatusAdmitter(templatePlugin, oc, o.RouterName) - plugin := controller.NewUniqueHost(statusPlugin, o.RouteSelectionFunc(), statusPlugin) + var nextPlugin router.Plugin = statusPlugin + if o.ExtendedValidation { + nextPlugin = controller.NewExtendedValidator(nextPlugin, controller.RejectionRecorder(statusPlugin)) + } + plugin := controller.NewUniqueHost(nextPlugin, o.RouteSelectionFunc(), controller.RejectionRecorder(statusPlugin)) factory := o.RouterSelection.NewFactory(oc, kc) controller := factory.Create(plugin) diff --git a/pkg/route/api/types.go b/pkg/route/api/types.go index 2e9bcb8ef3ff..b1e0a3bba0a5 100644 --- a/pkg/route/api/types.go +++ b/pkg/route/api/types.go @@ -72,6 +72,8 @@ type RouteIngressConditionType string const ( // RouteAdmitted means the route is able to service requests for the provided Host RouteAdmitted RouteIngressConditionType = "Admitted" + // RouteExtendedValidationFailed means the route configuration failed an extended validation check. + RouteExtendedValidationFailed RouteIngressConditionType = "ExtendedValidationFailed" // TODO: add other route condition types ) diff --git a/pkg/route/api/validation/validation.go b/pkg/route/api/validation/validation.go index d0e094bbf1ca..dd6a033acaf3 100644 --- a/pkg/route/api/validation/validation.go +++ b/pkg/route/api/validation/validation.go @@ -1,6 +1,9 @@ package validation import ( + "crypto/tls" + "crypto/x509" + "encoding/pem" "fmt" "strings" @@ -76,6 +79,67 @@ func ValidateRouteStatusUpdate(route *routeapi.Route, older *routeapi.Route) fie return allErrs } +// ExtendedValidateRoute performs an extended validation on the route +// including checking that the TLS config is valid. +func ExtendedValidateRoute(route *routeapi.Route) field.ErrorList { + tlsConfig := route.Spec.TLS + result := field.ErrorList{} + + if tlsConfig == nil { + return result + } + + tlsFieldPath := field.NewPath("spec").Child("tls") + if errs := validateTLS(route, tlsFieldPath); len(errs) != 0 { + result = append(result, errs...) + } + + // TODO: Check if we can be stricter with validating the certificate + // is for the route hostname. Don't want existing routes to + // break, so disable the hostname validation for now. + // hostname := route.Spec.Host + hostname := "" + var certPool *x509.CertPool + + if len(tlsConfig.CACertificate) > 0 { + certPool = x509.NewCertPool() + if ok := certPool.AppendCertsFromPEM([]byte(tlsConfig.CACertificate)); !ok { + result = append(result, field.Invalid(tlsFieldPath.Child("caCertificate"), tlsConfig.CACertificate, "failed to parse CA certificate")) + } + } + + verifyOptions := &x509.VerifyOptions{ + DNSName: hostname, + Roots: certPool, + } + + if len(tlsConfig.Certificate) > 0 { + if _, err := validateCertificatePEM(tlsConfig.Certificate, verifyOptions); err != nil { + result = append(result, field.Invalid(tlsFieldPath.Child("certificate"), tlsConfig.Certificate, err.Error())) + } + + certKeyBytes := []byte{} + certKeyBytes = append(certKeyBytes, []byte(tlsConfig.Certificate)...) + if len(tlsConfig.Key) > 0 { + certKeyBytes = append(certKeyBytes, byte('\n')) + certKeyBytes = append(certKeyBytes, []byte(tlsConfig.Key)...) + } + + if _, err := tls.X509KeyPair(certKeyBytes, certKeyBytes); err != nil { + result = append(result, field.Invalid(tlsFieldPath.Child("key"), tlsConfig.Key, err.Error())) + } + } + + if len(tlsConfig.DestinationCACertificate) > 0 { + roots := x509.NewCertPool() + if ok := roots.AppendCertsFromPEM([]byte(tlsConfig.DestinationCACertificate)); !ok { + result = append(result, field.Invalid(tlsFieldPath.Child("destinationCACertificate"), tlsConfig.DestinationCACertificate, "failed to parse destination CA certificate")) + } + } + + return result +} + // validateTLS tests fields for different types of TLS combinations are set. Called // by ValidateRoute. func validateTLS(route *routeapi.Route, fldPath *field.Path) field.ErrorList { @@ -158,3 +222,38 @@ func validateInsecureEdgeTerminationPolicy(tls *routeapi.TLSConfig, fldPath *fie return nil } + +// validateCertificatePEM checks if a certificate PEM is valid and +// optionally verifies the certificate using the options. +func validateCertificatePEM(certPEM string, options *x509.VerifyOptions) (*x509.Certificate, error) { + var data *pem.Block + for remaining := []byte(certPEM); len(remaining) > 0; { + block, rest := pem.Decode(remaining) + if block == nil { + return nil, fmt.Errorf("error decoding certificate data") + } + if block.Type == "CERTIFICATE" { + data = block + break + } + remaining = rest + } + + if data == nil || len(data.Bytes) < 1 { + return nil, fmt.Errorf("invalid/empty certificate data") + } + + cert, err := x509.ParseCertificate(data.Bytes) + if err != nil { + return nil, fmt.Errorf("error parsing certificate: %s", err.Error()) + } + + if options != nil { + _, err = cert.Verify(*options) + if err != nil { + return cert, fmt.Errorf("error verifying certificate: %s", err.Error()) + } + } + + return cert, nil +} diff --git a/pkg/route/api/validation/validation_test.go b/pkg/route/api/validation/validation_test.go index bf23701a5cf4..37c2f7a58248 100644 --- a/pkg/route/api/validation/validation_test.go +++ b/pkg/route/api/validation/validation_test.go @@ -9,6 +9,109 @@ import ( "github.com/openshift/origin/pkg/route/api" ) +const ( + testExpiredCAUnknownCertificate = `-----BEGIN CERTIFICATE----- +MIIDIjCCAgqgAwIBAgIBBjANBgkqhkiG9w0BAQUFADCBoTELMAkGA1UEBhMCVVMx +CzAJBgNVBAgMAlNDMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl +ZmF1bHQgQ29tcGFueSBMdGQxEDAOBgNVBAsMB1Rlc3QgQ0ExGjAYBgNVBAMMEXd3 +dy5leGFtcGxlY2EuY29tMSIwIAYJKoZIhvcNAQkBFhNleGFtcGxlQGV4YW1wbGUu +Y29tMB4XDTE2MDExMzE5NDA1N1oXDTI2MDExMDE5NDA1N1owfDEYMBYGA1UEAxMP +d3d3LmV4YW1wbGUuY29tMQswCQYDVQQIEwJTQzELMAkGA1UEBhMCVVMxIjAgBgkq +hkiG9w0BCQEWE2V4YW1wbGVAZXhhbXBsZS5jb20xEDAOBgNVBAoTB0V4YW1wbGUx +EDAOBgNVBAsTB0V4YW1wbGUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM0B +u++oHV1wcphWRbMLUft8fD7nPG95xs7UeLPphFZuShIhhdAQMpvcsFeg+Bg9PWCu +v3jZljmk06MLvuWLfwjYfo9q/V+qOZVfTVHHbaIO5RTXJMC2Nn+ACF0kHBmNcbth +OOgF8L854a/P8tjm1iPR++vHnkex0NH7lyosVc/vAgMBAAGjDTALMAkGA1UdEwQC +MAAwDQYJKoZIhvcNAQEFBQADggEBADjFm5AlNH3DNT1Uzx3m66fFjqqrHEs25geT +yA3rvBuynflEHQO95M/8wCxYVyuAx4Z1i4YDC7tx0vmOn/2GXZHY9MAj1I8KCnwt +Jik7E2r1/yY0MrkawljOAxisXs821kJ+Z/51Ud2t5uhGxS6hJypbGspMS7OtBbw7 +8oThK7cWtCXOldNF6ruqY1agWnhRdAq5qSMnuBXuicOP0Kbtx51a1ugE3SnvQenJ +nZxdtYUXvEsHZC/6bAtTfNh+/SwgxQJuL2ZM+VG3X2JIKY8xTDui+il7uTh422lq +wED8uwKl+bOj6xFDyw4gWoBxRobsbFaME8pkykP1+GnKDberyAM= +-----END CERTIFICATE-----` + + testExpiredCertPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDNAbvvqB1dcHKYVkWzC1H7fHw+5zxvecbO1Hiz6YRWbkoSIYXQ +EDKb3LBXoPgYPT1grr942ZY5pNOjC77li38I2H6Pav1fqjmVX01Rx22iDuUU1yTA +tjZ/gAhdJBwZjXG7YTjoBfC/OeGvz/LY5tYj0fvrx55HsdDR+5cqLFXP7wIDAQAB +AoGAfE7P4Zsj6zOzGPI/Izj7Bi5OvGnEeKfzyBiH9Dflue74VRQkqqwXs/DWsNv3 +c+M2Y3iyu5ncgKmUduo5X8D9To2ymPRLGuCdfZTxnBMpIDKSJ0FTwVPkr6cYyyBk +5VCbc470pQPxTAAtl2eaO1sIrzR4PcgwqrSOjwBQQocsGAECQQD8QOra/mZmxPbt +bRh8U5lhgZmirImk5RY3QMPI/1/f4k+fyjkU5FRq/yqSyin75aSAXg8IupAFRgyZ +W7BT6zwBAkEA0A0ugAGorpCbuTa25SsIOMxkEzCiKYvh0O+GfGkzWG4lkSeJqGME +keuJGlXrZNKNoCYLluAKLPmnd72X2yTL7wJARM0kAXUP0wn324w8+HQIyqqBj/gF +Vt9Q7uMQQ3s72CGu3ANZDFS2nbRZFU5koxrggk6lRRk1fOq9NvrmHg10AQJABOea +pgfj+yGLmkUw8JwgGH6xCUbHO+WBUFSlPf+Y50fJeO+OrjqPXAVKeSV3ZCwWjKT4 +9viXJNJJ4WfF0bO/XwJAOMB1wQnEOSZ4v+laMwNtMq6hre5K8woqteXICoGcIWe8 +u3YLAbyW/lHhOCiZu2iAI8AbmXem9lW6Tr7p/97s0w== +-----END RSA PRIVATE KEY-----` + + testCertificate = `-----BEGIN CERTIFICATE----- +MIICwjCCAiugAwIBAgIBATANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEL +MAkGA1UECAwCQ0ExETAPBgNVBAoMCFNlY3VyaXR5MRswGQYDVQQLDBJPcGVuU2hp +ZnQzIHRlc3QgQ0ExFzAVBgNVBAMMDmhlYWRlci50ZXN0IENBMB4XDTE2MDMxMjA0 +MjEwM1oXDTM2MDMxMjA0MjEwM1owWDEUMBIGA1UEAwwLaGVhZGVyLnRlc3QxCzAJ +BgNVBAgMAkNBMQswCQYDVQQGEwJVUzERMA8GA1UECgwIU2VjdXJpdHkxEzARBgNV +BAsMCk9wZW5TaGlmdDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0 +XEAzUMflZy8zluwzqMKnu8jYK3yUoEGLN0Bw0A/7ydno1g0E92ee8M9p59TCCWA6 +nKnt1DEK5285xAKs9AveutSYiDkpf2px59GvCVx2ecfFBTECWHMAJ/6Y7pqlWOt2 +hvPx5rP+jVeNLAfK9d+f57FGvWXrQAcBnFTegS6J910kbvDgNP4Nerj6RPAx2UOq +6URqA4j7qZs63nReeu/1t//BQHNokKddfxw2ZXcL/5itgpPug16thp+ugGVdjcFs +aasLJOjErUS0D+7bot98FL0TSpxWqwtCF117bSLY7UczZFNAZAOnZBFmSZBxcJJa +TZzkda0Oiqo0J3GPcZ+rAgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQEL +BQADgYEACkdKRUm9ERjgbe6w0fw4VY1s5XC9qR1m5AwLMVVwKxHJVG2zMzeDTHyg +3cjxmfZdFU9yxmNUCh3mRsi2+qjEoFfGRyMwMMx7cduYhsFY3KA+Fl4vBRXAuPLR +eCI4ErCPi+Y08vOto9VVXg2f4YFQYLq1X6TiXD5RpQAN0t8AYk4= +-----END CERTIFICATE-----` + + testPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA9FxAM1DH5WcvM5bsM6jCp7vI2Ct8lKBBizdAcNAP+8nZ6NYN +BPdnnvDPaefUwglgOpyp7dQxCudvOcQCrPQL3rrUmIg5KX9qcefRrwlcdnnHxQUx +AlhzACf+mO6apVjrdobz8eaz/o1XjSwHyvXfn+exRr1l60AHAZxU3oEuifddJG7w +4DT+DXq4+kTwMdlDqulEagOI+6mbOt50Xnrv9bf/wUBzaJCnXX8cNmV3C/+YrYKT +7oNerYafroBlXY3BbGmrCyToxK1EtA/u26LffBS9E0qcVqsLQhdde20i2O1HM2RT +QGQDp2QRZkmQcXCSWk2c5HWtDoqqNCdxj3GfqwIDAQABAoIBAEfl+NHge+CIur+w +MXGFvziBLThFm1NTz9U5fZFz9q/8FUzH5m7GqMuASVb86oHpJlI4lFsw6vktXXGe +tbbT28Y+LJ1wv3jxT42SSwT4eSc278uNmnz5L2UlX2j6E7CA+E8YqCBN5DoKtm8I +PIbAT3sKPgP1aE6OuUEFEYeidOIMvjco2aQH0338sl6cObkQFEgnWf2ncun3KGnb +s+dMO5EdYLo0rOdDXY88sElfqiNYYl/FRu9O3OfqHvScA5uo9FlIhukcrRkbjFcq +j/7k4tt0iLs9B2j+4ihBWYo5eRFIde4Izj6a6ArEk0ShEUvwlZBuGMM/vs+jvbDK +l3+0NpECgYEA/+qxwvOGjmlYNKFK/rzxd51EnfCISnV+tb17pNyRmlGToi1/LmmV ++jcJfcwlf2o8mTFn3xAdD3fSaHF7t8Li7xDwH2S+sSuFE/8bhgHUvw1S7oILMYyO +hO6sWG+JocMhr8IejaAnQxav9VvP01YDfw/XBB0O1EIuzzr2KHq+AGMCgYEA9HCY +JGTcv7lfs3kcCAkDtjl8NbjNRMxRErG0dfYS+6OSaXOOMg1TsaSNEgjOGyUX+yQ4 +4vtKcLwHk7+qz3ZPbhS6m7theZG9jUwMrQRGyCE7z3JUy8vmV/N+HP0V+boT+4KM +Tai3+I3hf9+QMHYx/Z/VA0K6f27LwP+kEL9C8hkCgYEAoiHeXNRL+w1ihHVrPdgW +YuGQBz/MGOA3VoylON1Eoa/tCGIqoQzjp5IWwUwEtaRon+VdGUTsJFCVTPYYm2Ms +wqjIeBsrdLNNrE2C8nNWhXO7hr98t/eEk1NifOStHX6yaNdi4/cC6M4GzDtOf2WO +8YDniAOg0Xjcjw2bxil9FmECgYBuUeq4cjUW6okArSYzki30rhka/d7WsAffEgjK +PFbw7zADG74PZOhjAksQ2px6r9EU7ZInDxbXrmUVD6n9m/3ZRs25v2YMwfP0s1/9 +LjLr2+PsikMu/0VkaGaAmtCyNoMSPicoXX86VH5zgejHlnCVcO9oW1NkdBLNdhML +4+ZI8QKBgQDb+SH7i50Yu3adwvPkDSp3ACCzPoHXno79a7Y5S2JzpFtNq+cNLWEb +HP8gHJSZnaGrLKmjwNeQNsARYajKmDKO5HJ9g5H5Hae8enOb2yie541dneDT8rID +4054dMQJnijd8620yf8wiNy05ZPOQQ0JvA/rW3WWZc5PGm8c2PsVjg== +-----END RSA PRIVATE KEY-----` + + testCACertificate = `-----BEGIN CERTIFICATE----- +MIIClDCCAf2gAwIBAgIJAPU57OGhuqJtMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJDQTERMA8GA1UECgwIU2VjdXJpdHkxGzAZBgNVBAsM +Ek9wZW5TaGlmdDMgdGVzdCBDQTEXMBUGA1UEAwwOaGVhZGVyLnRlc3QgQ0EwHhcN +MTYwMzEyMDQyMTAzWhcNMzYwMzEyMDQyMTAzWjBjMQswCQYDVQQGEwJVUzELMAkG +A1UECAwCQ0ExETAPBgNVBAoMCFNlY3VyaXR5MRswGQYDVQQLDBJPcGVuU2hpZnQz +IHRlc3QgQ0ExFzAVBgNVBAMMDmhlYWRlci50ZXN0IENBMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCsdVIJ6GSrkFdE9LzsMItYGE4q3qqSqIbs/uwMoVsMT+33 +pLeyzeecPuoQsdO6SEuqhUM1ivUN4GyXIR1+aW2baMwMXpjX9VIJu5d4FqtGi6SD +RfV+tbERWwifPJlN+ryuvqbbDxrjQeXhemeo7yrJdgJ1oyDmoM5pTiSUUmltvQID +AQABo1AwTjAdBgNVHQ4EFgQUOVuieqGfp2wnKo7lX2fQt+Yk1C4wHwYDVR0jBBgw +FoAUOVuieqGfp2wnKo7lX2fQt+Yk1C4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B +AQsFAAOBgQA8VhmNeicRnKgXInVyYZDjL0P4WRbKJY7DkJxRMRWxikbEVHdySki6 +jegpqgJqYbzU6EiuTS2sl2bAjIK9nGUtTDt1PJIC1Evn5Q6v5ylNflpv6GxtUbCt +bGvtpjWA4r9WASIDPFsxk/cDEEEO6iPxgMOf5MdpQC2y2MU0rzF/Gg== +-----END CERTIFICATE-----` + + testDestinationCACertificate = testCACertificate +) + // TestValidateRouteBad ensures not specifying a required field results in error and a fully specified // route passes successfully func TestValidateRoute(t *testing.T) { @@ -429,6 +532,17 @@ func TestValidateTLSInsecureEdgeTerminationPolicy(t *testing.T) { }, }, }, + { + name: "Reencrypt termination DestCACert", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationReencrypt, + DestinationCACertificate: testDestinationCACertificate, + }, + }, + }, + }, } insecureTypes := []api.InsecureEdgeTerminationPolicyType{ @@ -544,3 +658,289 @@ func TestValidateNoTLSInsecureEdgeTerminationPolicy(t *testing.T) { } } } + +// TestExtendedValidateRoute ensures that a route's certificate and keys +// are valid. +func TestExtendedValidateRoute(t *testing.T) { + tests := []struct { + name string + route *api.Route + expectedErrors int + }{ + { + name: "No TLS Termination", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{ + Termination: "", + }, + }, + }, + expectedErrors: 1, + }, + { + name: "Passthrough termination OK", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationPassthrough, + }, + }, + }, + expectedErrors: 0, + }, + { + name: "Reencrypt termination OK with certs", + route: &api.Route{ + Spec: api.RouteSpec{ + Host: "www.example.com", + + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationReencrypt, + Certificate: testCertificate, + Key: testPrivateKey, + CACertificate: testCACertificate, + DestinationCACertificate: testDestinationCACertificate, + }, + }, + }, + expectedErrors: 0, + }, + { + name: "Reencrypt termination OK with bad config", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationReencrypt, + Certificate: "def", + Key: "ghi", + CACertificate: "jkl", + DestinationCACertificate: "abc", + }, + }, + }, + expectedErrors: 4, + }, + { + name: "Reencrypt termination OK without certs", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationReencrypt, + DestinationCACertificate: testDestinationCACertificate, + }, + }, + }, + expectedErrors: 0, + }, + { + name: "Reencrypt termination bad config without certs", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationReencrypt, + DestinationCACertificate: "abc", + }, + }, + }, + expectedErrors: 1, + }, + { + name: "Reencrypt termination no dest cert", + route: &api.Route{ + Spec: api.RouteSpec{ + Host: "www.example.com", + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationReencrypt, + Certificate: testCertificate, + Key: testPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + expectedErrors: 1, + }, + { + name: "Edge termination OK with certs without host", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationEdge, + Certificate: testCertificate, + Key: testPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + expectedErrors: 0, + }, + { + name: "Edge termination OK with certs", + route: &api.Route{ + Spec: api.RouteSpec{ + Host: "www.example.com", + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationEdge, + Certificate: testCertificate, + Key: testPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + expectedErrors: 0, + }, + { + name: "Edge termination bad config with certs", + route: &api.Route{ + Spec: api.RouteSpec{ + Host: "www.example.com", + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationEdge, + Certificate: "abc", + Key: "abc", + CACertificate: "abc", + }, + }, + }, + expectedErrors: 3, + }, + { + name: "Edge termination mismatched key and cert", + route: &api.Route{ + Spec: api.RouteSpec{ + Host: "www.example.com", + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationEdge, + Certificate: testCertificate, + Key: testExpiredCertPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + expectedErrors: 1, + }, + { + name: "Edge termination expired cert", + route: &api.Route{ + Spec: api.RouteSpec{ + Host: "www.example.com", + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationEdge, + Certificate: testExpiredCAUnknownCertificate, + Key: testExpiredCertPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + expectedErrors: 1, + }, + { + name: "Edge termination expired cert key mismatch", + route: &api.Route{ + Spec: api.RouteSpec{ + Host: "www.example.com", + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationEdge, + Certificate: testExpiredCAUnknownCertificate, + Key: testPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + expectedErrors: 2, + }, + { + name: "Edge termination OK without certs", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationEdge, + }, + }, + }, + expectedErrors: 0, + }, + { + name: "Edge termination, dest cert", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationEdge, + DestinationCACertificate: "abc", + }, + }, + }, + expectedErrors: 2, + }, + { + name: "Passthrough termination, cert", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{Termination: api.TLSTerminationPassthrough, Certificate: "test"}, + }, + }, + expectedErrors: 3, + }, + { + name: "Passthrough termination, key", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{Termination: api.TLSTerminationPassthrough, Key: "test"}, + }, + }, + expectedErrors: 1, + }, + { + name: "Passthrough termination, ca cert", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{Termination: api.TLSTerminationPassthrough, CACertificate: "test"}, + }, + }, + expectedErrors: 2, + }, + { + name: "Passthrough termination, dest ca cert", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{Termination: api.TLSTerminationPassthrough, DestinationCACertificate: "test"}, + }, + }, + expectedErrors: 2, + }, + { + name: "Invalid termination type", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{ + Termination: "invalid", + }, + }, + }, + expectedErrors: 1, + }, + { + name: "Double escaped newlines", + route: &api.Route{ + Spec: api.RouteSpec{ + TLS: &api.TLSConfig{ + Termination: api.TLSTerminationReencrypt, + Certificate: "d\\nef", + Key: "g\\nhi", + CACertificate: "j\\nkl", + DestinationCACertificate: "j\\nkl", + }, + }, + }, + expectedErrors: 4, + }, + } + + for _, tc := range tests { + errs := ExtendedValidateRoute(tc.route) + + if len(errs) != tc.expectedErrors { + t.Errorf("Test case %s expected %d error(s), got %d. %v", tc.name, tc.expectedErrors, len(errs), errs) + } + } +} diff --git a/pkg/router/controller/extended_validator.go b/pkg/router/controller/extended_validator.go new file mode 100644 index 000000000000..bf912e91bffa --- /dev/null +++ b/pkg/router/controller/extended_validator.go @@ -0,0 +1,75 @@ +package controller + +import ( + "fmt" + "reflect" + + "github.com/golang/glog" + kapi "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/sets" + "k8s.io/kubernetes/pkg/watch" + + routeapi "github.com/openshift/origin/pkg/route/api" + "github.com/openshift/origin/pkg/route/api/validation" + "github.com/openshift/origin/pkg/router" +) + +// ExtendedValidator implements the router.Plugin interface to provide +// extended config validation for template based, backend-agnostic routers. +type ExtendedValidator struct { + // plugin is the next plugin in the chain. + plugin router.Plugin + + // recorder is an interface for indicating route rejections. + recorder RejectionRecorder + + // invalidRoutes is a map of invalid routes previously encountered. + invalidRoutes map[string]routeapi.Route +} + +// ExtendedValidator creates a plugin wrapper that ensures only routes that +// pass extended validation are relayed to the next plugin in the chain. +// Recorder is an interface for indicating why a route was rejected. +func NewExtendedValidator(plugin router.Plugin, recorder RejectionRecorder) *ExtendedValidator { + return &ExtendedValidator{ + plugin: plugin, + recorder: recorder, + invalidRoutes: make(map[string]routeapi.Route), + } +} + +// HandleEndpoints processes watch events on the Endpoints resource. +func (p *ExtendedValidator) HandleEndpoints(eventType watch.EventType, endpoints *kapi.Endpoints) error { + return p.plugin.HandleEndpoints(eventType, endpoints) +} + +// HandleRoute processes watch events on the Route resource. +func (p *ExtendedValidator) HandleRoute(eventType watch.EventType, route *routeapi.Route) error { + // Check if previously seen route and its Spec is unchanged. + routeName := routeNameKey(route) + old, ok := p.invalidRoutes[routeName] + if ok && reflect.DeepEqual(old.Spec, route.Spec) { + // Route spec was unchanged and it is already marked in + // error, we don't need to do anything more. + return fmt.Errorf("invalid route configuration") + } + + if errs := validation.ExtendedValidateRoute(route); len(errs) > 0 { + errmsg := "" + for i := 0; i < len(errs); i++ { + errmsg = errmsg + "\n - " + errs[i].Error() + } + glog.Errorf("Skipping route %s due to invalid configuration: %s", routeName, errmsg) + + p.recorder.RecordRouteRejection(route, "ExtendedValidationFailed", errmsg) + return fmt.Errorf("invalid route configuration") + } + + return p.plugin.HandleRoute(eventType, route) +} + +// HandleAllowedNamespaces limits the scope of valid routes to only those that match +// the provided namespace list. +func (p *ExtendedValidator) HandleNamespaces(namespaces sets.String) error { + return p.plugin.HandleNamespaces(namespaces) +} diff --git a/pkg/router/template/plugin_test.go b/pkg/router/template/plugin_test.go index 8d0484c91bd4..03971825dcae 100644 --- a/pkg/router/template/plugin_test.go +++ b/pkg/router/template/plugin_test.go @@ -16,6 +16,109 @@ import ( "github.com/openshift/origin/pkg/router/controller" ) +const ( + testExpiredCAUnknownCertificate = `-----BEGIN CERTIFICATE----- +MIIDIjCCAgqgAwIBAgIBBjANBgkqhkiG9w0BAQUFADCBoTELMAkGA1UEBhMCVVMx +CzAJBgNVBAgMAlNDMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl +ZmF1bHQgQ29tcGFueSBMdGQxEDAOBgNVBAsMB1Rlc3QgQ0ExGjAYBgNVBAMMEXd3 +dy5leGFtcGxlY2EuY29tMSIwIAYJKoZIhvcNAQkBFhNleGFtcGxlQGV4YW1wbGUu +Y29tMB4XDTE2MDExMzE5NDA1N1oXDTI2MDExMDE5NDA1N1owfDEYMBYGA1UEAxMP +d3d3LmV4YW1wbGUuY29tMQswCQYDVQQIEwJTQzELMAkGA1UEBhMCVVMxIjAgBgkq +hkiG9w0BCQEWE2V4YW1wbGVAZXhhbXBsZS5jb20xEDAOBgNVBAoTB0V4YW1wbGUx +EDAOBgNVBAsTB0V4YW1wbGUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM0B +u++oHV1wcphWRbMLUft8fD7nPG95xs7UeLPphFZuShIhhdAQMpvcsFeg+Bg9PWCu +v3jZljmk06MLvuWLfwjYfo9q/V+qOZVfTVHHbaIO5RTXJMC2Nn+ACF0kHBmNcbth +OOgF8L854a/P8tjm1iPR++vHnkex0NH7lyosVc/vAgMBAAGjDTALMAkGA1UdEwQC +MAAwDQYJKoZIhvcNAQEFBQADggEBADjFm5AlNH3DNT1Uzx3m66fFjqqrHEs25geT +yA3rvBuynflEHQO95M/8wCxYVyuAx4Z1i4YDC7tx0vmOn/2GXZHY9MAj1I8KCnwt +Jik7E2r1/yY0MrkawljOAxisXs821kJ+Z/51Ud2t5uhGxS6hJypbGspMS7OtBbw7 +8oThK7cWtCXOldNF6ruqY1agWnhRdAq5qSMnuBXuicOP0Kbtx51a1ugE3SnvQenJ +nZxdtYUXvEsHZC/6bAtTfNh+/SwgxQJuL2ZM+VG3X2JIKY8xTDui+il7uTh422lq +wED8uwKl+bOj6xFDyw4gWoBxRobsbFaME8pkykP1+GnKDberyAM= +-----END CERTIFICATE-----` + + testExpiredCertPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDNAbvvqB1dcHKYVkWzC1H7fHw+5zxvecbO1Hiz6YRWbkoSIYXQ +EDKb3LBXoPgYPT1grr942ZY5pNOjC77li38I2H6Pav1fqjmVX01Rx22iDuUU1yTA +tjZ/gAhdJBwZjXG7YTjoBfC/OeGvz/LY5tYj0fvrx55HsdDR+5cqLFXP7wIDAQAB +AoGAfE7P4Zsj6zOzGPI/Izj7Bi5OvGnEeKfzyBiH9Dflue74VRQkqqwXs/DWsNv3 +c+M2Y3iyu5ncgKmUduo5X8D9To2ymPRLGuCdfZTxnBMpIDKSJ0FTwVPkr6cYyyBk +5VCbc470pQPxTAAtl2eaO1sIrzR4PcgwqrSOjwBQQocsGAECQQD8QOra/mZmxPbt +bRh8U5lhgZmirImk5RY3QMPI/1/f4k+fyjkU5FRq/yqSyin75aSAXg8IupAFRgyZ +W7BT6zwBAkEA0A0ugAGorpCbuTa25SsIOMxkEzCiKYvh0O+GfGkzWG4lkSeJqGME +keuJGlXrZNKNoCYLluAKLPmnd72X2yTL7wJARM0kAXUP0wn324w8+HQIyqqBj/gF +Vt9Q7uMQQ3s72CGu3ANZDFS2nbRZFU5koxrggk6lRRk1fOq9NvrmHg10AQJABOea +pgfj+yGLmkUw8JwgGH6xCUbHO+WBUFSlPf+Y50fJeO+OrjqPXAVKeSV3ZCwWjKT4 +9viXJNJJ4WfF0bO/XwJAOMB1wQnEOSZ4v+laMwNtMq6hre5K8woqteXICoGcIWe8 +u3YLAbyW/lHhOCiZu2iAI8AbmXem9lW6Tr7p/97s0w== +-----END RSA PRIVATE KEY-----` + + testCertificate = `-----BEGIN CERTIFICATE----- +MIICwjCCAiugAwIBAgIBATANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEL +MAkGA1UECAwCQ0ExETAPBgNVBAoMCFNlY3VyaXR5MRswGQYDVQQLDBJPcGVuU2hp +ZnQzIHRlc3QgQ0ExFzAVBgNVBAMMDmhlYWRlci50ZXN0IENBMB4XDTE2MDMxMjA0 +MjEwM1oXDTM2MDMxMjA0MjEwM1owWDEUMBIGA1UEAwwLaGVhZGVyLnRlc3QxCzAJ +BgNVBAgMAkNBMQswCQYDVQQGEwJVUzERMA8GA1UECgwIU2VjdXJpdHkxEzARBgNV +BAsMCk9wZW5TaGlmdDMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0 +XEAzUMflZy8zluwzqMKnu8jYK3yUoEGLN0Bw0A/7ydno1g0E92ee8M9p59TCCWA6 +nKnt1DEK5285xAKs9AveutSYiDkpf2px59GvCVx2ecfFBTECWHMAJ/6Y7pqlWOt2 +hvPx5rP+jVeNLAfK9d+f57FGvWXrQAcBnFTegS6J910kbvDgNP4Nerj6RPAx2UOq +6URqA4j7qZs63nReeu/1t//BQHNokKddfxw2ZXcL/5itgpPug16thp+ugGVdjcFs +aasLJOjErUS0D+7bot98FL0TSpxWqwtCF117bSLY7UczZFNAZAOnZBFmSZBxcJJa +TZzkda0Oiqo0J3GPcZ+rAgMBAAGjDTALMAkGA1UdEwQCMAAwDQYJKoZIhvcNAQEL +BQADgYEACkdKRUm9ERjgbe6w0fw4VY1s5XC9qR1m5AwLMVVwKxHJVG2zMzeDTHyg +3cjxmfZdFU9yxmNUCh3mRsi2+qjEoFfGRyMwMMx7cduYhsFY3KA+Fl4vBRXAuPLR +eCI4ErCPi+Y08vOto9VVXg2f4YFQYLq1X6TiXD5RpQAN0t8AYk4= +-----END CERTIFICATE-----` + + testPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA9FxAM1DH5WcvM5bsM6jCp7vI2Ct8lKBBizdAcNAP+8nZ6NYN +BPdnnvDPaefUwglgOpyp7dQxCudvOcQCrPQL3rrUmIg5KX9qcefRrwlcdnnHxQUx +AlhzACf+mO6apVjrdobz8eaz/o1XjSwHyvXfn+exRr1l60AHAZxU3oEuifddJG7w +4DT+DXq4+kTwMdlDqulEagOI+6mbOt50Xnrv9bf/wUBzaJCnXX8cNmV3C/+YrYKT +7oNerYafroBlXY3BbGmrCyToxK1EtA/u26LffBS9E0qcVqsLQhdde20i2O1HM2RT +QGQDp2QRZkmQcXCSWk2c5HWtDoqqNCdxj3GfqwIDAQABAoIBAEfl+NHge+CIur+w +MXGFvziBLThFm1NTz9U5fZFz9q/8FUzH5m7GqMuASVb86oHpJlI4lFsw6vktXXGe +tbbT28Y+LJ1wv3jxT42SSwT4eSc278uNmnz5L2UlX2j6E7CA+E8YqCBN5DoKtm8I +PIbAT3sKPgP1aE6OuUEFEYeidOIMvjco2aQH0338sl6cObkQFEgnWf2ncun3KGnb +s+dMO5EdYLo0rOdDXY88sElfqiNYYl/FRu9O3OfqHvScA5uo9FlIhukcrRkbjFcq +j/7k4tt0iLs9B2j+4ihBWYo5eRFIde4Izj6a6ArEk0ShEUvwlZBuGMM/vs+jvbDK +l3+0NpECgYEA/+qxwvOGjmlYNKFK/rzxd51EnfCISnV+tb17pNyRmlGToi1/LmmV ++jcJfcwlf2o8mTFn3xAdD3fSaHF7t8Li7xDwH2S+sSuFE/8bhgHUvw1S7oILMYyO +hO6sWG+JocMhr8IejaAnQxav9VvP01YDfw/XBB0O1EIuzzr2KHq+AGMCgYEA9HCY +JGTcv7lfs3kcCAkDtjl8NbjNRMxRErG0dfYS+6OSaXOOMg1TsaSNEgjOGyUX+yQ4 +4vtKcLwHk7+qz3ZPbhS6m7theZG9jUwMrQRGyCE7z3JUy8vmV/N+HP0V+boT+4KM +Tai3+I3hf9+QMHYx/Z/VA0K6f27LwP+kEL9C8hkCgYEAoiHeXNRL+w1ihHVrPdgW +YuGQBz/MGOA3VoylON1Eoa/tCGIqoQzjp5IWwUwEtaRon+VdGUTsJFCVTPYYm2Ms +wqjIeBsrdLNNrE2C8nNWhXO7hr98t/eEk1NifOStHX6yaNdi4/cC6M4GzDtOf2WO +8YDniAOg0Xjcjw2bxil9FmECgYBuUeq4cjUW6okArSYzki30rhka/d7WsAffEgjK +PFbw7zADG74PZOhjAksQ2px6r9EU7ZInDxbXrmUVD6n9m/3ZRs25v2YMwfP0s1/9 +LjLr2+PsikMu/0VkaGaAmtCyNoMSPicoXX86VH5zgejHlnCVcO9oW1NkdBLNdhML +4+ZI8QKBgQDb+SH7i50Yu3adwvPkDSp3ACCzPoHXno79a7Y5S2JzpFtNq+cNLWEb +HP8gHJSZnaGrLKmjwNeQNsARYajKmDKO5HJ9g5H5Hae8enOb2yie541dneDT8rID +4054dMQJnijd8620yf8wiNy05ZPOQQ0JvA/rW3WWZc5PGm8c2PsVjg== +-----END RSA PRIVATE KEY-----` + + testCACertificate = `-----BEGIN CERTIFICATE----- +MIIClDCCAf2gAwIBAgIJAPU57OGhuqJtMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJDQTERMA8GA1UECgwIU2VjdXJpdHkxGzAZBgNVBAsM +Ek9wZW5TaGlmdDMgdGVzdCBDQTEXMBUGA1UEAwwOaGVhZGVyLnRlc3QgQ0EwHhcN +MTYwMzEyMDQyMTAzWhcNMzYwMzEyMDQyMTAzWjBjMQswCQYDVQQGEwJVUzELMAkG +A1UECAwCQ0ExETAPBgNVBAoMCFNlY3VyaXR5MRswGQYDVQQLDBJPcGVuU2hpZnQz +IHRlc3QgQ0ExFzAVBgNVBAMMDmhlYWRlci50ZXN0IENBMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQCsdVIJ6GSrkFdE9LzsMItYGE4q3qqSqIbs/uwMoVsMT+33 +pLeyzeecPuoQsdO6SEuqhUM1ivUN4GyXIR1+aW2baMwMXpjX9VIJu5d4FqtGi6SD +RfV+tbERWwifPJlN+ryuvqbbDxrjQeXhemeo7yrJdgJ1oyDmoM5pTiSUUmltvQID +AQABo1AwTjAdBgNVHQ4EFgQUOVuieqGfp2wnKo7lX2fQt+Yk1C4wHwYDVR0jBBgw +FoAUOVuieqGfp2wnKo7lX2fQt+Yk1C4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B +AQsFAAOBgQA8VhmNeicRnKgXInVyYZDjL0P4WRbKJY7DkJxRMRWxikbEVHdySki6 +jegpqgJqYbzU6EiuTS2sl2bAjIK9nGUtTDt1PJIC1Evn5Q6v5ylNflpv6GxtUbCt +bGvtpjWA4r9WASIDPFsxk/cDEEEO6iPxgMOf5MdpQC2y2MU0rzF/Gg== +-----END CERTIFICATE-----` + + testDestinationCACertificate = testCACertificate +) + // TestRouter provides an implementation of the plugin's router interface suitable for unit testing. type TestRouter struct { State map[string]ServiceUnit @@ -536,6 +639,360 @@ func TestHandleRoute(t *testing.T) { } } +// TestHandleRouteExtendedValidation test route watch events with extended route configuration validation. +func TestHandleRouteExtendedValidation(t *testing.T) { + rejections := &fakeRejections{} + router := newTestRouter(make(map[string]ServiceUnit)) + templatePlugin := newDefaultTemplatePlugin(router, true) + // TODO: move tests that rely on unique hosts to pkg/router/controller and remove them from + // here + extendedValidatorPlugin := controller.NewExtendedValidator(templatePlugin, rejections) + plugin := controller.NewUniqueHost(extendedValidatorPlugin, controller.HostForRoute, rejections) + + original := unversioned.Time{Time: time.Now()} + + //add + route := &routeapi.Route{ + ObjectMeta: kapi.ObjectMeta{ + CreationTimestamp: original, + Namespace: "foo", + Name: "test", + }, + Spec: routeapi.RouteSpec{ + Host: "www.example.com", + To: kapi.ObjectReference{ + Name: "TestService", + }, + }, + } + serviceUnitKey := fmt.Sprintf("%s/%s", route.Namespace, route.Spec.To.Name) + + plugin.HandleRoute(watch.Added, route) + + if !router.Committed { + t.Errorf("Expected router to be committed after HandleRoute call") + } + + actualSU, ok := router.FindServiceUnit(serviceUnitKey) + + if !ok { + t.Errorf("TestHandleRoute was unable to find the service unit %s after HandleRoute was called", route.Spec.To.Name) + } else { + serviceAliasCfg, ok := actualSU.ServiceAliasConfigs[router.routeKey(route)] + + if !ok { + t.Errorf("TestHandleRoute expected route key %s", router.routeKey(route)) + } else { + if serviceAliasCfg.Host != route.Spec.Host || serviceAliasCfg.Path != route.Spec.Path { + t.Errorf("Expected route did not match service alias config %v : %v", route, serviceAliasCfg) + } + } + } + + if len(rejections.rejections) > 0 { + t.Fatalf("did not expect a recorded rejection: %#v", rejections) + } + + tests := []struct { + name string + route *routeapi.Route + errorExpected bool + }{ + { + name: "No TLS Termination", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.no.tls.test", + TLS: &routeapi.TLSConfig{ + Termination: "", + }, + }, + }, + errorExpected: true, + }, + { + name: "Passthrough termination OK", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.passthrough.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationPassthrough, + }, + }, + }, + errorExpected: false, + }, + { + name: "Reencrypt termination OK with certs", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.example.com", + + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationReencrypt, + Certificate: testCertificate, + Key: testPrivateKey, + CACertificate: testCACertificate, + DestinationCACertificate: testDestinationCACertificate, + }, + }, + }, + errorExpected: false, + }, + { + name: "Reencrypt termination OK with bad config", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.reencypt.badconfig.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationReencrypt, + Certificate: "def", + Key: "ghi", + CACertificate: "jkl", + DestinationCACertificate: "abc", + }, + }, + }, + errorExpected: true, + }, + { + name: "Reencrypt termination OK without certs", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.reencypt.nocerts.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationReencrypt, + DestinationCACertificate: testDestinationCACertificate, + }, + }, + }, + errorExpected: false, + }, + { + name: "Reencrypt termination bad config without certs", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.reencypt.badconfignocerts.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationReencrypt, + DestinationCACertificate: "abc", + }, + }, + }, + errorExpected: true, + }, + { + name: "Reencrypt termination no dest cert", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.reencypt.nodestcert.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationReencrypt, + Certificate: testCertificate, + Key: testPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + errorExpected: true, + }, + { + name: "Edge termination OK with certs without host", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationEdge, + Certificate: testCertificate, + Key: testPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + errorExpected: false, + }, + { + name: "Edge termination OK with certs", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.example.com", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationEdge, + Certificate: testCertificate, + Key: testPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + errorExpected: false, + }, + { + name: "Edge termination bad config with certs", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.edge.badconfig.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationEdge, + Certificate: "abc", + Key: "abc", + CACertificate: "abc", + }, + }, + }, + errorExpected: true, + }, + { + name: "Edge termination mismatched key and cert", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.edge.mismatchdkeyandcert.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationEdge, + Certificate: testCertificate, + Key: testExpiredCertPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + errorExpected: true, + }, + { + name: "Edge termination expired cert", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.edge.expiredcert.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationEdge, + Certificate: testExpiredCAUnknownCertificate, + Key: testExpiredCertPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + errorExpected: true, + }, + { + name: "Edge termination expired cert key mismatch", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.edge.expiredcertkeymismatch.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationEdge, + Certificate: testExpiredCAUnknownCertificate, + Key: testPrivateKey, + CACertificate: testCACertificate, + }, + }, + }, + errorExpected: true, + }, + { + name: "Edge termination OK without certs", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.edge.nocerts.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationEdge, + }, + }, + }, + errorExpected: false, + }, + { + name: "Edge termination, bad dest cert", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.edge.baddestcert.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationEdge, + DestinationCACertificate: "abc", + }, + }, + }, + errorExpected: true, + }, + { + name: "Passthrough termination, bad cert", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.passthrough.badcert.test", + TLS: &routeapi.TLSConfig{Termination: routeapi.TLSTerminationPassthrough, Certificate: "test"}, + }, + }, + errorExpected: true, + }, + { + name: "Passthrough termination, bad key", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.passthrough.badkey.test", + TLS: &routeapi.TLSConfig{Termination: routeapi.TLSTerminationPassthrough, Key: "test"}, + }, + }, + errorExpected: true, + }, + { + name: "Passthrough termination, bad ca cert", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.passthrough.badcacert.test", + TLS: &routeapi.TLSConfig{Termination: routeapi.TLSTerminationPassthrough, CACertificate: "test"}, + }, + }, + errorExpected: true, + }, + { + name: "Passthrough termination, bad dest ca cert", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.passthrough.baddestcacert.test", + TLS: &routeapi.TLSConfig{Termination: routeapi.TLSTerminationPassthrough, DestinationCACertificate: "test"}, + }, + }, + errorExpected: true, + }, + { + name: "Invalid termination type", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + TLS: &routeapi.TLSConfig{ + Termination: "invalid", + }, + }, + }, + errorExpected: false, + }, + { + name: "Double escaped newlines", + route: &routeapi.Route{ + Spec: routeapi.RouteSpec{ + Host: "www.reencrypt.doubleescapednewlines.test", + TLS: &routeapi.TLSConfig{ + Termination: routeapi.TLSTerminationReencrypt, + Certificate: "d\\nef", + Key: "g\\nhi", + CACertificate: "j\\nkl", + DestinationCACertificate: "j\\nkl", + }, + }, + }, + errorExpected: true, + }, + } + + for _, tc := range tests { + err := plugin.HandleRoute(watch.Added, tc.route) + if tc.errorExpected { + if err == nil { + t.Fatalf("test case %s: expected an error, got none", tc.name) + } + } else { + if err != nil { + t.Fatalf("test case %s: expected no errors, got %v", tc.name, err) + } + } + } +} + func TestNamespaceScopingFromEmpty(t *testing.T) { router := newTestRouter(make(map[string]ServiceUnit)) templatePlugin := newDefaultTemplatePlugin(router, true) diff --git a/pkg/router/template/router.go b/pkg/router/template/router.go index 654248fc4495..bc58b94e8985 100644 --- a/pkg/router/template/router.go +++ b/pkg/router/template/router.go @@ -306,7 +306,7 @@ func (r *templateRouter) reloadRouter() error { cmd := exec.Command(r.reloadScriptPath) out, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("error reloading router: %v\n%s", err, out) + return fmt.Errorf("error reloading router: %v\n%s", err, string(out)) } glog.Infof("Router reloaded:\n%s", out) return nil diff --git a/pkg/router/template/router_test.go b/pkg/router/template/router_test.go index 9acd3876cfc8..5be460f65918 100644 --- a/pkg/router/template/router_test.go +++ b/pkg/router/template/router_test.go @@ -319,7 +319,7 @@ func TestAddRoute(t *testing.T) { saCfg, ok := su.ServiceAliasConfigs[routeKey] if !ok { - t.Errorf("Unable to find created serivce alias config for route %s", routeKey) + t.Errorf("Unable to find created service alias config for route %s", routeKey) } else { if saCfg.Host != route.Spec.Host || saCfg.Path != route.Spec.Path || !compareTLS(route, saCfg, t) { t.Errorf("Route %v did not match serivce alias config %v", route, saCfg)