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