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

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

Merged
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
11 changes: 11 additions & 0 deletions api/utils/keypaths/keypaths.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const (
fileNameKnownHosts = "known_hosts"
// fileExtTLSCert is the suffix/extension of a file where a TLS cert is stored.
fileExtTLSCert = "-x509.pem"
// FileExtTLSCertFuture is the suffix/extension of a file where a TLS cert
// is stored in the future (v17+).
FileExtTLSCertFuture = ".crt"
// fileNameTLSCerts is a file where TLS Cert Authorities are stored.
fileNameTLSCerts = "certs.pem"
// fileExtCert is the suffix/extension of a file where an SSH Cert is stored.
Expand Down Expand Up @@ -166,6 +169,14 @@ func TLSCertPath(baseDir, proxy, username string) string {
return filepath.Join(ProxyKeyDir(baseDir, proxy), username+fileExtTLSCert)
}

// TLSCertPathFuture returns the path to where the users's TLS certificate
// for the given proxy will be store in the future (v17+).
//
// <baseDir>/keys/<proxy>/<username>.crt
func TLSCertPathFuture(baseDir, proxy, username string) string {
return filepath.Join(ProxyKeyDir(baseDir, proxy), username+FileExtTLSCertFuture)
}

// PublicKeyPath returns the path to the users's public key
// for the given proxy.
//
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"
"os"
"time"
Expand Down Expand Up @@ -82,21 +83,31 @@ func (s *Store) AddKey(key *Key) error {
return nil
}

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

// GetKey gets the requested key with trusted the requested certificates. The key's
Expand All @@ -106,7 +117,7 @@ func IsNoCredentialsError(err error) bool {
func (s *Store) GetKey(idx KeyIndex, opts ...CertOption) (*Key, error) {
key, err := s.KeyStore.GetKey(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 @@ -194,8 +205,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,
}, 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 @@ -20,6 +20,7 @@ package client

import (
"errors"
"fmt"
iofs "io/fs"
"os"
"path/filepath"
Expand Down Expand Up @@ -112,6 +113,12 @@ func (fs *FSKeyStore) tlsCertPath(idx KeyIndex) string {
return keypaths.TLSCertPath(fs.KeyDir, idx.ProxyHost, idx.Username)
}

// tlsCertPathFuture returns the future TLS certificate path used in Teleport v17 and
// newer given KeyIndex.
func (fs *FSKeyStore) tlsCertPathFuture(idx KeyIndex) string {
return keypaths.TLSCertPathFuture(fs.KeyDir, idx.ProxyHost, idx.Username)
}

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

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

func newFutureCertPathError(wrappedError error, expectedPath, foundPath string) *FutureCertPathError {
return &FutureCertPathError{
wrappedError: wrappedError,
expectedPath: expectedPath,
foundPath: foundPath,
}
}

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

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

// GetKey returns the user's key including the specified certs.
// If the key is not found, returns trace.NotFound error.
func (fs *FSKeyStore) GetKey(idx KeyIndex, opts ...CertOption) (*Key, error) {
Expand All @@ -322,6 +356,12 @@ func (fs *FSKeyStore) GetKey(idx KeyIndex, opts ...CertOption) (*Key, error) {
tlsCertFile := fs.tlsCertPath(idx)
tlsCert, err := os.ReadFile(tlsCertFile)
if err != nil {
if trace.IsNotFound(err) {
if _, statErr := os.Stat(fs.tlsCertPathFuture(idx)); statErr == nil {
return nil, newFutureCertPathError(err, fs.tlsCertPath(idx), fs.tlsCertPathFuture(idx))
}
return nil, err
}
return nil, trace.ConvertSystemError(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 @@ -437,6 +437,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.FutureCertPathError)) {
// Intentionally avoid wrapping the error because the caller
// ignores NotFound errors.
return nil, trace.Errorf("it appears tsh v17 or newer 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