istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/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  	"context"
    19  	"fmt"
    20  	"net/http"
    21  	"os"
    22  	"strings"
    23  	"time"
    24  
    25  	"google.golang.org/grpc/metadata"
    26  	"google.golang.org/grpc/peer"
    27  
    28  	"istio.io/istio/pkg/env"
    29  	istiolog "istio.io/istio/pkg/log"
    30  )
    31  
    32  var securityLog = istiolog.RegisterScope("security", "security debugging")
    33  
    34  const (
    35  	// etc/certs files are used with external CA managing the certs,
    36  	// i.e. mounted Secret or external plugin.
    37  	// If present, FileMountedCerts should be true.
    38  
    39  	// DefaultCertChainFilePath is the well-known path for an existing certificate chain file
    40  	DefaultCertChainFilePath = "./etc/certs/cert-chain.pem"
    41  
    42  	// DefaultKeyFilePath is the well-known path for an existing key file
    43  	DefaultKeyFilePath = "./etc/certs/key.pem"
    44  
    45  	// DefaultRootCertFilePath is the well-known path for an existing root certificate file
    46  	DefaultRootCertFilePath = "./etc/certs/root-cert.pem"
    47  
    48  	// WorkloadIdentitySocketPath is the well-known path to the Unix Domain Socket for SDS.
    49  	WorkloadIdentitySocketPath = "./var/run/secrets/workload-spiffe-uds/socket"
    50  
    51  	// CredentialNameSocketPath is the well-known path to the Unix Domain Socket for Credential Name.
    52  	CredentialNameSocketPath = "./var/run/secrets/credential-uds/socket"
    53  
    54  	// CredentialMetaDataName is the name in node meta data.
    55  	CredentialMetaDataName = "credential"
    56  
    57  	// SDSExternalClusterName is the name of the cluster for external SDS connections which is defined via CredentialNameSocketPath
    58  	SDSExternalClusterName = "sds-external"
    59  
    60  	// SDSExternalCredentialPrefix is the prefix for the credentialName which will utilize external SDS connections defined via CredentialNameSocketPath
    61  	SDSExternalCredentialPrefix = "sds://"
    62  
    63  	// WorkloadIdentityCredentialsPath is the well-known path to a folder with workload certificate files.
    64  	WorkloadIdentityCredentialsPath = "./var/run/secrets/workload-spiffe-credentials"
    65  
    66  	// WorkloadIdentityCertChainPath is the well-known path to a workload certificate chain file.
    67  	WorkloadIdentityCertChainPath = WorkloadIdentityCredentialsPath + "/cert-chain.pem"
    68  
    69  	// WorkloadIdentityKeyPath is the well-known path to a workload key file.
    70  	WorkloadIdentityKeyPath = WorkloadIdentityCredentialsPath + "/key.pem"
    71  
    72  	// WorkloadIdentityRootCertPath is the well-known path to a workload root certificate file.
    73  	WorkloadIdentityRootCertPath = WorkloadIdentityCredentialsPath + "/root-cert.pem"
    74  
    75  	// GkeWorkloadCertChainFilePath is the well-known path for the GKE workload certificate chain file.
    76  	// Quoted from https://cloud.google.com/traffic-director/docs/security-proxyless-setup#create-service:
    77  	// "On creation, each Pod gets a volume at /var/run/secrets/workload-spiffe-credentials."
    78  	GkeWorkloadCertChainFilePath = WorkloadIdentityCredentialsPath + "/certificates.pem"
    79  
    80  	// GkeWorkloadKeyFilePath is the well-known path for the GKE workload certificate key file
    81  	GkeWorkloadKeyFilePath = WorkloadIdentityCredentialsPath + "/private_key.pem"
    82  
    83  	// GkeWorkloadRootCertFilePath is the well-known path for the GKE workload root certificate file
    84  	GkeWorkloadRootCertFilePath = WorkloadIdentityCredentialsPath + "/ca_certificates.pem"
    85  
    86  	// SystemRootCerts is special case input for root cert configuration to use system root certificates.
    87  	SystemRootCerts = "SYSTEM"
    88  
    89  	// RootCertReqResourceName is resource name of discovery request for root certificate.
    90  	RootCertReqResourceName = "ROOTCA"
    91  
    92  	// WorkloadKeyCertResourceName is the resource name of the discovery request for workload
    93  	// identity.
    94  	WorkloadKeyCertResourceName = "default"
    95  
    96  	// GCE is Credential fetcher type of Google plugin
    97  	GCE = "GoogleComputeEngine"
    98  
    99  	// JWT is a Credential fetcher type that reads from a JWT token file
   100  	JWT = "JWT"
   101  
   102  	// Mock is Credential fetcher type of mock plugin
   103  	Mock = "Mock" // testing only
   104  
   105  	// GoogleCAProvider uses the Google CA for workload certificate signing
   106  	GoogleCAProvider = "GoogleCA"
   107  
   108  	// GoogleCASProvider uses the Google certificate Authority Service to sign workload certificates
   109  	GoogleCASProvider = "GoogleCAS"
   110  
   111  	// GkeWorkloadCertificateProvider uses the GKE workload certificates
   112  	GkeWorkloadCertificateProvider = "GkeWorkloadCertificate"
   113  
   114  	// FileRootSystemCACert is a unique resource name signaling that the system CA certificate should be used
   115  	FileRootSystemCACert = "file-root:system"
   116  )
   117  
   118  // TODO: For 1.8, make sure MeshConfig is updated with those settings,
   119  // they should be dynamic to allow migrations without restart.
   120  // Both are critical.
   121  var (
   122  	// Require3PToken disables the use of K8S 1P tokens. Note that 1P tokens can be used to request
   123  	// 3P TOKENS. A 1P token is the token automatically mounted by Kubelet and used for authentication with
   124  	// the Apiserver.
   125  	Require3PToken = env.Register("REQUIRE_3P_TOKEN", false,
   126  		"Reject k8s default tokens, without audience. If false, default K8S token will be accepted")
   127  
   128  	// TokenAudiences specifies a list of audiences for SDS trustworthy JWT. This is to make sure that the CSR requests
   129  	// contain the JWTs intended for Citadel.
   130  	TokenAudiences = strings.Split(env.Register("TOKEN_AUDIENCES", "istio-ca",
   131  		"A list of comma separated audiences to check in the JWT token before issuing a certificate. "+
   132  			"The token is accepted if it matches with one of the audiences").Get(), ",")
   133  )
   134  
   135  const (
   136  	BearerTokenPrefix = "Bearer "
   137  
   138  	K8sTokenPrefix = "Istio "
   139  
   140  	// CertSigner info
   141  	CertSigner = "CertSigner"
   142  
   143  	// ImpersonatedIdentity declares the identity we are requesting a certificate on behalf of.
   144  	// This is constrained to only allow identities in CATrustedNodeAccounts, and only to impersonate identities
   145  	// on their node.
   146  	ImpersonatedIdentity = "ImpersonatedIdentity"
   147  )
   148  
   149  type ImpersonatedIdentityContextKey struct{}
   150  
   151  // Options provides all of the configuration parameters for secret discovery service
   152  // and CA configuration. Used in both Istiod and Agent.
   153  // TODO: ProxyConfig should have most of those, and be passed to all components
   154  // (as source of truth)
   155  type Options struct {
   156  	// CAEndpoint is the CA endpoint to which node agent sends CSR request.
   157  	CAEndpoint string
   158  
   159  	// CAEndpointSAN overrides the ServerName extracted from CAEndpoint.
   160  	CAEndpointSAN string
   161  
   162  	// The CA provider name.
   163  	CAProviderName string
   164  
   165  	// TrustDomain corresponds to the trust root of a system.
   166  	// https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md#21-trust-domain
   167  	TrustDomain string
   168  
   169  	// WorkloadRSAKeySize is the size of a private key for a workload certificate.
   170  	WorkloadRSAKeySize int
   171  
   172  	// Whether to generate PKCS#8 private keys.
   173  	Pkcs8Keys bool
   174  
   175  	// OutputKeyCertToDir is the directory for output the key and certificate
   176  	OutputKeyCertToDir string
   177  
   178  	// ProvCert is the directory for client to provide the key and certificate to CA server when authenticating
   179  	// with mTLS. This is not used for workload mTLS communication, and is
   180  	ProvCert string
   181  
   182  	// ClusterID is the cluster where the agent resides.
   183  	// Normally initialized from ISTIO_META_CLUSTER_ID - after a tortuous journey it
   184  	// makes its way into the ClusterID metadata of Citadel gRPC request to create the cert.
   185  	// Didn't find much doc - but I suspect used for 'central cluster' use cases - so should
   186  	// match the cluster name set in the MC setup.
   187  	ClusterID string
   188  
   189  	// The type of Elliptical Signature algorithm to use
   190  	// when generating private keys. Currently only ECDSA is supported.
   191  	ECCSigAlg string
   192  
   193  	// The type of curve to use when generating private keys with ECC. Currently only ECDSA is supported.
   194  	ECCCurve string
   195  
   196  	// FileMountedCerts indicates whether the proxy is using file
   197  	// mounted certs created by a foreign CA. Refresh is managed by the external
   198  	// CA, by updating the Secret or VM file. We will watch the file for changes
   199  	// or check before the cert expires. This assumes the certs are in the
   200  	// well-known ./etc/certs location.
   201  	FileMountedCerts bool
   202  
   203  	// PilotCertProvider is the provider of the Pilot certificate (PILOT_CERT_PROVIDER env)
   204  	// Determines the root CA file to use for connecting to CA gRPC:
   205  	// - istiod
   206  	// - kubernetes
   207  	// - custom
   208  	// - none
   209  	PilotCertProvider string
   210  
   211  	// secret TTL.
   212  	SecretTTL time.Duration
   213  
   214  	// The ratio of cert lifetime to refresh a cert. For example, at 0.10 and 1 hour TTL,
   215  	// we would refresh 6 minutes before expiration.
   216  	SecretRotationGracePeriodRatio float64
   217  
   218  	// STS port
   219  	STSPort int
   220  
   221  	// credential fetcher.
   222  	CredFetcher CredFetcher
   223  
   224  	// credential identity provider
   225  	CredIdentityProvider string
   226  
   227  	// Namespace corresponding to workload
   228  	WorkloadNamespace string
   229  
   230  	// Name of the Service Account
   231  	ServiceAccount string
   232  
   233  	// XDS auth provider
   234  	XdsAuthProvider string
   235  
   236  	// Cert signer info
   237  	CertSigner string
   238  
   239  	// Delay in reading certificates from file after the change is detected. This is useful in cases
   240  	// where the write operation of key and cert take longer.
   241  	FileDebounceDuration time.Duration
   242  
   243  	// Root Cert read from the OS
   244  	CARootPath string
   245  
   246  	// The path for an existing certificate chain file
   247  	CertChainFilePath string
   248  	// The path for an existing key file
   249  	KeyFilePath string
   250  	// The path for an existing root certificate bundle
   251  	RootCertFilePath string
   252  }
   253  
   254  // Client interface defines the clients need to implement to talk to CA for CSR.
   255  // The Agent will create a key pair and a CSR, and use an implementation of this
   256  // interface to get back a signed certificate. There is no guarantee that the SAN
   257  // in the request will be returned - server may replace it.
   258  type Client interface {
   259  	CSRSign(csrPEM []byte, certValidTTLInSec int64) ([]string, error)
   260  	Close()
   261  	// Retrieve CA root certs If CA publishes API endpoint for this
   262  	GetRootCertBundle() ([]string, error)
   263  }
   264  
   265  // SecretManager defines secrets management interface which is used by SDS.
   266  type SecretManager interface {
   267  	// GenerateSecret generates new secret for the given resource.
   268  	//
   269  	// The current implementation also watched the generated secret and trigger a callback when it is
   270  	// near expiry. It will constructs the SAN based on the token's 'sub' claim, expected to be in
   271  	// the K8S format. No other JWTs are currently supported due to client logic. If JWT is
   272  	// missing/invalid, the resourceName is used.
   273  	GenerateSecret(resourceName string) (*SecretItem, error)
   274  }
   275  
   276  // SecretItem is the cached item in in-memory secret store.
   277  type SecretItem struct {
   278  	CertificateChain []byte
   279  	PrivateKey       []byte
   280  
   281  	RootCert []byte
   282  
   283  	// ResourceName passed from envoy SDS discovery request.
   284  	// "ROOTCA" for root cert request, "default" for key/cert request.
   285  	ResourceName string
   286  
   287  	CreatedTime time.Time
   288  
   289  	ExpireTime time.Time
   290  }
   291  
   292  type CredFetcher interface {
   293  	// GetPlatformCredential fetches workload credential provided by the platform.
   294  	GetPlatformCredential() (string, error)
   295  
   296  	// GetIdentityProvider returns the name of the IdentityProvider that can authenticate the workload credential.
   297  	GetIdentityProvider() string
   298  
   299  	// Stop releases resources and cleans up.
   300  	Stop()
   301  }
   302  
   303  // AuthSource represents where authentication result is derived from.
   304  type AuthSource int
   305  
   306  const (
   307  	AuthSourceClientCertificate AuthSource = iota
   308  	AuthSourceIDToken
   309  )
   310  
   311  const (
   312  	authorizationMeta = "authorization"
   313  )
   314  
   315  type AuthContext struct {
   316  	// grpc context
   317  	GrpcContext context.Context
   318  	// http request
   319  	Request *http.Request
   320  }
   321  
   322  // RemoteAddress returns the authenticated remote address from AuthContext.
   323  func (ac *AuthContext) RemoteAddress() string {
   324  	if ac.GrpcContext != nil {
   325  		return GetConnectionAddress(ac.GrpcContext)
   326  	} else if ac.Request != nil {
   327  		return ac.Request.RemoteAddr
   328  	}
   329  	return ""
   330  }
   331  
   332  // Header returns the authenticated remote address from AuthContext.
   333  func (ac *AuthContext) Header(header string) []string {
   334  	if ac.GrpcContext != nil {
   335  		if meta, ok := metadata.FromIncomingContext(ac.GrpcContext); ok {
   336  			return meta.Get(header)
   337  		}
   338  	} else if ac.Request != nil {
   339  		return ac.Request.Header.Values(header)
   340  	}
   341  	return nil
   342  }
   343  
   344  // Caller carries the identity and authentication source of a caller.
   345  type Caller struct {
   346  	AuthSource AuthSource
   347  	Identities []string
   348  
   349  	KubernetesInfo KubernetesInfo
   350  }
   351  
   352  // KubernetesInfo defines Kubernetes specific information extracted from the caller.
   353  // This involves additional metadata about the caller beyond just its SPIFFE identity.
   354  type KubernetesInfo struct {
   355  	PodName           string
   356  	PodNamespace      string
   357  	PodUID            string
   358  	PodServiceAccount string
   359  }
   360  
   361  func (k KubernetesInfo) String() string {
   362  	return fmt.Sprintf("Pod{Name: %s, Namespace: %s, UID: %s, ServiceAccount: %s}", k.PodName, k.PodNamespace, k.PodUID, k.PodServiceAccount)
   363  }
   364  
   365  // Authenticator determines the caller identity based on request context.
   366  type Authenticator interface {
   367  	Authenticate(ctx AuthContext) (*Caller, error)
   368  	AuthenticatorType() string
   369  }
   370  
   371  // authenticationManager orchestrates all authenticators to perform authentication.
   372  type authenticationManager struct {
   373  	Authenticators []Authenticator
   374  	// authFailMsgs contains list of messages that authenticator wants to record - mainly used for logging.
   375  	authFailMsgs []string
   376  }
   377  
   378  // Authenticate loops through all the configured Authenticators and returns if one of the authenticator succeeds.
   379  func (am *authenticationManager) authenticate(ctx context.Context) *Caller {
   380  	req := AuthContext{GrpcContext: ctx}
   381  	for _, authn := range am.Authenticators {
   382  		u, err := authn.Authenticate(req)
   383  		if u != nil && len(u.Identities) > 0 && err == nil {
   384  			securityLog.Debugf("Authentication successful through auth source %v", u.AuthSource)
   385  			return u
   386  		}
   387  		am.authFailMsgs = append(am.authFailMsgs, fmt.Sprintf("Authenticator %s: %v", authn.AuthenticatorType(), err))
   388  	}
   389  	return nil
   390  }
   391  
   392  func GetConnectionAddress(ctx context.Context) string {
   393  	peerInfo, ok := peer.FromContext(ctx)
   394  	peerAddr := "unknown"
   395  	if ok {
   396  		peerAddr = peerInfo.Addr.String()
   397  	}
   398  	return peerAddr
   399  }
   400  
   401  func (am *authenticationManager) FailedMessages() string {
   402  	return strings.Join(am.authFailMsgs, "; ")
   403  }
   404  
   405  func ExtractBearerToken(ctx context.Context) (string, error) {
   406  	md, ok := metadata.FromIncomingContext(ctx)
   407  	if !ok {
   408  		return "", fmt.Errorf("no metadata is attached")
   409  	}
   410  
   411  	authHeader, exists := md[authorizationMeta]
   412  	if !exists {
   413  		return "", fmt.Errorf("no HTTP authorization header exists")
   414  	}
   415  
   416  	for _, value := range authHeader {
   417  		if strings.HasPrefix(value, BearerTokenPrefix) {
   418  			return strings.TrimPrefix(value, BearerTokenPrefix), nil
   419  		}
   420  	}
   421  
   422  	return "", fmt.Errorf("no bearer token exists in HTTP authorization header")
   423  }
   424  
   425  func ExtractRequestToken(req *http.Request) (string, error) {
   426  	value := req.Header.Get(authorizationMeta)
   427  	if value == "" {
   428  		return "", fmt.Errorf("no HTTP authorization header exists")
   429  	}
   430  
   431  	if strings.HasPrefix(value, BearerTokenPrefix) {
   432  		return strings.TrimPrefix(value, BearerTokenPrefix), nil
   433  	}
   434  	if strings.HasPrefix(value, K8sTokenPrefix) {
   435  		return strings.TrimPrefix(value, K8sTokenPrefix), nil
   436  	}
   437  
   438  	return "", fmt.Errorf("no bearer token exists in HTTP authorization header")
   439  }
   440  
   441  // GetOSRootFilePath returns the first file path detected from a list of known CA certificate file paths.
   442  // If none of the known CA certificate files are found, a warning in printed and an empty string is returned.
   443  func GetOSRootFilePath() string {
   444  	// Get and store the OS CA certificate path for Linux systems
   445  	// Source of CA File Paths: https://golang.org/src/crypto/x509/root_linux.go
   446  	certFiles := []string{
   447  		"/etc/ssl/certs/ca-certificates.crt",                // Debian/Ubuntu/Gentoo etc.
   448  		"/etc/pki/tls/certs/ca-bundle.crt",                  // Fedora/RHEL 6
   449  		"/etc/ssl/ca-bundle.pem",                            // OpenSUSE
   450  		"/etc/pki/tls/cacert.pem",                           // OpenELEC
   451  		"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7
   452  		"/etc/ssl/cert.pem",                                 // Alpine Linux
   453  		"/usr/local/etc/ssl/cert.pem",                       // FreeBSD
   454  		"/etc/ssl/certs/ca-certificates",                    // Talos Linux
   455  	}
   456  
   457  	for _, cert := range certFiles {
   458  		if _, err := os.Stat(cert); err == nil {
   459  			istiolog.Debugf("Using OS CA certificate for proxy: %s", cert)
   460  			return cert
   461  		}
   462  	}
   463  	istiolog.Warn("OS CA Cert could not be found for agent")
   464  	return ""
   465  }
   466  
   467  // CheckWorkloadCertificate returns true when the workload certificate
   468  // files are present under the provided paths. Otherwise, return false.
   469  func CheckWorkloadCertificate(certChainFilePath, keyFilePath, rootCertFilePath string) bool {
   470  	if _, err := os.Stat(certChainFilePath); err != nil {
   471  		return false
   472  	}
   473  	if _, err := os.Stat(keyFilePath); err != nil {
   474  		return false
   475  	}
   476  	if _, err := os.Stat(rootCertFilePath); err != nil {
   477  		return false
   478  	}
   479  	return true
   480  }
   481  
   482  type SdsCertificateConfig struct {
   483  	CertificatePath   string
   484  	PrivateKeyPath    string
   485  	CaCertificatePath string
   486  }
   487  
   488  const (
   489  	ResourceSeparator = "~"
   490  )
   491  
   492  // GetResourceName converts a SdsCertificateConfig to a string to be used as an SDS resource name
   493  func (s SdsCertificateConfig) GetResourceName() string {
   494  	if s.IsKeyCertificate() {
   495  		return "file-cert:" + s.CertificatePath + ResourceSeparator + s.PrivateKeyPath // Format: file-cert:%s~%s
   496  	}
   497  	return ""
   498  }
   499  
   500  // GetRootResourceName converts a SdsCertificateConfig to a string to be used as an SDS resource name for the root
   501  func (s SdsCertificateConfig) GetRootResourceName() string {
   502  	if s.IsRootCertificate() {
   503  		return "file-root:" + s.CaCertificatePath // Format: file-root:%s
   504  	}
   505  	return ""
   506  }
   507  
   508  // IsRootCertificate returns true if this config represents a root certificate config.
   509  func (s SdsCertificateConfig) IsRootCertificate() bool {
   510  	return s.CaCertificatePath != ""
   511  }
   512  
   513  // IsKeyCertificate returns true if this config represents key certificate config.
   514  func (s SdsCertificateConfig) IsKeyCertificate() bool {
   515  	return s.CertificatePath != "" && s.PrivateKeyPath != ""
   516  }
   517  
   518  // SdsCertificateConfigFromResourceName converts the provided resource name into a SdsCertificateConfig
   519  // If the resource name is not valid, false is returned.
   520  func SdsCertificateConfigFromResourceName(resource string) (SdsCertificateConfig, bool) {
   521  	if strings.HasPrefix(resource, "file-cert:") {
   522  		filesString := strings.TrimPrefix(resource, "file-cert:")
   523  		split := strings.Split(filesString, ResourceSeparator)
   524  		if len(split) != 2 {
   525  			return SdsCertificateConfig{}, false
   526  		}
   527  		return SdsCertificateConfig{split[0], split[1], ""}, true
   528  	} else if strings.HasPrefix(resource, "file-root:") {
   529  		filesString := strings.TrimPrefix(resource, "file-root:")
   530  		split := strings.Split(filesString, ResourceSeparator)
   531  
   532  		if len(split) != 1 {
   533  			return SdsCertificateConfig{}, false
   534  		}
   535  		return SdsCertificateConfig{"", "", split[0]}, true
   536  	}
   537  	return SdsCertificateConfig{}, false
   538  }
   539  
   540  // SdsCertificateConfigFromResourceNameForOSCACert converts the OS resource name into a SdsCertificateConfig
   541  func SdsCertificateConfigFromResourceNameForOSCACert(resource string) (SdsCertificateConfig, bool) {
   542  	if resource == "" {
   543  		return SdsCertificateConfig{}, false
   544  	}
   545  	return SdsCertificateConfig{"", "", resource}, true
   546  }