github.com/oam-dev/kubevela@v1.9.11/pkg/auth/kubeconfig.go (about)

     1  /*
     2  Copyright 2022 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package auth
    18  
    19  import (
    20  	"context"
    21  	"crypto/rand"
    22  	"crypto/rsa"
    23  	"crypto/x509"
    24  	"crypto/x509/pkix"
    25  	"encoding/pem"
    26  	"fmt"
    27  	"io"
    28  	"os"
    29  	"time"
    30  
    31  	"github.com/pkg/errors"
    32  	authenticationv1 "k8s.io/api/authentication/v1"
    33  	certificatesv1 "k8s.io/api/certificates/v1"
    34  	certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
    35  	corev1 "k8s.io/api/core/v1"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/types"
    38  	"k8s.io/apimachinery/pkg/util/version"
    39  	"k8s.io/apimachinery/pkg/util/wait"
    40  	"k8s.io/apiserver/pkg/authentication/serviceaccount"
    41  	"k8s.io/apiserver/pkg/authentication/user"
    42  	"k8s.io/client-go/kubernetes"
    43  	"k8s.io/client-go/tools/clientcmd"
    44  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    45  	"k8s.io/utils/pointer"
    46  
    47  	"github.com/oam-dev/kubevela/pkg/utils"
    48  )
    49  
    50  // DefaultExpireTime is default expire time for both X.509 and SA token apply
    51  const DefaultExpireTime = time.Hour * 24 * 365
    52  
    53  // KubeConfigGenerateOptions options for create KubeConfig
    54  type KubeConfigGenerateOptions struct {
    55  	X509           *KubeConfigGenerateX509Options
    56  	ServiceAccount *KubeConfigGenerateServiceAccountOptions
    57  }
    58  
    59  // KubeConfigGenerateX509Options options for create X509 based KubeConfig
    60  type KubeConfigGenerateX509Options struct {
    61  	User           string
    62  	Groups         []string
    63  	ExpireTime     time.Duration
    64  	PrivateKeyBits int
    65  }
    66  
    67  // KubeConfigGenerateServiceAccountOptions options for create ServiceAccount based KubeConfig
    68  type KubeConfigGenerateServiceAccountOptions struct {
    69  	ServiceAccountName      string
    70  	ServiceAccountNamespace string
    71  	ExpireTime              time.Duration
    72  }
    73  
    74  // KubeConfigWithUserGenerateOption option for setting user in KubeConfig
    75  type KubeConfigWithUserGenerateOption string
    76  
    77  // ApplyToOptions .
    78  func (opt KubeConfigWithUserGenerateOption) ApplyToOptions(options *KubeConfigGenerateOptions) {
    79  	options.X509.User = string(opt)
    80  }
    81  
    82  // KubeConfigWithGroupGenerateOption option for setting group in KubeConfig
    83  type KubeConfigWithGroupGenerateOption string
    84  
    85  // ApplyToOptions .
    86  func (opt KubeConfigWithGroupGenerateOption) ApplyToOptions(options *KubeConfigGenerateOptions) {
    87  	for _, group := range options.X509.Groups {
    88  		if group == string(opt) {
    89  			return
    90  		}
    91  	}
    92  	options.X509.Groups = append(options.X509.Groups, string(opt))
    93  }
    94  
    95  // KubeConfigWithServiceAccountGenerateOption option for setting service account in KubeConfig
    96  type KubeConfigWithServiceAccountGenerateOption types.NamespacedName
    97  
    98  // ApplyToOptions .
    99  func (opt KubeConfigWithServiceAccountGenerateOption) ApplyToOptions(options *KubeConfigGenerateOptions) {
   100  	options.X509 = nil
   101  	options.ServiceAccount = &KubeConfigGenerateServiceAccountOptions{
   102  		ServiceAccountName:      opt.Name,
   103  		ServiceAccountNamespace: opt.Namespace,
   104  		ExpireTime:              DefaultExpireTime,
   105  	}
   106  }
   107  
   108  // KubeConfigWithIdentityGenerateOption option for setting identity in KubeConfig
   109  type KubeConfigWithIdentityGenerateOption Identity
   110  
   111  // ApplyToOptions .
   112  func (opt KubeConfigWithIdentityGenerateOption) ApplyToOptions(options *KubeConfigGenerateOptions) {
   113  	if opt.User != "" {
   114  		KubeConfigWithUserGenerateOption(opt.User).ApplyToOptions(options)
   115  	}
   116  	for _, group := range opt.Groups {
   117  		KubeConfigWithGroupGenerateOption(group).ApplyToOptions(options)
   118  	}
   119  	if opt.ServiceAccount != "" {
   120  		(KubeConfigWithServiceAccountGenerateOption{
   121  			Name:      opt.ServiceAccount,
   122  			Namespace: opt.ServiceAccountNamespace,
   123  		}).ApplyToOptions(options)
   124  	}
   125  }
   126  
   127  // KubeConfigGenerateOption option for create KubeConfig
   128  type KubeConfigGenerateOption interface {
   129  	ApplyToOptions(options *KubeConfigGenerateOptions)
   130  }
   131  
   132  func newKubeConfigGenerateOptions(options ...KubeConfigGenerateOption) *KubeConfigGenerateOptions {
   133  	opts := &KubeConfigGenerateOptions{
   134  		X509: &KubeConfigGenerateX509Options{
   135  			User:           user.Anonymous,
   136  			Groups:         []string{KubeVelaClientGroup},
   137  			ExpireTime:     DefaultExpireTime,
   138  			PrivateKeyBits: 2048,
   139  		},
   140  		ServiceAccount: nil,
   141  	}
   142  	for _, op := range options {
   143  		op.ApplyToOptions(opts)
   144  	}
   145  	return opts
   146  }
   147  
   148  const (
   149  	// KubeVelaClientGroup the default group to be added to the generated X509 KubeConfig
   150  	KubeVelaClientGroup = "kubevela:client"
   151  	// CSRNamePrefix the prefix of the CSR name
   152  	CSRNamePrefix = "kubevela-csr"
   153  )
   154  
   155  // GenerateKubeConfig generate KubeConfig for users with given options.
   156  func GenerateKubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, options ...KubeConfigGenerateOption) (*clientcmdapi.Config, error) {
   157  	opts := newKubeConfigGenerateOptions(options...)
   158  	if opts.X509 != nil {
   159  		return generateX509KubeConfig(ctx, cli, cfg, writer, opts.X509)
   160  	} else if opts.ServiceAccount != nil {
   161  		return generateServiceAccountKubeConfig(ctx, cli, cfg, writer, opts.ServiceAccount)
   162  	}
   163  	return nil, errors.New("either x509 or serviceaccount must be set for creating KubeConfig")
   164  }
   165  
   166  func genKubeConfig(cfg *clientcmdapi.Config, authInfo *clientcmdapi.AuthInfo, caData []byte) (*clientcmdapi.Config, error) {
   167  	if len(cfg.Clusters) == 0 {
   168  		return nil, fmt.Errorf("there is no clusters in the cluster config")
   169  	}
   170  	exportCfg := cfg.DeepCopy()
   171  	var exportContext *clientcmdapi.Context
   172  	if len(cfg.Contexts) > 0 {
   173  		exportContext = cfg.Contexts[cfg.CurrentContext].DeepCopy()
   174  		exportCfg.Contexts = map[string]*clientcmdapi.Context{cfg.CurrentContext: exportContext}
   175  	} else {
   176  		exportCfg.Contexts = map[string]*clientcmdapi.Context{}
   177  		for name := range cfg.Clusters {
   178  			exportContext = &clientcmdapi.Context{
   179  				Cluster:  name,
   180  				AuthInfo: authInfo.Username,
   181  			}
   182  			exportCfg.Contexts["local"] = exportContext
   183  		}
   184  		exportCfg.CurrentContext = "local"
   185  	}
   186  	exportCluster := cfg.Clusters[exportContext.Cluster].DeepCopy()
   187  	if caData != nil {
   188  		exportCluster.CertificateAuthorityData = caData
   189  	}
   190  	exportCfg.Clusters = map[string]*clientcmdapi.Cluster{exportContext.Cluster: exportCluster}
   191  	exportCfg.AuthInfos = map[string]*clientcmdapi.AuthInfo{exportContext.AuthInfo: authInfo}
   192  	return exportCfg, nil
   193  }
   194  
   195  func makeCertAndKey(writer io.Writer, opts *KubeConfigGenerateX509Options) ([]byte, []byte, error) {
   196  	// generate private key
   197  	privateKey, err := rsa.GenerateKey(rand.Reader, opts.PrivateKeyBits)
   198  	if err != nil {
   199  		return nil, nil, err
   200  	}
   201  	keyBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
   202  	_, _ = fmt.Fprintf(writer, "Private key generated.\n")
   203  
   204  	template := &x509.CertificateRequest{
   205  		Subject: pkix.Name{
   206  			CommonName:   opts.User,
   207  			Organization: opts.Groups,
   208  		},
   209  		SignatureAlgorithm: x509.SHA256WithRSA,
   210  	}
   211  
   212  	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey)
   213  	if err != nil {
   214  		return nil, nil, err
   215  	}
   216  	csrPemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
   217  	_, _ = fmt.Fprintf(writer, "Certificate request generated.\n")
   218  	return csrPemBytes, keyBytes, nil
   219  }
   220  
   221  func makeCSRName(user string) string {
   222  	return fmt.Sprintf("%s-%s", CSRNamePrefix, user)
   223  }
   224  
   225  func generateX509KubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, options *KubeConfigGenerateX509Options) (*clientcmdapi.Config, error) {
   226  	info, _ := cli.Discovery().ServerVersion()
   227  	if info == nil || version.MustParseGeneric(info.String()).AtLeast(version.MustParseSemantic("v1.19.0")) {
   228  
   229  		return generateX509KubeConfigV1(ctx, cli, cfg, writer, options)
   230  	}
   231  	return generateX509KubeConfigV1Beta(ctx, cli, cfg, writer, options)
   232  }
   233  
   234  func generateX509KubeConfigV1(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, opts *KubeConfigGenerateX509Options) (*clientcmdapi.Config, error) {
   235  	csrPemBytes, keyBytes, err := makeCertAndKey(writer, opts)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	csr := &certificatesv1.CertificateSigningRequest{}
   240  	csr.Name = makeCSRName(opts.User)
   241  	csr.Spec.SignerName = certificatesv1.KubeAPIServerClientSignerName
   242  	csr.Spec.Usages = []certificatesv1.KeyUsage{certificatesv1.UsageClientAuth}
   243  	csr.Spec.Request = csrPemBytes
   244  	csr.Spec.ExpirationSeconds = pointer.Int32(int32(opts.ExpireTime.Seconds()))
   245  	if _, err := cli.CertificatesV1().CertificateSigningRequests().Create(ctx, csr, metav1.CreateOptions{}); err != nil {
   246  		return nil, err
   247  	}
   248  	_, _ = fmt.Fprintf(writer, "Certificate signing request %s generated.\n", csr.Name)
   249  	defer func() {
   250  		_ = cli.CertificatesV1().CertificateSigningRequests().Delete(ctx, csr.Name, metav1.DeleteOptions{})
   251  	}()
   252  	csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{
   253  		Type:           certificatesv1.CertificateApproved,
   254  		Status:         corev1.ConditionTrue,
   255  		Reason:         "Self-generated and auto-approved by KubeVela",
   256  		Message:        "This CSR was approved by KubeVela",
   257  		LastUpdateTime: metav1.Now(),
   258  	})
   259  	if csr, err = cli.CertificatesV1().CertificateSigningRequests().UpdateApproval(ctx, csr.Name, csr, metav1.UpdateOptions{}); err != nil {
   260  		return nil, err
   261  	}
   262  	_, _ = fmt.Fprintf(writer, "Certificate signing request %s approved.\n", csr.Name)
   263  
   264  	if err := wait.Poll(time.Second, time.Minute, func() (done bool, err error) {
   265  		if csr, err = cli.CertificatesV1().CertificateSigningRequests().Get(ctx, csr.Name, metav1.GetOptions{}); err != nil {
   266  			return false, err
   267  		}
   268  		if csr.Status.Certificate == nil {
   269  			return false, nil
   270  		}
   271  		return true, nil
   272  	}); err != nil {
   273  		return nil, err
   274  	}
   275  	_, _ = fmt.Fprintf(writer, "Signed certificate retrieved.\n")
   276  
   277  	return genKubeConfig(cfg, &clientcmdapi.AuthInfo{
   278  		ClientKeyData:         keyBytes,
   279  		ClientCertificateData: csr.Status.Certificate,
   280  	}, nil)
   281  }
   282  
   283  func generateX509KubeConfigV1Beta(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, opts *KubeConfigGenerateX509Options) (*clientcmdapi.Config, error) {
   284  	csrPemBytes, keyBytes, err := makeCertAndKey(writer, opts)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  	csr := &certificatesv1beta1.CertificateSigningRequest{}
   289  	csr.Name = makeCSRName(opts.User)
   290  	var name = certificatesv1beta1.KubeAPIServerClientSignerName
   291  	csr.Spec.SignerName = &name
   292  	csr.Spec.Usages = []certificatesv1beta1.KeyUsage{certificatesv1beta1.UsageClientAuth}
   293  	csr.Spec.Request = csrPemBytes
   294  	csr.Spec.ExpirationSeconds = pointer.Int32(int32(opts.ExpireTime.Seconds()))
   295  	// create
   296  	if _, err = cli.CertificatesV1beta1().CertificateSigningRequests().Create(ctx, csr, metav1.CreateOptions{}); err != nil {
   297  		return nil, err
   298  	}
   299  	_, _ = fmt.Fprintf(writer, "Certificate signing request %s generated.\n", csr.Name)
   300  	defer func() {
   301  		_ = cli.CertificatesV1beta1().CertificateSigningRequests().Delete(ctx, csr.Name, metav1.DeleteOptions{})
   302  	}()
   303  
   304  	// approval
   305  	csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1beta1.CertificateSigningRequestCondition{
   306  		Type:           certificatesv1beta1.CertificateApproved,
   307  		Status:         corev1.ConditionTrue,
   308  		Reason:         "Self-generated and auto-approved by KubeVela",
   309  		Message:        "This CSR was approved by KubeVela",
   310  		LastUpdateTime: metav1.Now(),
   311  	})
   312  	if csr, err = cli.CertificatesV1beta1().CertificateSigningRequests().UpdateApproval(ctx, csr, metav1.UpdateOptions{}); err != nil {
   313  		return nil, err
   314  	}
   315  	_, _ = fmt.Fprintf(writer, "Certificate signing request %s approved.\n", csr.Name)
   316  
   317  	// waiting and get the status
   318  	if err = wait.Poll(time.Second, time.Minute, func() (done bool, err error) {
   319  		if csr, err = cli.CertificatesV1beta1().CertificateSigningRequests().Get(ctx, csr.Name, metav1.GetOptions{}); err != nil {
   320  			return false, err
   321  		}
   322  		if csr.Status.Certificate == nil {
   323  			return false, nil
   324  		}
   325  		return true, nil
   326  	}); err != nil {
   327  		return nil, err
   328  	}
   329  	_, _ = fmt.Fprintf(writer, "Signed certificate retrieved.\n")
   330  
   331  	return genKubeConfig(cfg, &clientcmdapi.AuthInfo{
   332  		ClientKeyData:         keyBytes,
   333  		ClientCertificateData: csr.Status.Certificate,
   334  	}, nil)
   335  }
   336  
   337  func generateServiceAccountKubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, opts *KubeConfigGenerateServiceAccountOptions) (*clientcmdapi.Config, error) {
   338  	var (
   339  		token string
   340  		CA    []byte
   341  	)
   342  	sa, err := cli.CoreV1().ServiceAccounts(opts.ServiceAccountNamespace).Get(ctx, opts.ServiceAccountName, metav1.GetOptions{})
   343  	if err != nil {
   344  		return nil, err
   345  	}
   346  	_, _ = fmt.Fprintf(writer, "ServiceAccount %s/%s found.\n", opts.ServiceAccountNamespace, opts.ServiceAccountName)
   347  	if len(sa.Secrets) == 0 {
   348  		_, _ = fmt.Fprintf(writer, "ServiceAccount %s/%s has no secret. Requesting token", opts.ServiceAccountNamespace, opts.ServiceAccountName)
   349  		request := authenticationv1.TokenRequest{
   350  			Spec: authenticationv1.TokenRequestSpec{
   351  				Audiences:         []string{},
   352  				ExpirationSeconds: pointer.Int64(int64(opts.ExpireTime.Seconds())),
   353  			},
   354  		}
   355  		tokenRequest, err := cli.CoreV1().ServiceAccounts(opts.ServiceAccountNamespace).CreateToken(ctx, opts.ServiceAccountName, &request, metav1.CreateOptions{})
   356  		if err != nil {
   357  			return nil, errors.Wrap(err, "failed to request token")
   358  		}
   359  		token = tokenRequest.Status.Token
   360  		CAConfigMap, err := cli.CoreV1().ConfigMaps(sa.Namespace).Get(ctx, "kube-root-ca.crt", metav1.GetOptions{})
   361  		if err != nil {
   362  			return nil, errors.Wrap(err, "failed to get root CA secret")
   363  		}
   364  		CA = []byte(CAConfigMap.Data["ca.crt"])
   365  	} else {
   366  		secretKey := sa.Secrets[0]
   367  		if secretKey.Namespace == "" {
   368  			secretKey.Namespace = sa.Namespace
   369  		}
   370  		secret, err := cli.CoreV1().Secrets(secretKey.Namespace).Get(ctx, secretKey.Name, metav1.GetOptions{})
   371  		if err != nil {
   372  			return nil, err
   373  		}
   374  		_, _ = fmt.Fprintf(writer, "ServiceAccount secret %s/%s found.\n", secretKey.Namespace, secret.Name)
   375  		if len(secret.Data["token"]) == 0 {
   376  			return nil, errors.Errorf("no token found in secret %s/%s", secret.Namespace, secret.Name)
   377  		}
   378  		_, _ = fmt.Fprintf(writer, "ServiceAccount token found.\n")
   379  		token = string(secret.Data["token"])
   380  		CA = secret.Data["ca.crt"]
   381  	}
   382  	return genKubeConfig(cfg, &clientcmdapi.AuthInfo{
   383  		Token: token,
   384  	}, CA)
   385  }
   386  
   387  // ReadIdentityFromKubeConfig extract identity from kubeconfig
   388  func ReadIdentityFromKubeConfig(kubeconfigPath string) (*Identity, error) {
   389  	cfg, err := clientcmd.LoadFromFile(kubeconfigPath)
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  	ctx, exists := cfg.Contexts[cfg.CurrentContext]
   394  	if !exists {
   395  		return nil, fmt.Errorf("cannot find current-context %s", cfg.CurrentContext)
   396  	}
   397  	authInfo, exists := cfg.AuthInfos[ctx.AuthInfo]
   398  	if !exists {
   399  		return nil, fmt.Errorf("cannot find auth-info %s", ctx.AuthInfo)
   400  	}
   401  
   402  	identity := &Identity{}
   403  	token := authInfo.Token
   404  	if token == "" && authInfo.TokenFile != "" {
   405  		bs, err := os.ReadFile(authInfo.TokenFile)
   406  		if err != nil {
   407  			return nil, fmt.Errorf("failed to read token file %s: %w", authInfo.TokenFile, err)
   408  		}
   409  		token = string(bs)
   410  	}
   411  	if token != "" {
   412  		sub, err := utils.GetTokenSubject(token)
   413  		if err != nil {
   414  			return nil, fmt.Errorf("failed to recognize serviceaccount: %w", err)
   415  		}
   416  		identity.ServiceAccountNamespace, identity.ServiceAccount, err = serviceaccount.SplitUsername(sub)
   417  		if err != nil {
   418  			return nil, fmt.Errorf("cannot parse serviceaccount from %s: %w", sub, err)
   419  		}
   420  		return identity, nil
   421  	}
   422  
   423  	certData := authInfo.ClientCertificateData
   424  	if len(certData) == 0 && authInfo.ClientCertificate != "" {
   425  		certData, err = os.ReadFile(authInfo.ClientCertificate)
   426  		if err != nil {
   427  			return nil, fmt.Errorf("failed to read cert file %s: %w", authInfo.ClientCertificate, err)
   428  		}
   429  	}
   430  	if len(certData) > 0 {
   431  		name, err := utils.GetCertificateSubject(certData)
   432  		if err != nil {
   433  			return nil, fmt.Errorf("failed to get subject from certificate data: %w", err)
   434  		}
   435  		identity.User = name.CommonName
   436  		identity.Groups = name.Organization
   437  		return identity, nil
   438  	}
   439  	return nil, fmt.Errorf("cannot find client certificate or serviceaccount token in kubeconfig")
   440  }