istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/credentials/kube/secrets.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 kube
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"strings"
    22  	"sync"
    23  	"time"
    24  
    25  	authorizationv1 "k8s.io/api/authorization/v1"
    26  	v1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/fields"
    29  	sa "k8s.io/apiserver/pkg/authentication/serviceaccount"
    30  	authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
    31  
    32  	"istio.io/istio/pilot/pkg/credentials"
    33  	securitymodel "istio.io/istio/pilot/pkg/security/model"
    34  	"istio.io/istio/pkg/kube"
    35  	"istio.io/istio/pkg/kube/controllers"
    36  	"istio.io/istio/pkg/kube/kclient"
    37  	"istio.io/istio/pkg/log"
    38  )
    39  
    40  const (
    41  	// The ID/name for the certificate chain in kubernetes generic secret.
    42  	GenericScrtCert = "cert"
    43  	// The ID/name for the private key in kubernetes generic secret.
    44  	GenericScrtKey = "key"
    45  	// The ID/name for the CA certificate in kubernetes generic secret.
    46  	GenericScrtCaCert = "cacert"
    47  	// The ID/name for the CRL in kubernetes generic secret.
    48  	GenericScrtCRL = "crl"
    49  
    50  	// The ID/name for the certificate chain in kubernetes tls secret.
    51  	TLSSecretCert = "tls.crt"
    52  	// The ID/name for the k8sKey in kubernetes tls secret.
    53  	TLSSecretKey = "tls.key"
    54  	// The ID/name for the certificate OCSP staple in kubernetes tls secret
    55  	TLSSecretOcspStaple = "tls.ocsp-staple"
    56  	// The ID/name for the CA certificate in kubernetes tls secret
    57  	TLSSecretCaCert = "ca.crt"
    58  	// The ID/name for the CRL in kubernetes tls secret.
    59  	TLSSecretCrl = "ca.crl"
    60  )
    61  
    62  type CredentialsController struct {
    63  	secrets kclient.Client[*v1.Secret]
    64  	sar     authorizationv1client.SubjectAccessReviewInterface
    65  
    66  	mu                 sync.RWMutex
    67  	authorizationCache map[authorizationKey]authorizationResponse
    68  }
    69  
    70  type authorizationKey string
    71  
    72  type authorizationResponse struct {
    73  	expiration time.Time
    74  	authorized error
    75  }
    76  
    77  var _ credentials.Controller = &CredentialsController{}
    78  
    79  func NewCredentialsController(kc kube.Client, handlers []func(name string, namespace string)) *CredentialsController {
    80  	// We only care about TLS certificates and docker config for Wasm image pulling.
    81  	// Unfortunately, it is not as simple as selecting type=kubernetes.io/tls and type=kubernetes.io/dockerconfigjson.
    82  	// Because of legacy reasons and supporting an extra ca.crt, we also support generic types.
    83  	// Its also likely users have started to use random types and expect them to continue working.
    84  	// This makes the assumption we will never care about Helm secrets or SA token secrets - two common
    85  	// large secrets in clusters.
    86  	// This is a best effort optimization only; the code would behave correctly if we watched all secrets.
    87  	fieldSelector := fields.AndSelectors(
    88  		fields.OneTermNotEqualSelector("type", "helm.sh/release.v1"),
    89  		fields.OneTermNotEqualSelector("type", string(v1.SecretTypeServiceAccountToken))).String()
    90  	secrets := kclient.NewFiltered[*v1.Secret](kc, kclient.Filter{
    91  		FieldSelector: fieldSelector,
    92  	})
    93  
    94  	for _, h := range handlers {
    95  		h := h
    96  		// register handler before informer starts
    97  		secrets.AddEventHandler(controllers.ObjectHandler(func(o controllers.Object) {
    98  			h(o.GetName(), o.GetNamespace())
    99  		}))
   100  	}
   101  
   102  	return &CredentialsController{
   103  		secrets:            secrets,
   104  		sar:                kc.Kube().AuthorizationV1().SubjectAccessReviews(),
   105  		authorizationCache: make(map[authorizationKey]authorizationResponse),
   106  	}
   107  }
   108  
   109  func (s *CredentialsController) Close() {
   110  	s.secrets.ShutdownHandlers()
   111  }
   112  
   113  func (s *CredentialsController) HasSynced() bool {
   114  	return s.secrets.HasSynced()
   115  }
   116  
   117  const cacheTTL = time.Minute
   118  
   119  // clearExpiredCache iterates through the cache and removes all expired entries. Should be called with mutex held.
   120  func (s *CredentialsController) clearExpiredCache() {
   121  	for k, v := range s.authorizationCache {
   122  		if v.expiration.Before(time.Now()) {
   123  			delete(s.authorizationCache, k)
   124  		}
   125  	}
   126  }
   127  
   128  // cachedAuthorization checks the authorization cache
   129  // nolint
   130  func (s *CredentialsController) cachedAuthorization(user string) (error, bool) {
   131  	key := authorizationKey(user)
   132  	s.mu.Lock()
   133  	defer s.mu.Unlock()
   134  	s.clearExpiredCache()
   135  	// No need to check expiration, we will evict expired entries above
   136  	got, f := s.authorizationCache[key]
   137  	if !f {
   138  		return nil, false
   139  	}
   140  	return got.authorized, true
   141  }
   142  
   143  // cachedAuthorization checks the authorization cache
   144  func (s *CredentialsController) insertCache(user string, response error) {
   145  	s.mu.Lock()
   146  	defer s.mu.Unlock()
   147  	key := authorizationKey(user)
   148  	expDelta := cacheTTL
   149  	if response == nil {
   150  		// Cache success a bit longer, there is no need to quickly revoke access
   151  		expDelta *= 5
   152  	}
   153  	log.Debugf("cached authorization for user %s: %v", user, response)
   154  	s.authorizationCache[key] = authorizationResponse{
   155  		expiration: time.Now().Add(expDelta),
   156  		authorized: response,
   157  	}
   158  }
   159  
   160  func (s *CredentialsController) Authorize(serviceAccount, namespace string) error {
   161  	user := sa.MakeUsername(namespace, serviceAccount)
   162  	if cached, f := s.cachedAuthorization(user); f {
   163  		return cached
   164  	}
   165  	err := func() error {
   166  		resp, err := s.sar.Create(context.Background(), &authorizationv1.SubjectAccessReview{
   167  			ObjectMeta: metav1.ObjectMeta{},
   168  			Spec: authorizationv1.SubjectAccessReviewSpec{
   169  				ResourceAttributes: &authorizationv1.ResourceAttributes{
   170  					Namespace: namespace,
   171  					Verb:      "list",
   172  					Resource:  "secrets",
   173  				},
   174  				User: user,
   175  			},
   176  		}, metav1.CreateOptions{})
   177  		if err != nil {
   178  			return err
   179  		}
   180  		if !resp.Status.Allowed {
   181  			return fmt.Errorf("%s/%s is not authorized to read secrets: %v", serviceAccount, namespace, resp.Status.Reason)
   182  		}
   183  		return nil
   184  	}()
   185  	s.insertCache(user, err)
   186  	return err
   187  }
   188  
   189  func (s *CredentialsController) GetCertInfo(name, namespace string) (certInfo *credentials.CertInfo, err error) {
   190  	k8sSecret := s.secrets.Get(name, namespace)
   191  	if k8sSecret == nil {
   192  		return nil, fmt.Errorf("secret %v/%v not found", namespace, name)
   193  	}
   194  
   195  	return ExtractCertInfo(k8sSecret)
   196  }
   197  
   198  func (s *CredentialsController) GetCaCert(name, namespace string) (certInfo *credentials.CertInfo, err error) {
   199  	k8sSecret := s.secrets.Get(name, namespace)
   200  	if k8sSecret == nil {
   201  		strippedName := strings.TrimSuffix(name, securitymodel.SdsCaSuffix)
   202  		// Could not fetch cert, look for secret without -cacert suffix
   203  		k8sSecret := s.secrets.Get(strippedName, namespace)
   204  		if k8sSecret == nil {
   205  			return nil, fmt.Errorf("secret %v/%v not found", namespace, strippedName)
   206  		}
   207  		return extractRoot(k8sSecret)
   208  	}
   209  	return extractRoot(k8sSecret)
   210  }
   211  
   212  func (s *CredentialsController) GetDockerCredential(name, namespace string) ([]byte, error) {
   213  	k8sSecret := s.secrets.Get(name, namespace)
   214  	if k8sSecret == nil {
   215  		return nil, fmt.Errorf("secret %v/%v not found", namespace, name)
   216  	}
   217  	if k8sSecret.Type != v1.SecretTypeDockerConfigJson {
   218  		return nil, fmt.Errorf("type of secret %v/%v is not %v", namespace, name, v1.SecretTypeDockerConfigJson)
   219  	}
   220  	if cred, found := k8sSecret.Data[v1.DockerConfigJsonKey]; found {
   221  		return cred, nil
   222  	}
   223  	return nil, fmt.Errorf("cannot find docker config at secret %v/%v", namespace, name)
   224  }
   225  
   226  func hasKeys(d map[string][]byte, keys ...string) bool {
   227  	for _, k := range keys {
   228  		_, f := d[k]
   229  		if !f {
   230  			return false
   231  		}
   232  	}
   233  	return true
   234  }
   235  
   236  func hasValue(d map[string][]byte, keys ...string) bool {
   237  	for _, k := range keys {
   238  		v := d[k]
   239  		if len(v) == 0 {
   240  			return false
   241  		}
   242  	}
   243  	return true
   244  }
   245  
   246  // ExtractCertInfo extracts server key, certificate, and OCSP staple
   247  func ExtractCertInfo(scrt *v1.Secret) (certInfo *credentials.CertInfo, err error) {
   248  	ret := &credentials.CertInfo{}
   249  	if hasValue(scrt.Data, GenericScrtCert, GenericScrtKey) {
   250  		ret.Cert = scrt.Data[GenericScrtCert]
   251  		ret.Key = scrt.Data[GenericScrtKey]
   252  		ret.CRL = scrt.Data[GenericScrtCRL]
   253  		return ret, nil
   254  	}
   255  	if hasValue(scrt.Data, TLSSecretCert, TLSSecretKey) {
   256  		ret.Cert = scrt.Data[TLSSecretCert]
   257  		ret.Key = scrt.Data[TLSSecretKey]
   258  		ret.Staple = scrt.Data[TLSSecretOcspStaple]
   259  		ret.CRL = scrt.Data[TLSSecretCrl]
   260  		return ret, nil
   261  	}
   262  	// No cert found. Try to generate a helpful error message
   263  	if hasKeys(scrt.Data, GenericScrtCert, GenericScrtKey) {
   264  		return nil, fmt.Errorf("found keys %q and %q, but they were empty", GenericScrtCert, GenericScrtKey)
   265  	}
   266  	if hasKeys(scrt.Data, TLSSecretCert, TLSSecretKey) {
   267  		return nil, fmt.Errorf("found keys %q and %q, but they were empty", TLSSecretCert, TLSSecretKey)
   268  	}
   269  	found := truncatedKeysMessage(scrt.Data)
   270  	return nil, fmt.Errorf("found secret, but didn't have expected keys (%s and %s) or (%s and %s); found: %s",
   271  		GenericScrtCert, GenericScrtKey, TLSSecretCert, TLSSecretKey, found)
   272  }
   273  
   274  func truncatedKeysMessage(data map[string][]byte) string {
   275  	keys := []string{}
   276  	for k := range data {
   277  		keys = append(keys, k)
   278  	}
   279  	sort.Strings(keys)
   280  	if len(keys) < 3 {
   281  		return strings.Join(keys, ", ")
   282  	}
   283  	return fmt.Sprintf("%s, and %d more...", strings.Join(keys[:3], ", "), len(keys)-3)
   284  }
   285  
   286  // extractRoot extracts the root certificate
   287  func extractRoot(scrt *v1.Secret) (certInfo *credentials.CertInfo, err error) {
   288  	ret := &credentials.CertInfo{}
   289  	if hasValue(scrt.Data, GenericScrtCaCert) {
   290  		ret.Cert = scrt.Data[GenericScrtCaCert]
   291  		ret.CRL = scrt.Data[GenericScrtCRL]
   292  		return ret, nil
   293  	}
   294  	if hasValue(scrt.Data, TLSSecretCaCert) {
   295  		ret.Cert = scrt.Data[TLSSecretCaCert]
   296  		ret.CRL = scrt.Data[TLSSecretCrl]
   297  		return ret, nil
   298  	}
   299  	// No cert found. Try to generate a helpful error message
   300  	if hasKeys(scrt.Data, GenericScrtCaCert) {
   301  		return nil, fmt.Errorf("found key %q, but it was empty", GenericScrtCaCert)
   302  	}
   303  	if hasKeys(scrt.Data, TLSSecretCaCert) {
   304  		return nil, fmt.Errorf("found key %q, but it was empty", TLSSecretCaCert)
   305  	}
   306  	found := truncatedKeysMessage(scrt.Data)
   307  	return nil, fmt.Errorf("found secret, but didn't have expected keys %s or %s; found: %s",
   308  		GenericScrtCaCert, TLSSecretCaCert, found)
   309  }