github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/client/credentials.go (about) 1 /* 2 Copyright 2021 Gravitational, Inc. 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 client 18 19 import ( 20 "crypto" 21 "crypto/tls" 22 "crypto/x509" 23 "os" 24 "sync" 25 26 "github.com/gravitational/trace" 27 "golang.org/x/crypto/ssh" 28 "golang.org/x/net/http2" 29 30 "github.com/gravitational/teleport/api/constants" 31 "github.com/gravitational/teleport/api/defaults" 32 "github.com/gravitational/teleport/api/identityfile" 33 "github.com/gravitational/teleport/api/profile" 34 "github.com/gravitational/teleport/api/utils" 35 "github.com/gravitational/teleport/api/utils/keys" 36 "github.com/gravitational/teleport/api/utils/sshutils" 37 ) 38 39 // Credentials are used to authenticate the API auth client. Some Credentials 40 // also provide other functionality, such as automatic address discovery and 41 // ssh connectivity. 42 // 43 // See the examples below for an example of each loader. 44 type Credentials interface { 45 // TLSConfig returns TLS configuration used to authenticate the client. 46 TLSConfig() (*tls.Config, error) 47 // SSHClientConfig returns SSH configuration used to connect to the 48 // Auth server through a reverse tunnel. 49 SSHClientConfig() (*ssh.ClientConfig, error) 50 } 51 52 // CredentialsWithDefaultAddrs additionally provides default addresses sourced 53 // from the credential which are used when the client has not been explicitly 54 // configured with an address. 55 type CredentialsWithDefaultAddrs interface { 56 Credentials 57 // DefaultAddrs is called by the API client when it has not been 58 // explicitly configured with an address to connect to. It may return a 59 // slice of addresses to be tried. 60 DefaultAddrs() ([]string, error) 61 } 62 63 // LoadTLS is used to load Credentials directly from a *tls.Config. 64 // 65 // TLS creds can only be used to connect directly to a Teleport Auth server. 66 func LoadTLS(tlsConfig *tls.Config) Credentials { 67 return &tlsConfigCreds{ 68 tlsConfig: tlsConfig, 69 } 70 } 71 72 // tlsConfigCreds use a defined *tls.Config to provide client credentials. 73 type tlsConfigCreds struct { 74 tlsConfig *tls.Config 75 } 76 77 // TLSConfig returns TLS configuration. 78 func (c *tlsConfigCreds) TLSConfig() (*tls.Config, error) { 79 if c.tlsConfig == nil { 80 return nil, trace.BadParameter("tls config is nil") 81 } 82 return configureTLS(c.tlsConfig), nil 83 } 84 85 // SSHClientConfig returns SSH configuration. 86 func (c *tlsConfigCreds) SSHClientConfig() (*ssh.ClientConfig, error) { 87 return nil, trace.NotImplemented("no ssh config") 88 } 89 90 // LoadKeyPair is used to load Credentials from a certicate keypair on disk. 91 // 92 // KeyPair Credentials can only be used to connect directly to a Teleport Auth server. 93 // 94 // New KeyPair files can be generated with tsh or tctl. 95 // 96 // $ tctl auth sign --format=tls --user=api-user --out=path/to/certs 97 // 98 // The certificates' time to live can be specified with --ttl. 99 // 100 // See the example below for usage. 101 func LoadKeyPair(certFile, keyFile, caFile string) Credentials { 102 return &keypairCreds{ 103 certFile: certFile, 104 keyFile: keyFile, 105 caFile: caFile, 106 } 107 } 108 109 // keypairCreds use keypair certificates to provide client credentials. 110 type keypairCreds struct { 111 certFile string 112 keyFile string 113 caFile string 114 } 115 116 // TLSConfig returns TLS configuration. 117 func (c *keypairCreds) TLSConfig() (*tls.Config, error) { 118 cert, err := tls.LoadX509KeyPair(c.certFile, c.keyFile) 119 if err != nil { 120 return nil, trace.Wrap(err) 121 } 122 123 cas, err := os.ReadFile(c.caFile) 124 if err != nil { 125 return nil, trace.ConvertSystemError(err) 126 } 127 128 pool := x509.NewCertPool() 129 if ok := pool.AppendCertsFromPEM(cas); !ok { 130 return nil, trace.BadParameter("invalid TLS CA cert PEM") 131 } 132 133 return configureTLS(&tls.Config{ 134 Certificates: []tls.Certificate{cert}, 135 RootCAs: pool, 136 }), nil 137 } 138 139 // SSHClientConfig returns SSH configuration. 140 func (c *keypairCreds) SSHClientConfig() (*ssh.ClientConfig, error) { 141 return nil, trace.NotImplemented("no ssh config") 142 } 143 144 // LoadIdentityFile is used to load Credentials from an identity file on disk. 145 // 146 // Identity Credentials can be used to connect to an auth server directly 147 // or through a reverse tunnel. 148 // 149 // A new identity file can be generated with tsh or tctl. 150 // 151 // $ tsh login --user=api-user --out=identity-file-path 152 // $ tctl auth sign --user=api-user --out=identity-file-path 153 // 154 // The identity file's time to live can be specified with --ttl. 155 // 156 // See the example below for usage. 157 func LoadIdentityFile(path string) Credentials { 158 return &identityCredsFile{ 159 path: path, 160 } 161 } 162 163 // identityCredsFile use an identity file to provide client credentials. 164 type identityCredsFile struct { 165 identityFile *identityfile.IdentityFile 166 path string 167 } 168 169 // TLSConfig returns TLS configuration. 170 func (c *identityCredsFile) TLSConfig() (*tls.Config, error) { 171 if err := c.load(); err != nil { 172 return nil, trace.Wrap(err) 173 } 174 175 tlsConfig, err := c.identityFile.TLSConfig() 176 if err != nil { 177 return nil, trace.Wrap(err) 178 } 179 180 return configureTLS(tlsConfig), nil 181 } 182 183 // SSHClientConfig returns SSH configuration. 184 func (c *identityCredsFile) SSHClientConfig() (*ssh.ClientConfig, error) { 185 if err := c.load(); err != nil { 186 return nil, trace.Wrap(err) 187 } 188 189 sshConfig, err := c.identityFile.SSHClientConfig() 190 if err != nil { 191 return nil, trace.Wrap(err) 192 } 193 194 return sshConfig, nil 195 } 196 197 // load is used to lazy load the identity file from persistent storage. 198 // This allows LoadIdentity to avoid possible errors for UX purposes. 199 func (c *identityCredsFile) load() error { 200 if c.identityFile != nil { 201 return nil 202 } 203 var err error 204 if c.identityFile, err = identityfile.ReadFile(c.path); err != nil { 205 return trace.BadParameter("identity file could not be decoded: %v", err) 206 } 207 return nil 208 } 209 210 // LoadIdentityFileFromString is used to load Credentials from a string containing identity file contents. 211 // 212 // Identity Credentials can be used to connect to an auth server directly 213 // or through a reverse tunnel. 214 // 215 // A new identity file can be generated with tsh or tctl. 216 // 217 // $ tsh login --user=api-user --out=identity-file-path 218 // $ tctl auth sign --user=api-user --out=identity-file-path 219 // 220 // The identity file's time to live can be specified with --ttl. 221 // 222 // See the example below for usage. 223 func LoadIdentityFileFromString(content string) Credentials { 224 return &identityCredsString{ 225 content: content, 226 } 227 } 228 229 // identityCredsString use an identity file loaded to string to provide client credentials. 230 type identityCredsString struct { 231 identityFile *identityfile.IdentityFile 232 content string 233 } 234 235 // TLSConfig returns TLS configuration. 236 func (c *identityCredsString) TLSConfig() (*tls.Config, error) { 237 if err := c.load(); err != nil { 238 return nil, trace.Wrap(err) 239 } 240 241 tlsConfig, err := c.identityFile.TLSConfig() 242 if err != nil { 243 return nil, trace.Wrap(err) 244 } 245 246 return configureTLS(tlsConfig), nil 247 } 248 249 // SSHClientConfig returns SSH configuration. 250 func (c *identityCredsString) SSHClientConfig() (*ssh.ClientConfig, error) { 251 if err := c.load(); err != nil { 252 return nil, trace.Wrap(err) 253 } 254 255 sshConfig, err := c.identityFile.SSHClientConfig() 256 if err != nil { 257 return nil, trace.Wrap(err) 258 } 259 260 return sshConfig, nil 261 } 262 263 // load is used to lazy load the identity file from a string. 264 func (c *identityCredsString) load() error { 265 if c.identityFile != nil { 266 return nil 267 } 268 var err error 269 if c.identityFile, err = identityfile.FromString(c.content); err != nil { 270 return trace.BadParameter("identity file could not be decoded: %v", err) 271 } 272 return nil 273 } 274 275 // LoadProfile is used to load Credentials from a tsh profile on disk. 276 // 277 // dir is the profile directory. It will defaults to "~/.tsh". 278 // 279 // name is the profile name. It will default to the currently active tsh profile. 280 // 281 // Profile Credentials can be used to connect to an auth server directly 282 // or through a reverse tunnel. 283 // 284 // Profile Credentials will automatically attempt to find your reverse 285 // tunnel address and make a connection through it. 286 // 287 // A new profile can be generated with tsh. 288 // 289 // $ tsh login --user=api-user 290 func LoadProfile(dir, name string) Credentials { 291 return &profileCreds{ 292 dir: dir, 293 name: name, 294 } 295 } 296 297 // profileCreds use a tsh profile to provide client credentials. 298 type profileCreds struct { 299 dir string 300 name string 301 profile *profile.Profile 302 } 303 304 // TLSConfig returns TLS configuration. 305 func (c *profileCreds) TLSConfig() (*tls.Config, error) { 306 if err := c.load(); err != nil { 307 return nil, trace.Wrap(err) 308 } 309 310 tlsConfig, err := c.profile.TLSConfig() 311 if err != nil { 312 return nil, trace.Wrap(err) 313 } 314 315 return configureTLS(tlsConfig), nil 316 } 317 318 // SSHClientConfig returns SSH configuration. 319 func (c *profileCreds) SSHClientConfig() (*ssh.ClientConfig, error) { 320 if err := c.load(); err != nil { 321 return nil, trace.Wrap(err) 322 } 323 324 sshConfig, err := c.profile.SSHClientConfig() 325 if err != nil { 326 return nil, trace.Wrap(err) 327 } 328 329 return sshConfig, nil 330 } 331 332 // DefaultAddrs implements CredentialsWithDefaultAddrs by providing the 333 // WebProxyAddr from the credential 334 func (c *profileCreds) DefaultAddrs() ([]string, error) { 335 if err := c.load(); err != nil { 336 return nil, trace.Wrap(err) 337 } 338 return []string{c.profile.WebProxyAddr}, nil 339 } 340 341 // load is used to lazy load the profile from persistent storage. 342 // This allows LoadProfile to avoid possible errors for UX purposes. 343 func (c *profileCreds) load() error { 344 if c.profile != nil { 345 return nil 346 } 347 var err error 348 if c.profile, err = profile.FromDir(c.dir, c.name); err != nil { 349 return trace.BadParameter("profile could not be decoded: %v", err) 350 } 351 return nil 352 } 353 354 func configureTLS(c *tls.Config) *tls.Config { 355 tlsConfig := c.Clone() 356 tlsConfig.NextProtos = utils.Deduplicate(append(tlsConfig.NextProtos, http2.NextProtoTLS)) 357 358 // If SNI isn't set, set it to the default name that can be found 359 // on all Teleport issued certificates. This is needed because we 360 // don't always know which host we will be connecting to. 361 if tlsConfig.ServerName == "" { 362 tlsConfig.ServerName = constants.APIDomain 363 } 364 365 // This logic still appears to be necessary to force client to always send 366 // a certificate regardless of the server setting. Otherwise the client may pick 367 // not to send the client certificate by looking at certificate request. 368 if len(tlsConfig.Certificates) > 0 { 369 cert := tlsConfig.Certificates[0] 370 tlsConfig.Certificates = nil 371 tlsConfig.GetClientCertificate = func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) { 372 return &cert, nil 373 } 374 } 375 376 return tlsConfig 377 } 378 379 // DynamicIdentityFileCreds allows a changing identity file to be used as the 380 // source of authentication for Client. It does not automatically watch the 381 // identity file or reload on an interval, this is left as an exercise for the 382 // consumer. 383 type DynamicIdentityFileCreds struct { 384 // mu protects the fields that may change if the underlying identity file 385 // is reloaded. 386 mu sync.RWMutex 387 tlsCert *tls.Certificate 388 tlsRootCAs *x509.CertPool 389 sshCert *ssh.Certificate 390 sshKey crypto.Signer 391 sshKnownHosts []ssh.PublicKey 392 393 // Path is the path to the identity file to load and reload. 394 Path string 395 } 396 397 // NewDynamicIdentityFileCreds returns a DynamicIdentityFileCreds which has 398 // been initially loaded and is ready for use. 399 func NewDynamicIdentityFileCreds(path string) (*DynamicIdentityFileCreds, error) { 400 d := &DynamicIdentityFileCreds{ 401 Path: path, 402 } 403 if err := d.Reload(); err != nil { 404 return nil, trace.Wrap(err) 405 } 406 return d, nil 407 } 408 409 // Reload causes the identity file to be re-read from the disk. It will return 410 // an error if loading the credentials fails. 411 func (d *DynamicIdentityFileCreds) Reload() error { 412 id, err := identityfile.ReadFile(d.Path) 413 if err != nil { 414 return trace.Wrap(err) 415 } 416 417 // This section is essentially id.TLSConfig() 418 cert, err := keys.X509KeyPair(id.Certs.TLS, id.PrivateKey) 419 if err != nil { 420 return trace.Wrap(err) 421 } 422 pool := x509.NewCertPool() 423 for _, caCerts := range id.CACerts.TLS { 424 if !pool.AppendCertsFromPEM(caCerts) { 425 return trace.BadParameter("invalid CA cert PEM") 426 } 427 } 428 429 // This sections is essentially id.SSHClientConfig() 430 sshCert, err := sshutils.ParseCertificate(id.Certs.SSH) 431 if err != nil { 432 return trace.Wrap(err) 433 } 434 sshPrivateKey, err := keys.ParsePrivateKey(id.PrivateKey) 435 if err != nil { 436 return trace.Wrap(err) 437 } 438 knownHosts, err := sshutils.ParseKnownHosts(id.CACerts.SSH) 439 if err != nil { 440 return trace.Wrap(err) 441 } 442 443 d.mu.Lock() 444 defer d.mu.Unlock() 445 d.tlsRootCAs = pool 446 d.tlsCert = &cert 447 d.sshCert = sshCert 448 d.sshKey = sshPrivateKey 449 d.sshKnownHosts = knownHosts 450 return nil 451 } 452 453 // TLSConfig returns TLS configuration. Implementing the Credentials interface. 454 func (d *DynamicIdentityFileCreds) TLSConfig() (*tls.Config, error) { 455 d.mu.RLock() 456 defer d.mu.RUnlock() 457 // Build a "dynamic" tls.Config which can support a changing cert and root 458 // CA pool. 459 cfg := &tls.Config{ 460 // Set the default NextProto of "h2". Based on the value in 461 // configureTLS() 462 NextProtos: []string{http2.NextProtoTLS}, 463 464 // GetClientCertificate is used instead of the static Certificates 465 // field. 466 Certificates: nil, 467 GetClientCertificate: func( 468 _ *tls.CertificateRequestInfo, 469 ) (*tls.Certificate, error) { 470 // GetClientCertificate callback is used to allow us to dynamically 471 // change the certificate when reloaded. 472 d.mu.RLock() 473 defer d.mu.RUnlock() 474 return d.tlsCert, nil 475 }, 476 477 // VerifyConnection is used instead of the static RootCAs field. 478 // However, there's some client code which relies on the static RootCAs 479 // field. So we set it to a copy of the current root CAs pool to support 480 // those - e.g ALPNDialerConfig.GetClusterCAs 481 RootCAs: d.tlsRootCAs.Clone(), 482 // InsecureSkipVerify is forced true to ensure that only our 483 // VerifyConnection callback is used to verify the server's presented 484 // certificate. 485 InsecureSkipVerify: true, 486 VerifyConnection: func(state tls.ConnectionState) error { 487 // This VerifyConnection callback is based on the standard library 488 // implementation of verifyServerCertificate in the `tls` package. 489 // We provide our own implementation so we can dynamically handle 490 // a changing CA Roots pool. 491 d.mu.RLock() 492 defer d.mu.RUnlock() 493 opts := x509.VerifyOptions{ 494 DNSName: state.ServerName, 495 Intermediates: x509.NewCertPool(), 496 Roots: d.tlsRootCAs.Clone(), 497 } 498 for _, cert := range state.PeerCertificates[1:] { 499 // Whilst we don't currently use intermediate certs at 500 // Teleport, including this here means that we are 501 // future-proofed in case we do. 502 opts.Intermediates.AddCert(cert) 503 } 504 _, err := state.PeerCertificates[0].Verify(opts) 505 return err 506 }, 507 // Set ServerName for SNI & Certificate Validation to the sentinel 508 // teleport.cluster.local which is included on all Teleport Auth Server 509 // certificates. Based on the value in configureTLS() 510 ServerName: constants.APIDomain, 511 } 512 513 return cfg, nil 514 } 515 516 // SSHClientConfig returns SSH configuration, implementing the Credentials 517 // interface. 518 func (d *DynamicIdentityFileCreds) SSHClientConfig() (*ssh.ClientConfig, error) { 519 hostKeyCallback, err := sshutils.NewHostKeyCallback(sshutils.HostKeyCallbackConfig{ 520 GetHostCheckers: func() ([]ssh.PublicKey, error) { 521 d.mu.RLock() 522 defer d.mu.RUnlock() 523 return d.sshKnownHosts, nil 524 }, 525 }) 526 if err != nil { 527 return nil, trace.Wrap(err) 528 } 529 530 // Build a "dynamic" ssh config. Based roughly on 531 // `sshutils.ProxyClientSSHConfig` with modifications to make it work with 532 // dynamically changing credentials and CAs. 533 cfg := &ssh.ClientConfig{ 534 Auth: []ssh.AuthMethod{ 535 ssh.PublicKeysCallback(func() (signers []ssh.Signer, err error) { 536 d.mu.RLock() 537 defer d.mu.RUnlock() 538 sshSigner, err := sshutils.SSHSigner(d.sshCert, d.sshKey) 539 if err != nil { 540 return nil, trace.Wrap(err) 541 } 542 return []ssh.Signer{sshSigner}, nil 543 }), 544 }, 545 HostKeyCallback: hostKeyCallback, 546 Timeout: defaults.DefaultIOTimeout, 547 // We use this because we can't always guarantee that a user will have 548 // a principal other than this (they may not have access to SSH nodes) 549 // and the actual user here doesn't matter for auth server API 550 // authentication. All that matters is that the principal specified here 551 // is stable across all certificates issued to the user, since this 552 // value cannot be changed in a following rotation - 553 // SSHSessionJoinPrincipal is included on all user ssh certs. 554 // 555 // This is a bit of a hack - the ideal solution is a refactor of the 556 // API client in order to support the SSH config being generated at 557 // time of use, rather than a single SSH config being made dynamic. 558 // ~ noah 559 User: "-teleport-internal-join", 560 } 561 return cfg, nil 562 }