Skip to content

Commit

Permalink
wip: perform route validation on external v1 api
Browse files Browse the repository at this point in the history
to allow for reuse in ocp kas on variants serving route via CRD
  • Loading branch information
benluddy committed Sep 26, 2022
1 parent 6ee7ddb commit 3a28992
Show file tree
Hide file tree
Showing 4 changed files with 463 additions and 395 deletions.
347 changes: 34 additions & 313 deletions pkg/route/apis/route/validation/validation.go
@@ -1,344 +1,65 @@
package validation

import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"

routev1 "github.com/openshift/api/route/v1"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/sets"
kvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/core/validation"

routeapi "github.com/openshift/openshift-apiserver/pkg/route/apis/route"
routev1 "github.com/openshift/api/route/v1"
routeinternal "github.com/openshift/openshift-apiserver/pkg/route/apis/route"
routev1conversion "github.com/openshift/openshift-apiserver/pkg/route/apis/route/v1"
)

var ValidateRouteName = apimachineryvalidation.NameIsDNSSubdomain

// ValidateRoute tests if required fields in the route are set.
func ValidateRoute(route *routeapi.Route) field.ErrorList {
return validateRoute(route, true)
}

// validLabels - used in the ValidateRouteUpdate function to check if "older" routes conform to DNS1123Labels or not
func validLabels(host string) bool {
if len(host) == 0 {
return true
}
return checkLabelSegments(host)
}

