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  }