1/* 2Copyright 2019 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package egressselector 18 19import ( 20 "fmt" 21 "io/ioutil" 22 "strings" 23 24 "k8s.io/apimachinery/pkg/runtime" 25 "k8s.io/apimachinery/pkg/util/sets" 26 "k8s.io/apimachinery/pkg/util/validation/field" 27 "k8s.io/apiserver/pkg/apis/apiserver" 28 "k8s.io/apiserver/pkg/apis/apiserver/install" 29 "k8s.io/apiserver/pkg/apis/apiserver/v1beta1" 30 "k8s.io/utils/path" 31 "sigs.k8s.io/yaml" 32) 33 34var cfgScheme = runtime.NewScheme() 35 36// validEgressSelectorNames contains the set of valid egress selctor names. 37// 'master' is deprecated in favor of 'controlplane' and will be removed in v1.22. 38var validEgressSelectorNames = sets.NewString("master", "controlplane", "cluster", "etcd") 39 40func init() { 41 install.Install(cfgScheme) 42} 43 44// ReadEgressSelectorConfiguration reads the egress selector configuration at the specified path. 45// It returns the loaded egress selector configuration if the input file aligns with the required syntax. 46// If it does not align with the provided syntax, it returns a default configuration which should function as a no-op. 47// It does this by returning a nil configuration, which preserves backward compatibility. 48// This works because prior to this there was no egress selector configuration. 49// It returns an error if the file did not exist. 50func ReadEgressSelectorConfiguration(configFilePath string) (*apiserver.EgressSelectorConfiguration, error) { 51 if configFilePath == "" { 52 return nil, nil 53 } 54 // a file was provided, so we just read it. 55 data, err := ioutil.ReadFile(configFilePath) 56 if err != nil { 57 return nil, fmt.Errorf("unable to read egress selector configuration from %q [%v]", configFilePath, err) 58 } 59 var decodedConfig v1beta1.EgressSelectorConfiguration 60 err = yaml.Unmarshal(data, &decodedConfig) 61 if err != nil { 62 // we got an error where the decode wasn't related to a missing type 63 return nil, err 64 } 65 if decodedConfig.Kind != "EgressSelectorConfiguration" { 66 return nil, fmt.Errorf("invalid service configuration object %q", decodedConfig.Kind) 67 } 68 internalConfig := &apiserver.EgressSelectorConfiguration{} 69 if err := cfgScheme.Convert(&decodedConfig, internalConfig, nil); err != nil { 70 // we got an error where the decode wasn't related to a missing type 71 return nil, err 72 } 73 return internalConfig, nil 74} 75 76// ValidateEgressSelectorConfiguration checks the apiserver.EgressSelectorConfiguration for 77// common configuration errors. It will return error for problems such as configuring mtls/cert 78// settings for protocol which do not support security. It will also try to catch errors such as 79// incorrect file paths. It will return nil if it does not find anything wrong. 80func ValidateEgressSelectorConfiguration(config *apiserver.EgressSelectorConfiguration) field.ErrorList { 81 allErrs := field.ErrorList{} 82 if config == nil { 83 return allErrs // Treating a nil configuration as valid 84 } 85 for _, service := range config.EgressSelections { 86 fldPath := field.NewPath("service", "connection") 87 switch service.Connection.ProxyProtocol { 88 case apiserver.ProtocolDirect: 89 allErrs = append(allErrs, validateDirectConnection(service.Connection, fldPath)...) 90 case apiserver.ProtocolHTTPConnect: 91 allErrs = append(allErrs, validateHTTPConnectTransport(service.Connection.Transport, fldPath)...) 92 case apiserver.ProtocolGRPC: 93 allErrs = append(allErrs, validateGRPCTransport(service.Connection.Transport, fldPath)...) 94 default: 95 allErrs = append(allErrs, field.NotSupported( 96 fldPath.Child("protocol"), 97 service.Connection.ProxyProtocol, 98 []string{ 99 string(apiserver.ProtocolDirect), 100 string(apiserver.ProtocolHTTPConnect), 101 string(apiserver.ProtocolGRPC), 102 })) 103 } 104 } 105 106 var foundControlPlane, foundMaster bool 107 for _, service := range config.EgressSelections { 108 canonicalName := strings.ToLower(service.Name) 109 110 if !validEgressSelectorNames.Has(canonicalName) { 111 allErrs = append(allErrs, field.NotSupported(field.NewPath("egressSelection", "name"), canonicalName, validEgressSelectorNames.List())) 112 continue 113 } 114 115 if canonicalName == "master" { 116 foundMaster = true 117 } 118 119 if canonicalName == "controlplane" { 120 foundControlPlane = true 121 } 122 } 123 124 // error if both master and controlplane egress selectors are set 125 if foundMaster && foundControlPlane { 126 allErrs = append(allErrs, field.Forbidden(field.NewPath("egressSelection", "name"), "both egressSelection names 'master' and 'controlplane' are specified, only one is allowed")) 127 } 128 129 return allErrs 130} 131 132func validateHTTPConnectTransport(transport *apiserver.Transport, fldPath *field.Path) field.ErrorList { 133 allErrs := field.ErrorList{} 134 if transport == nil { 135 allErrs = append(allErrs, field.Required( 136 fldPath.Child("transport"), 137 "transport must be set for HTTPConnect")) 138 return allErrs 139 } 140 141 if transport.TCP != nil && transport.UDS != nil { 142 allErrs = append(allErrs, field.Invalid( 143 fldPath.Child("tcp"), 144 transport.TCP, 145 "TCP and UDS cannot both be set")) 146 } else if transport.TCP == nil && transport.UDS == nil { 147 allErrs = append(allErrs, field.Required( 148 fldPath.Child("tcp"), 149 "One of TCP or UDS must be set")) 150 } else if transport.TCP != nil { 151 allErrs = append(allErrs, validateTCPConnection(transport.TCP, fldPath)...) 152 } else if transport.UDS != nil { 153 allErrs = append(allErrs, validateUDSConnection(transport.UDS, fldPath)...) 154 } 155 return allErrs 156} 157 158func validateGRPCTransport(transport *apiserver.Transport, fldPath *field.Path) field.ErrorList { 159 allErrs := field.ErrorList{} 160 if transport == nil { 161 allErrs = append(allErrs, field.Required( 162 fldPath.Child("transport"), 163 "transport must be set for GRPC")) 164 return allErrs 165 } 166 167 if transport.UDS != nil { 168 allErrs = append(allErrs, validateUDSConnection(transport.UDS, fldPath)...) 169 } else { 170 allErrs = append(allErrs, field.Required( 171 fldPath.Child("uds"), 172 "UDS must be set with GRPC")) 173 } 174 return allErrs 175} 176 177func validateDirectConnection(connection apiserver.Connection, fldPath *field.Path) field.ErrorList { 178 if connection.Transport != nil { 179 return field.ErrorList{field.Invalid( 180 fldPath.Child("transport"), 181 "direct", 182 "Transport config should be absent for direct connect"), 183 } 184 } 185 186 return nil 187} 188 189func validateUDSConnection(udsConfig *apiserver.UDSTransport, fldPath *field.Path) field.ErrorList { 190 allErrs := field.ErrorList{} 191 if udsConfig.UDSName == "" { 192 allErrs = append(allErrs, field.Invalid( 193 fldPath.Child("udsName"), 194 "nil", 195 "UDSName should be present for UDS connections")) 196 } 197 return allErrs 198} 199 200func validateTCPConnection(tcpConfig *apiserver.TCPTransport, fldPath *field.Path) field.ErrorList { 201 allErrs := field.ErrorList{} 202 203 if strings.HasPrefix(tcpConfig.URL, "http://") { 204 if tcpConfig.TLSConfig != nil { 205 allErrs = append(allErrs, field.Invalid( 206 fldPath.Child("tlsConfig"), 207 "nil", 208 "TLSConfig config should not be present when using HTTP")) 209 } 210 } else if strings.HasPrefix(tcpConfig.URL, "https://") { 211 return validateTLSConfig(tcpConfig.TLSConfig, fldPath) 212 } else { 213 allErrs = append(allErrs, field.Invalid( 214 fldPath.Child("url"), 215 tcpConfig.URL, 216 "supported connection protocols are http:// and https://")) 217 } 218 return allErrs 219} 220 221func validateTLSConfig(tlsConfig *apiserver.TLSConfig, fldPath *field.Path) field.ErrorList { 222 allErrs := field.ErrorList{} 223 224 if tlsConfig == nil { 225 allErrs = append(allErrs, field.Required( 226 fldPath.Child("tlsConfig"), 227 "TLSConfig must be present when using HTTPS")) 228 return allErrs 229 } 230 if tlsConfig.CABundle != "" { 231 if exists, err := path.Exists(path.CheckFollowSymlink, tlsConfig.CABundle); !exists || err != nil { 232 allErrs = append(allErrs, field.Invalid( 233 fldPath.Child("tlsConfig", "caBundle"), 234 tlsConfig.CABundle, 235 "TLS config ca bundle does not exist")) 236 } 237 } 238 if tlsConfig.ClientCert == "" { 239 allErrs = append(allErrs, field.Invalid( 240 fldPath.Child("tlsConfig", "clientCert"), 241 "nil", 242 "Using TLS requires clientCert")) 243 } else if exists, err := path.Exists(path.CheckFollowSymlink, tlsConfig.ClientCert); !exists || err != nil { 244 allErrs = append(allErrs, field.Invalid( 245 fldPath.Child("tlsConfig", "clientCert"), 246 tlsConfig.ClientCert, 247 "TLS client cert does not exist")) 248 } 249 if tlsConfig.ClientKey == "" { 250 allErrs = append(allErrs, field.Invalid( 251 fldPath.Child("tlsConfig", "clientKey"), 252 "nil", 253 "Using TLS requires requires clientKey")) 254 } else if exists, err := path.Exists(path.CheckFollowSymlink, tlsConfig.ClientKey); !exists || err != nil { 255 allErrs = append(allErrs, field.Invalid( 256 fldPath.Child("tlsConfig", "clientKey"), 257 tlsConfig.ClientKey, 258 "TLS client key does not exist")) 259 } 260 return allErrs 261} 262