diff --git a/api/profile/profile.go b/api/profile/profile.go index 833124017f471..1e2ec7b476245 100644 --- a/api/profile/profile.go +++ b/api/profile/profile.go @@ -37,6 +37,8 @@ import ( "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/api/utils/sshutils" + libdefaults "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/utils" ) const ( @@ -486,3 +488,14 @@ func (p *Profile) AppCertPath(appName string) string { func (p *Profile) AppKeyPath(appName string) string { return keypaths.AppKeyPath(p.Dir, p.Name(), p.Username, p.SiteName, appName) } + +// WebProxyHostPort returns the host and port of the web proxy. +func (p *Profile) WebProxyHostPort() (string, int) { + if p.WebProxyAddr != "" { + addr, err := utils.ParseAddr(p.WebProxyAddr) + if err == nil { + return addr.Host(), addr.Port(libdefaults.HTTPListenPort) + } + } + return "unknown", libdefaults.HTTPListenPort +} diff --git a/api/utils/keys/hardwarekey/hardwarekey.go b/api/utils/keys/hardwarekey/hardwarekey.go index de446c52ba3cd..5007f9bbb84b1 100644 --- a/api/utils/keys/hardwarekey/hardwarekey.go +++ b/api/utils/keys/hardwarekey/hardwarekey.go @@ -38,6 +38,10 @@ type Service interface { // GetFullKeyRef gets the full [PrivateKeyRef] for an existing hardware private // key in the given slot of the hardware key with the given serial number. GetFullKeyRef(serialNumber uint32, slotKey PIVSlotKey) (*PrivateKeyRef, error) + // SetPrompt sets the hardware key prompt used by the hardware key service, if applicable. + // This is used by Teleport Connect which sets the prompt later than the hardware key service, + // due to process initialization constraints. + SetPrompt(prompt Prompt) } // Signer is a hardware key implementation of [crypto.Signer]. diff --git a/api/utils/keys/piv/service.go b/api/utils/keys/piv/service.go index efbdb174cc25e..ef4c0ee60d149 100644 --- a/api/utils/keys/piv/service.go +++ b/api/utils/keys/piv/service.go @@ -35,23 +35,16 @@ import ( "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) -// TODO(Joerger): Rather than using a global cache and mutexes, clients should be updated -// to create a single YubiKeyService and ensure it is reused across the program execution. -var ( +// YubiKeyService is a YubiKey PIV implementation of [hardwarekey.Service]. +type YubiKeyService struct { + prompt hardwarekey.Prompt + promptMux sync.Mutex + // yubiKeys is a shared, thread-safe [YubiKey] cache by serial number. It allows for // separate goroutines to share a YubiKey connection to work around the single PC/SC // transaction (connection) per-yubikey limit. - yubiKeys map[uint32]*YubiKey = map[uint32]*YubiKey{} + yubiKeys map[uint32]*YubiKey yubiKeysMux sync.Mutex - - // promptMux is used to prevent over-prompting, especially for back-to-back sign requests - // since touch/PIN from the first signature should be cached for following signatures. - promptMux sync.Mutex -) - -// YubiKeyService is a YubiKey PIV implementation of [hardwarekey.Service]. -type YubiKeyService struct { - prompt hardwarekey.Prompt } // Returns a new [YubiKeyService]. If [customPrompt] is nil, the default CLI prompt will be used. @@ -64,7 +57,8 @@ func NewYubiKeyService(customPrompt hardwarekey.Prompt) *YubiKeyService { } return &YubiKeyService{ - prompt: customPrompt, + prompt: customPrompt, + yubiKeys: map[uint32]*YubiKey{}, } } @@ -170,8 +164,8 @@ func (s *YubiKeyService) Sign(ctx context.Context, ref *hardwarekey.PrivateKeyRe return nil, trace.Wrap(err) } - promptMux.Lock() - defer promptMux.Unlock() + s.promptMux.Lock() + defer s.promptMux.Unlock() return y.sign(ctx, ref, keyInfo, s.prompt, rand, digest, opts) } @@ -224,13 +218,20 @@ func (s *YubiKeyService) GetFullKeyRef(serialNumber uint32, slotKey hardwarekey. return ref, nil } +// SetPrompt sets the hardware key prompt. +func (s *YubiKeyService) SetPrompt(prompt hardwarekey.Prompt) { + s.promptMux.Lock() + defer s.promptMux.Unlock() + s.prompt = prompt +} + // Get the given YubiKey with the serial number. If the provided serialNumber is "0", // return the first YubiKey found in the smart card list. func (s *YubiKeyService) getYubiKey(serialNumber uint32) (*YubiKey, error) { - yubiKeysMux.Lock() - defer yubiKeysMux.Unlock() + s.yubiKeysMux.Lock() + defer s.yubiKeysMux.Unlock() - if y, ok := yubiKeys[serialNumber]; ok { + if y, ok := s.yubiKeys[serialNumber]; ok { return y, nil } @@ -239,7 +240,7 @@ func (s *YubiKeyService) getYubiKey(serialNumber uint32) (*YubiKey, error) { return nil, trace.Wrap(err) } - yubiKeys[y.serialNumber] = y + s.yubiKeys[y.serialNumber] = y return y, nil } @@ -247,8 +248,8 @@ func (s *YubiKeyService) getYubiKey(serialNumber uint32) (*YubiKey, error) { // If the user provides the default PIN, they will be prompted to set a // non-default PIN and PUK before continuing. func (s *YubiKeyService) checkOrSetPIN(ctx context.Context, y *YubiKey, keyInfo hardwarekey.ContextualKeyInfo) error { - promptMux.Lock() - defer promptMux.Unlock() + s.promptMux.Lock() + defer s.promptMux.Unlock() pin, err := s.prompt.AskPIN(ctx, hardwarekey.PINOptional, keyInfo) if err != nil { @@ -270,8 +271,8 @@ func (s *YubiKeyService) checkOrSetPIN(ctx context.Context, y *YubiKey, keyInfo } func (s *YubiKeyService) promptOverwriteSlot(ctx context.Context, msg string, keyInfo hardwarekey.ContextualKeyInfo) error { - promptMux.Lock() - defer promptMux.Unlock() + s.promptMux.Lock() + defer s.promptMux.Unlock() promptQuestion := fmt.Sprintf("%v\nWould you like to overwrite this slot's private key and certificate?", msg) if confirmed, confirmErr := s.prompt.ConfirmSlotOverwrite(ctx, promptQuestion, keyInfo); confirmErr != nil { diff --git a/api/utils/keys/piv/service_fake.go b/api/utils/keys/piv/service_fake.go index 58f4fac8447df..9e2e3b1e3f106 100644 --- a/api/utils/keys/piv/service_fake.go +++ b/api/utils/keys/piv/service_fake.go @@ -20,15 +20,8 @@ import ( "github.com/gravitational/teleport/api/utils/keys/hardwarekey" ) -// TODO(Joerger): Rather than using a global service, clients should be updated to -// create a single YubiKeyService and ensure it is reused across the program -// execution. At this point, it may make more sense to directly inject the mocked -// hardware key service into the test instead of using the pivtest build tag to do it. -var mockedHardwareKeyService = hardwarekey.NewMockHardwareKeyService(nil /*prompt*/) - -// Returns a globally shared [hardwarekey.MockHardwareKeyService]. Test callers should +// Returns a new [hardwarekey.MockHardwareKeyService]. Test callers should // prefer [hardwarekey.NewMockHardwareKeyService] when possible. func NewYubiKeyService(prompt hardwarekey.Prompt) *hardwarekey.MockHardwareKeyService { - mockedHardwareKeyService.SetPrompt(prompt) - return mockedHardwareKeyService + return hardwarekey.NewMockHardwareKeyService(prompt) } diff --git a/api/utils/keys/piv/service_unavailable.go b/api/utils/keys/piv/service_unavailable.go index 24918a072690c..0b1e59836876f 100644 --- a/api/utils/keys/piv/service_unavailable.go +++ b/api/utils/keys/piv/service_unavailable.go @@ -48,3 +48,5 @@ func (s *unavailableYubiKeyPIVService) Sign(_ context.Context, _ *hardwarekey.Pr func (s *unavailableYubiKeyPIVService) GetFullKeyRef(serialNumber uint32, slotKey hardwarekey.PIVSlotKey) (*hardwarekey.PrivateKeyRef, error) { return nil, trace.Wrap(errPIVUnavailable) } + +func (s *unavailableYubiKeyPIVService) SetPrompt(_ hardwarekey.Prompt) {} diff --git a/integration/helpers/instance.go b/integration/helpers/instance.go index 2aad22eaedc00..f62b3639fd2f4 100644 --- a/integration/helpers/instance.go +++ b/integration/helpers/instance.go @@ -1468,7 +1468,7 @@ func (i *TeleInstance) NewUnauthenticatedClient(cfg ClientConfig) (tc *client.Te HostPort: cfg.Port, HostLogin: cfg.Login, InsecureSkipVerify: true, - KeysDir: keyDir, + ClientStore: client.NewFSClientStore(keyDir), SiteName: cfg.Cluster, ForwardAgent: fwdAgentMode, Labels: cfg.Labels, @@ -1479,7 +1479,7 @@ func (i *TeleInstance) NewUnauthenticatedClient(cfg ClientConfig) (tc *client.Te TLSRoutingEnabled: i.IsSinglePortSetup, TLSRoutingConnUpgradeRequired: cfg.ALBAddr != "", Tracer: tracing.NoopProvider().Tracer("test"), - EnableEscapeSequences: cfg.EnableEscapeSequences, + DisableEscapeSequences: !cfg.EnableEscapeSequences, Stderr: cfg.Stderr, Stdin: cfg.Stdin, Stdout: cfg.Stdout, diff --git a/integration/integration_test.go b/integration/integration_test.go index eb6c13168db4e..53eb8bac55381 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -23,7 +23,6 @@ import ( "bytes" "context" "crypto/tls" - "crypto/x509" "encoding/json" "errors" "fmt" @@ -75,7 +74,6 @@ import ( "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" apiutils "github.com/gravitational/teleport/api/utils" - "github.com/gravitational/teleport/api/utils/keypaths" "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/integration/helpers" "github.com/gravitational/teleport/lib" @@ -2439,28 +2437,9 @@ func twoClustersTunnel(t *testing.T, suite *integrationTestSuite, now time.Time, err = tc.UpdateTrustedCA(ctx, a.GetSiteAPI(a.Secrets.SiteName)) require.NoError(t, err) - // The known_hosts file should have two certificates, the way bytes.Split - // works that means the output will be 3 (2 certs + 1 empty). - buffer, err := os.ReadFile(keypaths.KnownHostsPath(tc.KeysDir)) + trustedCerts, err := tc.ClientStore.GetTrustedCerts(tc.WebProxyHost()) require.NoError(t, err) - parts := bytes.Split(buffer, []byte("\n")) - require.Len(t, parts, 3) - - roots := x509.NewCertPool() - werr := filepath.Walk(keypaths.CAsDir(tc.KeysDir, Host), func(path string, info fs.FileInfo, err error) error { - require.NoError(t, err) - if info.IsDir() { - return nil - } - buffer, err = os.ReadFile(path) - require.NoError(t, err) - ok := roots.AppendCertsFromPEM(buffer) - require.True(t, ok) - return nil - }) - require.NoError(t, werr) - ok := roots.AppendCertsFromPEM(buffer) - require.True(t, ok) + require.Len(t, trustedCerts, 2) // wait for active tunnel connections to be established helpers.WaitForActiveTunnelConnections(t, b.Tunnel, a.Secrets.SiteName, 1) diff --git a/integration/kube_integration_test.go b/integration/kube_integration_test.go index 4ae64768ecbea..6ccdebf8f65b7 100644 --- a/integration/kube_integration_test.go +++ b/integration/kube_integration_test.go @@ -2111,7 +2111,7 @@ func kubeJoin(ctx context.Context, kubeConfig kube.ProxyConfig, tc *client.Telep KubeProxyAddr: tc.Config.KubeProxyAddr, WebProxyAddr: tc.Config.WebProxyAddr, TLSRoutingConnUpgradeRequired: tc.Config.TLSRoutingConnUpgradeRequired, - EnableEscapeSequences: tc.Config.EnableEscapeSequences, + EnableEscapeSequences: !tc.Config.DisableEscapeSequences, Tracker: meta, TLSConfig: tlsConfig, Mode: mode, diff --git a/integration/proxy/teleterm_test.go b/integration/proxy/teleterm_test.go index 4deec70fe6986..9f80ce2ee6894 100644 --- a/integration/proxy/teleterm_test.go +++ b/integration/proxy/teleterm_test.go @@ -51,6 +51,7 @@ import ( "github.com/gravitational/teleport/lib/auth/mocku2f" wancli "github.com/gravitational/teleport/lib/auth/webauthncli" wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" + "github.com/gravitational/teleport/lib/client" libclient "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" "github.com/gravitational/teleport/lib/client/mfa" @@ -241,12 +242,12 @@ func testGatewayCertRenewal(ctx context.Context, t *testing.T, params gatewayCer fakeClock := clockwork.NewFakeClockAt(time.Now()) storage, err := clusters.NewStorage(clusters.Config{ - Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, // Inject a fake clock into clusters.Storage so we can control when the middleware thinks the // db cert has expired. Clock: fakeClock, WebauthnLogin: webauthnLogin, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -876,8 +877,8 @@ func testTeletermAppGatewayTargetPortValidation(t *testing.T, pack *appaccess.Pa require.NoError(t, err) storage, err := clusters.NewStorage(clusters.Config{ - Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) daemonService, err := daemon.New(daemon.Config{ diff --git a/integration/teleterm_test.go b/integration/teleterm_test.go index 25c25507829fc..f425ebe4e5552 100644 --- a/integration/teleterm_test.go +++ b/integration/teleterm_test.go @@ -255,8 +255,8 @@ func testAddingRootCluster(t *testing.T, pack *dbhelpers.DatabasePack, creds *he t.Helper() storage, err := clusters.NewStorage(clusters.Config{ - Dir: t.TempDir(), InsecureSkipVerify: true, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -287,8 +287,8 @@ func testListRootClustersReturnsLoggedInUser(t *testing.T, pack *dbhelpers.Datab tc := mustLogin(t, pack.Root.User.GetName(), pack, creds) storage, err := clusters.NewStorage(clusters.Config{ - Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -369,8 +369,8 @@ func testGetClusterReturnsPropertiesFromAuthServer(t *testing.T, pack *dbhelpers tc := mustLogin(t, userName, pack, creds) storage, err := clusters.NewStorage(clusters.Config{ - Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -421,8 +421,8 @@ func testHeadlessWatcher(t *testing.T, pack *dbhelpers.DatabasePack, creds *help tc := mustLogin(t, pack.Root.User.GetName(), pack, creds) storage, err := clusters.NewStorage(clusters.Config{ - Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -488,9 +488,9 @@ func testClientCache(t *testing.T, pack *dbhelpers.DatabasePack, creds *helpers. storageFakeClock := clockwork.NewFakeClockAt(time.Now()) storage, err := clusters.NewStorage(clusters.Config{ - Dir: tc.KeysDir, Clock: storageFakeClock, InsecureSkipVerify: tc.InsecureSkipVerify, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -748,8 +748,8 @@ func testCreateConnectMyComputerRole(t *testing.T, pack *dbhelpers.DatabasePack) // Prepare daemon.Service. storage, err := clusters.NewStorage(clusters.Config{ - Dir: t.TempDir(), InsecureSkipVerify: true, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -862,10 +862,10 @@ func testCreateConnectMyComputerToken(t *testing.T, pack *dbhelpers.DatabasePack // Prepare daemon.Service. storage, err := clusters.NewStorage(clusters.Config{ - Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, Clock: fakeClock, WebauthnLogin: webauthnLogin, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -924,8 +924,8 @@ func testWaitForConnectMyComputerNodeJoin(t *testing.T, pack *dbhelpers.Database tc := mustLogin(t, pack.Root.User.GetName(), pack, creds) storage, err := clusters.NewStorage(clusters.Config{ - Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -1008,8 +1008,8 @@ func testDeleteConnectMyComputerNode(t *testing.T, pack *dbhelpers.DatabasePack) tc := mustLogin(t, userName, pack, creds) storage, err := clusters.NewStorage(clusters.Config{ - Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -1235,8 +1235,8 @@ func testListDatabaseUsers(t *testing.T, pack *dbhelpers.DatabasePack) { tc := mustLogin(t, rootUserName, pack, creds) storage, err := clusters.NewStorage(clusters.Config{ - Dir: tc.KeysDir, InsecureSkipVerify: tc.InsecureSkipVerify, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) diff --git a/lib/benchmark/benchmark.go b/lib/benchmark/benchmark.go index 0fdc8f5f9679b..c9e66773514f4 100644 --- a/lib/benchmark/benchmark.go +++ b/lib/benchmark/benchmark.go @@ -283,8 +283,9 @@ func work(ctx context.Context, m benchMeasure, send chan<- benchMeasure, workloa // makeTeleportClient creates an instance of a teleport client func makeTeleportClient(host, login, proxy string) (*client.TeleportClient, error) { c := client.Config{ - Host: host, - Tracer: tracing.NoopProvider().Tracer("test"), + Host: host, + Tracer: tracing.NoopProvider().Tracer("test"), + ClientStore: client.NewFSClientStore(""), } if login != "" { @@ -295,8 +296,7 @@ func makeTeleportClient(host, login, proxy string) (*client.TeleportClient, erro c.SSHProxyAddr = proxy } - profileStore := client.NewFSProfileStore("") - if err := c.LoadProfile(profileStore, proxy); err != nil { + if err := c.LoadProfile(proxy); err != nil { return nil, trace.Wrap(err) } tc, err := client.NewClient(&c) diff --git a/lib/client/api.go b/lib/client/api.go index f333f3e047cf0..626c2ce60a224 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -72,7 +72,6 @@ import ( "github.com/gravitational/teleport/api/utils/grpc/interceptors" "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/api/utils/keys/hardwarekey" - "github.com/gravitational/teleport/api/utils/keys/piv" "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/auth/touchid" @@ -256,6 +255,7 @@ type Config struct { // Agent is an SSH agent to use for local Agent procedures. Defaults to in-memory agent keyring. Agent agent.ExtendedAgent + // ClientStore is the store for client data, e.g. keys, certs, profiles. ClientStore *Store // ForwardAgent is used by the client to request agent forwarding from the server. @@ -388,10 +388,10 @@ type Config struct { // off - do not attempt to load keys into agent AddKeysToAgent string - // EnableEscapeSequences will scan Stdin for SSH escape sequences during + // DisableEscapeSequences will disable scanning Stdin for SSH escape sequences during // command/shell execution. This also requires Stdin to be an interactive // terminal. - EnableEscapeSequences bool + DisableEscapeSequences bool // MockSSOLogin is used in tests for mocking the SSO login response. MockSSOLogin SSOLoginFunc @@ -409,9 +409,6 @@ type Config struct { // user home dir. OverridePostgresServiceFilePath string - // HomePath is where tsh stores profiles - HomePath string - // TLSRoutingEnabled indicates that proxy supports ALPN SNI server where // all proxy services are exposed on a single TLS listener (Proxy Web Listener). TLSRoutingEnabled bool @@ -496,11 +493,6 @@ type Config struct { // SSOMFACeremonyConstructor is a custom SSO MFA ceremony constructor. SSOMFACeremonyConstructor func(rd *sso.Redirector) mfa.SSOMFACeremony - // CustomHardwareKeyPrompt is a custom hardware key prompt to use when asking - // for a hardware key PIN, touch, etc. - // If empty, a default CLI prompt is used. - CustomHardwareKeyPrompt hardwarekey.Prompt - // DisableSSHResumption disables transparent SSH connection resumption. DisableSSHResumption bool @@ -535,18 +527,76 @@ type CachePolicy struct { NeverExpires bool } -// MakeDefaultConfig returns default client config -func MakeDefaultConfig() *Config { +// MakeDefaultConfig returns default client config. +// If store is not provided, it will default to in-memory storage without +// hardware key support. This should only be used with static auth methods +// (TLS and AuthMethods fields). +func MakeDefaultConfig(store *Store) *Config { + if store == nil { + store = NewMemClientStore() + } return &Config{ - Stdout: os.Stdout, - Stderr: os.Stderr, - Stdin: os.Stdin, - AddKeysToAgent: AddKeysToAgentAuto, - EnableEscapeSequences: true, - Tracer: tracing.NoopProvider().Tracer("TeleportClient"), + Stdout: os.Stdout, + Stderr: os.Stderr, + Stdin: os.Stdin, + AddKeysToAgent: AddKeysToAgentAuto, + Tracer: tracing.NoopProvider().Tracer("TeleportClient"), + ClientStore: store, } } +func (c *Config) CheckAndSetDefaults() error { + if c.ClientStore == nil { + if c.TLS == nil && c.AuthMethods == nil { + return trace.BadParameter("either client store is or static auth methods are required") + } + // Client will use static auth methods instead of client store. + // Initialize empty client store to prevent panics. + c.ClientStore = NewMemClientStore() + } + + // validate configuration + var err error + if c.Username == "" { + c.Username, err = Username() + if err != nil { + return trace.Wrap(err) + } + log.InfoContext(context.Background(), "No teleport login given, using default", "default_login", c.Username) + } + if c.WebProxyAddr == "" { + return trace.BadParameter("No proxy address specified, missed --proxy flag?") + } + if c.HostLogin == "" { + c.HostLogin, err = Username() + if err != nil { + return trace.Wrap(err) + } + log.InfoContext(context.Background(), "no host login given, using default", "default_host_login", c.HostLogin) + } + if len(c.JumpHosts) > 1 { + return trace.BadParameter("only one jump host is supported, got %v", len(c.JumpHosts)) + } + + // set defaults + if c.Stdout == nil { + c.Stdout = os.Stdout + } + if c.Stderr == nil { + c.Stderr = os.Stderr + } + if c.Stdin == nil { + c.Stdin = os.Stdin + } + if c.AddKeysToAgent == "" { + c.AddKeysToAgent = AddKeysToAgentAuto + } + if c.Tracer == nil { + c.Tracer = tracing.NoopProvider().Tracer("TeleportClient") + } + return nil +} + // VirtualPathKind is the suffix component for env vars denoting the type of // file that will be loaded. type VirtualPathKind string @@ -846,13 +896,16 @@ func IsErrorResolvableWithRelogin(err error) bool { IsNoCredentialsError(err) } -// GetProfile gets the profile for the specified proxy address, or -// the current profile if no proxy is specified. -func (c *Config) GetProfile(ps ProfileStore, proxyAddr string) (*profile.Profile, error) { +// GetProfile gets the client profile for the specified proxy address. +func (c *Config) GetProfile(proxyAddr string) (*profile.Profile, error) { + if c.ClientStore == nil { + return nil, trace.BadParameter("client store can not be nil") + } + var proxyHost string var err error if proxyAddr == "" { - proxyHost, err = ps.CurrentProfile() + proxyHost, err = c.ClientStore.CurrentProfile() if err != nil { return nil, trace.Wrap(err) } @@ -863,18 +916,17 @@ func (c *Config) GetProfile(ps ProfileStore, proxyAddr string) (*profile.Profile } } - profile, err := ps.GetProfile(proxyHost) + profile, err := c.ClientStore.GetProfile(proxyHost) if err != nil { return nil, trace.Wrap(err) } return profile, nil } -// LoadProfile populates Config with the values stored in the given -// profiles directory. If profileDir is an empty string, the default profile -// directory ~/.tsh is used. -func (c *Config) LoadProfile(ps ProfileStore, proxyAddr string) error { - profile, err := c.GetProfile(ps, proxyAddr) +// LoadProfile populates Config with the values stored in the client +// profile for the specified proxy address. +func (c *Config) LoadProfile(proxyAddr string) error { + profile, err := c.GetProfile(proxyAddr) if err != nil { return trace.Wrap(err) } @@ -889,7 +941,6 @@ func (c *Config) LoadProfile(ps ProfileStore, proxyAddr string) error { c.MongoProxyAddr = profile.MongoProxyAddr c.TLSRoutingEnabled = profile.TLSRoutingEnabled c.TLSRoutingConnUpgradeRequired = profile.TLSRoutingConnUpgradeRequired - c.KeysDir = profile.Dir c.AuthConnector = profile.AuthConnector c.LoadAllCAs = profile.LoadAllCAs c.PrivateKeyPolicy = profile.PrivateKeyPolicy @@ -1240,63 +1291,14 @@ type ShellCreatedCallback func(s *tracessh.Session, c *tracessh.Client, terminal // NewClient creates a TeleportClient object and fully configures it func NewClient(c *Config) (tc *TeleportClient, err error) { - if len(c.JumpHosts) > 1 { - return nil, trace.BadParameter("only one jump host is supported, got %v", len(c.JumpHosts)) - } - // validate configuration - if c.Username == "" { - c.Username, err = Username() - if err != nil { - return nil, trace.Wrap(err) - } - log.InfoContext(context.Background(), "No teleport login given, using default", "default_login", c.Username) - } - if c.WebProxyAddr == "" { - return nil, trace.BadParameter("No proxy address specified, missed --proxy flag?") - } - if c.HostLogin == "" { - c.HostLogin, err = Username() - if err != nil { - return nil, trace.Wrap(err) - } - log.InfoContext(context.Background(), "no host login given, using default", "default_host_login", c.HostLogin) - } - - if c.Tracer == nil { - c.Tracer = tracing.NoopProvider().Tracer(teleport.ComponentTeleport) + if err := c.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) } tc = &TeleportClient{ Config: *c, } - if tc.Stdout == nil { - tc.Stdout = os.Stdout - } - if tc.Stderr == nil { - tc.Stderr = os.Stderr - } - if tc.Stdin == nil { - tc.Stdin = os.Stdin - } - - if tc.ClientStore == nil { - if tc.TLS != nil || tc.AuthMethods != nil { - // Client will use static auth methods instead of client store. - // Initialize empty client store to prevent panics. - tc.ClientStore = NewMemClientStore() - } else { - // TODO (Joerger): init hardware key service (and client store) earlier where it can - // be properly shared. - hardwareKeyService := piv.NewYubiKeyService(tc.CustomHardwareKeyPrompt) - tc.ClientStore = NewFSClientStore(c.KeysDir, WithHardwareKeyService(hardwareKeyService)) - if c.AddKeysToAgent == AddKeysToAgentOnly { - // Store client keys in memory, but still save trusted certs and profile to disk. - tc.ClientStore.KeyStore = NewMemKeyStore() - } - } - } - // Create a buffered channel to hold events that occurred during this session. // This channel must be buffered because the SSH connection directly feeds // into it. Delays in pulling messages off the global SSH request channel diff --git a/lib/client/api_login_test.go b/lib/client/api_login_test.go index 15d09fc03b671..1ee05cab92dae 100644 --- a/lib/client/api_login_test.go +++ b/lib/client/api_login_test.go @@ -273,7 +273,8 @@ func TestTeleportClient_Login_local(t *testing.T) { otpKey := sa.OTPKey // Prepare client config. - cfg := client.MakeDefaultConfig() + cfg := &client.Config{} + cfg.ClientStore = client.NewFSClientStore(t.TempDir()) cfg.Stdout = io.Discard cfg.Stderr = io.Discard cfg.Stdin = &bytes.Buffer{} @@ -283,7 +284,6 @@ func TestTeleportClient_Login_local(t *testing.T) { // Replace "127.0.0.1" with "localhost". The proxy address becomes the origin // for Webauthn requests, and Webauthn doesn't take IP addresses. cfg.WebProxyAddr = strings.Replace(sa.ProxyWebAddr, "127.0.0.1", "localhost", 1 /* n */) - cfg.KeysDir = t.TempDir() cfg.InsecureSkipVerify = true // Prepare the client proper. @@ -339,7 +339,8 @@ func TestTeleportClient_DeviceLogin(t *testing.T) { require.NoError(t, err, "UpsertAuthPreference failed") // Prepare client config, it won't change throughout the test. - cfg := client.MakeDefaultConfig() + cfg := &client.Config{} + cfg.ClientStore = client.NewFSClientStore(t.TempDir()) cfg.Stdout = io.Discard cfg.Stderr = io.Discard cfg.Stdin = &bytes.Buffer{} @@ -347,7 +348,6 @@ func TestTeleportClient_DeviceLogin(t *testing.T) { cfg.HostLogin = username cfg.AddKeysToAgent = client.AddKeysToAgentNo cfg.WebProxyAddr = sa.ProxyWebAddr - cfg.KeysDir = t.TempDir() cfg.InsecureSkipVerify = true teleportClient, err := client.NewClient(cfg) @@ -674,11 +674,11 @@ func TestRetryWithRelogin(t *testing.T) { clock := clockwork.NewFakeClockAt(time.Now()) sa := newStandaloneTeleport(t, clock) - cfg := client.MakeDefaultConfig() + cfg := &client.Config{} + cfg.ClientStore = client.NewFSClientStore(t.TempDir()) cfg.Username = sa.Username cfg.HostLogin = sa.Username cfg.WebProxyAddr = sa.ProxyWebAddr - cfg.KeysDir = t.TempDir() cfg.InsecureSkipVerify = true cfg.AllowStdinHijack = true diff --git a/lib/client/api_test.go b/lib/client/api_test.go index 248f6979049c6..09ba001069d2c 100644 --- a/lib/client/api_test.go +++ b/lib/client/api_test.go @@ -197,13 +197,13 @@ func TestParseProxyHostString(t *testing.T) { func TestNew(t *testing.T) { conf := Config{ - Host: "localhost", - HostLogin: "vincent", - HostPort: 22, - KeysDir: t.TempDir(), - Username: "localuser", - SiteName: "site", - Tracer: tracing.NoopProvider().Tracer("test"), + Host: "localhost", + HostLogin: "vincent", + HostPort: 22, + Username: "localuser", + SiteName: "site", + Tracer: tracing.NoopProvider().Tracer("test"), + ClientStore: NewMemClientStore(), } err := conf.ParseProxyHost("proxy") require.NoError(t, err) @@ -1157,13 +1157,11 @@ func TestLoadTLSConfigForClusters(t *testing.T) { } func TestConnectToProxyCancelledContext(t *testing.T) { - cfg := MakeDefaultConfig() - + cfg := &Config{} cfg.Agent = &mockAgent{} cfg.AuthMethods = []ssh.AuthMethod{ssh.Password("xyz")} cfg.AddKeysToAgent = AddKeysToAgentNo cfg.WebProxyAddr = "dummy" - cfg.KeysDir = t.TempDir() cfg.TLSRoutingEnabled = true clt, err := NewClient(cfg) diff --git a/lib/client/client.go b/lib/client/client.go index 76bfe1b238cb6..1041ffec5b34e 100644 --- a/lib/client/client.go +++ b/lib/client/client.go @@ -404,7 +404,7 @@ func (c *NodeClient) RunInteractiveShell(ctx context.Context, mode types.Session env[teleport.SSHSessionWebProxyAddr] = c.ProxyPublicAddr } - nodeSession, err := newSession(ctx, c, sessToJoin, env, c.TC.Stdin, c.TC.Stdout, c.TC.Stderr, c.TC.EnableEscapeSequences) + nodeSession, err := newSession(ctx, c, sessToJoin, env, c.TC.Stdin, c.TC.Stdout, c.TC.Stderr, !c.TC.DisableEscapeSequences) if err != nil { return trace.Wrap(err) } @@ -605,7 +605,7 @@ func (c *NodeClient) RunCommand(ctx context.Context, command []string, opts ...R } } - nodeSession, err := newSession(ctx, c, nil, c.TC.newSessionEnv(), c.TC.Stdin, stdout, stderr, c.TC.EnableEscapeSequences) + nodeSession, err := newSession(ctx, c, nil, c.TC.newSessionEnv(), c.TC.Stdin, stdout, stderr, !c.TC.DisableEscapeSequences) if err != nil { return trace.Wrap(err) } diff --git a/lib/client/conntest/ssh.go b/lib/client/conntest/ssh.go index 282c3dab8499c..d14201d8c142c 100644 --- a/lib/client/conntest/ssh.go +++ b/lib/client/conntest/ssh.go @@ -185,7 +185,7 @@ func (s *SSHConnectionTester) TestConnection(ctx context.Context, req TestConnec processStdout := &bytes.Buffer{} - clientConf := client.MakeDefaultConfig() + clientConf := &client.Config{} clientConf.AddKeysToAgent = client.AddKeysToAgentNo clientConf.AuthMethods = []ssh.AuthMethod{keyAuthMethod} clientConf.Host = req.ResourceName diff --git a/lib/client/db/dbcmd/dbcmd_test.go b/lib/client/db/dbcmd/dbcmd_test.go index 0a1d8dd46d1f5..ccb3039e60cec 100644 --- a/lib/client/db/dbcmd/dbcmd_test.go +++ b/lib/client/db/dbcmd/dbcmd_test.go @@ -61,11 +61,11 @@ func (f fakeExec) LookPath(path string) (string, error) { func TestCLICommandBuilderGetConnectCommand(t *testing.T) { conf := &client.Config{ - HomePath: t.TempDir(), Host: "localhost", WebProxyAddr: "proxy.example.com", SiteName: "db.example.com", Tracer: tracing.NoopProvider().Tracer("test"), + ClientStore: client.NewMemClientStore(), } tc, err := client.NewClient(conf) @@ -801,11 +801,11 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { func TestCLICommandBuilderGetConnectCommandAlternatives(t *testing.T) { conf := &client.Config{ - HomePath: t.TempDir(), Host: "localhost", WebProxyAddr: "proxy.example.com", SiteName: "db.example.com", Tracer: tracing.NoopProvider().Tracer("test"), + ClientStore: client.NewMemClientStore(), } tc, err := client.NewClient(conf) @@ -970,13 +970,12 @@ func TestCLICommandBuilderGetConnectCommandAlternatives(t *testing.T) { func TestConvertCommandError(t *testing.T) { t.Parallel() - homePath := t.TempDir() conf := &client.Config{ - HomePath: homePath, Host: "localhost", WebProxyAddr: "localhost", SiteName: "db.example.com", Tracer: tracing.NoopProvider().Tracer("test"), + ClientStore: client.NewMemClientStore(), } tc, err := client.NewClient(conf) @@ -985,7 +984,6 @@ func TestConvertCommandError(t *testing.T) { profile := &client.ProfileStatus{ Name: "example.com", Username: "bob", - Dir: homePath, Cluster: "example.com", } diff --git a/lib/client/identityfile/identity.go b/lib/client/identityfile/identity.go index 2192e360efc41..8614ced48bbde 100644 --- a/lib/client/identityfile/identity.go +++ b/lib/client/identityfile/identity.go @@ -829,26 +829,15 @@ func KeyRingFromIdentityFile(identityPath, proxyHost, clusterName string) (*clie return keyRing, nil } -// NewClientStoreFromIdentityFile initializes a new in-memory client store -// and loads data from the given identity file into it. A temporary profile -// is also added to its profile store with the limited profile data available -// in the identity file. +// LoadIdentityFileIntoClientStore loads the identityFile from the given path +// into the given client store, assimilating it with other keys in the store. +// A temporary profile is also added to its profile store with the limited profile +// data available in the identity file. // // Use [proxyAddr] to specify the host:port-like address of the proxy. // This is necessary because identity files do not store the proxy address. // Additionally, the [clusterName] argument can ve used to target a leaf cluster // rather than the default root cluster. -func NewClientStoreFromIdentityFile(identityFile, proxyAddr, clusterName string, opts ...client.StoreConfigOpt) (*client.Store, error) { - clientStore := client.NewMemClientStore(opts...) - if err := LoadIdentityFileIntoClientStore(clientStore, identityFile, proxyAddr, clusterName); err != nil { - return nil, trace.Wrap(err) - } - - return clientStore, nil -} - -// LoadIdentityFileIntoClientStore loads the identityFile from the given path -// into the given client store, assimilating it with other keys in the store. func LoadIdentityFileIntoClientStore(store *client.Store, identityFile, proxyAddr, clusterName string) error { if proxyAddr == "" { return trace.BadParameter("missing a Proxy address when loading an Identity File.") diff --git a/lib/client/identityfile/identity_test.go b/lib/client/identityfile/identity_test.go index fe1d9df9a9857..b006a3377737b 100644 --- a/lib/client/identityfile/identity_test.go +++ b/lib/client/identityfile/identity_test.go @@ -422,7 +422,7 @@ func TestKeyFromIdentityFile(t *testing.T) { }) } -func TestNewClientStoreFromIdentityFile(t *testing.T) { +func TestLoadIdentityFileIntoClientStore(t *testing.T) { t.Parallel() keyRing := newClientKeyRing(t) keyRing.ProxyHost = "proxy.example.com" @@ -439,7 +439,8 @@ func TestNewClientStoreFromIdentityFile(t *testing.T) { }) require.NoError(t, err) - clientStore, err := NewClientStoreFromIdentityFile(identityFilePath, keyRing.ProxyHost+":3080", keyRing.ClusterName) + clientStore := client.NewMemClientStore() + err = LoadIdentityFileIntoClientStore(clientStore, identityFilePath, keyRing.ProxyHost+":3080", keyRing.ClusterName) require.NoError(t, err) currentProfile, err := clientStore.CurrentProfile() diff --git a/lib/client/profile.go b/lib/client/profile.go index baa6812205512..21fb7e0a323ea 100644 --- a/lib/client/profile.go +++ b/lib/client/profile.go @@ -639,7 +639,7 @@ func (p *ProfileStatus) DatabaseServices() (result []string) { // DatabasesForCluster returns a list of databases for this profile, for the // specified cluster name. -func (p *ProfileStatus) DatabasesForCluster(clusterName string) ([]tlsca.RouteToDatabase, error) { +func (p *ProfileStatus) DatabasesForCluster(clusterName string, store *Store) ([]tlsca.RouteToDatabase, error) { if clusterName == "" || clusterName == p.Cluster { return p.Databases, nil } @@ -649,7 +649,7 @@ func (p *ProfileStatus) DatabasesForCluster(clusterName string) ([]tlsca.RouteTo Username: p.Username, ClusterName: clusterName, } - store := NewFSKeyStore(p.Dir) + keyRing, err := store.GetKeyRing(idx, nil /*hwks*/, WithDBCerts{}) if err != nil { return nil, trace.Wrap(err) @@ -659,7 +659,7 @@ func (p *ProfileStatus) DatabasesForCluster(clusterName string) ([]tlsca.RouteTo // AppsForCluster returns a list of apps for this profile, for the // specified cluster name. -func (p *ProfileStatus) AppsForCluster(clusterName string) ([]tlsca.RouteToApp, error) { +func (p *ProfileStatus) AppsForCluster(clusterName string, store *Store) ([]tlsca.RouteToApp, error) { if clusterName == "" || clusterName == p.Cluster { return p.Apps, nil } @@ -670,7 +670,6 @@ func (p *ProfileStatus) AppsForCluster(clusterName string) ([]tlsca.RouteToApp, ClusterName: clusterName, } - store := NewFSKeyStore(p.Dir) keyRing, err := store.GetKeyRing(idx, nil /*hwks*/, WithAppCerts{}) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/client/weblogin_test.go b/lib/client/weblogin_test.go index 1008308411d50..252a291b9aec1 100644 --- a/lib/client/weblogin_test.go +++ b/lib/client/weblogin_test.go @@ -130,12 +130,12 @@ func TestSSHAgentPasswordlessLogin(t *testing.T) { ctx := context.Background() // Prepare client config, it won't change throughout the test. - cfg := client.MakeDefaultConfig() + cfg := &client.Config{} + cfg.ClientStore = client.NewFSClientStore(t.TempDir()) cfg.AddKeysToAgent = client.AddKeysToAgentNo // Replace "127.0.0.1" with "localhost". The proxy address becomes the origin // for Webauthn requests, and Webauthn doesn't take IP addresses. cfg.WebProxyAddr = strings.Replace(sa.ProxyWebAddr, "127.0.0.1", "localhost", 1 /* n */) - cfg.KeysDir = t.TempDir() cfg.InsecureSkipVerify = true solvePwdless := func(ctx context.Context, origin string, assertion *wantypes.CredentialAssertion, prompt wancli.LoginPrompt) (*proto.MFAAuthenticateResponse, error) { diff --git a/lib/teleterm/clusters/cluster.go b/lib/teleterm/clusters/cluster.go index cdd849cea5fc7..61497191dd2ce 100644 --- a/lib/teleterm/clusters/cluster.go +++ b/lib/teleterm/clusters/cluster.go @@ -51,8 +51,6 @@ type Cluster struct { ProfileName string // Logger is a component logger Logger *slog.Logger - // dir is the directory where cluster certificates are stored - dir string // Status is the cluster status status client.ProfileStatus // If not empty, it means that there was a problem with reading the cluster status. diff --git a/lib/teleterm/clusters/config.go b/lib/teleterm/clusters/config.go index d1410b0eb9890..2480f0ddb59b6 100644 --- a/lib/teleterm/clusters/config.go +++ b/lib/teleterm/clusters/config.go @@ -25,14 +25,11 @@ import ( "github.com/jonboulle/clockwork" "github.com/gravitational/teleport" - "github.com/gravitational/teleport/api/utils/keys/hardwarekey" "github.com/gravitational/teleport/lib/client" ) // Config is the cluster service config type Config struct { - // Dir is the directory to store cluster profiles - Dir string // Clock is a clock for time-related operations Clock clockwork.Clock // InsecureSkipVerify is an option to skip TLS cert check @@ -44,15 +41,14 @@ type Config struct { WebauthnLogin client.WebauthnLoginFunc // AddKeysToAgent is passed to [client.Config]. AddKeysToAgent string - // CustomHardwareKeyPrompt is a custom hardware key prompt to use when asking - // for a hardware key PIN, touch, etc. - CustomHardwareKeyPrompt hardwarekey.Prompt + // ClientStore is stores client data. + ClientStore *client.Store } // CheckAndSetDefaults checks the configuration for its validity and sets default values if needed func (c *Config) CheckAndSetDefaults() error { - if c.Dir == "" { - return trace.BadParameter("missing working directory") + if c.ClientStore == nil { + return trace.BadParameter("missing client store") } if c.Clock == nil { diff --git a/lib/teleterm/clusters/storage.go b/lib/teleterm/clusters/storage.go index 7ababc194d611..ef1fb983e20a9 100644 --- a/lib/teleterm/clusters/storage.go +++ b/lib/teleterm/clusters/storage.go @@ -42,14 +42,12 @@ func NewStorage(cfg Config) (*Storage, error) { // ListProfileNames returns just the names of profiles in s.Dir. func (s *Storage) ListProfileNames() ([]string, error) { - profileStore := client.NewFSProfileStore(s.Dir) - pfNames, err := profileStore.ListProfiles() - return pfNames, trace.Wrap(err) + return s.ClientStore.ListProfiles() } // ListRootClusters reads root clusters from profiles. func (s *Storage) ListRootClusters() ([]*Cluster, error) { - pfNames, err := s.ListProfileNames() + pfNames, err := s.ClientStore.ListProfiles() if err != nil { return nil, trace.Wrap(err) } @@ -112,8 +110,7 @@ func (s *Storage) ResolveCluster(resourceURI uri.ResourceURI) (*Cluster, *client // Remove removes a cluster func (s *Storage) Remove(ctx context.Context, profileName string) error { - profileStore := client.NewFSProfileStore(s.Dir) - return profileStore.DeleteProfile(profileName) + return s.ClientStore.DeleteProfile(profileName) } // Add adds a cluster @@ -138,7 +135,7 @@ func (s *Storage) Add(ctx context.Context, webProxyAddress string) (*Cluster, *c } } - cluster, clusterClient, err := s.addCluster(ctx, s.Dir, webProxyAddress) + cluster, clusterClient, err := s.addCluster(ctx, webProxyAddress) if err != nil { return nil, nil, trace.Wrap(err) } @@ -149,19 +146,15 @@ func (s *Storage) Add(ctx context.Context, webProxyAddress string) (*Cluster, *c // addCluster adds a new cluster. This makes the underlying profile .yaml file to be saved to the // tsh home dir without logging in the user yet. Adding a cluster makes it show up in the UI as the // list of clusters depends on the profiles in the home dir of tsh. -func (s *Storage) addCluster(ctx context.Context, dir, webProxyAddress string) (*Cluster, *client.TeleportClient, error) { +func (s *Storage) addCluster(ctx context.Context, webProxyAddress string) (*Cluster, *client.TeleportClient, error) { if webProxyAddress == "" { return nil, nil, trace.BadParameter("cluster address is missing") } - if dir == "" { - return nil, nil, trace.BadParameter("cluster directory is missing") - } - profileName := parseName(webProxyAddress) clusterURI := uri.NewClusterURI(profileName) - cfg := s.makeDefaultClientConfig(clusterURI) + cfg := s.makeClientConfig() cfg.WebProxyAddr = webProxyAddress clusterClient, err := client.NewClient(cfg) @@ -196,7 +189,6 @@ func (s *Storage) addCluster(ctx context.Context, dir, webProxyAddress string) ( Name: pingResponse.ClusterName, ProfileName: profileName, clusterClient: clusterClient, - dir: s.Dir, clock: s.Clock, Logger: clusterLog, }, clusterClient, nil @@ -211,10 +203,8 @@ func (s *Storage) fromProfile(profileName, leafClusterName string) (*Cluster, *c clusterNameForKey := profileName clusterURI := uri.NewClusterURI(profileName) - profileStore := client.NewFSProfileStore(s.Dir) - - cfg := s.makeDefaultClientConfig(clusterURI) - if err := cfg.LoadProfile(profileStore, profileName); err != nil { + cfg := s.makeClientConfig() + if err := cfg.LoadProfile(profileName); err != nil { return nil, nil, trace.Wrap(err) } @@ -235,7 +225,6 @@ func (s *Storage) fromProfile(profileName, leafClusterName string) (*Cluster, *c Name: clusterClient.SiteName, ProfileName: profileName, clusterClient: clusterClient, - dir: s.Dir, clock: s.Clock, statusError: err, Logger: s.Logger.With("cluster", clusterURI), @@ -275,15 +264,12 @@ func (s *Storage) loadProfileStatusAndClusterKey(clusterClient *client.TeleportC return status, nil } -func (s *Storage) makeDefaultClientConfig(rootClusterURI uri.ResourceURI) *client.Config { - cfg := client.MakeDefaultConfig() - - cfg.HomePath = s.Dir - cfg.KeysDir = s.Dir +func (s *Storage) makeClientConfig() *client.Config { + cfg := &client.Config{} cfg.InsecureSkipVerify = s.InsecureSkipVerify cfg.AddKeysToAgent = s.AddKeysToAgent cfg.WebauthnLogin = s.WebauthnLogin - cfg.CustomHardwareKeyPrompt = s.CustomHardwareKeyPrompt + cfg.ClientStore = s.ClientStore cfg.DTAuthnRunCeremony = dtauthn.NewCeremony().Run cfg.DTAutoEnroll = dtenroll.AutoEnroll return cfg diff --git a/lib/teleterm/daemon/daemon_test.go b/lib/teleterm/daemon/daemon_test.go index 725b224574aa9..92c5aad725d8b 100644 --- a/lib/teleterm/daemon/daemon_test.go +++ b/lib/teleterm/daemon/daemon_test.go @@ -342,11 +342,9 @@ func TestGatewayCRUD(t *testing.T) { } func TestUpdateTshdEventsServerAddress(t *testing.T) { - homeDir := t.TempDir() - storage, err := clusters.NewStorage(clusters.Config{ - Dir: homeDir, InsecureSkipVerify: true, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -376,11 +374,9 @@ func TestUpdateTshdEventsServerAddress(t *testing.T) { } func TestUpdateTshdEventsServerAddress_CredsErr(t *testing.T) { - homeDir := t.TempDir() - storage, err := clusters.NewStorage(clusters.Config{ - Dir: homeDir, InsecureSkipVerify: true, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -480,8 +476,8 @@ func TestRetryWithRelogin(t *testing.T) { t.Parallel() storage, err := clusters.NewStorage(clusters.Config{ - Dir: t.TempDir(), InsecureSkipVerify: true, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) @@ -533,8 +529,8 @@ func TestConcurrentHeadlessAuthPrompts(t *testing.T) { ctx := context.Background() storage, err := clusters.NewStorage(clusters.Config{ - Dir: t.TempDir(), InsecureSkipVerify: true, + ClientStore: client.NewFSClientStore(t.TempDir()), }) require.NoError(t, err) diff --git a/lib/teleterm/teleterm.go b/lib/teleterm/teleterm.go index e721285906a45..9bb88e95b559a 100644 --- a/lib/teleterm/teleterm.go +++ b/lib/teleterm/teleterm.go @@ -32,6 +32,8 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/gravitational/teleport/api/utils/keys/piv" + "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/teleterm/apiserver" "github.com/gravitational/teleport/lib/teleterm/clusteridcache" "github.com/gravitational/teleport/lib/teleterm/clusters" @@ -51,11 +53,12 @@ func Serve(ctx context.Context, cfg Config) error { clock := clockwork.NewRealClock() + hwks := piv.NewYubiKeyService(nil /*prompt*/) storage, err := clusters.NewStorage(clusters.Config{ - Dir: cfg.HomeDir, Clock: clock, InsecureSkipVerify: cfg.InsecureSkipVerify, AddKeysToAgent: cfg.AddKeysToAgent, + ClientStore: client.NewFSClientStore(cfg.HomeDir, client.WithHardwareKeyService(hwks)), }) if err != nil { return trace.Wrap(err) @@ -77,7 +80,7 @@ func Serve(ctx context.Context, cfg Config) error { // TODO(gzdunek): Move tshdEventsClient out of daemonService so that we can // construct the prompt before creating Storage. - storage.CustomHardwareKeyPrompt = daemonService.NewHardwareKeyPrompt() + hwks.SetPrompt(daemonService.NewHardwareKeyPrompt()) apiServer, err := apiserver.New(apiserver.Config{ HostAddr: cfg.Addr, diff --git a/lib/vnet/profile_osconfig_provider_darwin.go b/lib/vnet/profile_osconfig_provider_darwin.go index 910ac503958b7..177b366c07271 100644 --- a/lib/vnet/profile_osconfig_provider_darwin.go +++ b/lib/vnet/profile_osconfig_provider_darwin.go @@ -226,7 +226,7 @@ func (p *profileOSConfigProvider) getClient(ctx context.Context, profileName, le clientConfig := &client.Config{ ClientStore: p.clientStore, } - if err := clientConfig.LoadProfile(p.clientStore, profileName); err != nil { + if err := clientConfig.LoadProfile(profileName); err != nil { return nil, trace.Wrap(err, "loading client profile") } if leafClusterName != "" { diff --git a/tool/tctl/common/config/profile.go b/tool/tctl/common/config/profile.go index e6252a551ed23..1774e253ed90b 100644 --- a/tool/tctl/common/config/profile.go +++ b/tool/tctl/common/config/profile.go @@ -49,9 +49,8 @@ func LoadConfigFromProfile(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authcl hwks := piv.NewYubiKeyService(nil /*prompt*/) clientStore := client.NewFSClientStore(cfg.TeleportHome, client.WithHardwareKeyService(hwks)) if ccf.IdentityFilePath != "" { - var err error - clientStore, err = identityfile.NewClientStoreFromIdentityFile(ccf.IdentityFilePath, proxyAddr, "", client.WithHardwareKeyService(hwks)) - if err != nil { + clientStore = client.NewMemClientStore(client.WithHardwareKeyService(hwks)) + if err := identityfile.LoadIdentityFileIntoClientStore(clientStore, ccf.IdentityFilePath, proxyAddr, ""); err != nil { return nil, trace.Wrap(err) } } @@ -72,17 +71,20 @@ func LoadConfigFromProfile(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authcl return nil, trace.BadParameter("your credentials have expired, please log in using `tsh login`") } - c := client.MakeDefaultConfig() slog.DebugContext(ctx, "Found profile", "proxy", logutils.StringerAttr(&profile.ProxyURL), "user", profile.Username, ) - if err := c.LoadProfile(clientStore, proxyAddr); err != nil { + + // TODO: we shouldn't need to re-retrieve profile. The profile status above + // should embed the profile, or profile status should be removed altogether. + p, err := clientStore.GetProfile(proxyAddr) + if err != nil { return nil, trace.Wrap(err) } - webProxyHost, _ := c.WebProxyHostPort() - idx := client.KeyRingIndex{ProxyHost: webProxyHost, Username: c.Username, ClusterName: profile.Cluster} + webProxyHost, _ := p.WebProxyHostPort() + idx := client.KeyRingIndex{ProxyHost: webProxyHost, Username: p.Username, ClusterName: profile.Cluster} keyRing, err := clientStore.GetKeyRing(idx, client.WithSSHCerts{}) if err != nil { return nil, trace.Wrap(err) @@ -110,7 +112,7 @@ func LoadConfigFromProfile(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authcl } // Do not override auth servers from command line if len(ccf.AuthServerAddr) == 0 { - webProxyAddr, err := utils.ParseAddr(c.WebProxyAddr) + webProxyAddr, err := utils.ParseAddr(p.WebProxyAddr) if err != nil { return nil, trace.Wrap(err) } @@ -121,7 +123,7 @@ func LoadConfigFromProfile(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authcl authConfig.Log = cfg.Logger authConfig.DialOpts = append(authConfig.DialOpts, metadata.WithUserAgentFromTeleportComponent(teleport.ComponentTCTL)) - if c.TLSRoutingEnabled { + if p.TLSRoutingEnabled { cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) } diff --git a/tool/tsh/common/app.go b/tool/tsh/common/app.go index b8db1716205d4..c1b3ed612dec5 100644 --- a/tool/tsh/common/app.go +++ b/tool/tsh/common/app.go @@ -294,7 +294,8 @@ func onAppLogout(cf *CLIConf) error { if err != nil { return trace.Wrap(err) } - activeRoutes, err := profile.AppsForCluster(tc.SiteName) + + activeRoutes, err := profile.AppsForCluster(tc.SiteName, tc.ClientStore) if err != nil { return trace.Wrap(err) } @@ -365,7 +366,7 @@ func onAppConfig(cf *CLIConf) error { if err != nil { return trace.Wrap(err) } - routes, err := profile.AppsForCluster(tc.SiteName) + routes, err := profile.AppsForCluster(tc.SiteName, tc.ClientStore) if err != nil { return trace.Wrap(err) } diff --git a/tool/tsh/common/config.go b/tool/tsh/common/config.go index 6f87a3e89a752..5144a1d579003 100644 --- a/tool/tsh/common/config.go +++ b/tool/tsh/common/config.go @@ -24,8 +24,9 @@ import ( "github.com/gravitational/trace" - "github.com/gravitational/teleport/api/profile" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/utils/keypaths" + "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/config/openssh" ) @@ -75,7 +76,23 @@ func onConfig(cf *CLIConf) error { return trace.Wrap(err) } - keysDir := profile.FullProfilePath(tc.Config.KeysDir) + var keysDir string + switch s := tc.ClientStore.KeyStore.(type) { + case *client.FSKeyStore: + keysDir = s.KeyDir + default: + switch { + case cf.IdentityFileIn != "": + return trace.BadParameter("tsh config command is not supported with identity file. You can flatten your identity file into a tsh profile with \"tsh login -i\" and retry") + case cf.AuthConnector == constants.HeadlessConnector: + return trace.BadParameter("tsh config command is not supported the --headless flag option") + case cf.AddKeysToAgent == client.AddKeysToAgentOnly: + return trace.BadParameter("tsh config command is not supported the --add-keys-to-agent=only flag option") + default: + return trace.BadParameter("tsh config command is not supported with a tsh profile outside of file storage") + } + } + knownHostsPath := keypaths.KnownHostsPath(keysDir) identityFilePath := keypaths.UserSSHKeyPath(keysDir, proxyHost, tc.Config.Username) diff --git a/tool/tsh/common/db.go b/tool/tsh/common/db.go index ff5fe4073653a..7e4dd6ba9265c 100644 --- a/tool/tsh/common/db.go +++ b/tool/tsh/common/db.go @@ -95,7 +95,7 @@ func onListDatabases(cf *CLIConf) error { logger.DebugContext(cf.Context, "Failed to fetch user roles", "error", err) } - activeDatabases, err := profile.DatabasesForCluster(tc.SiteName) + activeDatabases, err := profile.DatabasesForCluster(tc.SiteName, tc.ClientStore) if err != nil { return trace.Wrap(err) } @@ -251,7 +251,7 @@ func onDatabaseLogin(cf *CLIConf) error { if err != nil { return trace.Wrap(err) } - routes, err := profile.DatabasesForCluster(tc.SiteName) + routes, err := profile.DatabasesForCluster(tc.SiteName, tc.ClientStore) if err != nil { return trace.Wrap(err) } @@ -367,7 +367,7 @@ func onDatabaseLogout(cf *CLIConf) error { if err != nil { return trace.Wrap(err) } - activeRoutes, err := profile.DatabasesForCluster(tc.SiteName) + activeRoutes, err := profile.DatabasesForCluster(tc.SiteName, tc.ClientStore) if err != nil { return trace.Wrap(err) } @@ -447,7 +447,7 @@ func onDatabaseEnv(cf *CLIConf) error { if err != nil { return trace.Wrap(err) } - routes, err := profile.DatabasesForCluster(tc.SiteName) + routes, err := profile.DatabasesForCluster(tc.SiteName, tc.ClientStore) if err != nil { return trace.Wrap(err) } @@ -509,7 +509,7 @@ func onDatabaseConfig(cf *CLIConf) error { if err != nil { return trace.Wrap(err) } - routes, err := profile.DatabasesForCluster(tc.SiteName) + routes, err := profile.DatabasesForCluster(tc.SiteName, tc.ClientStore) if err != nil { return trace.Wrap(err) } @@ -751,7 +751,7 @@ func onDatabaseConnect(cf *CLIConf) error { if err != nil { return trace.Wrap(err) } - routes, err := profile.DatabasesForCluster(tc.SiteName) + routes, err := profile.DatabasesForCluster(tc.SiteName, tc.ClientStore) if err != nil { return trace.Wrap(err) } @@ -1085,7 +1085,10 @@ func chooseOneDatabase(cf *CLIConf, databases types.Databases) (types.Database, "%v not found, use '%v' to see registered databases", selectors, formatDatabaseListCommand(cf.SiteName)) } - errMsg := formatAmbiguousDB(cf, selectors, databases) + errMsg, err := formatAmbiguousDB(cf, selectors, databases) + if err != nil { + return nil, trace.Wrap(err) + } return nil, trace.BadParameter("%s", errMsg) } @@ -1356,7 +1359,7 @@ func needDatabaseRelogin(cf *CLIConf, tc *client.TeleportClient, route tlsca.Rou } } found := false - activeDatabases, err := profile.DatabasesForCluster(tc.SiteName) + activeDatabases, err := profile.DatabasesForCluster(tc.SiteName, tc.ClientStore) if err != nil { return false, trace.Wrap(err) } @@ -1811,10 +1814,10 @@ func getDbCmdAlternatives(clusterFlag string, route tlsca.RouteToDatabase) []str // formatAmbiguousDB is a helper func that formats an ambiguous database error // message. -func formatAmbiguousDB(cf *CLIConf, selectors resourceSelectors, matchedDBs types.Databases) string { +func formatAmbiguousDB(cf *CLIConf, selectors resourceSelectors, matchedDBs types.Databases) (string, error) { var activeDBs []tlsca.RouteToDatabase if profile, err := cf.ProfileStatus(); err == nil { - if dbs, err := profile.DatabasesForCluster(cf.SiteName); err == nil { + if dbs, err := profile.DatabasesForCluster(cf.SiteName, cf.clientStore); err == nil { activeDBs = dbs } } @@ -1827,7 +1830,7 @@ func formatAmbiguousDB(cf *CLIConf, selectors resourceSelectors, matchedDBs type listCommand := formatDatabaseListCommand(cf.SiteName) fullNameExample := matchedDBs[0].GetName() - return formatAmbiguityErrTemplate(cf, selectors, listCommand, sb.String(), fullNameExample) + return formatAmbiguityErrTemplate(cf, selectors, listCommand, sb.String(), fullNameExample), nil } // resourceSelectors is a helper struct for gathering up the selectors for a diff --git a/tool/tsh/common/git_list_test.go b/tool/tsh/common/git_list_test.go index cf4d52e318043..7f2e863be748c 100644 --- a/tool/tsh/common/git_list_test.go +++ b/tool/tsh/common/git_list_test.go @@ -128,13 +128,11 @@ func TestGitListCommand(t *testing.T) { } // Create a empty profile so we don't ping proxy. - clientStore, err := initClientStore(cf, cf.Proxy) - require.NoError(t, err) profile := &profile.Profile{ SSHProxyAddr: "proxy:3023", WebProxyAddr: "proxy:3080", } - err = clientStore.SaveProfile(profile, true) + err := cf.clientStore.SaveProfile(profile, true) require.NoError(t, err) cmd := gitListCommand{ diff --git a/tool/tsh/common/kube.go b/tool/tsh/common/kube.go index 6d5907fa4edde..a499c005d1ed1 100644 --- a/tool/tsh/common/kube.go +++ b/tool/tsh/common/kube.go @@ -214,7 +214,7 @@ func (c *kubeJoinCommand) run(cf *CLIConf) error { KubeProxyAddr: tc.Config.KubeProxyAddr, WebProxyAddr: tc.Config.WebProxyAddr, TLSRoutingConnUpgradeRequired: tc.Config.TLSRoutingConnUpgradeRequired, - EnableEscapeSequences: tc.Config.EnableEscapeSequences, + EnableEscapeSequences: !tc.Config.DisableEscapeSequences, Tracker: meta, TLSConfig: tlsConfig, Mode: types.SessionParticipantMode(c.mode), diff --git a/tool/tsh/common/proxy.go b/tool/tsh/common/proxy.go index 45a112a254ad3..c40cae26adfb1 100644 --- a/tool/tsh/common/proxy.go +++ b/tool/tsh/common/proxy.go @@ -143,7 +143,7 @@ func onProxyCommandDB(cf *CLIConf) error { if err != nil { return trace.Wrap(err) } - routes, err := profile.DatabasesForCluster(tc.SiteName) + routes, err := profile.DatabasesForCluster(tc.SiteName, tc.ClientStore) if err != nil { return trace.Wrap(err) } diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index d0f78f0105c22..aaa9011b45aa2 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -578,6 +578,11 @@ type CLIConf struct { // lookPathOverride overrides return of LookPath(). used in tests. lookPathOverride string + + // clientStore is the client identity storage interface. This store must be initialized once + // and only once in order to ensure key (and hardware key) storage is synced across the process. + clientStore *client.Store + clientStoreInitOnce sync.Once } // Stdout returns the stdout writer. @@ -1929,7 +1934,6 @@ func onLogin(cf *CLIConf, reExecArgs ...string) error { if err != nil { return trace.Wrap(err) } - tc.HomePath = cf.HomePath // The user is not logged in and has typed in `tsh --proxy=... login`, if // the running binary needs to be updated, update and re-exec. @@ -4173,7 +4177,7 @@ func makeClientForProxy(cf *CLIConf, proxy string) (*client.TeleportClient, erro // Load SSH key for the cluster indicated in the profile. // Handle gracefully if the profile is empty, the key cannot // be found, or the key isn't supported as an agent key. - profile, profileError := c.GetProfile(c.ClientStore, proxy) + profile, profileError := c.GetProfile(proxy) if profileError == nil { if err := tc.LoadKeyForCluster(ctx, profile.SiteName); err != nil { if !trace.IsNotFound(err) && !trace.IsConnectionProblem(err) && !trace.IsCompareFailed(err) { @@ -4284,8 +4288,7 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err } // 1: start with the defaults - c := client.MakeDefaultConfig() - + c := &client.Config{} c.DialOpts = append(c.DialOpts, metadata.WithUserAgentFromTeleportComponent(teleport.ComponentTSH)) c.Tracer = cf.tracer @@ -4392,14 +4395,22 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err } } - c.ClientStore, err = initClientStore(cf, proxy) - if err != nil { + if err := cf.initClientStore(); err != nil { return nil, trace.Wrap(err) } + c.ClientStore = cf.clientStore + + // If the client store was initialized for the identity file, but the wrong (or missing) + // proxy address, re-load the identity file for the provided proxy address. + if cf.IdentityFileIn != "" && cf.Proxy != proxy { + if err = identityfile.LoadIdentityFileIntoClientStore(cf.clientStore, cf.IdentityFileIn, proxy, c.SiteName); err == nil { + return nil, trace.Wrap(err) + } + } // load profile. if no --proxy is given the currently active profile is used, otherwise // fetch profile for exact proxy we are trying to connect to. - profileErr := c.LoadProfile(c.ClientStore, proxy) + profileErr := c.LoadProfile(proxy) if profileErr != nil && !trace.IsNotFound(profileErr) { fmt.Printf("WARNING: Failed to load tsh profile for %q: %v\n", proxy, profileErr) } @@ -4543,7 +4554,7 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err c.AddKeysToAgent = client.AddKeysToAgentNo } - c.EnableEscapeSequences = cf.EnableEscapeSequences + c.DisableEscapeSequences = !cf.EnableEscapeSequences // pass along mock functions if provided (only used in tests) c.MockSSOLogin = cf.MockSSOLogin @@ -4556,13 +4567,6 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err c.OverrideMySQLOptionFilePath = cf.overrideMySQLOptionFilePath c.OverridePostgresServiceFilePath = cf.overridePostgresServiceFilePath - // Set tsh home directory - c.HomePath = cf.HomePath - - if c.KeysDir == "" { - c.KeysDir = c.HomePath - } - if cf.IdentityFileIn != "" { c.NonInteractive = true } @@ -4592,32 +4596,39 @@ func setEnvVariables(c *client.Config, options Options) { } } -func initClientStore(cf *CLIConf, proxy string) (*client.Store, error) { - hwks := piv.NewYubiKeyService(nil /*prompt*/) +// initClientStore initializes the client identity store which will be used by the +// client to interface with client identity material. +func (c *CLIConf) initClientStore() error { + var err error + c.clientStoreInitOnce.Do(func() { + hwks := piv.NewYubiKeyService(nil /*prompt*/) - switch { - case cf.IdentityFileIn != "": - // Import identity file keys to in-memory client store. - clientStore, err := identityfile.NewClientStoreFromIdentityFile(cf.IdentityFileIn, proxy, cf.SiteName, client.WithHardwareKeyService(hwks)) - if err != nil { - return nil, trace.Wrap(err) - } - return clientStore, nil + switch { + case c.IdentityFileIn != "", c.IdentityFileOut != "", c.AuthConnector == constants.HeadlessConnector: + // Store client keys in memory, where they can be exported to non-standard + // FS formats (e.g. identity file) or used for a single client call in memory. + c.clientStore = client.NewMemClientStore(client.WithHardwareKeyService(hwks)) - case cf.IdentityFileOut != "", cf.AuthConnector == constants.HeadlessConnector: - // Store client keys in memory, where they can be exported to non-standard - // FS formats (e.g. identity file) or used for a single client call in memory. - return client.NewMemClientStore(client.WithHardwareKeyService(hwks)), nil + case c.AddKeysToAgent == client.AddKeysToAgentOnly: + // Store client keys in memory, but save trusted certs and profile to disk. + c.clientStore = client.NewFSClientStore(c.HomePath, client.WithHardwareKeyService(hwks)) + c.clientStore.KeyStore = client.NewMemKeyStore() - case cf.AddKeysToAgent == client.AddKeysToAgentOnly: - // Store client keys in memory, but save trusted certs and profile to disk. - clientStore := client.NewFSClientStore(cf.HomePath, client.WithHardwareKeyService(hwks)) - clientStore.KeyStore = client.NewMemKeyStore() - return clientStore, nil + default: + c.clientStore = client.NewFSClientStore(c.HomePath, client.WithHardwareKeyService(hwks)) + } - default: - return client.NewFSClientStore(cf.HomePath, client.WithHardwareKeyService(hwks)), nil - } + // If an identity file is provided, opportunistically try to load it into the keystore. It may + // fail if the user did not provide the --proxy flag, but in some cases the proxy, the proxy + // address will be provided later on and the client will attempt to load the identity file then. + if c.IdentityFileIn != "" { + if err = identityfile.LoadIdentityFileIntoClientStore(c.clientStore, c.IdentityFileIn, c.Proxy, c.SiteName); err == nil { + slog.DebugContext(c.Context, "failed to load identity file into client store", "err", err) + } + } + }) + + return trace.Wrap(err) } func (c *CLIConf) ProfileStatus() (*client.ProfileStatus, error) { @@ -4625,11 +4636,11 @@ func (c *CLIConf) ProfileStatus() (*client.ProfileStatus, error) { return c.profileStatusOverride, nil } - clientStore, err := initClientStore(c, c.Proxy) - if err != nil { + if err := c.initClientStore(); err != nil { return nil, trace.Wrap(err) } - profile, err := clientStore.ReadProfileStatus(c.Proxy) + + profile, err := c.clientStore.ReadProfileStatus(c.Proxy) if err != nil { return nil, trace.Wrap(err) } @@ -4637,11 +4648,11 @@ func (c *CLIConf) ProfileStatus() (*client.ProfileStatus, error) { } func (c *CLIConf) FullProfileStatus() (*client.ProfileStatus, []*client.ProfileStatus, error) { - clientStore, err := initClientStore(c, c.Proxy) - if err != nil { + if err := c.initClientStore(); err != nil { return nil, nil, trace.Wrap(err) } - currentProfile, profiles, err := clientStore.FullProfileStatus() + + currentProfile, profiles, err := c.clientStore.FullProfileStatus() if err != nil { return nil, nil, trace.Wrap(err) } @@ -4651,19 +4662,18 @@ func (c *CLIConf) FullProfileStatus() (*client.ProfileStatus, []*client.ProfileS // ListProfiles returns a list of profiles the current user has // credentials for. func (c *CLIConf) ListProfiles() ([]*client.ProfileStatus, error) { - clientStore, err := initClientStore(c, c.Proxy) - if err != nil { + if err := c.initClientStore(); err != nil { return nil, trace.Wrap(err) } - profileNames, err := clientStore.ListProfiles() + profileNames, err := c.clientStore.ListProfiles() if err != nil { return nil, trace.Wrap(err) } profiles := make([]*client.ProfileStatus, 0, len(profileNames)) for _, profileName := range profileNames { - status, err := clientStore.ReadProfileStatus(profileName) + status, err := c.clientStore.ReadProfileStatus(profileName) if err != nil { return nil, trace.Wrap(err) } @@ -4675,17 +4685,16 @@ func (c *CLIConf) ListProfiles() ([]*client.ProfileStatus, error) { // GetProfile loads user profile. func (c *CLIConf) GetProfile() (*profile.Profile, error) { - store, err := initClientStore(c, c.Proxy) - if err != nil { + if err := c.initClientStore(); err != nil { return nil, trace.Wrap(err) } - profileName, err := client.ProfileNameFromProxyAddress(store, c.Proxy) + profileName, err := client.ProfileNameFromProxyAddress(c.clientStore, c.Proxy) if err != nil { return nil, trace.Wrap(err) } - profile, err := store.GetProfile(profileName) + profile, err := c.clientStore.GetProfile(profileName) return profile, trace.Wrap(err) } @@ -4815,30 +4824,25 @@ func parseCertificateCompatibilityFlag(compatibility string, certificateFormat s // flattenIdentity reads an identity file and flattens it into a tsh profile on disk. func flattenIdentity(cf *CLIConf) error { - // Save the identity file path for later - identityFile := cf.IdentityFileIn - - // We clear the identity flag so that the client store will be backed - // by the filesystem instead (in ~/.tsh or TELEPORT_HOME). - cf.IdentityFileIn = "" - - // Load client config as normal to parse client inputs and add defaults. - c, err := loadClientConfigFromCLIConf(cf, cf.Proxy) - if err != nil { - return trace.Wrap(err) - } - // Proxy address may be loaded from existing tsh profile or from --proxy flag. - if c.WebProxyAddr == "" { + if cf.Proxy == "" { return trace.BadParameter("No proxy address specified, missed --proxy flag?") } + // Usually, initializing the client store with an identity file would result in + // an in-memory client store with a profile for cf.Proxy pre-loaded. Instead, + // initialize an FS client store and load the identity file into it. + cf.clientStoreInitOnce.Do(func() { + hwks := piv.NewYubiKeyService(nil /*prompt*/) + cf.clientStore = client.NewFSClientStore(cf.HomePath, client.WithHardwareKeyService(hwks)) + }) + // Load the identity file key and partial profile into the client store. - if err := identityfile.LoadIdentityFileIntoClientStore(c.ClientStore, identityFile, c.WebProxyAddr, c.SiteName); err != nil { + if err := identityfile.LoadIdentityFileIntoClientStore(cf.clientStore, cf.IdentityFileIn, cf.Proxy, cf.SiteName); err != nil { return trace.Wrap(err) } - fmt.Printf("Successfully flattened Identity file %q into a tsh profile.\n", identityFile) + fmt.Printf("Successfully flattened Identity file %q into a tsh profile.\n", cf.IdentityFileIn) // onStatus will ping the proxy to fill in cluster profile information missing in the // client store, then print the login status. diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 49156db42a2f1..20ad32dbb9ac1 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -927,13 +927,12 @@ func TestMakeClient(t *testing.T) { conf.HomePath = t.TempDir() // Create a empty profile so we don't ping proxy. - clientStore, err := initClientStore(&conf, conf.Proxy) - require.NoError(t, err) + conf.initClientStore() profile := &profile.Profile{ SSHProxyAddr: "proxy:3023", WebProxyAddr: "proxy:3080", } - err = clientStore.SaveProfile(profile, true) + err = conf.clientStore.SaveProfile(profile, true) require.NoError(t, err) tc, err = makeClient(&conf) @@ -6458,13 +6457,13 @@ func TestProxyTemplatesMakeClient(t *testing.T) { } // Create a empty profile so we don't ping proxy. - clientStore, err := initClientStore(conf, conf.Proxy) + err := conf.initClientStore() require.NoError(t, err) profile := &profile.Profile{ SSHProxyAddr: "proxy:3023", WebProxyAddr: "proxy:3080", } - err = clientStore.SaveProfile(profile, true) + err = conf.clientStore.SaveProfile(profile, true) require.NoError(t, err) modify(conf) diff --git a/tool/tsh/common/vnet_client_application.go b/tool/tsh/common/vnet_client_application.go index 87bba66f56729..b253c352f1409 100644 --- a/tool/tsh/common/vnet_client_application.go +++ b/tool/tsh/common/vnet_client_application.go @@ -233,7 +233,7 @@ func (p *vnetClientApplication) newTeleportClient(ctx context.Context, profileNa cfg := &client.Config{ ClientStore: p.clientStore, } - if err := cfg.LoadProfile(p.clientStore, profileName); err != nil { + if err := cfg.LoadProfile(profileName); err != nil { return nil, trace.Wrap(err, "loading client profile") } if leafClusterName != "" {