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  }