Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v17] improve error message when v17 tctl reads v16 tsh profile #49333

Merged
merged 1 commit into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions api/utils/keypaths/keypaths.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ const (
sshDirSuffix = "-ssh"
// fileNameKnownHosts is a file where known hosts are stored.
fileNameKnownHosts = "known_hosts"
// fileExtTLSCertLegacy is the legacy suffix/extension of a file where a TLS cert is stored.
fileExtTLSCertLegacy = "-x509.pem"
// FileExtTLSCertLegacy is the legacy suffix/extension of a file where a TLS cert is stored.
FileExtTLSCertLegacy = "-x509.pem"
// FileExtTLSCert is the suffix/extension of a file where a TLS cert is stored.
FileExtTLSCert = ".crt"
// FileExtKubeCred is the suffix/extension of a file where a kubernetes
Expand Down Expand Up @@ -188,6 +188,14 @@ func TLSCertPath(baseDir, proxy, username string) string {
return filepath.Join(ProxyKeyDir(baseDir, proxy), username+FileExtTLSCert)
}

// TLSCertPathLegacy returns the legacy path used in Teleport 16.x and older to the
// users's TLS certificate for the given proxy.
//
// <baseDir>/keys/<proxy>/<username>-x509.pem
func TLSCertPathLegacy(baseDir, proxy, username string) string {
return filepath.Join(ProxyKeyDir(baseDir, proxy), username+FileExtTLSCertLegacy)
}

// PublicKeyPath returns the path to the users's public key
// for the given proxy.
//
Expand Down Expand Up @@ -378,14 +386,6 @@ func IdentitySSHCertPath(path string) string {
return path + fileExtSSHCert
}

// TrimCertPathSuffix returns the given path with any cert suffix/extension trimmed off.
func TrimCertPathSuffix(path string) string {
trimmedPath := strings.TrimSuffix(path, fileExtTLSCertLegacy)
trimmedPath = strings.TrimSuffix(trimmedPath, FileExtTLSCert)
trimmedPath = strings.TrimSuffix(trimmedPath, fileExtSSHCert)
return trimmedPath
}

// TrimKeyPathSuffix returns the given path with any key suffix/extension trimmed off.
func TrimKeyPathSuffix(path string) string {
return strings.TrimSuffix(path, fileExtTLSKey)
Expand Down
38 changes: 25 additions & 13 deletions lib/client/client_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package client

import (
"errors"
"fmt"
"net/url"
"time"

Expand Down Expand Up @@ -87,21 +88,31 @@ func (s *Store) SetCustomHardwareKeyPrompt(prompt keys.HardwareKeyPrompt) {
s.KeyStore.SetCustomHardwareKeyPrompt(prompt)
}

var (
// ErrNoCredentials is returned by the client store when a specific key is not found.
// This error can be used to determine whether a client should retrieve new credentials,
// like how it is used with lib/client.RetryWithRelogin.
ErrNoCredentials = &trace.NotFoundError{Message: "no credentials"}
// ErrNoProfile is returned by the client store when a specific profile is not found.
var ErrNoProfile = &trace.NotFoundError{Message: "no profile"}

// ErrNoProfile is returned by the client store when a specific profile is not found.
// This error can be used to determine whether a client should retrieve new credentials,
// like how it is used with lib/client.RetryWithRelogin.
ErrNoProfile = &trace.NotFoundError{Message: "no profile"}
)
// noCredentialsError is returned by the client store when a specific key is not found.
// It unwraps to the original error to allow checks for underlying error types.
// Use [IsNoCredentialsError] instead of checking for this type directly.
type noCredentialsError struct {
wrappedError error
}

func newNoCredentialsError(wrappedError error) *noCredentialsError {
return &noCredentialsError{wrappedError}
}

func (e *noCredentialsError) Error() string {
return fmt.Sprintf("no credentials: %v", e.wrappedError)
}

func (e *noCredentialsError) Unwrap() error {
return e.wrappedError
}

// IsNoCredentialsError returns whether the given error implies that the user should retrieve new credentials.
func IsNoCredentialsError(err error) bool {
return errors.Is(err, ErrNoCredentials) || errors.Is(err, ErrNoProfile)
return errors.As(err, new(*noCredentialsError)) || errors.Is(err, ErrNoProfile)
}

// GetKeyRing gets the requested key ring with trusted the requested
Expand All @@ -111,7 +122,7 @@ func IsNoCredentialsError(err error) bool {
func (s *Store) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing, error) {
keyRing, err := s.KeyStore.GetKeyRing(idx, opts...)
if trace.IsNotFound(err) {
return nil, trace.Wrap(ErrNoCredentials, err.Error())
return nil, newNoCredentialsError(err)
} else if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -199,8 +210,9 @@ func (s *Store) ReadProfileStatus(profileName string) (*ProfileStatus, error) {
Username: profile.Username,
Cluster: profile.SiteName,
KubeEnabled: profile.KubeProxyAddr != "",
// Set ValidUntil to now to show that the keys are not available.
// Set ValidUntil to now and GetKeyRingError to show that the keys are not available.
ValidUntil: time.Now(),
GetKeyRingError: err,
SAMLSingleLogoutEnabled: profile.SAMLSingleLogoutEnabled,
SSOHost: profile.SSOHost,
}, nil
Expand Down
40 changes: 40 additions & 0 deletions lib/client/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"bytes"
"context"
"errors"
"fmt"
iofs "io/fs"
"os"
"path/filepath"
Expand Down Expand Up @@ -130,6 +131,12 @@ func (fs *FSKeyStore) tlsCertPath(idx KeyRingIndex) string {
return keypaths.TLSCertPath(fs.KeyDir, idx.ProxyHost, idx.Username)
}

// tlsCertPathLegacy returns the legacy TLS certificate path used in Teleport v16 and
// older given KeyRingIndex.
func (fs *FSKeyStore) tlsCertPathLegacy(idx KeyRingIndex) string {
return keypaths.TLSCertPathLegacy(fs.KeyDir, idx.ProxyHost, idx.Username)
}

// sshDir returns the SSH certificate path for the given KeyRingIndex.
func (fs *FSKeyStore) sshDir(proxy, user string) string {
return keypaths.SSHDir(fs.KeyDir, proxy, user)
Expand Down Expand Up @@ -497,6 +504,33 @@ func (fs *FSKeyStore) DeleteKeys() error {
return nil
}

// LegacyCertPathError will be returned when [(*FSKeyStore).GetKeyRing] does not
// find a user TLS certificate at the expected path used in v17+ but does find
// one at the legacy path used in Teleport v16-.
type LegacyCertPathError struct {
wrappedError error
expectedPath, foundPath string
}

func newLegacyCertPathError(wrappedError error, expectedPath, foundPath string) *LegacyCertPathError {
return &LegacyCertPathError{
wrappedError: wrappedError,
expectedPath: expectedPath,
foundPath: foundPath,
}
}

// Error implements the error interface.
func (e *LegacyCertPathError) Error() string {
return fmt.Sprintf(
"user TLS certificate was found at unsupported legacy path (expected path: %s, found path: %s)",
e.expectedPath, e.foundPath)
}

func (e *LegacyCertPathError) Unwrap() error {
return e.wrappedError
}

// GetKeyRing returns the user's key including the specified certs.
// If the key is not found, returns trace.NotFound error.
func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing, error) {
Expand All @@ -512,6 +546,12 @@ func (fs *FSKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRing

tlsCred, err := readTLSCredential(fs.userTLSKeyPath(idx), fs.tlsCertPath(idx), fs.CustomHardwareKeyPrompt)
if err != nil {
if trace.IsNotFound(err) {
if _, statErr := os.Stat(fs.tlsCertPathLegacy(idx)); statErr == nil {
return nil, newLegacyCertPathError(err, fs.tlsCertPath(idx), fs.tlsCertPathLegacy(idx))
}
return nil, err
}
return nil, trace.Wrap(err)
}

Expand Down
3 changes: 3 additions & 0 deletions lib/client/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ type ProfileStatus struct {
// ValidUntil is the time at which this SSH certificate will expire.
ValidUntil time.Time

// GetKeyRingError is any error encountered while loading the KeyRing.
GetKeyRingError error

// Extensions is a list of enabled SSH features for the certificate.
Extensions []string

Expand Down
8 changes: 8 additions & 0 deletions tool/tctl/common/tctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,14 @@ func LoadConfigFromProfile(ccf *GlobalCLIFlags, cfg *servicecfg.Config) (*authcl
return nil, trace.Wrap(err)
}
if profile.IsExpired(time.Now()) {
if profile.GetKeyRingError != nil {
if errors.As(profile.GetKeyRingError, new(*client.LegacyCertPathError)) {
// Intentionally avoid wrapping the error because the caller
// ignores NotFound errors.
return nil, trace.Errorf("it appears tsh v16 or older was used to log in, make sure to use tsh and tctl on the same major version\n\t%v", profile.GetKeyRingError)
}
return nil, trace.Wrap(profile.GetKeyRingError)
}
return nil, trace.BadParameter("your credentials have expired, please login using `tsh login`")
}

Expand Down
Loading