// checkLabelSegments - function that checks if hostname labels conform to DNS1123Labels
func checkLabelSegments(host string) bool {
segments := strings.Split(host, ".")
for _, s := range segments {
errs := kvalidation.IsDNS1123Label(s)
if len(errs) > 0 {
return false
}
func toRouteV1(internal *routeinternal.Route) (*routev1.Route, field.ErrorList) {
var external routev1.Route
if err := routev1conversion.Convert_route_Route_To_v1_Route(internal, &external, nil); err != nil {
return nil, field.ErrorList{field.InternalError(field.NewPath(""), err)}
}
return true
return &external, nil
}

// validateRoute - private function to validate route
func validateRoute(route *routeapi.Route, checkHostname bool) field.ErrorList {
//ensure meta is set properly
result := validation.ValidateObjectMeta(&route.ObjectMeta, true, ValidateRouteName, field.NewPath("metadata"))

specPath := field.NewPath("spec")

//host is not required but if it is set ensure it meets DNS requirements
if len(route.Spec.Host) > 0 {
if len(kvalidation.IsDNS1123Subdomain(route.Spec.Host)) != 0 {
result = append(result, field.Invalid(specPath.Child("host"), route.Spec.Host, "host must conform to DNS 952 subdomain conventions"))
}

// Check the hostname only if the old route did not have an invalid DNS1123Label
// and the new route cares about DNS compliant labels.
if checkHostname && route.Annotations[routev1.AllowNonDNSCompliantHostAnnotation] != "true" {
segments := strings.Split(route.Spec.Host, ".")
for _, s := range segments {
errs := kvalidation.IsDNS1123Label(s)
for _, e := range errs {
result = append(result, field.Invalid(specPath.Child("host"), route.Spec.Host, e))
}
}
}
}

if len(route.Spec.Subdomain) > 0 {
// Subdomain is not lenient because it was never used outside of
// routes.
//
// TODO: Use ValidateSubdomain from library-go.
if len(route.Spec.Subdomain) > kvalidation.DNS1123SubdomainMaxLength {
result = append(result, field.Invalid(field.NewPath("spec.subdomain"), route.Spec.Subdomain, kvalidation.MaxLenError(kvalidation.DNS1123SubdomainMaxLength)))
}
for _, label := range strings.Split(route.Spec.Subdomain, ".") {
if errs := kvalidation.IsDNS1123Label(label); len(errs) > 0 {
result = append(result, field.Invalid(field.NewPath("spec.subdomain"), label, strings.Join(errs, ", ")))
}
}
}

if err := validateWildcardPolicy(route.Spec.Host, route.Spec.WildcardPolicy, specPath.Child("wildcardPolicy")); err != nil {
result = append(result, err)
}

if len(route.Spec.Path) > 0 && !strings.HasPrefix(route.Spec.Path, "/") {
result = append(result, field.Invalid(specPath.Child("path"), route.Spec.Path, "path must begin with /"))
func ValidateRoute(route *routeinternal.Route) field.ErrorList {
external, errs := toRouteV1(route)
if len(errs) > 0 {
return errs
}

if len(route.Spec.Path) > 0 && route.Spec.TLS != nil &&
route.Spec.TLS.Termination == routeapi.TLSTerminationPassthrough {
result = append(result, field.Invalid(specPath.Child("path"), route.Spec.Path, "passthrough termination does not support paths"))
}

if len(route.Spec.To.Name) == 0 {
result = append(result, field.Required(specPath.Child("to", "name"), ""))
}
if route.Spec.To.Kind != "Service" {
result = append(result, field.Invalid(specPath.Child("to", "kind"), route.Spec.To.Kind, "must reference a Service"))
}
if route.Spec.To.Weight != nil && (*route.Spec.To.Weight < 0 || *route.Spec.To.Weight > 256) {
result = append(result, field.Invalid(specPath.Child("to", "weight"), route.Spec.To.Weight, "weight must be an integer between 0 and 256"))
}

backendPath := specPath.Child("alternateBackends")
if len(route.Spec.AlternateBackends) > 3 {
result = append(result, field.Required(backendPath, "cannot specify more than 3 alternate backends"))
}
for i, svc := range route.Spec.AlternateBackends {
if len(svc.Name) == 0 {
result = append(result, field.Required(backendPath.Index(i).Child("name"), ""))
}
if svc.Kind != "Service" {
result = append(result, field.Invalid(backendPath.Index(i).Child("kind"), svc.Kind, "must reference a Service"))
}
if svc.Weight != nil && (*svc.Weight < 0 || *svc.Weight > 256) {
result = append(result, field.Invalid(backendPath.Index(i).Child("weight"), svc.Weight, "weight must be an integer between 0 and 256"))
}
}
return validateRouteV1(external)
}

if route.Spec.Port != nil {
switch target := route.Spec.Port.TargetPort; {
case target.Type == intstr.Int && target.IntVal == 0,
target.Type == intstr.String && len(target.StrVal) == 0:
result = append(result, field.Required(specPath.Child("port", "targetPort"), ""))
}
func ValidateRouteUpdate(route *routeinternal.Route, oldRoute *routeinternal.Route) field.ErrorList {
external, errs := toRouteV1(route)
if len(errs) > 0 {
return errs
}

if errs := validateTLS(route, specPath.Child("tls")); len(errs) != 0 {
result = append(result, errs...)
oldExternal, errs := toRouteV1(oldRoute)
if len(errs) > 0 {
return errs
}

return result
return validateRouteUpdateV1(external, oldExternal)
}

func ValidateRouteUpdate(route *routeapi.Route, older *routeapi.Route) field.ErrorList {
allErrs := validation.ValidateObjectMetaUpdate(&route.ObjectMeta, &older.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, validation.ValidateImmutableField(route.Spec.WildcardPolicy, older.Spec.WildcardPolicy, field.NewPath("spec", "wildcardPolicy"))...)
hostnameUpdated := route.Spec.Host != older.Spec.Host
allErrs = append(allErrs, validateRoute(route, hostnameUpdated && validLabels(older.Spec.Host))...)
return allErrs
}

// ValidateRouteStatusUpdate validates status updates for routes.
//
// Note that this function shouldn't call ValidateRouteUpdate, otherwise
// we are risking to break existing routes.
func ValidateRouteStatusUpdate(route *routeapi.Route, older *routeapi.Route) field.ErrorList {
allErrs := validation.ValidateObjectMetaUpdate(&route.ObjectMeta, &older.ObjectMeta, field.NewPath("metadata"))

// TODO: validate route status
return allErrs
}

type blockVerifierFunc func(block *pem.Block) (*pem.Block, error)

func publicKeyBlockVerifier(block *pem.Block) (*pem.Block, error) {
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
block = &pem.Block{
Type: "PUBLIC KEY",
}
if block.Bytes, err = x509.MarshalPKIXPublicKey(key); err != nil {
return nil, err
}
return block, nil
}

func certificateBlockVerifier(block *pem.Block) (*pem.Block, error) {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
block = &pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}
return block, nil
}

func privateKeyBlockVerifier(block *pem.Block) (*pem.Block, error) {
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
key, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("block %s is not valid", block.Type)
}
}
}
switch t := key.(type) {
case *rsa.PrivateKey:
block = &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(t),
}
case *ecdsa.PrivateKey:
block = &pem.Block{
Type: "ECDSA PRIVATE KEY",
}
if block.Bytes, err = x509.MarshalECPrivateKey(t); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("block private key %T is not valid", key)
}
return block, nil
}

func ignoreBlockVerifier(block *pem.Block) (*pem.Block, error) {
return nil, nil
}

var knownBlockDecoders = map[string]blockVerifierFunc{
"RSA PRIVATE KEY": privateKeyBlockVerifier,
"ECDSA PRIVATE KEY": privateKeyBlockVerifier,
"PRIVATE KEY": privateKeyBlockVerifier,
"PUBLIC KEY": publicKeyBlockVerifier,
// Potential "in the wild" PEM encoded blocks that can be normalized
"RSA PUBLIC KEY": publicKeyBlockVerifier,
"DSA PUBLIC KEY": publicKeyBlockVerifier,
"ECDSA PUBLIC KEY": publicKeyBlockVerifier,
"CERTIFICATE": certificateBlockVerifier,
// Blocks that should be dropped
"EC PARAMETERS": ignoreBlockVerifier,
}

// validateTLS tests fields for different types of TLS combinations are set. Called
// by ValidateRoute.
func validateTLS(route *routeapi.Route, fldPath *field.Path) field.ErrorList {
result := field.ErrorList{}
tls := route.Spec.TLS

// no tls config present, no need for validation
if tls == nil {
return nil
}

switch tls.Termination {
// reencrypt may specify destination ca cert
// cert, key, cacert may not be specified because the route may be a wildcard
case routeapi.TLSTerminationReencrypt:
//passthrough term should not specify any cert
case routeapi.TLSTerminationPassthrough:
if len(tls.Certificate) > 0 {
result = append(result, field.Invalid(fldPath.Child("certificate"), "redacted certificate data", "passthrough termination does not support certificates"))
}

if len(tls.Key) > 0 {
result = append(result, field.Invalid(fldPath.Child("key"), "redacted key data", "passthrough termination does not support certificates"))
}

if len(tls.CACertificate) > 0 {
result = append(result, field.Invalid(fldPath.Child("caCertificate"), "redacted ca certificate data", "passthrough termination does not support certificates"))
}

if len(tls.DestinationCACertificate) > 0 {
result = append(result, field.Invalid(fldPath.Child("destinationCACertificate"), "redacted destination ca certificate data", "passthrough termination does not support certificates"))
}
// edge cert should only specify cert, key, and cacert but those certs
// may not be specified if the route is a wildcard route
case routeapi.TLSTerminationEdge:
if len(tls.DestinationCACertificate) > 0 {
result = append(result, field.Invalid(fldPath.Child("destinationCACertificate"), "redacted destination ca certificate data", "edge termination does not support destination certificates"))
}
default:
validValues := []string{string(routeapi.TLSTerminationEdge), string(routeapi.TLSTerminationPassthrough), string(routeapi.TLSTerminationReencrypt)}
result = append(result, field.NotSupported(fldPath.Child("termination"), tls.Termination, validValues))
}

if err := validateInsecureEdgeTerminationPolicy(tls, fldPath.Child("insecureEdgeTerminationPolicy")); err != nil {
result = append(result, err)
}

return result
}

// validateInsecureEdgeTerminationPolicy tests fields for different types of
// insecure options. Called by validateTLS.
func validateInsecureEdgeTerminationPolicy(tls *routeapi.TLSConfig, fldPath *field.Path) *field.Error {
// Check insecure option value if specified (empty is ok).
if len(tls.InsecureEdgeTerminationPolicy) == 0 {
return nil
}

// It is an edge-terminated or reencrypt route, check insecure option value is
// one of None(for disable), Allow or Redirect.
allowedValues := map[routeapi.InsecureEdgeTerminationPolicyType]struct{}{
routeapi.InsecureEdgeTerminationPolicyNone: {},
routeapi.InsecureEdgeTerminationPolicyAllow: {},
routeapi.InsecureEdgeTerminationPolicyRedirect: {},
func ValidateRouteStatusUpdate(route *routeinternal.Route, oldRoute *routeinternal.Route) field.ErrorList {
external, errs := toRouteV1(route)
if len(errs) > 0 {
return errs
}

switch tls.Termination {
case routeapi.TLSTerminationReencrypt:
fallthrough
case routeapi.TLSTerminationEdge:
if _, ok := allowedValues[tls.InsecureEdgeTerminationPolicy]; !ok {
msg := fmt.Sprintf("invalid value for InsecureEdgeTerminationPolicy option, acceptable values are %s, %s, %s, or empty", routeapi.InsecureEdgeTerminationPolicyNone, routeapi.InsecureEdgeTerminationPolicyAllow, routeapi.InsecureEdgeTerminationPolicyRedirect)
return field.Invalid(fldPath, tls.InsecureEdgeTerminationPolicy, msg)
}
case routeapi.TLSTerminationPassthrough:
if routeapi.InsecureEdgeTerminationPolicyNone != tls.InsecureEdgeTerminationPolicy && routeapi.InsecureEdgeTerminationPolicyRedirect != tls.InsecureEdgeTerminationPolicy {
msg := fmt.Sprintf("invalid value for InsecureEdgeTerminationPolicy option, acceptable values are %s, %s, or empty", routeapi.InsecureEdgeTerminationPolicyNone, routeapi.InsecureEdgeTerminationPolicyRedirect)
return field.Invalid(fldPath, tls.InsecureEdgeTerminationPolicy, msg)
}
oldExternal, errs := toRouteV1(oldRoute)
if len(errs) > 0 {
return errs
}

return nil
return validateRouteStatusUpdateV1(external, oldExternal)
}

var (
allowedWildcardPolicies = []string{string(routeapi.WildcardPolicyNone), string(routeapi.WildcardPolicySubdomain)}
allowedWildcardPoliciesSet = sets.NewString(allowedWildcardPolicies...)
)

// validateWildcardPolicy tests that the wildcard policy is either empty or one of the supported types.
func validateWildcardPolicy(host string, policy routeapi.WildcardPolicyType, fldPath *field.Path) *field.Error {
if len(policy) == 0 {
func Warnings(route *routeinternal.Route) []string {
external, errs := toRouteV1(route)
if errs != nil {
return nil
}

// Check if policy is one of None or Subdomain.
if !allowedWildcardPoliciesSet.Has(string(policy)) {
return field.NotSupported(fldPath, policy, allowedWildcardPolicies)
}

if policy == routeapi.WildcardPolicySubdomain && len(host) == 0 {
return field.Invalid(fldPath, policy, "host name not specified for wildcard policy")
}

return nil
return warningsV1(external)
}

0 comments on commit 3a28992

Please sign in to comment.