github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/lxd/credentials.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lxd 5 6 import ( 7 "crypto/x509" 8 "encoding/base64" 9 "fmt" 10 "io" 11 "net/url" 12 "os" 13 "path/filepath" 14 "runtime" 15 16 "github.com/canonical/lxd/shared/api" 17 "github.com/juju/errors" 18 "github.com/juju/utils/v3" 19 20 "github.com/juju/juju/cloud" 21 "github.com/juju/juju/container/lxd" 22 "github.com/juju/juju/environs" 23 environscloudspec "github.com/juju/juju/environs/cloudspec" 24 "github.com/juju/juju/juju/osenv" 25 "github.com/juju/juju/pki" 26 pkiassertion "github.com/juju/juju/pki/assertion" 27 "github.com/juju/juju/provider/lxd/lxdnames" 28 ) 29 30 const ( 31 credAttrServerCert = "server-cert" 32 credAttrClientCert = "client-cert" 33 credAttrClientKey = "client-key" 34 credAttrTrustPassword = "trust-password" 35 ) 36 37 // CertificateReadWriter groups methods that is required to read and write 38 // certificates at a given path. 39 type CertificateReadWriter interface { 40 // Read takes a path and returns both a cert and key PEM. 41 // Returns an error if there was an issue reading the certs. 42 Read(path string) (certPEM, keyPEM []byte, err error) 43 44 // Write takes a path and cert, key PEM and stores them. 45 // Returns an error if there was an issue writing the certs. 46 Write(path string, certPEM, keyPEM []byte) error 47 } 48 49 // CertificateGenerator groups methods for generating a new certificate 50 type CertificateGenerator interface { 51 // Generate creates client or server certificate and key pair, 52 // returning them as byte arrays in memory. 53 Generate(client bool, addHosts bool) (certPEM, keyPEM []byte, err error) 54 } 55 56 // environProviderCredentials implements environs.ProviderCredentials. 57 type environProviderCredentials struct { 58 certReadWriter CertificateReadWriter 59 certGenerator CertificateGenerator 60 serverFactory ServerFactory 61 lxcConfigReader LXCConfigReader 62 } 63 64 // CredentialSchemas is part of the environs.ProviderCredentials interface. 65 func (environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema { 66 return map[cloud.AuthType]cloud.CredentialSchema{ 67 cloud.CertificateAuthType: { 68 { 69 Name: credAttrServerCert, 70 CredentialAttr: cloud.CredentialAttr{ 71 Description: "the path to the PEM-encoded LXD server certificate file", 72 ExpandFilePath: true, 73 Hidden: true, 74 }, 75 }, { 76 Name: credAttrClientCert, 77 CredentialAttr: cloud.CredentialAttr{ 78 Description: "the path to the PEM-encoded LXD client certificate file", 79 ExpandFilePath: true, 80 Hidden: true, 81 }, 82 }, { 83 Name: credAttrClientKey, 84 CredentialAttr: cloud.CredentialAttr{ 85 Description: "the path to the PEM-encoded LXD client key file", 86 ExpandFilePath: true, 87 Hidden: true, 88 }, 89 }, 90 }, 91 cloud.InteractiveAuthType: { 92 { 93 Name: credAttrTrustPassword, 94 CredentialAttr: cloud.CredentialAttr{ 95 Description: "the LXD server trust password", 96 Hidden: true, 97 }, 98 }, 99 }, 100 } 101 } 102 103 // RegisterCredentials is part of the environs.ProviderCredentialsRegister interface. 104 func (p environProviderCredentials) RegisterCredentials(cld cloud.Cloud) (map[string]*cloud.CloudCredential, error) { 105 // only register credentials if the operator is attempting to access "lxd" 106 // or "localhost" 107 cloudName := cld.Name 108 if !lxdnames.IsDefaultCloud(cloudName) { 109 return make(map[string]*cloud.CloudCredential), nil 110 } 111 112 nopLogf := func(msg string, args ...interface{}) {} 113 certPEM, keyPEM, err := p.readOrGenerateCert(nopLogf) 114 if err != nil { 115 return nil, errors.Trace(err) 116 } 117 118 localCertCredential, err := p.detectLocalCredentials(certPEM, keyPEM) 119 if err != nil { 120 return nil, errors.Trace(err) 121 } 122 123 return map[string]*cloud.CloudCredential{ 124 cloudName: { 125 DefaultCredential: cloudName, 126 AuthCredentials: map[string]cloud.Credential{ 127 cloudName: *localCertCredential, 128 }, 129 }, 130 }, nil 131 } 132 133 // DetectCredentials is part of the environs.ProviderCredentials interface. 134 func (p environProviderCredentials) DetectCredentials(cloudName string) (*cloud.CloudCredential, error) { 135 nopLogf := func(msg string, args ...interface{}) {} 136 certPEM, keyPEM, err := p.readOrGenerateCert(nopLogf) 137 if err != nil { 138 return nil, errors.Trace(err) 139 } 140 141 remoteCertCredentials, err := p.detectRemoteCredentials() 142 if err != nil { 143 logger.Debugf("unable to detect remote LXC credentials: %s", err) 144 } 145 146 // If the cloud is built-in, we can start a local server to 147 // finalise the credential over the LXD Unix docket. 148 var localCertCredentials *cloud.Credential 149 if cloudName == "" || lxdnames.IsDefaultCloud(cloudName) { 150 if localCertCredentials, err = p.detectLocalCredentials(certPEM, keyPEM); err != nil { 151 logger.Debugf("unable to detect local LXC credentials: %s", err) 152 } 153 } 154 155 authCredentials := make(map[string]cloud.Credential) 156 for k, v := range remoteCertCredentials { 157 if cloudName != "" && k != cloudName { 158 continue 159 } 160 authCredentials[k] = v 161 } 162 if localCertCredentials != nil { 163 if cloudName == "" || lxdnames.IsDefaultCloud(cloudName) { 164 authCredentials[lxdnames.DefaultCloud] = *localCertCredentials 165 } 166 } 167 return &cloud.CloudCredential{ 168 AuthCredentials: authCredentials, 169 }, nil 170 } 171 172 // detectLocalCredentials will use the local server to read and finalize the 173 // cloud credentials. 174 func (p environProviderCredentials) detectLocalCredentials(certPEM, keyPEM []byte) (*cloud.Credential, error) { 175 svr, err := p.serverFactory.LocalServer() 176 if err != nil { 177 return nil, errors.NewNotFound(err, "failed to connect to local LXD") 178 } 179 180 label := fmt.Sprintf("LXD credential %q", lxdnames.DefaultCloud) 181 certCredential, err := p.finalizeLocalCredential( 182 io.Discard, svr, string(certPEM), string(keyPEM), label, 183 ) 184 return certCredential, errors.Trace(err) 185 } 186 187 // detectRemoteCredentials will attempt to gather all the potential existing 188 // remote lxc configurations found in `$HOME/.config/lxc/.config` file. 189 // Any setups found in the configuration will then be returned as a credential 190 // that can be automatically loaded into juju. 191 func (p environProviderCredentials) detectRemoteCredentials() (map[string]cloud.Credential, error) { 192 credentials := make(map[string]cloud.Credential) 193 for _, configDir := range configDirs() { 194 configPath := filepath.Join(configDir, "config.yml") 195 config, err := p.lxcConfigReader.ReadConfig(configPath) 196 if err != nil { 197 return nil, errors.Trace(err) 198 } 199 200 if len(config.Remotes) == 0 { 201 continue 202 } 203 204 certPEM, keyPEM, err := p.certReadWriter.Read(configDir) 205 if err != nil && !os.IsNotExist(errors.Cause(err)) { 206 return nil, errors.Trace(err) 207 } else if os.IsNotExist(err) { 208 continue 209 } 210 211 for name, remote := range config.Remotes { 212 if remote.Protocol != lxdnames.ProviderType { 213 continue 214 } 215 certPath := filepath.Join(configDir, "servercerts", fmt.Sprintf("%s.crt", name)) 216 authConfig := map[string]string{ 217 credAttrClientCert: string(certPEM), 218 credAttrClientKey: string(keyPEM), 219 } 220 serverCert, err := p.lxcConfigReader.ReadCert(certPath) 221 if err != nil { 222 if !os.IsNotExist(errors.Cause(err)) { 223 logger.Errorf("unable to read certificate from %s with error %s", certPath, err) 224 continue 225 } 226 } else { 227 authConfig[credAttrServerCert] = string(serverCert) 228 } 229 credential := cloud.NewCredential(cloud.CertificateAuthType, authConfig) 230 credential.Label = fmt.Sprintf("LXD credential %q", name) 231 credentials[name] = credential 232 } 233 } 234 return credentials, nil 235 } 236 237 func (p environProviderCredentials) readOrGenerateCert(logf func(string, ...interface{})) (certPEM, keyPEM []byte, _ error) { 238 for _, dir := range configDirs() { 239 certPEM, keyPEM, err := p.certReadWriter.Read(dir) 240 if err == nil { 241 logf("Loaded client cert/key from %q", dir) 242 return certPEM, keyPEM, nil 243 } else if !os.IsNotExist(errors.Cause(err)) { 244 return nil, nil, errors.Trace(err) 245 } 246 } 247 248 // No certs were found, so generate one and cache it in the 249 // Juju XDG_DATA dir. We cache the certificate so that we 250 // avoid uploading a new certificate each time we bootstrap. 251 jujuLXDDir := osenv.JujuXDGDataHomePath("lxd") 252 certPEM, keyPEM, err := p.certGenerator.Generate(true, true) 253 if err != nil { 254 return nil, nil, errors.Trace(err) 255 } 256 if err := p.certReadWriter.Write(jujuLXDDir, certPEM, keyPEM); err != nil { 257 return nil, nil, errors.Trace(err) 258 } 259 logf("Generating client cert/key in %q", jujuLXDDir) 260 return certPEM, keyPEM, nil 261 } 262 263 // ShouldFinalizeCredential is part of the environs.RequestFinalizeCredential 264 // interface. 265 // This is an optional interface to check if the server certificate has not 266 // been filled in. 267 func (p environProviderCredentials) ShouldFinalizeCredential(cred cloud.Credential) bool { 268 // We should always finalize the credentials so we can perform some sanity 269 // checking on them. 270 return true 271 } 272 273 // FinalizeCredential is part of the environs.ProviderCredentials interface. 274 func (p environProviderCredentials) FinalizeCredential( 275 ctx environs.FinalizeCredentialContext, 276 args environs.FinalizeCredentialParams, 277 ) (*cloud.Credential, error) { 278 switch authType := args.Credential.AuthType(); authType { 279 case cloud.InteractiveAuthType: 280 credAttrs := args.Credential.Attributes() 281 // We don't care if the password is empty, just that it exists. Empty 282 // passwords can be valid ones... 283 if _, ok := credAttrs[credAttrTrustPassword]; ok { 284 // check to see if the client cert, keys exist, if they do not, 285 // generate them for the user. 286 if _, ok := getClientCertificates(args.Credential); !ok { 287 stderr := ctx.GetStderr() 288 nopLogf := func(s string, args ...interface{}) { 289 fmt.Fprintf(stderr, s+"\n", args...) 290 } 291 clientCert, clientKey, err := p.readOrGenerateCert(nopLogf) 292 if err != nil { 293 return nil, err 294 } 295 296 credAttrs[credAttrClientCert] = string(clientCert) 297 credAttrs[credAttrClientKey] = string(clientKey) 298 299 credential := cloud.NewCredential(cloud.CertificateAuthType, credAttrs) 300 credential.Label = args.Credential.Label 301 302 args.Credential = credential 303 } 304 } 305 fallthrough 306 case cloud.CertificateAuthType: 307 return p.finalizeCredential(ctx, args) 308 default: 309 return &args.Credential, nil 310 } 311 } 312 313 // validateServerCertificate validates the server certificate we have 314 // been supplied with to make sure that it meets our expectations. Checks 315 // involve making sure there is a certificate contained in the pem data and that 316 // certificate can be used for server authentication. 317 // 318 // NOTE (tlm): This was added to help users who have potentially made a mistake 319 // in their credentials.yaml file when calling juju add-credential. The case 320 // that prompted this function was mixing the client certificate with the server 321 // certificate. 322 func validateServerCertificate(serverPemCertificate string) error { 323 certs, _, err := pki.UnmarshalPemData([]byte(serverPemCertificate)) 324 if err != nil { 325 return fmt.Errorf("parsing LXD server certificate as PEM: %w", err) 326 } 327 328 if len(certs) == 0 { 329 return errors.NotFoundf("LXD server certificate") 330 } 331 332 // We only care about the first certificate here as any more cert's in the 333 // file are considered to form the rest of the supporting chain. 334 if !pkiassertion.HasExtKeyUsage(certs[0], x509.ExtKeyUsageServerAuth) { 335 return errors.New("certificate from LXD server certificate file is not signed for server authentication") 336 } 337 338 return nil 339 } 340 341 func (p environProviderCredentials) finalizeCredential( 342 ctx environs.FinalizeCredentialContext, 343 args environs.FinalizeCredentialParams, 344 ) (*cloud.Credential, error) { 345 // Credential detection yields a partial certificate containing just 346 // the client certificate and key. We check if we have a partial 347 // credential, and fill in the server certificate if we can. 348 stderr := ctx.GetStderr() 349 credAttrs := args.Credential.Attributes() 350 351 certPEM := credAttrs[credAttrClientCert] 352 keyPEM := credAttrs[credAttrClientKey] 353 if certPEM == "" { 354 return nil, errors.NotValidf("missing or empty %q attribute", credAttrClientCert) 355 } 356 if keyPEM == "" { 357 return nil, errors.NotValidf("missing or empty %q attribute", credAttrClientKey) 358 } 359 360 // If the cloud is built-in, the endpoint will be empty 361 // and we can start a local server to finalise the credential 362 // over the LXD Unix docket. 363 if args.CloudEndpoint == "" { 364 svr, err := p.serverFactory.LocalServer() 365 if err != nil { 366 return nil, errors.Trace(err) 367 } 368 cred, err := p.finalizeLocalCredential( 369 stderr, svr, certPEM, keyPEM, 370 args.Credential.Label, 371 ) 372 return cred, errors.Trace(err) 373 } 374 375 if v, ok := credAttrs[credAttrServerCert]; ok && v != "" { 376 return &args.Credential, validateServerCertificate(v) 377 } 378 379 // We're not local, so set up the remote server and automate the remote 380 // certificate credentials. 381 return p.finalizeRemoteCredential( 382 stderr, 383 args.CloudEndpoint, 384 args.Credential, 385 ) 386 } 387 388 func (p environProviderCredentials) finalizeRemoteCredential( 389 output io.Writer, 390 endpoint string, 391 credentials cloud.Credential, 392 ) (*cloud.Credential, error) { 393 clientCert, ok := getClientCertificates(credentials) 394 if !ok { 395 return nil, errors.NotFoundf("client credentials") 396 } 397 if err := clientCert.Validate(); err != nil { 398 return nil, errors.Annotate(err, "client credentials") 399 } 400 401 credAttrs := credentials.Attributes() 402 trustPassword, ok := credAttrs[credAttrTrustPassword] 403 if !ok { 404 return nil, errors.NotValidf("missing %q attribute", credAttrTrustPassword) 405 } 406 407 insecureCreds := cloud.NewCredential(cloud.CertificateAuthType, credAttrs) 408 server, err := p.serverFactory.InsecureRemoteServer(CloudSpec{ 409 CloudSpec: environscloudspec.CloudSpec{ 410 Endpoint: endpoint, 411 Credential: &insecureCreds, 412 }, 413 }) 414 if err != nil { 415 return nil, errors.Trace(err) 416 } 417 418 clientX509Cert, err := clientCert.X509() 419 if err != nil { 420 return nil, errors.Annotate(err, "client credentials") 421 } 422 423 // check to see if the cert already exists 424 fingerprint, err := clientCert.Fingerprint() 425 if err != nil { 426 return nil, errors.Trace(err) 427 } 428 429 cert, _, err := server.GetCertificate(fingerprint) 430 if err != nil || cert == nil { 431 if err := server.CreateCertificate(api.CertificatesPost{ 432 CertificatePut: api.CertificatePut{ 433 Name: credentials.Label, 434 Type: "client", 435 Certificate: base64.StdEncoding.EncodeToString(clientX509Cert.Raw), 436 }, 437 Password: trustPassword, 438 }); err != nil { 439 return nil, errors.Trace(err) 440 } 441 _, _ = fmt.Fprintln(output, "Uploaded certificate to LXD server.") 442 } else { 443 _, _ = fmt.Fprintln(output, "Reusing certificate from LXD server.") 444 } 445 446 lxdServer, _, err := server.GetServer() 447 if err != nil { 448 return nil, errors.Trace(err) 449 } 450 lxdServerCert := lxdServer.Environment.Certificate 451 452 // request to make sure that we can actually query correctly in a secure 453 // manner. 454 attributes := make(map[string]string) 455 for k, v := range credAttrs { 456 if k == credAttrTrustPassword { 457 continue 458 } 459 attributes[k] = v 460 } 461 attributes[credAttrServerCert] = lxdServerCert 462 463 secureCreds := cloud.NewCredential(cloud.CertificateAuthType, attributes) 464 server, err = p.serverFactory.RemoteServer(CloudSpec{ 465 CloudSpec: environscloudspec.CloudSpec{ 466 Endpoint: endpoint, 467 Credential: &secureCreds, 468 }, 469 }) 470 if err != nil { 471 return nil, errors.Trace(err) 472 } 473 474 // Store the server's certificate in the credential. 475 out := cloud.NewCredential(cloud.CertificateAuthType, map[string]string{ 476 credAttrClientCert: string(clientCert.CertPEM), 477 credAttrClientKey: string(clientCert.KeyPEM), 478 credAttrServerCert: server.ServerCertificate(), 479 }) 480 out.Label = credentials.Label 481 return &out, nil 482 } 483 484 func (p environProviderCredentials) finalizeLocalCredential( 485 output io.Writer, 486 svr Server, 487 certPEM, keyPEM, label string, 488 ) (*cloud.Credential, error) { 489 490 // Upload the certificate to the server if necessary. 491 clientCert := &lxd.Certificate{ 492 Name: "juju", 493 CertPEM: []byte(certPEM), 494 KeyPEM: []byte(keyPEM), 495 } 496 fingerprint, err := clientCert.Fingerprint() 497 if err != nil { 498 return nil, errors.Trace(err) 499 } 500 if _, _, err := svr.GetCertificate(fingerprint); lxd.IsLXDNotFound(err) { 501 if addCertErr := svr.CreateClientCertificate(clientCert); addCertErr != nil { 502 // There is no specific error code returned when 503 // attempting to add a certificate that already 504 // exists in the database. We can just check 505 // again to see if another process added the 506 // certificate concurrently with us checking the 507 // first time. 508 if _, _, err := svr.GetCertificate(fingerprint); lxd.IsLXDNotFound(err) { 509 // The cert still isn't there, so report the AddCert error. 510 return nil, errors.Annotatef( 511 addCertErr, "adding certificate %q", clientCert.Name, 512 ) 513 } else if err != nil { 514 return nil, errors.Annotate(err, "querying certificates") 515 } 516 // The certificate is there now, which implies 517 // there was a concurrent AddCert by another 518 // process. Carry on. 519 } 520 fmt.Fprintln(output, "Uploaded certificate to LXD server.") 521 522 } else if err != nil { 523 return nil, errors.Annotate(err, "querying certificates") 524 } 525 526 // Store the server's certificate in the credential. 527 out := cloud.NewCredential(cloud.CertificateAuthType, map[string]string{ 528 credAttrClientCert: certPEM, 529 credAttrClientKey: keyPEM, 530 credAttrServerCert: svr.ServerCertificate(), 531 }) 532 out.Label = label 533 return &out, nil 534 } 535 536 // certificateReadWriter is the default implementation for reading and writing 537 // certificates to disk. 538 type certificateReadWriter struct{} 539 540 func (certificateReadWriter) Read(path string) ([]byte, []byte, error) { 541 clientCertPath := filepath.Join(path, "client.crt") 542 clientKeyPath := filepath.Join(path, "client.key") 543 certPEM, err := os.ReadFile(clientCertPath) 544 if err != nil { 545 return nil, nil, errors.Trace(err) 546 } 547 keyPEM, err := os.ReadFile(clientKeyPath) 548 if err != nil { 549 return nil, nil, errors.Trace(err) 550 } 551 return certPEM, keyPEM, nil 552 } 553 554 func (certificateReadWriter) Write(path string, certPEM, keyPEM []byte) error { 555 clientCertPath := filepath.Join(path, "client.crt") 556 clientKeyPath := filepath.Join(path, "client.key") 557 if err := os.MkdirAll(path, 0700); err != nil { 558 return errors.Trace(err) 559 } 560 if err := os.WriteFile(clientCertPath, certPEM, 0600); err != nil { 561 return errors.Trace(err) 562 } 563 if err := os.WriteFile(clientKeyPath, keyPEM, 0600); err != nil { 564 return errors.Trace(err) 565 } 566 return nil 567 } 568 569 // certificateGenerator is the default implementation for generating a 570 // certificate if it's not found on disk. 571 type certificateGenerator struct{} 572 573 func (certificateGenerator) Generate(client bool, addHosts bool) (certPEM, keyPEM []byte, err error) { 574 return lxd.GenerateMemCert(client, addHosts) 575 } 576 577 func endpointURL(endpoint string) (*url.URL, error) { 578 remoteURL, err := url.Parse(endpoint) 579 if err != nil || remoteURL.Scheme == "" { 580 remoteURL = &url.URL{ 581 Scheme: "https", 582 Host: endpoint, 583 } 584 } else { 585 // If the user specifies an endpoint, it must be either 586 // host:port, or https://host:port. We do not support 587 // unix:// endpoints at present. 588 if remoteURL.Scheme != "https" { 589 return nil, errors.Errorf( 590 "invalid URL %q: only HTTPS is supported", 591 endpoint, 592 ) 593 } 594 } 595 return remoteURL, nil 596 } 597 598 func getCertificates(credentials cloud.Credential) (client *lxd.Certificate, server string, ok bool) { 599 clientCert, ok := getClientCertificates(credentials) 600 if !ok { 601 return nil, "", false 602 } 603 credAttrs := credentials.Attributes() 604 serverCertPEM, ok := credAttrs[credAttrServerCert] 605 if !ok { 606 return nil, "", false 607 } 608 return clientCert, serverCertPEM, true 609 } 610 611 func getClientCertificates(credentials cloud.Credential) (client *lxd.Certificate, ok bool) { 612 credAttrs := credentials.Attributes() 613 clientCertPEM, ok := credAttrs[credAttrClientCert] 614 if !ok { 615 return nil, false 616 } 617 clientKeyPEM, ok := credAttrs[credAttrClientKey] 618 if !ok { 619 return nil, false 620 } 621 clientCert := &lxd.Certificate{ 622 Name: "juju", 623 CertPEM: []byte(clientCertPEM), 624 KeyPEM: []byte(clientKeyPEM), 625 } 626 return clientCert, true 627 } 628 629 func configDirs() []string { 630 dirs := []string{ 631 osenv.JujuXDGDataHomePath("lxd"), 632 } 633 if lxdConf := os.Getenv("LXD_CONF"); lxdConf != "" { 634 dirs = append(dirs, lxdConf) 635 } 636 dirs = append(dirs, filepath.Join(utils.Home(), ".config", "lxc")) 637 if runtime.GOOS == "linux" { 638 dirs = append(dirs, filepath.Join(utils.Home(), "snap", "lxd", "current", ".config", "lxc")) 639 dirs = append(dirs, filepath.Join(utils.Home(), "snap", "lxd", "common", "config")) 640 } 641 return dirs 642 }