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 © 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 }