istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/k8s/tokenreview/k8sauthn.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 tokenreview
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	k8sauth "k8s.io/api/authentication/v1"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/client-go/kubernetes"
    25  
    26  	"istio.io/istio/pkg/security"
    27  )
    28  
    29  // nolint: lll
    30  // From https://github.com/kubernetes/kubernetes/blob/4f2faa2f1ce8f49983173ef29214156afdf405f9/staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount/util.go#L41
    31  const (
    32  	// PodNameKey is the key used in a user's "extra" to specify the pod name of
    33  	// the authenticating request.
    34  	PodNameKey = "authentication.kubernetes.io/pod-name"
    35  	// PodUIDKey is the key used in a user's "extra" to specify the pod UID of
    36  	// the authenticating request.
    37  	PodUIDKey = "authentication.kubernetes.io/pod-uid"
    38  )
    39  
    40  // ValidateK8sJwt validates a k8s JWT at API server.
    41  // Return {<namespace>, <serviceaccountname>} in the targetToken when the validation passes.
    42  // Otherwise, return the error.
    43  // targetToken: the JWT of the K8s service account to be reviewed
    44  // aud: list of audiences to check. If empty 1st party tokens will be checked.
    45  func ValidateK8sJwt(kubeClient kubernetes.Interface, targetToken string, aud []string) (security.KubernetesInfo, error) {
    46  	tokenReview := &k8sauth.TokenReview{
    47  		Spec: k8sauth.TokenReviewSpec{
    48  			Token: targetToken,
    49  		},
    50  	}
    51  	if aud != nil {
    52  		tokenReview.Spec.Audiences = aud
    53  	}
    54  	reviewRes, err := kubeClient.AuthenticationV1().TokenReviews().Create(context.TODO(), tokenReview, metav1.CreateOptions{})
    55  	if err != nil {
    56  		return security.KubernetesInfo{}, err
    57  	}
    58  
    59  	return getTokenReviewResult(reviewRes)
    60  }
    61  
    62  func getTokenReviewResult(tokenReview *k8sauth.TokenReview) (security.KubernetesInfo, error) {
    63  	if tokenReview.Status.Error != "" {
    64  		return security.KubernetesInfo{}, fmt.Errorf("the service account authentication returns an error: %v",
    65  			tokenReview.Status.Error)
    66  	}
    67  	// An example SA token:
    68  	// {"alg":"RS256","typ":"JWT"}
    69  	// {"iss":"kubernetes/serviceaccount",
    70  	//  "kubernetes.io/serviceaccount/namespace":"default",
    71  	//  "kubernetes.io/serviceaccount/secret.name":"example-pod-sa-token-h4jqx",
    72  	//  "kubernetes.io/serviceaccount/service-account.name":"example-pod-sa",
    73  	//  "kubernetes.io/serviceaccount/service-account.uid":"ff578a9e-65d3-11e8-aad2-42010a8a001d",
    74  	//  "sub":"system:serviceaccount:default:example-pod-sa"
    75  	//  }
    76  
    77  	// An example token review status
    78  	// "status":{
    79  	//   "authenticated":true,
    80  	//   "user":{
    81  	//     "username":"system:serviceaccount:default:example-pod-sa",
    82  	//     "uid":"ff578a9e-65d3-11e8-aad2-42010a8a001d",
    83  	//     "groups":["system:serviceaccounts","system:serviceaccounts:default","system:authenticated"]
    84  	//    }
    85  	// }
    86  
    87  	if !tokenReview.Status.Authenticated {
    88  		return security.KubernetesInfo{}, fmt.Errorf("the token is not authenticated")
    89  	}
    90  	inServiceAccountGroup := false
    91  	for _, group := range tokenReview.Status.User.Groups {
    92  		if group == "system:serviceaccounts" {
    93  			inServiceAccountGroup = true
    94  			break
    95  		}
    96  	}
    97  	if !inServiceAccountGroup {
    98  		return security.KubernetesInfo{}, fmt.Errorf("the token is not a service account")
    99  	}
   100  	// "username" is in the form of system:serviceaccount:{namespace}:{service account name}",
   101  	// e.g., "username":"system:serviceaccount:default:example-pod-sa"
   102  	subStrings := strings.Split(tokenReview.Status.User.Username, ":")
   103  	if len(subStrings) != 4 {
   104  		return security.KubernetesInfo{}, fmt.Errorf("invalid username field in the token review result")
   105  	}
   106  
   107  	return security.KubernetesInfo{
   108  		PodName:           extractExtra(tokenReview, PodNameKey),
   109  		PodNamespace:      subStrings[2],
   110  		PodUID:            extractExtra(tokenReview, PodUIDKey),
   111  		PodServiceAccount: subStrings[3],
   112  	}, nil
   113  }
   114  
   115  func extractExtra(review *k8sauth.TokenReview, s string) string {
   116  	values, ok := review.Status.User.Extra[s]
   117  	if !ok || len(values) == 0 {
   118  		return ""
   119  	}
   120  	return values[0]
   121  }