istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/security/security.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package security 16 17 import ( 18 "fmt" 19 "net/netip" 20 "net/url" 21 "strconv" 22 "strings" 23 "unicode" 24 25 "github.com/hashicorp/go-multierror" 26 27 "istio.io/istio/pkg/config/host" 28 "istio.io/istio/pkg/log" 29 "istio.io/istio/pkg/util/sets" 30 ) 31 32 // JwksInfo provides values resulting from parsing a jwks URI. 33 type JwksInfo struct { 34 Hostname host.Name 35 Scheme string 36 Port int 37 UseSSL bool 38 } 39 40 const ( 41 attrRequestHeader = "request.headers" // header name is surrounded by brackets, e.g. "request.headers[User-Agent]". 42 attrSrcIP = "source.ip" // supports both single ip and cidr, e.g. "10.1.2.3" or "10.1.0.0/16". 43 attrRemoteIP = "remote.ip" // original client ip determined from x-forwarded-for or proxy protocol. 44 attrSrcNamespace = "source.namespace" // e.g. "default". 45 attrSrcPrincipal = "source.principal" // source identity, e,g, "cluster.local/ns/default/sa/productpage". 46 attrRequestPrincipal = "request.auth.principal" // authenticated principal of the request. 47 attrRequestAudiences = "request.auth.audiences" // intended audience(s) for this authentication information. 48 attrRequestPresenter = "request.auth.presenter" // authorized presenter of the credential. 49 attrRequestClaims = "request.auth.claims" // claim name is surrounded by brackets, e.g. "request.auth.claims[iss]". 50 attrDestIP = "destination.ip" // supports both single ip and cidr, e.g. "10.1.2.3" or "10.1.0.0/16". 51 attrDestPort = "destination.port" // must be in the range [0, 65535]. 52 attrDestLabel = "destination.labels" // label name is surrounded by brackets, e.g. "destination.labels[version]". 53 attrDestName = "destination.name" // short service name, e.g. "productpage". 54 attrDestNamespace = "destination.namespace" // e.g. "default". 55 attrDestUser = "destination.user" // service account, e.g. "bookinfo-productpage". 56 attrConnSNI = "connection.sni" // server name indication, e.g. "www.example.com". 57 attrExperimental = "experimental.envoy.filters." 58 ) 59 60 var ( 61 MatchOneTemplate = "{*}" 62 MatchAnyTemplate = "{**}" 63 ) 64 65 // ParseJwksURI parses the input URI and returns the corresponding hostname, port, and whether SSL is used. 66 // URI must start with "http://" or "https://", which corresponding to "http" or "https" scheme. 67 // Port number is extracted from URI if available (i.e from postfix :<port>, eg. ":80"), or assigned 68 // to a default value based on URI scheme (80 for http and 443 for https). 69 // Port name is set to URI scheme value. 70 func ParseJwksURI(jwksURI string) (JwksInfo, error) { 71 u, err := url.Parse(jwksURI) 72 if err != nil { 73 return JwksInfo{}, err 74 } 75 info := JwksInfo{} 76 switch u.Scheme { 77 case "http": 78 info.UseSSL = false 79 info.Port = 80 80 case "https": 81 info.UseSSL = true 82 info.Port = 443 83 default: 84 return JwksInfo{}, fmt.Errorf("URI scheme %q is not supported", u.Scheme) 85 } 86 87 if u.Port() != "" { 88 info.Port, err = strconv.Atoi(u.Port()) 89 if err != nil { 90 return JwksInfo{}, err 91 } 92 } 93 info.Hostname = host.Name(u.Hostname()) 94 info.Scheme = u.Scheme 95 96 return info, nil 97 } 98 99 func CheckEmptyValues(key string, values []string) error { 100 for _, value := range values { 101 if value == "" { 102 return fmt.Errorf("empty value not allowed, found in %s", key) 103 } 104 } 105 return nil 106 } 107 108 func CheckValidPathTemplate(key string, paths []string) error { 109 for _, path := range paths { 110 containsPathTemplate := ContainsPathTemplate(path) 111 globs := strings.Split(path, "/") 112 for _, glob := range globs { 113 // If glob is a supported path template, skip the check. 114 if glob == MatchAnyTemplate || glob == MatchOneTemplate { 115 continue 116 } 117 // If glob is not a supported path template and contains `{`, or `}` it is invalid. 118 if strings.ContainsAny(glob, "{}") { 119 return fmt.Errorf("invalid or unsupported path %s, found in %s."+ 120 "Contains '{' or '}' beyond a supported path template", path, key) 121 } 122 // If glob contains `*`, is not a supported path template and 123 // the path contains a supported path template, it is invalid. 124 if strings.Contains(glob, "*") && containsPathTemplate { 125 return fmt.Errorf("invalid or unsupported path %s, found in %s."+ 126 "Contains '*' beyond a supported path template", path, key) 127 } 128 } 129 } 130 return nil 131 } 132 133 // ContainsPathTemplate returns true if the path contains a valid path template. 134 func ContainsPathTemplate(value string) bool { 135 return strings.Contains(value, MatchOneTemplate) || strings.Contains(value, MatchAnyTemplate) 136 } 137 138 func ValidateAttribute(key string, values []string) error { 139 if err := CheckEmptyValues(key, values); err != nil { 140 return err 141 } 142 switch { 143 case hasPrefix(key, attrRequestHeader): 144 return validateMapKey(key) 145 case isEqual(key, attrSrcIP): 146 return ValidateIPs(values) 147 case isEqual(key, attrRemoteIP): 148 return ValidateIPs(values) 149 case isEqual(key, attrSrcNamespace): 150 case isEqual(key, attrSrcPrincipal): 151 case isEqual(key, attrRequestPrincipal): 152 case isEqual(key, attrRequestAudiences): 153 case isEqual(key, attrRequestPresenter): 154 case hasPrefix(key, attrRequestClaims): 155 return validateMapKey(key) 156 case isEqual(key, attrDestIP): 157 return ValidateIPs(values) 158 case isEqual(key, attrDestPort): 159 return ValidatePorts(values) 160 case isEqual(key, attrConnSNI): 161 case hasPrefix(key, attrExperimental): 162 return validateMapKey(key) 163 case isEqual(key, attrDestNamespace): 164 return fmt.Errorf("attribute %s is replaced by the metadata.namespace", key) 165 case hasPrefix(key, attrDestLabel): 166 return fmt.Errorf("attribute %s is replaced by the workload selector", key) 167 case isEqual(key, attrDestName, attrDestUser): 168 return fmt.Errorf("deprecated attribute %s: only supported in v1alpha1", key) 169 default: 170 return fmt.Errorf("unknown attribute: %s", key) 171 } 172 return nil 173 } 174 175 func isEqual(key string, values ...string) bool { 176 for _, v := range values { 177 if key == v { 178 return true 179 } 180 } 181 return false 182 } 183 184 func hasPrefix(key string, prefix string) bool { 185 return strings.HasPrefix(key, prefix) 186 } 187 188 func ValidateIPs(ips []string) error { 189 var errs *multierror.Error 190 for _, v := range ips { 191 if strings.Contains(v, "/") { 192 if _, err := netip.ParsePrefix(v); err != nil { 193 errs = multierror.Append(errs, fmt.Errorf("bad CIDR range (%s): %v", v, err)) 194 } 195 } else { 196 if _, err := netip.ParseAddr(v); err != nil { 197 errs = multierror.Append(errs, fmt.Errorf("bad IP address (%s)", v)) 198 } 199 } 200 } 201 return errs.ErrorOrNil() 202 } 203 204 func ValidatePorts(ports []string) error { 205 var errs *multierror.Error 206 for _, port := range ports { 207 p, err := strconv.ParseUint(port, 10, 32) 208 if err != nil || p > 65535 { 209 errs = multierror.Append(errs, fmt.Errorf("bad port (%s): %v", port, err)) 210 } 211 } 212 return errs.ErrorOrNil() 213 } 214 215 func validateMapKey(key string) error { 216 open := strings.Index(key, "[") 217 if strings.HasSuffix(key, "]") && open > 0 && open < len(key)-2 { 218 return nil 219 } 220 return fmt.Errorf("bad key (%s): should have format a[b]", key) 221 } 222 223 // ValidCipherSuites contains a list of all ciphers supported in Gateway.server.tls.cipherSuites 224 // Extracted from: `bssl ciphers -openssl-name ALL | rg -v PSK` 225 var ValidCipherSuites = sets.New( 226 "ECDHE-ECDSA-AES128-GCM-SHA256", 227 "ECDHE-RSA-AES128-GCM-SHA256", 228 "ECDHE-ECDSA-AES256-GCM-SHA384", 229 "ECDHE-RSA-AES256-GCM-SHA384", 230 "ECDHE-ECDSA-CHACHA20-POLY1305", 231 "ECDHE-RSA-CHACHA20-POLY1305", 232 "ECDHE-ECDSA-AES128-SHA", 233 "ECDHE-RSA-AES128-SHA", 234 "ECDHE-ECDSA-AES256-SHA", 235 "ECDHE-RSA-AES256-SHA", 236 "AES128-GCM-SHA256", 237 "AES256-GCM-SHA384", 238 "AES128-SHA", 239 "AES256-SHA", 240 "DES-CBC3-SHA", 241 ) 242 243 // ValidECDHCurves contains a list of all ecdh curves supported in MeshConfig.TlsDefaults.ecdhCurves 244 // Source: 245 // https://github.com/google/boringssl/blob/3743aafdacff2f7b083615a043a37101f740fa53/ssl/ssl_key_share.cc#L302-L309 246 var ValidECDHCurves = sets.New( 247 "P-224", 248 "P-256", 249 "P-521", 250 "P-384", 251 "X25519", 252 "CECPQ2", 253 ) 254 255 func IsValidCipherSuite(cs string) bool { 256 if cs == "" || cs == "ALL" { 257 return true 258 } 259 if !unicode.IsNumber(rune(cs[0])) && !unicode.IsLetter(rune(cs[0])) { 260 // Not all of these are correct, but this is needed to support advanced cases like - and + operators 261 // without needing to parse the full expression 262 return true 263 } 264 return ValidCipherSuites.Contains(cs) 265 } 266 267 func IsValidECDHCurve(cs string) bool { 268 if cs == "" { 269 return true 270 } 271 return ValidECDHCurves.Contains(cs) 272 } 273 274 // FilterCipherSuites filters out invalid cipher suites which would lead Envoy to NACKing. 275 func FilterCipherSuites(suites []string) []string { 276 if len(suites) == 0 { 277 return nil 278 } 279 ret := make([]string, 0, len(suites)) 280 validCiphers := sets.New[string]() 281 for _, s := range suites { 282 if IsValidCipherSuite(s) { 283 if !validCiphers.InsertContains(s) { 284 ret = append(ret, s) 285 } else if log.DebugEnabled() { 286 log.Debugf("ignoring duplicated cipherSuite: %q", s) 287 } 288 } else if log.DebugEnabled() { 289 log.Debugf("ignoring unsupported cipherSuite: %q", s) 290 } 291 } 292 return ret 293 }