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 }