github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/profile/profile.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 profile handles management of the Teleport profile directory (~/.tsh).
    18  package profile
    19  
    20  import (
    21  	"crypto/tls"
    22  	"crypto/x509"
    23  	"io/fs"
    24  	"net"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  
    29  	"github.com/gravitational/trace"
    30  	"golang.org/x/crypto/ssh"
    31  	"gopkg.in/yaml.v2"
    32  
    33  	"github.com/gravitational/teleport/api/utils"
    34  	"github.com/gravitational/teleport/api/utils/keypaths"
    35  	"github.com/gravitational/teleport/api/utils/keys"
    36  	"github.com/gravitational/teleport/api/utils/sshutils"
    37  )
    38  
    39  const (
    40  	// profileDir is the default root directory where tsh stores profiles.
    41  	profileDir = ".tsh"
    42  )
    43  
    44  // Profile is a collection of most frequently used CLI flags
    45  // for "tsh".
    46  //
    47  // Profiles can be stored in a profile file, allowing TSH users to
    48  // type fewer CLI args.
    49  type Profile struct {
    50  	// WebProxyAddr is the host:port the web proxy can be accessed at.
    51  	WebProxyAddr string `yaml:"web_proxy_addr,omitempty"`
    52  
    53  	// SSHProxyAddr is the host:port the SSH proxy can be accessed at.
    54  	SSHProxyAddr string `yaml:"ssh_proxy_addr,omitempty"`
    55  
    56  	// KubeProxyAddr is the host:port the Kubernetes proxy can be accessed at.
    57  	KubeProxyAddr string `yaml:"kube_proxy_addr,omitempty"`
    58  
    59  	// PostgresProxyAddr is the host:port the Postgres proxy can be accessed at.
    60  	PostgresProxyAddr string `yaml:"postgres_proxy_addr,omitempty"`
    61  
    62  	// MySQLProxyAddr is the host:port the MySQL proxy can be accessed at.
    63  	MySQLProxyAddr string `yaml:"mysql_proxy_addr,omitempty"`
    64  
    65  	// MongoProxyAddr is the host:port the Mongo proxy can be accessed at.
    66  	MongoProxyAddr string `yaml:"mongo_proxy_addr,omitempty"`
    67  
    68  	// Username is the Teleport username for the client.
    69  	Username string `yaml:"user,omitempty"`
    70  
    71  	// SiteName is equivalent to the --cluster flag
    72  	SiteName string `yaml:"cluster,omitempty"`
    73  
    74  	// DynamicForwardedPorts is a list of ports to use for dynamic port
    75  	// forwarding (SOCKS5).
    76  	DynamicForwardedPorts []string `yaml:"dynamic_forward_ports,omitempty"`
    77  
    78  	// Dir is the directory of this profile.
    79  	Dir string
    80  
    81  	// TLSRoutingEnabled indicates that proxy supports ALPN SNI server where
    82  	// all proxy services are exposed on a single TLS listener (Proxy Web Listener).
    83  	TLSRoutingEnabled bool `yaml:"tls_routing_enabled,omitempty"`
    84  
    85  	// TLSRoutingConnUpgradeRequired indicates that ALPN connection upgrades
    86  	// are required for making TLS routing requests.
    87  	//
    88  	// Note that this is applicable to the Proxy's Web port regardless of
    89  	// whether the Proxy is in single-port or multi-port configuration.
    90  	TLSRoutingConnUpgradeRequired bool `yaml:"tls_routing_conn_upgrade_required,omitempty"`
    91  
    92  	// AuthConnector (like "google", "passwordless").
    93  	// Equivalent to the --auth tsh flag.
    94  	AuthConnector string `yaml:"auth_connector,omitempty"`
    95  
    96  	// LoadAllCAs indicates that tsh should load the CAs of all clusters
    97  	// instead of just the current cluster.
    98  	LoadAllCAs bool `yaml:"load_all_cas,omitempty"`
    99  
   100  	// MFAMode ("auto", "platform", "cross-platform").
   101  	// Equivalent to the --mfa-mode tsh flag.
   102  	MFAMode string `yaml:"mfa_mode,omitempty"`
   103  
   104  	// PrivateKeyPolicy is a key policy enforced for this profile.
   105  	PrivateKeyPolicy keys.PrivateKeyPolicy `yaml:"private_key_policy"`
   106  
   107  	// PIVSlot is a specific piv slot that Teleport clients should use for hardware key support.
   108  	PIVSlot keys.PIVSlot `yaml:"piv_slot"`
   109  
   110  	// MissingClusterDetails means this profile was created with limited cluster details.
   111  	// Missing cluster details should be loaded into the profile by pinging the proxy.
   112  	MissingClusterDetails bool
   113  }
   114  
   115  // Copy returns a shallow copy of p, or nil if p is nil.
   116  func (p *Profile) Copy() *Profile {
   117  	if p == nil {
   118  		return nil
   119  	}
   120  	copy := *p
   121  	return &copy
   122  }
   123  
   124  // Name returns the name of the profile.
   125  func (p *Profile) Name() string {
   126  	addr, _, err := net.SplitHostPort(p.WebProxyAddr)
   127  	if err != nil {
   128  		return p.WebProxyAddr
   129  	}
   130  
   131  	return addr
   132  }
   133  
   134  // TLSConfig returns the profile's associated TLSConfig.
   135  func (p *Profile) TLSConfig() (*tls.Config, error) {
   136  	cert, err := keys.LoadX509KeyPair(p.TLSCertPath(), p.UserKeyPath())
   137  	if err != nil {
   138  		return nil, trace.Wrap(err)
   139  	}
   140  
   141  	pool, err := certPoolFromProfile(p)
   142  	if err != nil {
   143  		return nil, trace.Wrap(err)
   144  	}
   145  
   146  	return &tls.Config{
   147  		Certificates: []tls.Certificate{cert},
   148  		RootCAs:      pool,
   149  	}, nil
   150  }
   151  
   152  // RequireKubeLocalProxy returns true if this profile indicates a local proxy
   153  // is required for kube access.
   154  func (p *Profile) RequireKubeLocalProxy() bool {
   155  	return p.KubeProxyAddr == p.WebProxyAddr && p.TLSRoutingConnUpgradeRequired
   156  }
   157  
   158  func certPoolFromProfile(p *Profile) (*x509.CertPool, error) {
   159  	// Check if CAS dir exist if not try to load certs from legacy certs.pem file.
   160  	if _, err := os.Stat(p.TLSClusterCASDir()); err != nil {
   161  		if !os.IsNotExist(err) {
   162  			return nil, trace.Wrap(err)
   163  		}
   164  		pool, err := certPoolFromLegacyCAFile(p)
   165  		if err != nil {
   166  			return nil, trace.Wrap(err)
   167  		}
   168  		return pool, nil
   169  	}
   170  
   171  	// Load CertPool from CAS directory.
   172  	pool, err := certPoolFromCASDir(p)
   173  	if err != nil {
   174  		return nil, trace.Wrap(err)
   175  	}
   176  	return pool, nil
   177  }
   178  
   179  func certPoolFromCASDir(p *Profile) (*x509.CertPool, error) {
   180  	pool := x509.NewCertPool()
   181  	err := filepath.Walk(p.TLSClusterCASDir(), func(path string, info fs.FileInfo, err error) error {
   182  		if err != nil {
   183  			return trace.Wrap(err)
   184  		}
   185  		if info.IsDir() {
   186  			return nil
   187  		}
   188  		cert, err := os.ReadFile(path)
   189  		if err != nil {
   190  			return trace.ConvertSystemError(err)
   191  		}
   192  		if !pool.AppendCertsFromPEM(cert) {
   193  			return trace.BadParameter("invalid CA cert PEM %s", path)
   194  		}
   195  		return nil
   196  	})
   197  	if err != nil {
   198  		return nil, trace.Wrap(err)
   199  	}
   200  	return pool, nil
   201  }
   202  
   203  func certPoolFromLegacyCAFile(p *Profile) (*x509.CertPool, error) {
   204  	caCerts, err := os.ReadFile(p.TLSCAsPath())
   205  	if err != nil {
   206  		return nil, trace.ConvertSystemError(err)
   207  	}
   208  	pool := x509.NewCertPool()
   209  	if !pool.AppendCertsFromPEM(caCerts) {
   210  		return nil, trace.BadParameter("invalid CA cert PEM")
   211  	}
   212  	return pool, nil
   213  }
   214  
   215  // SSHClientConfig returns the profile's associated SSHClientConfig.
   216  func (p *Profile) SSHClientConfig() (*ssh.ClientConfig, error) {
   217  	cert, err := os.ReadFile(p.SSHCertPath())
   218  	if err != nil {
   219  		return nil, trace.Wrap(err)
   220  	}
   221  
   222  	sshCert, err := sshutils.ParseCertificate(cert)
   223  	if err != nil {
   224  		return nil, trace.Wrap(err)
   225  	}
   226  
   227  	caCerts, err := os.ReadFile(p.KnownHostsPath())
   228  	if err != nil {
   229  		return nil, trace.Wrap(err)
   230  	}
   231  
   232  	priv, err := keys.LoadPrivateKey(p.UserKeyPath())
   233  	if err != nil {
   234  		return nil, trace.Wrap(err)
   235  	}
   236  
   237  	ssh, err := sshutils.ProxyClientSSHConfig(sshCert, priv, caCerts)
   238  	if err != nil {
   239  		return nil, trace.Wrap(err)
   240  	}
   241  	return ssh, nil
   242  }
   243  
   244  // SetCurrentProfileName attempts to set the current profile name.
   245  func SetCurrentProfileName(dir string, name string) error {
   246  	if dir == "" {
   247  		return trace.BadParameter("cannot set current profile: missing dir")
   248  	}
   249  
   250  	path := keypaths.CurrentProfileFilePath(dir)
   251  	if err := os.WriteFile(path, []byte(strings.TrimSpace(name)+"\n"), 0o660); err != nil {
   252  		return trace.Wrap(err)
   253  	}
   254  	return nil
   255  }
   256  
   257  // RemoveProfile removes cluster profile file
   258  func RemoveProfile(dir, name string) error {
   259  	profilePath := filepath.Join(dir, name+".yaml")
   260  	if err := os.Remove(profilePath); err != nil {
   261  		return trace.ConvertSystemError(err)
   262  	}
   263  
   264  	return nil
   265  }
   266  
   267  // GetCurrentProfileName attempts to load the current profile name.
   268  func GetCurrentProfileName(dir string) (name string, err error) {
   269  	if dir == "" {
   270  		return "", trace.BadParameter("cannot get current profile: missing dir")
   271  	}
   272  
   273  	data, err := os.ReadFile(keypaths.CurrentProfileFilePath(dir))
   274  	if err != nil {
   275  		if os.IsNotExist(err) {
   276  			return "", trace.NotFound("current-profile is not set")
   277  		}
   278  		return "", trace.ConvertSystemError(err)
   279  	}
   280  	name = strings.TrimSpace(string(data))
   281  	if name == "" {
   282  		return "", trace.NotFound("current-profile is not set")
   283  	}
   284  	return name, nil
   285  }
   286  
   287  // ListProfileNames lists all available profiles.
   288  func ListProfileNames(dir string) ([]string, error) {
   289  	if dir == "" {
   290  		return nil, trace.BadParameter("cannot list profiles: missing dir")
   291  	}
   292  	files, err := os.ReadDir(dir)
   293  	if err != nil {
   294  		return nil, trace.Wrap(err)
   295  	}
   296  
   297  	var names []string
   298  	for _, file := range files {
   299  		if file.IsDir() {
   300  			continue
   301  		}
   302  
   303  		if file.Type()&os.ModeSymlink != 0 {
   304  			continue
   305  		}
   306  		if !strings.HasSuffix(file.Name(), ".yaml") {
   307  			continue
   308  		}
   309  		names = append(names, strings.TrimSuffix(file.Name(), ".yaml"))
   310  	}
   311  	return names, nil
   312  }
   313  
   314  // FullProfilePath returns the full path to the user profile directory.
   315  // If the parameter is empty, it returns expanded "~/.tsh", otherwise
   316  // returns its unmodified parameter
   317  func FullProfilePath(dir string) string {
   318  	if dir != "" {
   319  		return dir
   320  	}
   321  	return defaultProfilePath()
   322  }
   323  
   324  // defaultProfilePath retrieves the default path of the TSH profile.
   325  func defaultProfilePath() string {
   326  	// start with UserHomeDir, which is the fastest option as it
   327  	// relies only on environment variables and does not perform
   328  	// a user lookup (which can be very slow on large AD environments)
   329  	home, err := os.UserHomeDir()
   330  	if err == nil && home != "" {
   331  		return filepath.Join(home, profileDir)
   332  	}
   333  
   334  	home = os.TempDir()
   335  	if u, err := utils.CurrentUser(); err == nil && u.HomeDir != "" {
   336  		home = u.HomeDir
   337  	}
   338  	return filepath.Join(home, profileDir)
   339  }
   340  
   341  // FromDir reads the user profile from a given directory. If dir is empty,
   342  // this function defaults to the default tsh profile directory. If name is empty,
   343  // this function defaults to loading the currently active profile (if any).
   344  func FromDir(dir string, name string) (*Profile, error) {
   345  	dir = FullProfilePath(dir)
   346  	var err error
   347  	if name == "" {
   348  		name, err = GetCurrentProfileName(dir)
   349  		if err != nil {
   350  			return nil, trace.Wrap(err)
   351  		}
   352  	}
   353  	p, err := profileFromFile(keypaths.ProfileFilePath(dir, name))
   354  	if err != nil {
   355  		return nil, trace.Wrap(err)
   356  	}
   357  	return p, nil
   358  }
   359  
   360  // profileFromFile loads the profile from a YAML file.
   361  func profileFromFile(filePath string) (*Profile, error) {
   362  	bytes, err := os.ReadFile(filePath)
   363  	if err != nil {
   364  		return nil, trace.ConvertSystemError(err)
   365  	}
   366  	var p Profile
   367  	if err := yaml.Unmarshal(bytes, &p); err != nil {
   368  		return nil, trace.Wrap(err)
   369  	}
   370  
   371  	if p.Name() == "" {
   372  		return nil, trace.NotFound("invalid or empty profile at %q", filePath)
   373  	}
   374  
   375  	p.Dir = filepath.Dir(filePath)
   376  
   377  	// Older versions of tsh did not always store the cluster name in the
   378  	// profile. If no cluster name is found, fallback to the name of the profile
   379  	// for backward compatibility.
   380  	if p.SiteName == "" {
   381  		p.SiteName = p.Name()
   382  	}
   383  	return &p, nil
   384  }
   385  
   386  // SaveToDir saves this profile to the specified directory.
   387  // If makeCurrent is true, it makes this profile current.
   388  func (p *Profile) SaveToDir(dir string, makeCurrent bool) error {
   389  	if dir == "" {
   390  		return trace.BadParameter("cannot save profile: missing dir")
   391  	}
   392  	if err := p.saveToFile(keypaths.ProfileFilePath(dir, p.Name())); err != nil {
   393  		return trace.Wrap(err)
   394  	}
   395  	if makeCurrent {
   396  		return trace.Wrap(SetCurrentProfileName(dir, p.Name()))
   397  	}
   398  	return nil
   399  }
   400  
   401  // saveToFile saves this profile to the specified file.
   402  func (p *Profile) saveToFile(filepath string) error {
   403  	bytes, err := yaml.Marshal(&p)
   404  	if err != nil {
   405  		return trace.Wrap(err)
   406  	}
   407  	if err = os.WriteFile(filepath, bytes, 0o660); err != nil {
   408  		return trace.Wrap(err)
   409  	}
   410  	return nil
   411  }
   412  
   413  // KeyDir returns the path to the profile's directory.
   414  func (p *Profile) KeyDir() string {
   415  	return keypaths.KeyDir(p.Dir)
   416  }
   417  
   418  // ProxyKeyDir returns the path to the profile's key directory.
   419  func (p *Profile) ProxyKeyDir() string {
   420  	return keypaths.ProxyKeyDir(p.Dir, p.Name())
   421  }
   422  
   423  // UserKeyPath returns the path to the profile's private key.
   424  func (p *Profile) UserKeyPath() string {
   425  	return keypaths.UserKeyPath(p.Dir, p.Name(), p.Username)
   426  }
   427  
   428  // TLSCertPath returns the path to the profile's TLS certificate.
   429  func (p *Profile) TLSCertPath() string {
   430  	return keypaths.TLSCertPath(p.Dir, p.Name(), p.Username)
   431  }
   432  
   433  // TLSCAsLegacyPath returns the path to the profile's TLS certificate authorities.
   434  func (p *Profile) TLSCAsLegacyPath() string {
   435  	return keypaths.TLSCAsPath(p.Dir, p.Name())
   436  }
   437  
   438  // TLSCAPathCluster returns CA for particular cluster.
   439  func (p *Profile) TLSCAPathCluster(cluster string) string {
   440  	return keypaths.TLSCAsPathCluster(p.Dir, p.Name(), cluster)
   441  }
   442  
   443  // TLSClusterCASDir returns CAS directory where cluster CAs are stored.
   444  func (p *Profile) TLSClusterCASDir() string {
   445  	return keypaths.CAsDir(p.Dir, p.Name())
   446  }
   447  
   448  // TLSCAsPath returns the legacy path to the profile's TLS certificate authorities.
   449  func (p *Profile) TLSCAsPath() string {
   450  	return keypaths.TLSCAsPath(p.Dir, p.Name())
   451  }
   452  
   453  // SSHDir returns the path to the profile's ssh directory.
   454  func (p *Profile) SSHDir() string {
   455  	return keypaths.SSHDir(p.Dir, p.Name(), p.Username)
   456  }
   457  
   458  // SSHCertPath returns the path to the profile's ssh certificate.
   459  func (p *Profile) SSHCertPath() string {
   460  	return keypaths.SSHCertPath(p.Dir, p.Name(), p.Username, p.SiteName)
   461  }
   462  
   463  // PPKFilePath returns the path to the profile's PuTTY PPK-formatted keypair.
   464  func (p *Profile) PPKFilePath() string {
   465  	return keypaths.PPKFilePath(p.Dir, p.Name(), p.Username)
   466  }
   467  
   468  // KnownHostsPath returns the path to the profile's ssh certificate authorities.
   469  func (p *Profile) KnownHostsPath() string {
   470  	return keypaths.KnownHostsPath(p.Dir)
   471  }
   472  
   473  // AppCertPath returns the path to the profile's certificate for a given
   474  // application. Note that this function merely constructs the path - there
   475  // is no guarantee that there is an actual certificate at that location.
   476  func (p *Profile) AppCertPath(appName string) string {
   477  	return keypaths.AppCertPath(p.Dir, p.Name(), p.Username, p.SiteName, appName)
   478  }